Skip to content

Commit

Permalink
Add vue/require-slots-as-functions rule. (#1178)
Browse files Browse the repository at this point in the history
* Add `vue/require-slots-as-functions` rule.

* Update require-slots-as-functions.js
  • Loading branch information
ota-meshi committed Jun 5, 2020
1 parent fb1fb79 commit ffe9ece
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -75,6 +75,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
| [vue/require-component-is](./require-component-is.md) | require `v-bind:is` of `<component>` elements | |
| [vue/require-prop-type-constructor](./require-prop-type-constructor.md) | require prop type to be a constructor | :wrench: |
| [vue/require-render-return](./require-render-return.md) | enforce render function to always return value | |
| [vue/require-slots-as-functions](./require-slots-as-functions.md) | enforce properties of `$slots` to be used as a function | |
| [vue/require-toggle-inside-transition](./require-toggle-inside-transition.md) | require control the display of the content inside `<transition>` | |
| [vue/require-v-for-key](./require-v-for-key.md) | require `v-bind:key` with `v-for` directives | |
| [vue/require-valid-default-prop](./require-valid-default-prop.md) | enforce props default values to be valid | |
Expand Down
48 changes: 48 additions & 0 deletions docs/rules/require-slots-as-functions.md
@@ -0,0 +1,48 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/require-slots-as-functions
description: enforce properties of `$slots` to be used as a function
---
# vue/require-slots-as-functions
> enforce properties of `$slots` to be used as a function
- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.

## :book: Rule Details

This rule enforces the properties of `$slots` to be used as a function.
`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.

<eslint-code-block :rules="{'vue/require-slots-as-functions': ['error']}">

```vue
<script>
export default {
render(h) {
/* ✓ GOOD */
var children = this.$slots.default()
var children = this.$slots.default && this.$slots.default()
/* ✗ BAD */
var children = [...this.$slots.default]
var children = this.$slots.default.filter(test)
}
}
</script>
```

</eslint-code-block>

## :wrench: Options

Nothing.

## :books: Further reading

- [Vue RFCs - 0006-slots-unification](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0006-slots-unification.md)

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-slots-as-functions.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-slots-as-functions.js)
1 change: 1 addition & 0 deletions lib/configs/vue3-essential.js
Expand Up @@ -43,6 +43,7 @@ module.exports = {
'vue/require-component-is': 'error',
'vue/require-prop-type-constructor': 'error',
'vue/require-render-return': 'error',
'vue/require-slots-as-functions': 'error',
'vue/require-toggle-inside-transition': 'error',
'vue/require-v-for-key': 'error',
'vue/require-valid-default-prop': 'error',
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -112,6 +112,7 @@ module.exports = {
'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
'require-prop-types': require('./rules/require-prop-types'),
'require-render-return': require('./rules/require-render-return'),
'require-slots-as-functions': require('./rules/require-slots-as-functions'),
'require-toggle-inside-transition': require('./rules/require-toggle-inside-transition'),
'require-v-for-key': require('./rules/require-v-for-key'),
'require-valid-default-prop': require('./rules/require-valid-default-prop'),
Expand Down
124 changes: 124 additions & 0 deletions lib/rules/require-slots-as-functions.js
@@ -0,0 +1,124 @@
/**
* @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
* @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
*/

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce properties of `$slots` to be used as a function',
categories: ['vue3-essential'],
url: 'https://eslint.vuejs.org/rules/require-slots-as-functions.html'
},
fixable: null,
schema: [],
messages: {
unexpected: 'Property in `$slots` should be used as function.'
}
},

create(context) {
/**
* Verify the given node
* @param {MemberExpression | Identifier} node The node to verify
* @param {Expression} reportNode The node to report
*/
function verify(node, reportNode) {
const parent = node.parent

if (
parent.type === 'VariableDeclarator' &&
parent.id.type === 'Identifier'
) {
// const children = this.$slots.foo
verifyReferences(parent.id, reportNode)
return
}

if (
parent.type === 'AssignmentExpression' &&
parent.right === node &&
parent.left.type === 'Identifier'
) {
// children = this.$slots.foo
verifyReferences(parent.left, reportNode)
return
}

if (
// this.$slots.foo.xxx
parent.type === 'MemberExpression' ||
// var [foo] = this.$slots.foo
parent.type === 'VariableDeclarator' ||
// [...this.$slots.foo]
parent.type === 'SpreadElement' ||
// [this.$slots.foo]
parent.type === 'ArrayExpression'
) {
context.report({
node: reportNode,
messageId: 'unexpected'
})
}
}
/**
* Verify the references of the given node.
* @param {Identifier} node The node to verify
* @param {Expression} reportNode The node to report
*/
function verifyReferences(node, reportNode) {
// @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, reportNode)
}
}

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'
) {
return
}
if (!utils.isThis(object.object, context)) {
return
}
verify(node, node.property)
}
})
}
}
115 changes: 115 additions & 0 deletions tests/lib/rules/require-slots-as-functions.js
@@ -0,0 +1,115 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const rule = require('../../../lib/rules/require-slots-as-functions')

const RuleTester = require('eslint').RuleTester

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const ruleTester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
})
ruleTester.run('require-slots-as-functions', rule, {
valid: [
{
filename: 'test.vue',
code: `
<script>
export default {
render (h) {
var children = this.$slots.default()
var children = this.$slots.default && this.$slots.default()
return h('div', this.$slots.default)
}
}
</script>
`
},
{
filename: 'test.vue',
code: `
<script>
export default {
render (h) {
var children = unknown.$slots.default
var children = unknown.$slots.default.filter(test)
return h('div', [...children])
}
}
</script>
`
}
],

invalid: [
{
filename: 'test.vue',
code: `
<script>
export default {
render (h) {
var children = this.$slots.default
var children = this.$slots.default.filter(test)
return h('div', [...children])
}
}
</script>
`,
errors: [
{
message: 'Property in `$slots` should be used as function.',
line: 5,
column: 38,
endLine: 5,
endColumn: 45
},
{
message: 'Property in `$slots` should be used as function.',
line: 6,
column: 38,
endLine: 6,
endColumn: 45
}
]
},

{
filename: 'test.vue',
code: `
<script>
export default {
render (h) {
let children
const [node] = this.$slots.foo
const bar = [this.$slots[foo]]
children = this.$slots.foo
return h('div', children.filter(test))
}
}
</script>
`,
errors: [
'Property in `$slots` should be used as function.',
'Property in `$slots` should be used as function.',
'Property in `$slots` should be used as function.'
]
}
]
})

0 comments on commit ffe9ece

Please sign in to comment.