Skip to content
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

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

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/rules/README.md
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
@@ -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 @@ -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
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
@@ -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.'
]
}
]
})