Skip to content

Add vue/require-slots-as-functions rule. #1178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,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
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.'
]
}
]
})