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 ` + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```vue + /// + /// ``` + NoRequiredPropWithDefault, + vue, + suspicious, + pending +); + +impl Rule for NoRequiredPropWithDefault { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let is_vue = ctx.file_path().extension().is_some_and(|ext| ext == "vue"); + if is_vue { + self.run_on_vue(node, ctx); + } else { + self.check_define_component(node, ctx); + } + } +} + +impl NoRequiredPropWithDefault { + fn run_on_vue<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if ctx.frameworks_options() == FrameworkOptions::VueSetup { + self.run_on_setup(node, ctx); + } else { + self.run_on_composition(node, ctx); + } + } + + #[expect(clippy::unused_self)] + fn check_define_component(&self, node: &AstNode<'_>, ctx: &LintContext<'_>) { + // only check `defineComponent` method + // e.g. `let component = defineComponent({ props: { name: { required: true, default: 'a' } } })` + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + let Some(ident) = call_expr.callee.get_identifier_reference() else { + return; + }; + if ident.name.as_str() == "defineComponent" && 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); + } + } + + fn run_on_setup<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + let Some(ident) = call_expr.callee.get_identifier_reference() else { + return; + }; + + 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); + } + } + } + "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 Some(key_hash) = collect_hash_from_object_expr(second_obj_expr) else { + return; + }; + process_define_props_call(ctx, first_arg_expr, &key_hash); + } + } + _ => { + self.check_define_component(node, ctx); + } + } + } + + fn run_on_composition<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::ExportDefaultDeclaration(export_default_decl) => { + let ExportDefaultDeclarationKind::ObjectExpression(obj_expr) = + &export_default_decl.declaration + else { + return; + }; + handle_object_expression(ctx, obj_expr); + } + _ => { + self.check_define_component(node, ctx); + } + } + } +} + +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("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![ + ( + " + const a = defineComponent({ + props: { + 'name': { + required: true, + default: 'Hello' + } + } + }) + ", + None, + None, + Some(PathBuf::from("test.ts")), + ), + ( + 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..25a598397fa86 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/vue_no_required_prop_with_default.snap @@ -0,0 +1,197 @@ +--- +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:25] + 3 │ props: { + 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 │ 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.