Skip to content

Commit 52f34e9

Browse files
authored
Add vue/no-multiple-slot-args rule. (#1179)
* Add `vue/no-multiple-slot-args` rule. * Fixed testcase
1 parent ffe9ece commit 52f34e9

File tree

7 files changed

+345
-0
lines changed

7 files changed

+345
-0
lines changed

Diff for: docs/rules/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
147147
|:--------|:------------|:---|
148148
| [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: |
149149
| [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements | |
150+
| [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots | |
150151
| [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack | |
151152
| [vue/order-in-components](./order-in-components.md) | enforce order of properties in components | :wrench: |
152153
| [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
254255
|:--------|:------------|:---|
255256
| [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: |
256257
| [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements | |
258+
| [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots | |
257259
| [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack | |
258260
| [vue/order-in-components](./order-in-components.md) | enforce order of properties in components | :wrench: |
259261
| [vue/this-in-template](./this-in-template.md) | disallow usage of `this` in template | |

Diff for: docs/rules/no-multiple-slot-args.md

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-multiple-slot-args
5+
description: disallow to pass multiple arguments to scoped slots
6+
---
7+
# vue/no-multiple-slot-args
8+
> disallow to pass multiple arguments to scoped slots
9+
10+
- :gear: This rule is included in `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
11+
12+
## :book: Rule Details
13+
14+
This rule disallows to pass multiple arguments to scoped slots.
15+
In details, it reports call expressions if a call of `this.$scopedSlots` members has 2 or more arguments.
16+
17+
<eslint-code-block :rules="{'vue/no-multiple-slot-args': ['error']}">
18+
19+
```vue
20+
<script>
21+
export default {
22+
render(h) {
23+
/* ✓ GOOD */
24+
var children = this.$scopedSlots.default()
25+
var children = this.$scopedSlots.default(foo)
26+
var children = this.$scopedSlots.default({ foo, bar })
27+
28+
/* ✗ BAD */
29+
var children = this.$scopedSlots.default(foo, bar)
30+
var children = this.$scopedSlots.default(...foo)
31+
}
32+
}
33+
</script>
34+
```
35+
36+
</eslint-code-block>
37+
38+
## :wrench: Options
39+
40+
Nothing.
41+
42+
## :books: Further reading
43+
44+
- [vuejs/vue#9468](https://github.com/vuejs/vue/issues/9468#issuecomment-462210146)
45+
46+
## :mag: Implementation
47+
48+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-multiple-slot-args.js)
49+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-multiple-slot-args.js)

Diff for: lib/configs/recommended.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module.exports = {
88
rules: {
99
'vue/attributes-order': 'warn',
1010
'vue/component-tags-order': 'warn',
11+
'vue/no-multiple-slot-args': 'warn',
1112
'vue/no-v-html': 'warn',
1213
'vue/order-in-components': 'warn',
1314
'vue/this-in-template': 'warn'

Diff for: lib/configs/vue3-recommended.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module.exports = {
88
rules: {
99
'vue/attributes-order': 'warn',
1010
'vue/component-tags-order': 'warn',
11+
'vue/no-multiple-slot-args': 'warn',
1112
'vue/no-v-html': 'warn',
1213
'vue/order-in-components': 'warn',
1314
'vue/this-in-template': 'warn'

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ module.exports = {
7171
'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
7272
'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'),
7373
'no-multi-spaces': require('./rules/no-multi-spaces'),
74+
'no-multiple-slot-args': require('./rules/no-multiple-slot-args'),
7475
'no-multiple-template-root': require('./rules/no-multiple-template-root'),
7576
'no-mutating-props': require('./rules/no-mutating-props'),
7677
'no-parsing-error': require('./rules/no-parsing-error'),

Diff for: lib/rules/no-multiple-slot-args.js

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
const { findVariable } = require('eslint-utils')
13+
14+
/**
15+
* @typedef {import('vue-eslint-parser').AST.ESLintMemberExpression} MemberExpression
16+
* @typedef {import('vue-eslint-parser').AST.ESLintIdentifier} Identifier
17+
*/
18+
19+
// ------------------------------------------------------------------------------
20+
// Rule Definition
21+
// ------------------------------------------------------------------------------
22+
23+
module.exports = {
24+
meta: {
25+
type: 'problem',
26+
docs: {
27+
description: 'disallow to pass multiple arguments to scoped slots',
28+
categories: ['vue3-recommended', 'recommended'],
29+
url: 'https://eslint.vuejs.org/rules/no-multiple-slot-args.html'
30+
},
31+
fixable: null,
32+
schema: [],
33+
messages: {
34+
unexpected: 'Unexpected multiple arguments.',
35+
unexpectedSpread: 'Unexpected spread argument.'
36+
}
37+
},
38+
39+
create(context) {
40+
/**
41+
* Verify the given node
42+
* @param {MemberExpression | Identifier} node The node to verify
43+
*/
44+
function verify(node) {
45+
const parent = node.parent
46+
47+
if (
48+
parent.type === 'VariableDeclarator' &&
49+
parent.id.type === 'Identifier'
50+
) {
51+
// const foo = this.$scopedSlots.foo
52+
verifyReferences(parent.id)
53+
return
54+
}
55+
56+
if (
57+
parent.type === 'AssignmentExpression' &&
58+
parent.right === node &&
59+
parent.left.type === 'Identifier'
60+
) {
61+
// foo = this.$scopedSlots.foo
62+
verifyReferences(parent.left)
63+
return
64+
}
65+
66+
if (parent.type !== 'CallExpression' || parent.arguments.includes(node)) {
67+
return
68+
}
69+
70+
if (!parent.arguments.length) {
71+
return
72+
}
73+
if (parent.arguments.length > 1) {
74+
context.report({
75+
node: parent.arguments[1],
76+
messageId: 'unexpected'
77+
})
78+
}
79+
if (parent.arguments[0].type === 'SpreadElement') {
80+
context.report({
81+
node: parent.arguments[0],
82+
messageId: 'unexpectedSpread'
83+
})
84+
}
85+
}
86+
/**
87+
* Verify the references of the given node.
88+
* @param {Identifier} node The node to verify
89+
*/
90+
function verifyReferences(node) {
91+
// @ts-ignore
92+
const variable = findVariable(context.getScope(), node)
93+
if (!variable) {
94+
return
95+
}
96+
for (const reference of variable.references) {
97+
if (!reference.isRead()) {
98+
continue
99+
}
100+
/** @type {Identifier} */
101+
const id = reference.identifier
102+
verify(id)
103+
}
104+
}
105+
106+
return utils.defineVueVisitor(context, {
107+
/** @param {MemberExpression} node */
108+
MemberExpression(node) {
109+
const object = node.object
110+
if (object.type !== 'MemberExpression') {
111+
return
112+
}
113+
if (
114+
object.property.type !== 'Identifier' ||
115+
(object.property.name !== '$slots' &&
116+
object.property.name !== '$scopedSlots')
117+
) {
118+
return
119+
}
120+
if (!utils.isThis(object.object, context)) {
121+
return
122+
}
123+
verify(node)
124+
}
125+
})
126+
}
127+
}

Diff for: tests/lib/rules/no-multiple-slot-args.js

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const rule = require('../../../lib/rules/no-multiple-slot-args')
12+
13+
const RuleTester = require('eslint').RuleTester
14+
15+
// ------------------------------------------------------------------------------
16+
// Tests
17+
// ------------------------------------------------------------------------------
18+
19+
const ruleTester = new RuleTester({
20+
parser: require.resolve('vue-eslint-parser'),
21+
parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
22+
})
23+
ruleTester.run('no-multiple-slot-args', rule, {
24+
valid: [
25+
{
26+
filename: 'test.vue',
27+
code: `
28+
<script>
29+
export default {
30+
render (h) {
31+
var children = this.$scopedSlots.default()
32+
var children = this.$scopedSlots.foo(foo)
33+
const bar = this.$scopedSlots.bar
34+
bar(foo)
35+
}
36+
}
37+
</script>
38+
`
39+
},
40+
{
41+
filename: 'test.vue',
42+
code: `
43+
<script>
44+
export default {
45+
render (h) {
46+
unknown.$scopedSlots.default(foo, bar)
47+
}
48+
}
49+
</script>
50+
`
51+
},
52+
{
53+
filename: 'test.vue',
54+
code: `
55+
<script>
56+
export default {
57+
render (h) {
58+
// for Vue3
59+
var children = this.$slots.default()
60+
var children = this.$slots.foo(foo)
61+
const bar = this.$slots.bar
62+
bar(foo)
63+
}
64+
}
65+
</script>
66+
`
67+
},
68+
{
69+
filename: 'test.vue',
70+
code: `
71+
<script>
72+
export default {
73+
render (h) {
74+
this.$foo.default(foo, bar)
75+
}
76+
}
77+
</script>
78+
`
79+
}
80+
],
81+
82+
invalid: [
83+
{
84+
filename: 'test.vue',
85+
code: `
86+
<script>
87+
export default {
88+
render (h) {
89+
this.$scopedSlots.default(foo, bar)
90+
this.$scopedSlots.foo(foo, bar)
91+
}
92+
}
93+
</script>
94+
`,
95+
errors: [
96+
{
97+
message: 'Unexpected multiple arguments.',
98+
line: 5,
99+
column: 42,
100+
endLine: 5,
101+
endColumn: 45
102+
},
103+
{
104+
message: 'Unexpected multiple arguments.',
105+
line: 6,
106+
column: 38,
107+
endLine: 6,
108+
endColumn: 41
109+
}
110+
]
111+
},
112+
{
113+
filename: 'test.vue',
114+
code: `
115+
<script>
116+
export default {
117+
render (h) {
118+
let children
119+
120+
this.$scopedSlots.default(foo, { bar })
121+
122+
children = this.$scopedSlots.foo
123+
if (children) children(...foo)
124+
}
125+
}
126+
</script>
127+
`,
128+
errors: [
129+
{
130+
message: 'Unexpected multiple arguments.',
131+
line: 7,
132+
column: 42,
133+
endLine: 7,
134+
endColumn: 49
135+
},
136+
{
137+
message: 'Unexpected spread argument.',
138+
line: 10,
139+
column: 34,
140+
endLine: 10,
141+
endColumn: 40
142+
}
143+
]
144+
},
145+
{
146+
filename: 'test.vue',
147+
code: `
148+
<script>
149+
export default {
150+
render (h) {
151+
// for Vue3
152+
this.$slots.default(foo, bar)
153+
this.$slots.foo(foo, bar)
154+
}
155+
}
156+
</script>
157+
`,
158+
errors: [
159+
'Unexpected multiple arguments.',
160+
'Unexpected multiple arguments.'
161+
]
162+
}
163+
]
164+
})

0 commit comments

Comments
 (0)