diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index f57b812e27992..0db6cffdf6867 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -2445,6 +2445,10 @@ impl RuleRunner for crate::rules::vue::define_props_declaration::DefinePropsDecl const NODE_TYPES: Option<&AstTypesBitset> = None; } +impl RuleRunner for crate::rules::vue::no_multiple_slot_args::NoMultipleSlotArgs { + const NODE_TYPES: Option<&AstTypesBitset> = None; +} + 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 327ccb3148c6b..8571a09f8de50 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -626,6 +626,7 @@ pub(crate) mod node { pub(crate) mod vue { pub mod define_emits_declaration; pub mod define_props_declaration; + pub mod no_multiple_slot_args; pub mod valid_define_emits; pub mod valid_define_props; } @@ -1207,6 +1208,7 @@ oxc_macros::declare_all_lint_rules! { vitest::require_local_test_context_for_concurrent_snapshots, vue::define_emits_declaration, vue::define_props_declaration, + vue::no_multiple_slot_args, vue::valid_define_emits, vue::valid_define_props, } diff --git a/crates/oxc_linter/src/rules/vue/no_multiple_slot_args.rs b/crates/oxc_linter/src/rules/vue/no_multiple_slot_args.rs new file mode 100644 index 0000000000000..e9f2cd9ceaf06 --- /dev/null +++ b/crates/oxc_linter/src/rules/vue/no_multiple_slot_args.rs @@ -0,0 +1,399 @@ +use oxc_ast::{ + AstKind, + ast::{ + AssignmentTarget, Expression, IdentifierReference, MemberExpression, + VariableDeclarationKind, + }, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::{AstNode, context::LintContext, frameworks::FrameworkOptions, rule::Rule}; + +fn multiple_arguments_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Unexpected multiple arguments.") + .with_help("Pass only one argument to the slot function.") + .with_label(span) +} + +fn spread_argument_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Unexpected spread argument.") + .with_help("Do not use spread arguments when calling slot functions.") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoMultipleSlotArgs; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow passing multiple arguments to scoped slots. + /// + /// ### Why is this bad? + /// + /// Users have to use the arguments in fixed order and cannot omit the ones they don't need. + /// e.g. if you have a slot that passes in 5 arguments but the user actually only need the last 2 of them, + /// they will have to declare all 5 just to use the last 2. + /// + /// More information can be found in [vuejs/vue#9468](https://github.com/vuejs/vue/issues/9468#issuecomment-462210146) + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```vue + /// + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```vue + /// + /// ``` + NoMultipleSlotArgs, + vue, + restriction, + pending // TODO: Remove second argument, Spread argument is possible not supported +); + +impl Rule for NoMultipleSlotArgs { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + + if call_expr.arguments.is_empty() { + return; + } + + let member_expr = match call_expr.callee.get_inner_expression() { + Expression::StaticMemberExpression(member_expr) => member_expr.as_ref(), + Expression::ChainExpression(chain_expr) => { + if let Some(MemberExpression::StaticMemberExpression(member_expr)) = + chain_expr.expression.as_member_expression() + { + member_expr.as_ref() + } else { + return; + } + } + Expression::Identifier(identifier) => { + let Some(member_expr) = get_identifier_resolved_reference(identifier, ctx) else { + return; + }; + if let Expression::StaticMemberExpression(member_expr) = member_expr { + member_expr.as_ref() + } else { + return; + } + } + _ => return, + }; + + let inner = match member_expr.object.get_inner_expression() { + Expression::StaticMemberExpression(inner) => inner.as_ref(), + Expression::ChainExpression(chain_expr) => { + if let Some(MemberExpression::StaticMemberExpression(inner)) = + chain_expr.expression.as_member_expression() + { + inner.as_ref() + } else { + return; + } + } + _ => return, + }; + + match inner.object.get_inner_expression() { + Expression::ThisExpression(_) => {} + Expression::Identifier(identifier) => { + let Some(expression) = get_identifier_resolved_reference(identifier, ctx) else { + return; + }; + if !matches!(expression, Expression::ThisExpression(_)) { + return; + } + } + _ => return, + } + + if inner.property.name != "$slots" && inner.property.name != "$scopedSlots" { + return; + } + + if call_expr.arguments.len() > 1 { + ctx.diagnostic(multiple_arguments_diagnostic(call_expr.arguments[1].span())); + } else if call_expr.arguments[0].is_spread() { + ctx.diagnostic(spread_argument_diagnostic(call_expr.arguments[0].span())); + } + } + + fn should_run(&self, ctx: &crate::context::ContextHost) -> bool { + ctx.file_path().extension().is_some_and(|ext| ext == "vue") + && ctx.frameworks_options() != FrameworkOptions::VueSetup + } +} + +fn get_identifier_resolved_reference<'a>( + identifier: &IdentifierReference, + ctx: &LintContext<'a>, +) -> Option<&'a Expression<'a>> { + let reference = ctx.scoping().get_reference(identifier.reference_id()); + let symbol_id = reference.symbol_id()?; + let declaration = ctx.scoping().symbol_declaration(symbol_id); + let node = ctx.nodes().get_node(declaration); + + let AstKind::VariableDeclarator(declarator) = node.kind() else { + return None; + }; + + // `const` variable can not be overridden + if declarator.kind == VariableDeclarationKind::Const { + return declarator.init.as_ref(); + } + + find_latest_assignment(&identifier.name, declarator.span.end, identifier.span.start, ctx) +} + +fn find_latest_assignment<'a>( + identifier_name: &str, + start_index: u32, + end_index: u32, + ctx: &LintContext<'a>, +) -> Option<&'a Expression<'a>> { + let mut result = None; + for node in ctx.nodes() { + // The node is after the call expression, no need to continue searching + if node.span().start > end_index { + break; + } + + // The node is before the variable declaration, skip it + if node.span().start > start_index { + if let AstKind::AssignmentExpression(assign_expr) = node.kind() { + if let AssignmentTarget::AssignmentTargetIdentifier(assigned_id) = &assign_expr.left + { + if assigned_id.name == identifier_name { + result = Some(&assign_expr.right); + } + } + } + } + } + result +} + +#[test] +fn test() { + use crate::tester::Tester; + use std::path::PathBuf; + + let pass = vec![ + ( + " + + ", + 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")), + ), + ]; + + let fail = vec![ + ( + " + + ", + 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")), + ), + ( + " + + ", + None, + None, + Some(PathBuf::from("test.vue")), + ), + ( + " + + ", + None, + None, + Some(PathBuf::from("test.vue")), + ), + ]; + + Tester::new(NoMultipleSlotArgs::NAME, NoMultipleSlotArgs::PLUGIN, pass, fail) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/vue_no_multiple_slot_args.snap b/crates/oxc_linter/src/snapshots/vue_no_multiple_slot_args.snap new file mode 100644 index 0000000000000..93e800f537027 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/vue_no_multiple_slot_args.snap @@ -0,0 +1,200 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:5:45] + 4 │ render (h) { + 5 │ this.$scopedSlots.default(foo, bar) + · ─── + 6 │ this.$scopedSlots.foo(foo, bar) + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:6:41] + 5 │ this.$scopedSlots.default(foo, bar) + 6 │ this.$scopedSlots.foo(foo, bar) + · ─── + 7 │ } + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:5:49] + 4 │ render (h) { + 5 │ this?.$scopedSlots?.default?.(foo, bar) + · ─── + 6 │ this?.$scopedSlots?.foo?.(foo, bar) + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:6:45] + 5 │ this?.$scopedSlots?.default?.(foo, bar) + 6 │ this?.$scopedSlots?.foo?.(foo, bar) + · ─── + 7 │ const vm = this + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:8:47] + 7 │ const vm = this + 8 │ vm?.$scopedSlots?.default?.(foo, bar) + · ─── + 9 │ vm?.$scopedSlots?.foo?.(foo, bar) + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:9:43] + 8 │ vm?.$scopedSlots?.default?.(foo, bar) + 9 │ vm?.$scopedSlots?.foo?.(foo, bar) + · ─── + 10 │ } + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:5:47] + 4 │ render (h) { + 5 │ this.$scopedSlots.default?.(foo, bar) + · ─── + 6 │ this.$scopedSlots.foo?.(foo, bar) + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:6:43] + 5 │ this.$scopedSlots.default?.(foo, bar) + 6 │ this.$scopedSlots.foo?.(foo, bar) + · ─── + 7 │ const vm = this + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:8:45] + 7 │ const vm = this + 8 │ vm.$scopedSlots.default?.(foo, bar) + · ─── + 9 │ vm.$scopedSlots.foo?.(foo, bar) + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:9:41] + 8 │ vm.$scopedSlots.default?.(foo, bar) + 9 │ vm.$scopedSlots.foo?.(foo, bar) + · ─── + 10 │ } + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:5:52] + 4 │ render (h) { + 5 │ ;(this?.$scopedSlots)?.default?.(foo, bar) + · ─── + 6 │ ;(this?.$scopedSlots?.foo)?.(foo, bar) + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:6:48] + 5 │ ;(this?.$scopedSlots)?.default?.(foo, bar) + 6 │ ;(this?.$scopedSlots?.foo)?.(foo, bar) + · ─── + 7 │ const vm = this + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:8:50] + 7 │ const vm = this + 8 │ ;(vm?.$scopedSlots)?.default?.(foo, bar) + · ─── + 9 │ ;(vm?.$scopedSlots?.foo)?.(foo, bar) + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:9:46] + 8 │ ;(vm?.$scopedSlots)?.default?.(foo, bar) + 9 │ ;(vm?.$scopedSlots?.foo)?.(foo, bar) + · ─── + 10 │ } + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:5:49] + 4 │ render (h) { + 5 │ ;(this?.$scopedSlots).default(foo, bar) + · ─── + 6 │ ;(this?.$scopedSlots?.foo)(foo, bar) + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:6:46] + 5 │ ;(this?.$scopedSlots).default(foo, bar) + 6 │ ;(this?.$scopedSlots?.foo)(foo, bar) + · ─── + 7 │ const vm = this + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:8:47] + 7 │ const vm = this + 8 │ ;(vm?.$scopedSlots).default(foo, bar) + · ─── + 9 │ ;(vm?.$scopedSlots?.foo)(foo, bar) + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:9:44] + 8 │ ;(vm?.$scopedSlots).default(foo, bar) + 9 │ ;(vm?.$scopedSlots?.foo)(foo, bar) + · ─── + 10 │ } + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:7:45] + 6 │ + 7 │ this.$scopedSlots.default(foo, { bar }) + · ─────── + 8 │ + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected spread argument. + ╭─[no_multiple_slot_args.tsx:10:37] + 9 │ children = this.$scopedSlots.foo + 10 │ if (children) children(...foo) + · ────── + 11 │ } + ╰──── + help: Do not use spread arguments when calling slot functions. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:6:39] + 5 │ // for Vue3 + 6 │ this.$slots.default(foo, bar) + · ─── + 7 │ this.$slots.foo(foo, bar) + ╰──── + help: Pass only one argument to the slot function. + + ⚠ eslint-plugin-vue(no-multiple-slot-args): Unexpected multiple arguments. + ╭─[no_multiple_slot_args.tsx:7:35] + 6 │ this.$slots.default(foo, bar) + 7 │ this.$slots.foo(foo, bar) + · ─── + 8 │ } + ╰──── + help: Pass only one argument to the slot function.