From 0bb016bd4bbeaa1e624fc8582b18b044728cc476 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Wed, 5 May 2021 11:24:48 +0800 Subject: [PATCH 1/5] Add `vue/require-emit-types` rule --- docs/rules/README.md | 1 + docs/rules/require-emit-types.md | 61 ++++++ lib/configs/vue3-strongly-recommended.js | 1 + lib/index.js | 1 + lib/rules/require-emit-types.js | 69 ++++++ tests/lib/rules/require-emit-types.js | 266 +++++++++++++++++++++++ 6 files changed, 399 insertions(+) create mode 100644 docs/rules/require-emit-types.md create mode 100644 lib/rules/require-emit-types.js create mode 100644 tests/lib/rules/require-emit-types.js diff --git a/docs/rules/README.md b/docs/rules/README.md index e34e4ca2b..79d18b056 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -132,6 +132,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | [vue/one-component-per-file](./one-component-per-file.md) | enforce that each component should be in its own file | | | [vue/prop-name-casing](./prop-name-casing.md) | enforce specific casing for the Prop name in Vue components | | | [vue/require-default-prop](./require-default-prop.md) | require default value for props | | +| [vue/require-emit-types](./require-emit-types.md) | require type definitions in emits | | | [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` | | | [vue/require-prop-types](./require-prop-types.md) | require type definitions in props | | | [vue/singleline-html-element-content-newline](./singleline-html-element-content-newline.md) | require a line break before and after the contents of a singleline element | :wrench: | diff --git a/docs/rules/require-emit-types.md b/docs/rules/require-emit-types.md new file mode 100644 index 000000000..882abaf7d --- /dev/null +++ b/docs/rules/require-emit-types.md @@ -0,0 +1,61 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/require-emit-types +description: require type definitions in emits +--- +# vue/require-emit-types + +> require type definitions in emits + +- :exclamation: ***This rule has not been released yet.*** +- :gear: This rule is included in `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`. + +## :book: Rule Details + +This rule enforces that a `emits` statement contains type definition. + +Declaring `emits` with types can bring better maintenance. +Even if using with TypeScript, this can provide better type inference when annotating parameters with types. + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :books: Further Reading + +- [API Reference](https://v3.vuejs.org/api/options-data.html#emits) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-emit-types.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-emit-types.js) diff --git a/lib/configs/vue3-strongly-recommended.js b/lib/configs/vue3-strongly-recommended.js index 9adfcd7c7..94e7c3761 100644 --- a/lib/configs/vue3-strongly-recommended.js +++ b/lib/configs/vue3-strongly-recommended.js @@ -23,6 +23,7 @@ module.exports = { 'vue/one-component-per-file': 'warn', 'vue/prop-name-casing': 'warn', 'vue/require-default-prop': 'warn', + 'vue/require-emit-types': 'warn', 'vue/require-explicit-emits': 'warn', 'vue/require-prop-types': 'warn', 'vue/singleline-html-element-content-newline': 'warn', diff --git a/lib/index.js b/lib/index.js index d38cff322..7a465e1e1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -140,6 +140,7 @@ module.exports = { 'require-component-is': require('./rules/require-component-is'), 'require-default-prop': require('./rules/require-default-prop'), 'require-direct-export': require('./rules/require-direct-export'), + 'require-emit-types': require('./rules/require-emit-types'), 'require-explicit-emits': require('./rules/require-explicit-emits'), 'require-name-property': require('./rules/require-name-property'), 'require-prop-type-constructor': require('./rules/require-prop-type-constructor'), diff --git a/lib/rules/require-emit-types.js b/lib/rules/require-emit-types.js new file mode 100644 index 000000000..710c8215e --- /dev/null +++ b/lib/rules/require-emit-types.js @@ -0,0 +1,69 @@ +/** + * @fileoverview Emit definitions should be detailed + * @author Pig Fang + */ +'use strict' + +const utils = require('../utils') + +/** + * @typedef {import('../utils').ComponentArrayEmit} ComponentArrayEmit + * @typedef {import('../utils').ComponentObjectEmit} ComponentObjectEmit + */ + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'require type definitions in emits', + categories: ['vue3-strongly-recommended'], + url: 'https://eslint.vuejs.org/rules/require-emit-types.html' + }, + fixable: null, + messages: { + missing: 'Emit "{{name}}" should define at least its type.' + }, + schema: [] + }, + /** @param {RuleContext} context */ + create(context) { + // ---------------------------------------------------------------------- + // Helpers + // ---------------------------------------------------------------------- + + /** + * @param {ComponentArrayEmit|ComponentObjectEmit} emit + */ + function checker({ value, node, emitName }) { + const hasType = + !!value && + (value.type === 'ArrowFunctionExpression' || + value.type === 'FunctionExpression') + + if (!hasType) { + const name = + emitName || + (node.type === 'Identifier' && node.name) || + 'Unknown emit' + + context.report({ + node, + messageId: 'missing', + data: { name } + }) + } + } + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + return utils.executeOnVue(context, (obj) => { + utils.getComponentEmits(obj).forEach(checker) + }) + } +} diff --git a/tests/lib/rules/require-emit-types.js b/tests/lib/rules/require-emit-types.js new file mode 100644 index 000000000..0d3cbc70e --- /dev/null +++ b/tests/lib/rules/require-emit-types.js @@ -0,0 +1,266 @@ +/** + * @fileoverview Emit definitions should be detailed + * @author Pig Fang + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/require-emit-types') + +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester() +ruleTester.run('require-emit-types', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + export default { + ...foo, + emits: { + ...test(), + foo: (payload) => typeof payload === 'object' + } + } + `, + parserOptions: { ecmaVersion: 2018, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + export default { + emits: { + foo: (payload) => typeof payload === 'object' + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + export default { + emits: { + foo(payload) { + return typeof payload === 'object' + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + export default { + emits: { + foo: () => {} + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + export default { + emits: { + foo() {} + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + export default { + emits + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + export default { + emits: externalEmits + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + export default { + emits: [] + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + export default { + emits: {} + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + export default defineComponent({ + emits: { + foo: (payload: string | number) => true, + } + }) + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + parser: require.resolve('@typescript-eslint/parser') + }, + { + filename: 'test.vue', + code: ` + export default defineComponent({ + emits: { + foo(payload: string | number) { + return true + }, + }, + }) + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + parser: require.resolve('@typescript-eslint/parser') + } + ], + + invalid: [ + { + filename: 'test.vue', + code: ` + export default { + emits: ['foo', bar, \`baz\`, foo()] + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [ + { + messageId: 'missing', + data: { name: 'foo' }, + line: 3 + }, + { + messageId: 'missing', + data: { name: 'bar' }, + line: 3 + }, + { + messageId: 'missing', + data: { name: 'baz' }, + line: 3 + }, + { + messageId: 'missing', + data: { name: 'Unknown emit' }, + line: 3 + } + ] + }, + { + filename: 'test.js', + code: ` + new Vue({ + emits: ['foo', bar, \`baz\`, foo()] + }) + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [ + { + messageId: 'missing', + data: { name: 'foo' }, + line: 3 + }, + { + messageId: 'missing', + data: { name: 'bar' }, + line: 3 + }, + { + messageId: 'missing', + data: { name: 'baz' }, + line: 3 + }, + { + messageId: 'missing', + data: { name: 'Unknown emit' }, + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + export default { + emits: { + foo: null + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [ + { + messageId: 'missing', + data: { name: 'foo' }, + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + export default { + emits: { + foo: { + type: String + } + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [ + { + messageId: 'missing', + data: { name: 'foo' }, + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + export default defineComponent({ + emits: { + foo: {} as ((payload: string) => boolean) + } + }); + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + parser: require.resolve('@typescript-eslint/parser'), + errors: [ + { + messageId: 'missing', + data: { name: 'foo' }, + line: 4 + } + ] + } + ] +}) From a7613a067707373be2b57830e4aa53133dbbc518 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 7 May 2021 14:40:05 +0800 Subject: [PATCH 2/5] remove from categories --- lib/rules/require-emit-types.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/require-emit-types.js b/lib/rules/require-emit-types.js index 710c8215e..19fa9458b 100644 --- a/lib/rules/require-emit-types.js +++ b/lib/rules/require-emit-types.js @@ -20,7 +20,7 @@ module.exports = { type: 'suggestion', docs: { description: 'require type definitions in emits', - categories: ['vue3-strongly-recommended'], + categories: [], url: 'https://eslint.vuejs.org/rules/require-emit-types.html' }, fixable: null, From f41e86a023d3121842d42a2a05dadbc8243982c6 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 7 May 2021 15:17:50 +0800 Subject: [PATCH 3/5] allow identifiers --- lib/rules/require-emit-types.js | 4 +++- tests/lib/rules/require-emit-types.js | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/rules/require-emit-types.js b/lib/rules/require-emit-types.js index 19fa9458b..8994523f1 100644 --- a/lib/rules/require-emit-types.js +++ b/lib/rules/require-emit-types.js @@ -42,7 +42,9 @@ module.exports = { const hasType = !!value && (value.type === 'ArrowFunctionExpression' || - value.type === 'FunctionExpression') + value.type === 'FunctionExpression' || + // validator may from outer scope + value.type === 'Identifier') if (!hasType) { const name = diff --git a/tests/lib/rules/require-emit-types.js b/tests/lib/rules/require-emit-types.js index 0d3cbc70e..88af151b9 100644 --- a/tests/lib/rules/require-emit-types.js +++ b/tests/lib/rules/require-emit-types.js @@ -139,6 +139,30 @@ ruleTester.run('require-emit-types', rule, { `, parserOptions: { ecmaVersion: 6, sourceType: 'module' }, parser: require.resolve('@typescript-eslint/parser') + }, + { + filename: 'test.vue', + code: ` + function foo () {} + export default { + emits: { + foo + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } + }, + { + filename: 'test.vue', + code: ` + import { isNumber } from './mod' + export default { + emits: { + foo: isNumber + } + } + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' } } ], From fec232f98a976451ee34768dcd420aab45960dc1 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 7 May 2021 17:50:47 +0800 Subject: [PATCH 4/5] add suggestion for skipped validation --- lib/rules/require-emit-types.js | 21 +++++++++++- tests/lib/rules/require-emit-types.js | 46 +++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/lib/rules/require-emit-types.js b/lib/rules/require-emit-types.js index 8994523f1..448bb0bc3 100644 --- a/lib/rules/require-emit-types.js +++ b/lib/rules/require-emit-types.js @@ -25,7 +25,10 @@ module.exports = { }, fixable: null, messages: { - missing: 'Emit "{{name}}" should define at least its type.' + missing: 'Emit "{{name}}" should define at least its validator function.', + skipped: + 'Emit "{{name}}" should not skip validation, or you may define a validator function with no parameters.', + emptyValidation: 'Replace with a validator function with no parameters.' }, schema: [] }, @@ -52,6 +55,22 @@ module.exports = { (node.type === 'Identifier' && node.name) || 'Unknown emit' + if (value && value.type === 'Literal' && value.value === null) { + context.report({ + node, + messageId: 'skipped', + data: { name }, + suggest: [ + { + messageId: 'emptyValidation', + fix: (fixer) => fixer.replaceText(value, '() => true') + } + ] + }) + + return + } + context.report({ node, messageId: 'missing', diff --git a/tests/lib/rules/require-emit-types.js b/tests/lib/rules/require-emit-types.js index 88af151b9..e172342f5 100644 --- a/tests/lib/rules/require-emit-types.js +++ b/tests/lib/rules/require-emit-types.js @@ -236,14 +236,54 @@ ruleTester.run('require-emit-types', rule, { emits: { foo: null } + }`, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [ + { + messageId: 'skipped', + data: { name: 'foo' }, + line: 4, + suggestions: [ + { + messageId: 'emptyValidation', + output: ` + export default { + emits: { + foo: () => true + } + }` + } + ] } - `, + ] + }, + { + filename: 'test.vue', + code: ` + export default { + emits: { + foo: null, + bar: (payload) => {} + } + }`, parserOptions: { ecmaVersion: 6, sourceType: 'module' }, errors: [ { - messageId: 'missing', + messageId: 'skipped', data: { name: 'foo' }, - line: 4 + line: 4, + suggestions: [ + { + messageId: 'emptyValidation', + output: ` + export default { + emits: { + foo: () => true, + bar: (payload) => {} + } + }` + } + ] } ] }, From 4543c1a8b0530ea32fd627d03484a896f862f923 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 7 May 2021 17:54:40 +0800 Subject: [PATCH 5/5] rename rule --- docs/rules/README.md | 1 - ...quire-emit-types.md => require-emit-validator.md} | 12 ++++++------ lib/configs/vue3-strongly-recommended.js | 1 - lib/index.js | 2 +- ...quire-emit-types.js => require-emit-validator.js} | 2 +- ...quire-emit-types.js => require-emit-validator.js} | 4 ++-- 6 files changed, 10 insertions(+), 12 deletions(-) rename docs/rules/{require-emit-types.md => require-emit-validator.md} (78%) rename lib/rules/{require-emit-types.js => require-emit-validator.js} (97%) rename tests/lib/rules/{require-emit-types.js => require-emit-validator.js} (98%) diff --git a/docs/rules/README.md b/docs/rules/README.md index 79d18b056..e34e4ca2b 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -132,7 +132,6 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | [vue/one-component-per-file](./one-component-per-file.md) | enforce that each component should be in its own file | | | [vue/prop-name-casing](./prop-name-casing.md) | enforce specific casing for the Prop name in Vue components | | | [vue/require-default-prop](./require-default-prop.md) | require default value for props | | -| [vue/require-emit-types](./require-emit-types.md) | require type definitions in emits | | | [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` | | | [vue/require-prop-types](./require-prop-types.md) | require type definitions in props | | | [vue/singleline-html-element-content-newline](./singleline-html-element-content-newline.md) | require a line break before and after the contents of a singleline element | :wrench: | diff --git a/docs/rules/require-emit-types.md b/docs/rules/require-emit-validator.md similarity index 78% rename from docs/rules/require-emit-types.md rename to docs/rules/require-emit-validator.md index 882abaf7d..9ca6df169 100644 --- a/docs/rules/require-emit-types.md +++ b/docs/rules/require-emit-validator.md @@ -1,15 +1,15 @@ --- pageClass: rule-details sidebarDepth: 0 -title: vue/require-emit-types +title: vue/require-emit-validator description: require type definitions in emits --- -# vue/require-emit-types +# vue/require-emit-validator > require type definitions in emits - :exclamation: ***This rule has not been released yet.*** -- :gear: This rule is included in `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`. +- :gear: This rule is included in . ## :book: Rule Details @@ -18,7 +18,7 @@ This rule enforces that a `emits` statement contains type definition. Declaring `emits` with types can bring better maintenance. Even if using with TypeScript, this can provide better type inference when annotating parameters with types. - + ```vue