From ffe9ecea8895fbfa2fb1566898f8a5c828107c83 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Fri, 5 Jun 2020 11:14:24 +0900 Subject: [PATCH] Add `vue/require-slots-as-functions` rule. (#1178) * Add `vue/require-slots-as-functions` rule. * Update require-slots-as-functions.js --- docs/rules/README.md | 1 + docs/rules/require-slots-as-functions.md | 48 +++++++ lib/configs/vue3-essential.js | 1 + lib/index.js | 1 + lib/rules/require-slots-as-functions.js | 124 ++++++++++++++++++ tests/lib/rules/require-slots-as-functions.js | 115 ++++++++++++++++ 6 files changed, 290 insertions(+) create mode 100644 docs/rules/require-slots-as-functions.md create mode 100644 lib/rules/require-slots-as-functions.js create mode 100644 tests/lib/rules/require-slots-as-functions.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 40548ec13..dd5718b08 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -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 `` 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 `` | | | [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 | | diff --git a/docs/rules/require-slots-as-functions.md b/docs/rules/require-slots-as-functions.md new file mode 100644 index 000000000..8b4c01f91 --- /dev/null +++ b/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. + + + +```vue + +``` + + + +## :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) diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js index 0e3e02f74..b802623da 100644 --- a/lib/configs/vue3-essential.js +++ b/lib/configs/vue3-essential.js @@ -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', diff --git a/lib/index.js b/lib/index.js index 6eb4dc340..f41be8ba7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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'), diff --git a/lib/rules/require-slots-as-functions.js b/lib/rules/require-slots-as-functions.js new file mode 100644 index 000000000..07493ddbb --- /dev/null +++ b/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) + } + }) + } +} diff --git a/tests/lib/rules/require-slots-as-functions.js b/tests/lib/rules/require-slots-as-functions.js new file mode 100644 index 000000000..9d79a1ec7 --- /dev/null +++ b/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: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + } + ], + + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + 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: ` + + `, + 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.' + ] + } + ] +})