From c7efaef4dead80ce41fb28f715d27778276ad3fb Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Mon, 5 Jul 2021 23:36:48 +0900 Subject: [PATCH] Add `vue/valid-define-emits` rule --- docs/rules/README.md | 1 + docs/rules/valid-define-emits.md | 133 ++++++++++++++++++++++ lib/index.js | 1 + lib/rules/valid-define-emits.js | 144 ++++++++++++++++++++++++ tests/lib/rules/valid-define-emits.js | 156 ++++++++++++++++++++++++++ 5 files changed, 435 insertions(+) create mode 100644 docs/rules/valid-define-emits.md create mode 100644 lib/rules/valid-define-emits.js create mode 100644 tests/lib/rules/valid-define-emits.js diff --git a/docs/rules/README.md b/docs/rules/README.md index ae82c5d03..d3b3a3819 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -331,6 +331,7 @@ For example: | [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: | | [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: | | [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: | +| [vue/valid-define-emits](./valid-define-emits.md) | enforce valid `defineEmits` compiler macro | | | [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro | | | [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: | diff --git a/docs/rules/valid-define-emits.md b/docs/rules/valid-define-emits.md new file mode 100644 index 000000000..cf6ea6c15 --- /dev/null +++ b/docs/rules/valid-define-emits.md @@ -0,0 +1,133 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/valid-define-emits +description: enforce valid `defineEmits` compiler macro +--- +# vue/valid-define-emits + +> enforce valid `defineEmits` compiler macro + +- :exclamation: ***This rule has not been released yet.*** + +This rule checks whether `defineEmits` compiler macro is valid. + +## :book: Rule Details + +This rule reports `defineEmits` compiler macros in the following cases: + +- `defineEmits` are referencing locally declared variables. +- `defineEmits` has both a literal type and an argument. e.g. `defineEmits<(e: 'foo')=>void>(['bar'])` +- `defineEmits` has been called multiple times. +- Custom events are defined in both `defineEmits` and `export default {}`. +- Custom events are not defined in either `defineEmits` or `export default {}`. + + + +```vue + +``` + + + + + +```vue + +``` + + + +```vue + +``` + + + +```vue + + +``` + + + + + +```vue + +``` + + + +```vue + +``` + + + +```vue + +``` + + + + + +```vue + + +``` + + + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-define-emits.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-define-emits.js) diff --git a/lib/index.js b/lib/index.js index 0fcef5d94..7533e1673 100644 --- a/lib/index.js +++ b/lib/index.js @@ -171,6 +171,7 @@ module.exports = { 'v-on-function-call': require('./rules/v-on-function-call'), 'v-on-style': require('./rules/v-on-style'), 'v-slot-style': require('./rules/v-slot-style'), + 'valid-define-emits': require('./rules/valid-define-emits'), 'valid-define-props': require('./rules/valid-define-props'), 'valid-next-tick': require('./rules/valid-next-tick'), 'valid-template-root': require('./rules/valid-template-root'), diff --git a/lib/rules/valid-define-emits.js b/lib/rules/valid-define-emits.js new file mode 100644 index 000000000..6b79e83cc --- /dev/null +++ b/lib/rules/valid-define-emits.js @@ -0,0 +1,144 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const { findVariable } = require('eslint-utils') +const utils = require('../utils') + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'enforce valid `defineEmits` compiler macro', + // TODO Switch in the major version. + // categories: ['vue3-essential'], + categories: undefined, + url: 'https://eslint.vuejs.org/rules/valid-define-emits.html' + }, + fixable: null, + schema: [], + messages: { + hasTypeAndArg: '`defineEmits` has both a type-only emit and an argument.', + referencingLocally: + '`defineEmits` are referencing locally declared variables.', + multiple: '`defineEmits` has been called multiple times.', + notDefined: 'Custom events are not defined.', + definedInBoth: + 'Custom events are defined in both `defineEmits` and `export default {}`.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const scriptSetup = utils.getScriptSetupElement(context) + if (!scriptSetup) { + return {} + } + + /** @type {Set} */ + const emitsDefExpressions = new Set() + let hasDefaultExport = false + /** @type {CallExpression[]} */ + const defineEmitsNodes = [] + /** @type {CallExpression | null} */ + let emptyDefineEmits = null + + return utils.compositingVisitors( + utils.defineScriptSetupVisitor(context, { + onDefineEmitsEnter(node) { + defineEmitsNodes.push(node) + + if (node.arguments.length >= 1) { + if (node.typeParameters && node.typeParameters.params.length >= 1) { + // `defineEmits` has both a literal type and an argument. + context.report({ + node, + messageId: 'hasTypeAndArg' + }) + return + } + + emitsDefExpressions.add(node.arguments[0]) + } else { + if ( + !node.typeParameters || + node.typeParameters.params.length === 0 + ) { + emptyDefineEmits = node + } + } + }, + Identifier(node) { + for (const def of emitsDefExpressions) { + if (utils.inRange(def.range, node)) { + const variable = findVariable(context.getScope(), node) + if ( + variable && + variable.references.some((ref) => ref.identifier === node) + ) { + if ( + variable.defs.length && + variable.defs.every((def) => + utils.inRange(scriptSetup.range, def.name) + ) + ) { + //`defineEmits` are referencing locally declared variables. + context.report({ + node, + messageId: 'referencingLocally' + }) + } + } + } + } + } + }), + utils.defineVueVisitor(context, { + onVueObjectEnter(node, { type }) { + if (type !== 'export' || utils.inRange(scriptSetup.range, node)) { + return + } + + hasDefaultExport = Boolean(utils.findProperty(node, 'emits')) + } + }), + { + 'Program:exit'() { + if (!defineEmitsNodes.length) { + return + } + if (defineEmitsNodes.length > 1) { + // `defineEmits` has been called multiple times. + for (const node of defineEmitsNodes) { + context.report({ + node, + messageId: 'multiple' + }) + } + return + } + if (emptyDefineEmits) { + if (!hasDefaultExport) { + // Custom events are not defined. + context.report({ + node: emptyDefineEmits, + messageId: 'notDefined' + }) + } + } else { + if (hasDefaultExport) { + // Custom events are defined in both `defineEmits` and `export default {}`. + for (const node of defineEmitsNodes) { + context.report({ + node, + messageId: 'definedInBoth' + }) + } + } + } + } + } + ) + } +} diff --git a/tests/lib/rules/valid-define-emits.js b/tests/lib/rules/valid-define-emits.js new file mode 100644 index 000000000..309375e94 --- /dev/null +++ b/tests/lib/rules/valid-define-emits.js @@ -0,0 +1,156 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/valid-define-emits') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2015, sourceType: 'module' } +}) + +tester.run('valid-define-emits', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + { + filename: 'test.vue', + code: ` + + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: '`defineEmits` are referencing locally declared variables.', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { parser: require.resolve('@typescript-eslint/parser') }, + errors: [ + { + message: '`defineEmits` has both a type-only emit and an argument.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: '`defineEmits` has been called multiple times.', + line: 4 + }, + { + message: '`defineEmits` has been called multiple times.', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: + 'Custom events are defined in both `defineEmits` and `export default {}`.', + line: 9 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Custom events are not defined.', + line: 4 + } + ] + } + ] +})