Skip to content

Commit ffe9ece

Browse files
authored
Add vue/require-slots-as-functions rule. (#1178)
* Add `vue/require-slots-as-functions` rule. * Update require-slots-as-functions.js
1 parent fb1fb79 commit ffe9ece

File tree

6 files changed

+290
-0
lines changed

6 files changed

+290
-0
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
7575
| [vue/require-component-is](./require-component-is.md) | require `v-bind:is` of `<component>` elements | |
7676
| [vue/require-prop-type-constructor](./require-prop-type-constructor.md) | require prop type to be a constructor | :wrench: |
7777
| [vue/require-render-return](./require-render-return.md) | enforce render function to always return value | |
78+
| [vue/require-slots-as-functions](./require-slots-as-functions.md) | enforce properties of `$slots` to be used as a function | |
7879
| [vue/require-toggle-inside-transition](./require-toggle-inside-transition.md) | require control the display of the content inside `<transition>` | |
7980
| [vue/require-v-for-key](./require-v-for-key.md) | require `v-bind:key` with `v-for` directives | |
8081
| [vue/require-valid-default-prop](./require-valid-default-prop.md) | enforce props default values to be valid | |

Diff for: docs/rules/require-slots-as-functions.md

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/require-slots-as-functions
5+
description: enforce properties of `$slots` to be used as a function
6+
---
7+
# vue/require-slots-as-functions
8+
> enforce properties of `$slots` to be used as a function
9+
10+
- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
11+
12+
## :book: Rule Details
13+
14+
This rule enforces the properties of `$slots` to be used as a function.
15+
`this.$slots.default` was an array of VNode in Vue.js 2.x, but changed to a function that returns an array of VNode in Vue.js 3.x.
16+
17+
<eslint-code-block :rules="{'vue/require-slots-as-functions': ['error']}">
18+
19+
```vue
20+
<script>
21+
export default {
22+
render(h) {
23+
/* ✓ GOOD */
24+
var children = this.$slots.default()
25+
var children = this.$slots.default && this.$slots.default()
26+
27+
/* ✗ BAD */
28+
var children = [...this.$slots.default]
29+
var children = this.$slots.default.filter(test)
30+
}
31+
}
32+
</script>
33+
```
34+
35+
</eslint-code-block>
36+
37+
## :wrench: Options
38+
39+
Nothing.
40+
41+
## :books: Further reading
42+
43+
- [Vue RFCs - 0006-slots-unification](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0006-slots-unification.md)
44+
45+
## :mag: Implementation
46+
47+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-slots-as-functions.js)
48+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-slots-as-functions.js)

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

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ module.exports = {
4343
'vue/require-component-is': 'error',
4444
'vue/require-prop-type-constructor': 'error',
4545
'vue/require-render-return': 'error',
46+
'vue/require-slots-as-functions': 'error',
4647
'vue/require-toggle-inside-transition': 'error',
4748
'vue/require-v-for-key': 'error',
4849
'vue/require-valid-default-prop': 'error',

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ module.exports = {
112112
'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
113113
'require-prop-types': require('./rules/require-prop-types'),
114114
'require-render-return': require('./rules/require-render-return'),
115+
'require-slots-as-functions': require('./rules/require-slots-as-functions'),
115116
'require-toggle-inside-transition': require('./rules/require-toggle-inside-transition'),
116117
'require-v-for-key': require('./rules/require-v-for-key'),
117118
'require-valid-default-prop': require('./rules/require-valid-default-prop'),

Diff for: lib/rules/require-slots-as-functions.js

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
* @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
18+
*/
19+
20+
// ------------------------------------------------------------------------------
21+
// Rule Definition
22+
// ------------------------------------------------------------------------------
23+
24+
module.exports = {
25+
meta: {
26+
type: 'problem',
27+
docs: {
28+
description: 'enforce properties of `$slots` to be used as a function',
29+
categories: ['vue3-essential'],
30+
url: 'https://eslint.vuejs.org/rules/require-slots-as-functions.html'
31+
},
32+
fixable: null,
33+
schema: [],
34+
messages: {
35+
unexpected: 'Property in `$slots` should be used as function.'
36+
}
37+
},
38+
39+
create(context) {
40+
/**
41+
* Verify the given node
42+
* @param {MemberExpression | Identifier} node The node to verify
43+
* @param {Expression} reportNode The node to report
44+
*/
45+
function verify(node, reportNode) {
46+
const parent = node.parent
47+
48+
if (
49+
parent.type === 'VariableDeclarator' &&
50+
parent.id.type === 'Identifier'
51+
) {
52+
// const children = this.$slots.foo
53+
verifyReferences(parent.id, reportNode)
54+
return
55+
}
56+
57+
if (
58+
parent.type === 'AssignmentExpression' &&
59+
parent.right === node &&
60+
parent.left.type === 'Identifier'
61+
) {
62+
// children = this.$slots.foo
63+
verifyReferences(parent.left, reportNode)
64+
return
65+
}
66+
67+
if (
68+
// this.$slots.foo.xxx
69+
parent.type === 'MemberExpression' ||
70+
// var [foo] = this.$slots.foo
71+
parent.type === 'VariableDeclarator' ||
72+
// [...this.$slots.foo]
73+
parent.type === 'SpreadElement' ||
74+
// [this.$slots.foo]
75+
parent.type === 'ArrayExpression'
76+
) {
77+
context.report({
78+
node: reportNode,
79+
messageId: 'unexpected'
80+
})
81+
}
82+
}
83+
/**
84+
* Verify the references of the given node.
85+
* @param {Identifier} node The node to verify
86+
* @param {Expression} reportNode The node to report
87+
*/
88+
function verifyReferences(node, reportNode) {
89+
// @ts-ignore
90+
const variable = findVariable(context.getScope(), node)
91+
if (!variable) {
92+
return
93+
}
94+
for (const reference of variable.references) {
95+
if (!reference.isRead()) {
96+
continue
97+
}
98+
/** @type {Identifier} */
99+
const id = reference.identifier
100+
verify(id, reportNode)
101+
}
102+
}
103+
104+
return utils.defineVueVisitor(context, {
105+
/** @param {MemberExpression} node */
106+
MemberExpression(node) {
107+
const object = node.object
108+
if (object.type !== 'MemberExpression') {
109+
return
110+
}
111+
if (
112+
object.property.type !== 'Identifier' ||
113+
object.property.name !== '$slots'
114+
) {
115+
return
116+
}
117+
if (!utils.isThis(object.object, context)) {
118+
return
119+
}
120+
verify(node, node.property)
121+
}
122+
})
123+
}
124+
}

Diff for: tests/lib/rules/require-slots-as-functions.js

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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/require-slots-as-functions')
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('require-slots-as-functions', rule, {
24+
valid: [
25+
{
26+
filename: 'test.vue',
27+
code: `
28+
<script>
29+
export default {
30+
render (h) {
31+
var children = this.$slots.default()
32+
var children = this.$slots.default && this.$slots.default()
33+
34+
return h('div', this.$slots.default)
35+
}
36+
}
37+
</script>
38+
`
39+
},
40+
{
41+
filename: 'test.vue',
42+
code: `
43+
<script>
44+
export default {
45+
render (h) {
46+
var children = unknown.$slots.default
47+
var children = unknown.$slots.default.filter(test)
48+
49+
return h('div', [...children])
50+
}
51+
}
52+
</script>
53+
`
54+
}
55+
],
56+
57+
invalid: [
58+
{
59+
filename: 'test.vue',
60+
code: `
61+
<script>
62+
export default {
63+
render (h) {
64+
var children = this.$slots.default
65+
var children = this.$slots.default.filter(test)
66+
67+
return h('div', [...children])
68+
}
69+
}
70+
</script>
71+
`,
72+
errors: [
73+
{
74+
message: 'Property in `$slots` should be used as function.',
75+
line: 5,
76+
column: 38,
77+
endLine: 5,
78+
endColumn: 45
79+
},
80+
{
81+
message: 'Property in `$slots` should be used as function.',
82+
line: 6,
83+
column: 38,
84+
endLine: 6,
85+
endColumn: 45
86+
}
87+
]
88+
},
89+
90+
{
91+
filename: 'test.vue',
92+
code: `
93+
<script>
94+
export default {
95+
render (h) {
96+
let children
97+
98+
const [node] = this.$slots.foo
99+
const bar = [this.$slots[foo]]
100+
101+
children = this.$slots.foo
102+
103+
return h('div', children.filter(test))
104+
}
105+
}
106+
</script>
107+
`,
108+
errors: [
109+
'Property in `$slots` should be used as function.',
110+
'Property in `$slots` should be used as function.',
111+
'Property in `$slots` should be used as function.'
112+
]
113+
}
114+
]
115+
})

0 commit comments

Comments
 (0)