diff --git a/docs/rules/README.md b/docs/rules/README.md index dd5718b08..f3e2bfdf6 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -147,6 +147,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi |:--------|:------------|:---| | [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: | | [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements | | +| [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots | | | [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack | | | [vue/order-in-components](./order-in-components.md) | enforce order of properties in components | :wrench: | | [vue/this-in-template](./this-in-template.md) | disallow usage of `this` in template | | @@ -254,6 +255,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi |:--------|:------------|:---| | [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: | | [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements | | +| [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots | | | [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack | | | [vue/order-in-components](./order-in-components.md) | enforce order of properties in components | :wrench: | | [vue/this-in-template](./this-in-template.md) | disallow usage of `this` in template | | diff --git a/docs/rules/no-multiple-slot-args.md b/docs/rules/no-multiple-slot-args.md new file mode 100644 index 000000000..d0d319458 --- /dev/null +++ b/docs/rules/no-multiple-slot-args.md @@ -0,0 +1,49 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-multiple-slot-args +description: disallow to pass multiple arguments to scoped slots +--- +# vue/no-multiple-slot-args +> disallow to pass multiple arguments to scoped slots + +- :gear: This rule is included in `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`. + +## :book: Rule Details + +This rule disallows to pass multiple arguments to scoped slots. +In details, it reports call expressions if a call of `this.$scopedSlots` members has 2 or more arguments. + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :books: Further reading + +- [vuejs/vue#9468](https://github.com/vuejs/vue/issues/9468#issuecomment-462210146) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-multiple-slot-args.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-multiple-slot-args.js) diff --git a/lib/configs/recommended.js b/lib/configs/recommended.js index e3ca8eba7..e6388e158 100644 --- a/lib/configs/recommended.js +++ b/lib/configs/recommended.js @@ -8,6 +8,7 @@ module.exports = { rules: { 'vue/attributes-order': 'warn', 'vue/component-tags-order': 'warn', + 'vue/no-multiple-slot-args': 'warn', 'vue/no-v-html': 'warn', 'vue/order-in-components': 'warn', 'vue/this-in-template': 'warn' diff --git a/lib/configs/vue3-recommended.js b/lib/configs/vue3-recommended.js index 700f0f8f5..318690c33 100644 --- a/lib/configs/vue3-recommended.js +++ b/lib/configs/vue3-recommended.js @@ -8,6 +8,7 @@ module.exports = { rules: { 'vue/attributes-order': 'warn', 'vue/component-tags-order': 'warn', + 'vue/no-multiple-slot-args': 'warn', 'vue/no-v-html': 'warn', 'vue/order-in-components': 'warn', 'vue/this-in-template': 'warn' diff --git a/lib/index.js b/lib/index.js index f41be8ba7..c0041525f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -71,6 +71,7 @@ module.exports = { 'no-irregular-whitespace': require('./rules/no-irregular-whitespace'), 'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'), 'no-multi-spaces': require('./rules/no-multi-spaces'), + 'no-multiple-slot-args': require('./rules/no-multiple-slot-args'), 'no-multiple-template-root': require('./rules/no-multiple-template-root'), 'no-mutating-props': require('./rules/no-mutating-props'), 'no-parsing-error': require('./rules/no-parsing-error'), diff --git a/lib/rules/no-multiple-slot-args.js b/lib/rules/no-multiple-slot-args.js new file mode 100644 index 000000000..e8bcae96c --- /dev/null +++ b/lib/rules/no-multiple-slot-args.js @@ -0,0 +1,127 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') +const { findVariable } = require('eslint-utils') + +/** + * @typedef {import('vue-eslint-parser').AST.ESLintMemberExpression} MemberExpression + * @typedef {import('vue-eslint-parser').AST.ESLintIdentifier} Identifier + */ + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'disallow to pass multiple arguments to scoped slots', + categories: ['vue3-recommended', 'recommended'], + url: 'https://eslint.vuejs.org/rules/no-multiple-slot-args.html' + }, + fixable: null, + schema: [], + messages: { + unexpected: 'Unexpected multiple arguments.', + unexpectedSpread: 'Unexpected spread argument.' + } + }, + + create(context) { + /** + * Verify the given node + * @param {MemberExpression | Identifier} node The node to verify + */ + function verify(node) { + const parent = node.parent + + if ( + parent.type === 'VariableDeclarator' && + parent.id.type === 'Identifier' + ) { + // const foo = this.$scopedSlots.foo + verifyReferences(parent.id) + return + } + + if ( + parent.type === 'AssignmentExpression' && + parent.right === node && + parent.left.type === 'Identifier' + ) { + // foo = this.$scopedSlots.foo + verifyReferences(parent.left) + return + } + + if (parent.type !== 'CallExpression' || parent.arguments.includes(node)) { + return + } + + if (!parent.arguments.length) { + return + } + if (parent.arguments.length > 1) { + context.report({ + node: parent.arguments[1], + messageId: 'unexpected' + }) + } + if (parent.arguments[0].type === 'SpreadElement') { + context.report({ + node: parent.arguments[0], + messageId: 'unexpectedSpread' + }) + } + } + /** + * Verify the references of the given node. + * @param {Identifier} node The node to verify + */ + function verifyReferences(node) { + // @ts-ignore + const variable = findVariable(context.getScope(), node) + if (!variable) { + return + } + for (const reference of variable.references) { + if (!reference.isRead()) { + continue + } + /** @type {Identifier} */ + const id = reference.identifier + verify(id) + } + } + + return utils.defineVueVisitor(context, { + /** @param {MemberExpression} node */ + MemberExpression(node) { + const object = node.object + if (object.type !== 'MemberExpression') { + return + } + if ( + object.property.type !== 'Identifier' || + (object.property.name !== '$slots' && + object.property.name !== '$scopedSlots') + ) { + return + } + if (!utils.isThis(object.object, context)) { + return + } + verify(node) + } + }) + } +} diff --git a/tests/lib/rules/no-multiple-slot-args.js b/tests/lib/rules/no-multiple-slot-args.js new file mode 100644 index 000000000..fb42767b9 --- /dev/null +++ b/tests/lib/rules/no-multiple-slot-args.js @@ -0,0 +1,164 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-multiple-slot-args') + +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2018, sourceType: 'module' } +}) +ruleTester.run('no-multiple-slot-args', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + } + ], + + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Unexpected multiple arguments.', + line: 5, + column: 42, + endLine: 5, + endColumn: 45 + }, + { + message: 'Unexpected multiple arguments.', + line: 6, + column: 38, + endLine: 6, + endColumn: 41 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Unexpected multiple arguments.', + line: 7, + column: 42, + endLine: 7, + endColumn: 49 + }, + { + message: 'Unexpected spread argument.', + line: 10, + column: 34, + endLine: 10, + endColumn: 40 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Unexpected multiple arguments.', + 'Unexpected multiple arguments.' + ] + } + ] +})