From 2a67eb78c0871a37a0b5a453eac9921cd69042d0 Mon Sep 17 00:00:00 2001 From: yefan Date: Tue, 9 Sep 2025 19:20:23 +0800 Subject: [PATCH 1/6] implement --- .../src/generated/rule_runner_impls.rs | 5 + crates/oxc_linter/src/rules.rs | 2 + .../vue/no_required_prop_with_default.rs | 1368 +++++++++++++++++ .../vue_no_required_prop_with_default.snap | 4 + 4 files changed, 1379 insertions(+) create mode 100644 crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs create mode 100644 crates/oxc_linter/src/snapshots/vue_no_required_prop_with_default.snap diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index aaad904029e34..80879f7aa0aa9 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -2693,6 +2693,11 @@ impl RuleRunner for crate::rules::vue::require_typed_ref::RequireTypedRef { const NODE_TYPES: Option<&AstTypesBitset> = None; } +impl RuleRunner for crate::rules::vue::no_required_prop_with_default::NoRequiredPropWithDefault { + const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); + const ANY_NODE_TYPE: bool = true; +} + impl RuleRunner for crate::rules::vue::valid_define_emits::ValidDefineEmits { const NODE_TYPES: Option<&AstTypesBitset> = None; } diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index b4fa2e3c63a09..0807100974de7 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -631,6 +631,7 @@ pub(crate) mod vue { pub mod define_emits_declaration; pub mod define_props_declaration; pub mod no_multiple_slot_args; + pub mod no_required_prop_with_default; pub mod require_typed_ref; pub mod valid_define_emits; pub mod valid_define_props; @@ -1218,6 +1219,7 @@ oxc_macros::declare_all_lint_rules! { vue::define_emits_declaration, vue::define_props_declaration, vue::no_multiple_slot_args, + vue::no_required_prop_with_default, vue::require_typed_ref, vue::valid_define_emits, vue::valid_define_props, diff --git a/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs b/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs new file mode 100644 index 0000000000000..04b17ca471ef5 --- /dev/null +++ b/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs @@ -0,0 +1,1368 @@ +use oxc_ast::{ + AstKind, + ast::{ExportDefaultDeclarationKind, Expression, ObjectPropertyKind, TSMethodSignatureKind, TSSignature, TSType, TSTypeName}, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use rustc_hash::FxHashSet; +use oxc_span::{Span, GetSpan}; + +use crate::{AstNode, context::LintContext, rule::Rule}; + +fn no_required_prop_with_default_diagnostic(span: Span) -> OxcDiagnostic { + // See for details + OxcDiagnostic::warn("Should be an imperative statement about what is wrong") + .with_help("Should be a command-like statement that tells the user how to fix the issue") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoRequiredPropWithDefault; + +declare_oxc_lint!( + /// ### What it does + /// + /// Briefly describe the rule's purpose. + /// + /// ### Why is this bad? + /// + /// Explain why violating this rule is problematic. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// FIXME: Tests will fail if examples are missing or syntactically incorrect. + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// FIXME: Tests will fail if examples are missing or syntactically incorrect. + /// ``` + NoRequiredPropWithDefault, + vue, + style, + pending +); + +impl Rule for NoRequiredPropWithDefault { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::CallExpression(call_expr) => { + let Expression::Identifier(ident) = &call_expr.callee else { + return; + }; + if ident.name != "withDefaults" || call_expr.arguments.len() != 2 { + return; + } + let [first_arg, second_arg] = call_expr.arguments.as_slice() else { + return; + }; + if let Some(first_arg_expr) = first_arg.as_expression() + && let Some(second_arg_expr) = second_arg.as_expression() + { + let Expression::ObjectExpression(second_obj_expr) = + second_arg_expr.get_inner_expression() + else { + return; + }; + if second_obj_expr.properties.is_empty() { + return; + } + let mut key_hash = FxHashSet::::default(); + + for prop in second_obj_expr.properties.iter() { + if let ObjectPropertyKind::ObjectProperty(obj_prop) = prop + && let Some(key) = obj_prop.key.static_name() { + key_hash.insert(key.to_string()); + } + } + + let Expression::CallExpression(first_call_expr) = + first_arg_expr.get_inner_expression() + else { + return; + }; + let Expression::Identifier(first_call_ident) = &first_call_expr.callee else { + return; + }; + if first_call_ident.name != "defineProps" { + return; + } + let Some(type_arguments) = first_call_expr.type_arguments.as_ref() else { + return; + }; + let Some(first_type_argument) = type_arguments.params.first() else { + return; + }; + match first_type_argument { + TSType::TSTypeReference(type_ref) => { + let TSTypeName::IdentifierReference(ident_ref) = &type_ref.type_name + else { + return; + }; + let reference = ctx.scoping().get_reference(ident_ref.reference_id()); + if !reference.is_type() { + return; + } + let reference_node = + ctx.symbol_declaration(reference.symbol_id().unwrap()); + let AstKind::TSInterfaceDeclaration(interface_decl) = reference_node.kind() else { + return; + }; + let body = &interface_decl.body; + body.body.iter().for_each(|item| { + let (key_name, optional) = match item { + TSSignature::TSPropertySignature(prop_sign) => (prop_sign.key.static_name(), prop_sign.optional), + TSSignature::TSMethodSignature(method_sign) if method_sign.kind == TSMethodSignatureKind::Method => (method_sign.key.static_name(), method_sign.optional), + _ => (None, false), + }; + if let Some(key_name) = key_name && !optional { + if key_hash.contains(key_name.as_ref()) { + ctx.diagnostic(no_required_prop_with_default_diagnostic( + item.span() + )); + } + } + }); + } + TSType::TSTypeLiteral(type_literal) => { + type_literal.members.iter().for_each(|item| { + let (key_name, optional) = match item { + TSSignature::TSPropertySignature(prop_sign) => (prop_sign.key.static_name(), prop_sign.optional), + TSSignature::TSMethodSignature(method_sign) if method_sign.kind == TSMethodSignatureKind::Method => (method_sign.key.static_name(), method_sign.optional), + _ => (None, false), + }; + if let Some(key_name) = key_name && !optional { + if key_hash.contains(key_name.as_ref()) { + ctx.diagnostic(no_required_prop_with_default_diagnostic( + item.span() + )); + } + } + }); + } + _ => {} + } + } + } + AstKind::ExportDefaultDeclaration(export_default_decl) => { + let ExportDefaultDeclarationKind::ObjectExpression(obj_expr) = &export_default_decl.declaration else { + return; + }; + // find prop + let Some(prop) = obj_expr.properties.iter().find(|item| { + if let ObjectPropertyKind::ObjectProperty(obj_prop) = item + && let Some(key) = obj_prop.key.static_name() { + key == "props" + } else { + false + } + }) else { + return; + }; + if let ObjectPropertyKind::ObjectProperty(obj_prop) = prop + && let Expression::ObjectExpression(obj_expr) = obj_prop.value.get_inner_expression() { + obj_expr.properties.iter().for_each(|item| { + if let ObjectPropertyKind::ObjectProperty(p) = item + && let Some(key) = p.key.static_name() + && let Expression::ObjectExpression(inner_obj_expr) = p.value.get_inner_expression() { + // check inner_obj_expr.properties has 'default' key + let mut has_default_key = false; + let mut has_required_key = false; + for property in inner_obj_expr.properties.iter() { + if let ObjectPropertyKind::ObjectProperty(inner_p) = property + && let Some(inner_key) = inner_p.key.static_name() { + if inner_key == "default" { + has_default_key = true; + } + if inner_key == "required" { + let Expression::BooleanLiteral(inner_value) = &inner_p.value else { + continue; + }; + if inner_value.value { + has_required_key = true; + } else { + break; + } + } + + if has_default_key && has_required_key { + ctx.diagnostic(no_required_prop_with_default_diagnostic( + p.span + )); + break; + } + } + } + } + }) + } + + } + _ => {} + } + } + + fn should_run(&self, ctx: &crate::context::ContextHost) -> bool { + ctx.file_path().extension().is_some_and(|ext| ext == "vue") + } +} + +#[test] +fn test() { + use crate::tester::Tester; + use std::path::PathBuf; + + // let pass = vec![( + // r#" + // + // "#, + // None, + // None, + // Some(PathBuf::from("test2.vue")), + // )]; + + // let pass = vec![ + // ( + // r#" + // + // "#, + // None, + // None, + // Some(PathBuf::from("test.vue")), + // ), + // ]; + + // let fail = vec![ + // ]; + + let pass = vec![ + ( + r#" + + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + " + + ", + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + " + + ", + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + " + + ", + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r#" + + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + " + + ", + None, + None, + Some(PathBuf::from("test.vue")), + ), + ]; + + let fail = vec![ + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + " + + ", + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), + ( + " + + ", + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), + ( + " + + ", + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), + ( + " + + ", + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), + ( + " + + ", + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + " + + ", + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + None, + Some(PathBuf::from("test.vue")), + ), + ]; + + let _fix = vec![ + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + " + + ", + " + + ", + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + " + + ", + " + + ", + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + " + + ", + " + + ", + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + " + + ", + " + + ", + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + " + + ", + " + + ", + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ( + r#" + + "#, + r#" + + "#, + Some(serde_json::json!([{ "autofix": true }])), + ), + ]; + + Tester::new(NoRequiredPropWithDefault::NAME, NoRequiredPropWithDefault::PLUGIN, pass, fail) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/vue_no_required_prop_with_default.snap b/crates/oxc_linter/src/snapshots/vue_no_required_prop_with_default.snap new file mode 100644 index 0000000000000..2adf1447d2f84 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/vue_no_required_prop_with_default.snap @@ -0,0 +1,4 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + From 374faf911e2f7db05d8ddaf28b80840a237ed7f2 Mon Sep 17 00:00:00 2001 From: yefan Date: Tue, 16 Sep 2025 19:21:38 +0800 Subject: [PATCH 2/6] implement --- .../vue/no_required_prop_with_default.rs | 1770 +++++++++-------- .../vue_no_required_prop_with_default.snap | 182 ++ 2 files changed, 1124 insertions(+), 828 deletions(-) diff --git a/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs b/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs index 04b17ca471ef5..1c94a0a0306f4 100644 --- a/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs +++ b/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs @@ -1,18 +1,22 @@ use oxc_ast::{ AstKind, - ast::{ExportDefaultDeclarationKind, Expression, ObjectPropertyKind, TSMethodSignatureKind, TSSignature, TSType, TSTypeName}, + ast::{ + BindingPatternKind, ExportDefaultDeclarationKind, Expression, ObjectExpression, + ObjectPropertyKind, TSMethodSignatureKind, TSSignature, TSType, TSTypeName, + VariableDeclarator, + }, }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; use rustc_hash::FxHashSet; -use oxc_span::{Span, GetSpan}; use crate::{AstNode, context::LintContext, rule::Rule}; -fn no_required_prop_with_default_diagnostic(span: Span) -> OxcDiagnostic { - // See for details - OxcDiagnostic::warn("Should be an imperative statement about what is wrong") - .with_help("Should be a command-like statement that tells the user how to fix the issue") +fn no_required_prop_with_default_diagnostic(span: Span, prop_name: &str) -> OxcDiagnostic { + let msg = format!("Prop \"{prop_name}\" should be optional."); + OxcDiagnostic::warn(msg) + .with_help("Remove the `required: true` option, or drop the `required` key entirely to make this prop optional.") .with_label(span) } @@ -22,26 +26,48 @@ pub struct NoRequiredPropWithDefault; declare_oxc_lint!( /// ### What it does /// - /// Briefly describe the rule's purpose. + /// Enforce props with default values to be optional. /// /// ### Why is this bad? /// - /// Explain why violating this rule is problematic. + /// If a prop is declared with a default value, whether it is required or not, + /// we can always skip it in actual use. In that situation, the default value would be applied. + /// So, a required prop with a default value is essentially the same as an optional prop. /// /// ### Examples /// /// Examples of **incorrect** code for this rule: - /// ```js - /// FIXME: Tests will fail if examples are missing or syntactically incorrect. + /// ```vue + /// /// ``` /// /// Examples of **correct** code for this rule: - /// ```js - /// FIXME: Tests will fail if examples are missing or syntactically incorrect. + /// ```vue + /// /// ``` NoRequiredPropWithDefault, vue, - style, + suspicious, pending ); @@ -52,153 +78,71 @@ impl Rule for NoRequiredPropWithDefault { let Expression::Identifier(ident) = &call_expr.callee else { return; }; - if ident.name != "withDefaults" || call_expr.arguments.len() != 2 { - return; - } - let [first_arg, second_arg] = call_expr.arguments.as_slice() else { - return; - }; - if let Some(first_arg_expr) = first_arg.as_expression() - && let Some(second_arg_expr) = second_arg.as_expression() - { - let Expression::ObjectExpression(second_obj_expr) = - second_arg_expr.get_inner_expression() - else { - return; - }; - if second_obj_expr.properties.is_empty() { - return; - } - let mut key_hash = FxHashSet::::default(); - - for prop in second_obj_expr.properties.iter() { - if let ObjectPropertyKind::ObjectProperty(obj_prop) = prop - && let Some(key) = obj_prop.key.static_name() { - key_hash.insert(key.to_string()); + match ident.name.as_str() { + "defineProps" => { + if let Some(arge) = call_expr.arguments.first() { + let Some(Expression::ObjectExpression(obj)) = arge.as_expression() + else { + return; + }; + // Here we need to consider the following two examples + // 1. const props = defineProps({ name: { required: true, default: 'a' } }) + // 2. const { name = 'a' } = defineProps({ name: { required: true } }) + let key_hash = collect_hash_from_variable_declarator(ctx, node) + .unwrap_or_default(); + handle_prop_object(ctx, obj, Some(&key_hash)); + } + if call_expr.arguments.is_empty() { + // if `defineProps` is used without arguments, we need to check the type arguments + // e.g. `const { name = 'a' } = defineProps()` + let Some(type_args) = &call_expr.type_arguments else { + return; + }; + let Some(first_type_argument) = type_args.params.first() else { + return; + }; + if let Some(key_hash) = collect_hash_from_variable_declarator(ctx, node) + { + handle_type_argument(ctx, first_type_argument, &key_hash); + } } } - - let Expression::CallExpression(first_call_expr) = - first_arg_expr.get_inner_expression() - else { - return; - }; - let Expression::Identifier(first_call_ident) = &first_call_expr.callee else { - return; - }; - if first_call_ident.name != "defineProps" { - return; + "defineComponent" if call_expr.arguments.len() == 1 => { + let arg = &call_expr.arguments[0]; + let Some(Expression::ObjectExpression(obj)) = arg.as_expression() else { + return; + }; + handle_object_expression(ctx, obj); } - let Some(type_arguments) = first_call_expr.type_arguments.as_ref() else { - return; - }; - let Some(first_type_argument) = type_arguments.params.first() else { - return; - }; - match first_type_argument { - TSType::TSTypeReference(type_ref) => { - let TSTypeName::IdentifierReference(ident_ref) = &type_ref.type_name + "withDefaults" if call_expr.arguments.len() == 2 => { + let [first_arg, second_arg] = call_expr.arguments.as_slice() else { + return; + }; + if let (Some(first_arg_expr), Some(second_arg_expr)) = + (first_arg.as_expression(), second_arg.as_expression()) + { + let Expression::ObjectExpression(second_obj_expr) = + second_arg_expr.get_inner_expression() else { return; }; - let reference = ctx.scoping().get_reference(ident_ref.reference_id()); - if !reference.is_type() { - return; - } - let reference_node = - ctx.symbol_declaration(reference.symbol_id().unwrap()); - let AstKind::TSInterfaceDeclaration(interface_decl) = reference_node.kind() else { + let Some(key_hash) = collect_hash_from_object_expr(second_obj_expr) + else { return; }; - let body = &interface_decl.body; - body.body.iter().for_each(|item| { - let (key_name, optional) = match item { - TSSignature::TSPropertySignature(prop_sign) => (prop_sign.key.static_name(), prop_sign.optional), - TSSignature::TSMethodSignature(method_sign) if method_sign.kind == TSMethodSignatureKind::Method => (method_sign.key.static_name(), method_sign.optional), - _ => (None, false), - }; - if let Some(key_name) = key_name && !optional { - if key_hash.contains(key_name.as_ref()) { - ctx.diagnostic(no_required_prop_with_default_diagnostic( - item.span() - )); - } - } - }); - } - TSType::TSTypeLiteral(type_literal) => { - type_literal.members.iter().for_each(|item| { - let (key_name, optional) = match item { - TSSignature::TSPropertySignature(prop_sign) => (prop_sign.key.static_name(), prop_sign.optional), - TSSignature::TSMethodSignature(method_sign) if method_sign.kind == TSMethodSignatureKind::Method => (method_sign.key.static_name(), method_sign.optional), - _ => (None, false), - }; - if let Some(key_name) = key_name && !optional { - if key_hash.contains(key_name.as_ref()) { - ctx.diagnostic(no_required_prop_with_default_diagnostic( - item.span() - )); - } - } - }); + process_define_props_call(ctx, first_arg_expr, &key_hash); } - _ => {} } + _ => {} } } AstKind::ExportDefaultDeclaration(export_default_decl) => { - let ExportDefaultDeclarationKind::ObjectExpression(obj_expr) = &export_default_decl.declaration else { + let ExportDefaultDeclarationKind::ObjectExpression(obj_expr) = + &export_default_decl.declaration + else { return; }; - // find prop - let Some(prop) = obj_expr.properties.iter().find(|item| { - if let ObjectPropertyKind::ObjectProperty(obj_prop) = item - && let Some(key) = obj_prop.key.static_name() { - key == "props" - } else { - false - } - }) else { - return; - }; - if let ObjectPropertyKind::ObjectProperty(obj_prop) = prop - && let Expression::ObjectExpression(obj_expr) = obj_prop.value.get_inner_expression() { - obj_expr.properties.iter().for_each(|item| { - if let ObjectPropertyKind::ObjectProperty(p) = item - && let Some(key) = p.key.static_name() - && let Expression::ObjectExpression(inner_obj_expr) = p.value.get_inner_expression() { - // check inner_obj_expr.properties has 'default' key - let mut has_default_key = false; - let mut has_required_key = false; - for property in inner_obj_expr.properties.iter() { - if let ObjectPropertyKind::ObjectProperty(inner_p) = property - && let Some(inner_key) = inner_p.key.static_name() { - if inner_key == "default" { - has_default_key = true; - } - if inner_key == "required" { - let Expression::BooleanLiteral(inner_value) = &inner_p.value else { - continue; - }; - if inner_value.value { - has_required_key = true; - } else { - break; - } - } - - if has_default_key && has_required_key { - ctx.diagnostic(no_required_prop_with_default_diagnostic( - p.span - )); - break; - } - } - } - } - }) - } - + handle_object_expression(ctx, obj_expr); } _ => {} } @@ -209,59 +153,229 @@ impl Rule for NoRequiredPropWithDefault { } } +fn collect_hash_from_object_expr(obj: &ObjectExpression) -> Option> { + if obj.properties.is_empty() { + return None; + } + let key_hash = obj + .properties + .iter() + .filter_map(|item| { + if let ObjectPropertyKind::ObjectProperty(obj_prop) = item + && let Some(key) = obj_prop.key.static_name() + { + Some(key.to_string()) + } else { + None + } + }) + .collect(); + Some(key_hash) +} + +fn collect_hash_from_variable_declarator( + ctx: &LintContext<'_>, + node: &AstNode, +) -> Option> { + let var_decl = get_first_variable_decl_ancestor(ctx, node)?; + let BindingPatternKind::ObjectPattern(obj_pattern) = &var_decl.id.kind else { + return None; + }; + let key_hash: FxHashSet = obj_pattern + .properties + .iter() + .filter_map(|prop| prop.key.static_name()) + .map(|key| key.to_string()) + .collect(); + Some(key_hash) +} + +fn get_first_variable_decl_ancestor<'a>( + ctx: &LintContext<'a>, + node: &AstNode, +) -> Option<&'a VariableDeclarator<'a>> { + ctx.nodes().ancestors(node.id()).find_map(|ancestor| { + if let AstKind::VariableDeclarator(var_decl) = ancestor.kind() { + Some(var_decl) + } else { + None + } + }) +} + +fn process_define_props_call( + ctx: &LintContext, + first_arg_expr: &Expression, + key_hash: &FxHashSet, +) { + let Expression::CallExpression(first_call_expr) = first_arg_expr.get_inner_expression() else { + return; + }; + let Expression::Identifier(first_call_ident) = &first_call_expr.callee else { + return; + }; + if first_call_ident.name != "defineProps" { + return; + } + let Some(type_arguments) = first_call_expr.type_arguments.as_ref() else { + return; + }; + let Some(first_type_argument) = type_arguments.params.first() else { + return; + }; + + handle_type_argument(ctx, first_type_argument, key_hash); +} + +fn handle_type_argument(ctx: &LintContext, ts_type: &TSType, key_hash: &FxHashSet) { + match ts_type { + // e.g. `const props = defineProps()` + TSType::TSTypeReference(type_ref) => { + let TSTypeName::IdentifierReference(ident_ref) = &type_ref.type_name else { + return; + }; + // we need to find the reference of type_ref + let reference = ctx.scoping().get_reference(ident_ref.reference_id()); + if !reference.is_type() { + return; + } + let reference_node = ctx.symbol_declaration(reference.symbol_id().unwrap()); + let AstKind::TSInterfaceDeclaration(interface_decl) = reference_node.kind() else { + return; + }; + let body = &interface_decl.body; + body.body.iter().for_each(|item| { + let (key_name, optional) = match item { + TSSignature::TSPropertySignature(prop_sign) => { + (prop_sign.key.static_name(), prop_sign.optional) + } + TSSignature::TSMethodSignature(method_sign) + if method_sign.kind == TSMethodSignatureKind::Method => + { + (method_sign.key.static_name(), method_sign.optional) + } + _ => (None, false), + }; + if let Some(key_name) = key_name + && !optional + && key_hash.contains(key_name.as_ref()) + { + ctx.diagnostic(no_required_prop_with_default_diagnostic( + item.span(), + key_name.as_ref(), + )); + } + }); + } + // e.g. `const props = defineProps<{ name: string }>()` + TSType::TSTypeLiteral(type_literal) => { + type_literal.members.iter().for_each(|item| { + let (key_name, optional) = match item { + TSSignature::TSPropertySignature(prop_sign) => { + (prop_sign.key.static_name(), prop_sign.optional) + } + TSSignature::TSMethodSignature(method_sign) + if method_sign.kind == TSMethodSignatureKind::Method => + { + (method_sign.key.static_name(), method_sign.optional) + } + _ => (None, false), + }; + if let Some(key_name) = key_name + && !optional + && key_hash.contains(key_name.as_ref()) + { + ctx.diagnostic(no_required_prop_with_default_diagnostic( + item.span(), + key_name.as_ref(), + )); + } + }); + } + _ => {} + } +} + +fn handle_object_expression(ctx: &LintContext, obj: &ObjectExpression) { + let Some(prop) = obj.properties.iter().find(|item| { + if let ObjectPropertyKind::ObjectProperty(obj_prop) = item + && let Some(key) = obj_prop.key.static_name() + { + key == "props" + } else { + false + } + }) else { + return; + }; + let ObjectPropertyKind::ObjectProperty(prop_obj) = prop else { + return; + }; + let Expression::ObjectExpression(prop_obj_expr) = prop_obj.value.get_inner_expression() else { + return; + }; + handle_prop_object(ctx, prop_obj_expr, None); +} + +fn handle_prop_object( + ctx: &LintContext, + obj: &ObjectExpression, + key_hash: Option<&FxHashSet>, +) { + obj.properties.iter().for_each(|v| { + if let ObjectPropertyKind::ObjectProperty(inner_prop) = v + && let Some(inner_key) = inner_prop.key.static_name() + && let Expression::ObjectExpression(inner_prop_value_expr) = + inner_prop.value.get_inner_expression() + { + let mut has_default_key = false; + let mut has_required_key = false; + + // Sometimes the default value comes from the `ObjectPattern` of a `VariableDeclarator`, + // e.g. `const { name = 2 } = defineProps()` + if key_hash.is_some_and(|hash| hash.contains(inner_key.as_ref())) { + has_default_key = true; + } + + for property in &inner_prop_value_expr.properties { + if let ObjectPropertyKind::ObjectProperty(item_obj) = property + && let Some(item_key) = item_obj.key.static_name() + { + if item_key == "default" { + has_default_key = true; + } + if item_key == "required" { + let Expression::BooleanLiteral(inner_value) = &item_obj.value else { + continue; + }; + if inner_value.value { + has_required_key = true; + } else { + break; + } + } + + if has_default_key && has_required_key { + ctx.diagnostic(no_required_prop_with_default_diagnostic( + inner_prop.span(), + inner_key.as_ref(), + )); + break; + } + } + } + } + }); +} + #[test] fn test() { use crate::tester::Tester; use std::path::PathBuf; - // let pass = vec![( - // r#" - // - // "#, - // None, - // None, - // Some(PathBuf::from("test2.vue")), - // )]; - - // let pass = vec![ - // ( - // r#" - // - // "#, - // None, - // None, - // Some(PathBuf::from("test.vue")), - // ), - // ]; - - // let fail = vec![ - // ]; - let pass = vec![ - ( - r#" + ( + r#" "#, - None, - None, - Some(PathBuf::from("test.vue")), - ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, - ( - r#" + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" "#, - None, - None, - Some(PathBuf::from("test.vue")), - ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, - ( - r#" + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" "#, - None, - None, - Some(PathBuf::from("test.vue")), - ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, - ( - r#" + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" "#, - None, - None, - Some(PathBuf::from("test.vue")), - ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, - ( - r#" + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" "#, - None, - None, - Some(PathBuf::from("test.vue")), - ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, - ( - r#" + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" "#, - None, - None, - Some(PathBuf::from("test.vue")), - ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, - ( - " + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + " ", - None, - None, - Some(PathBuf::from("test.vue")), - ), - ( - " + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + " ", - None, - None, - Some(PathBuf::from("test.vue")), - ), - ( - " + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + " ", - None, - None, - Some(PathBuf::from("test.vue")), - ), - ( - r#" + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + r#" "#, - None, - None, - Some(PathBuf::from("test.vue")), - ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, - ( - r#" + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + r#" "#, - None, - None, - Some(PathBuf::from("test.vue")), - ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, - ( - " + None, + None, + Some(PathBuf::from("test.vue")), + ), // { "parserOptions": { "parser": require.resolve("@typescript-eslint/parser") } }, + ( + " ", - None, - None, - Some(PathBuf::from("test.vue")), - ), + None, + None, + Some(PathBuf::from("test.vue")), + ), ]; let fail = vec![ @@ -796,572 +910,572 @@ fn test() { ), ]; - let _fix = vec![ - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - " - - ", - " - - ", - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - " - - ", - " - - ", - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - " - - ", - " - - ", - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - " - - ", - " - - ", - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - " - - ", - " - - ", - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ( - r#" - - "#, - r#" - - "#, - Some(serde_json::json!([{ "autofix": true }])), - ), - ]; + // let _fix = vec![ + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // " + // + // ", + // " + // + // ", + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // " + // + // ", + // " + // + // ", + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // " + // + // ", + // " + // + // ", + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // " + // + // ", + // " + // + // ", + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // " + // + // ", + // " + // + // ", + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ( + // r#" + // + // "#, + // r#" + // + // "#, + // Some(serde_json::json!([{ "autofix": true }])), + // ), + // ]; Tester::new(NoRequiredPropWithDefault::NAME, NoRequiredPropWithDefault::PLUGIN, pass, fail) .test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/vue_no_required_prop_with_default.snap b/crates/oxc_linter/src/snapshots/vue_no_required_prop_with_default.snap index 2adf1447d2f84..41f49e32e3f8b 100644 --- a/crates/oxc_linter/src/snapshots/vue_no_required_prop_with_default.snap +++ b/crates/oxc_linter/src/snapshots/vue_no_required_prop_with_default.snap @@ -1,4 +1,186 @@ --- source: crates/oxc_linter/src/tester.rs --- + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ interface TestPropType { + 4 │ name: string + · ──────────── + 5 │ age?: number + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ interface TestPropType { + 4 │ name: string | number + · ───────────────────── + 5 │ age?: number + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "na::me" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ interface TestPropType { + 4 │ 'na::me': string + · ──────────────── + 5 │ age?: number + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:5:19] + 4 │ interface TestPropType { + 5 │ name: nameType + · ────────────── + 6 │ age?: number + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ interface TestPropType { + 4 │ name + · ──── + 5 │ } + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ interface TestPropType { + 4 │ name + · ──── + 5 │ age?: number + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "na\"me2" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ interface TestPropType { + 4 │ 'na\\"me2' + · ────────── + 5 │ age?: number + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "foo" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ interface TestPropType { + 4 │ foo(): void + · ─────────── + 5 │ age?: number + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ interface TestPropType { + 4 │ readonly name + · ───────────── + 5 │ age?: number + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ interface TestPropType { + 4 │ readonly 'name' + · ─────────────── + 5 │ age?: number + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:5:19] + 4 │ props: { + 5 │ ╭─▶ name: { + 6 │ │ required: true, + 7 │ │ default: 'Hello' + 8 │ ╰─▶ } + 9 │ } + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:5:19] + 4 │ props: { + 5 │ ╭─▶ 'name': { + 6 │ │ required: true, + 7 │ │ default: 'Hello' + 8 │ ╰─▶ } + 9 │ } + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:6:19] + 5 │ props: { + 6 │ ╭─▶ 'name': { + 7 │ │ required: true, + 8 │ │ default: 'Hello' + 9 │ ╰─▶ } + 10 │ } + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:6:19] + 5 │ props: { + 6 │ ╭─▶ name: { + 7 │ │ required: true, + 8 │ │ default: 'Hello' + 9 │ ╰─▶ } + 10 │ } + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:6:19] + 5 │ props: { + 6 │ ╭─▶ name: { + 7 │ │ required: true, + 8 │ │ default: 'Hello' + 9 │ ╰─▶ } + 10 │ } + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ const props = defineProps({ + 4 │ ╭─▶ name: { + 5 │ │ required: true, + 6 │ │ default: 'Hello' + 7 │ ╰─▶ } + 8 │ }) + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ interface TestPropType { + 4 │ name: string + · ──────────── + 5 │ } + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ const {name="World"} = defineProps<{ + 4 │ name: string + · ──────────── + 5 │ }>(); + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. + + ⚠ eslint-plugin-vue(no-required-prop-with-default): Prop "name" should be optional. + ╭─[no_required_prop_with_default.tsx:4:19] + 3 │ const {name="World"} = defineProps({ + 4 │ ╭─▶ name: { + 5 │ │ required: true, + 6 │ ╰─▶ } + 7 │ }); + ╰──── + help: Remove the `required: true` option, or drop the `required` key entirely to make this prop optional. From 16798ae7c3b309cc9378f8170a180f10384527ca Mon Sep 17 00:00:00 2001 From: yefan Date: Tue, 16 Sep 2025 19:35:49 +0800 Subject: [PATCH 3/6] fix --- crates/oxc_linter/src/generated/rule_runner_impls.rs | 3 +-- .../src/rules/vue/no_required_prop_with_default.rs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 80879f7aa0aa9..d1052c05b0c4f 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -2694,8 +2694,7 @@ impl RuleRunner for crate::rules::vue::require_typed_ref::RequireTypedRef { } impl RuleRunner for crate::rules::vue::no_required_prop_with_default::NoRequiredPropWithDefault { - const NODE_TYPES: &AstTypesBitset = &AstTypesBitset::new(); - const ANY_NODE_TYPE: bool = true; + const NODE_TYPES: Option<&AstTypesBitset> = None; } impl RuleRunner for crate::rules::vue::valid_define_emits::ValidDefineEmits { diff --git a/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs b/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs index 1c94a0a0306f4..c13c3de022855 100644 --- a/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs +++ b/crates/oxc_linter/src/rules/vue/no_required_prop_with_default.rs @@ -30,9 +30,9 @@ declare_oxc_lint!( /// /// ### Why is this bad? /// - /// If a prop is declared with a default value, whether it is required or not, - /// we can always skip it in actual use. In that situation, the default value would be applied. - /// So, a required prop with a default value is essentially the same as an optional prop. + /// If a prop is declared with a default value, whether it is required or not, + /// we can always skip it in actual use. In that situation, the default value would be applied. + /// So, a required prop with a default value is essentially the same as an optional prop. /// /// ### Examples /// From 90dfa2b5d88fbba588ec8147d553b1dd80a545f0 Mon Sep 17 00:00:00 2001 From: yefan Date: Wed, 17 Sep 2025 11:02:59 +0800 Subject: [PATCH 4/6] check setup --- crates/oxc_linter/src/context/mod.rs | 7 +++++++ .../src/rules/vue/no_required_prop_with_default.rs | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/oxc_linter/src/context/mod.rs b/crates/oxc_linter/src/context/mod.rs index ef2e45964152a..2f91e967bba71 100644 --- a/crates/oxc_linter/src/context/mod.rs +++ b/crates/oxc_linter/src/context/mod.rs @@ -17,6 +17,7 @@ use crate::{ config::GlobalValue, disable_directives::DisableDirectives, fixer::{Fix, FixKind, Message, PossibleFixes, RuleFix, RuleFixer}, + frameworks::FrameworkOptions, }; mod host; @@ -443,6 +444,12 @@ impl<'a> LintContext<'a> { self.parent.frameworks } + /// Returns the framework options for the current script block. + /// For Vue files, this can be `FrameworkOptions::VueSetup` if we're in a `