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.