diff --git a/docs/rules/require-emit-validator.md b/docs/rules/require-emit-validator.md new file mode 100644 index 000000000..9ca6df169 --- /dev/null +++ b/docs/rules/require-emit-validator.md @@ -0,0 +1,61 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/require-emit-validator +description: require type definitions in emits +--- +# vue/require-emit-validator + +> require type definitions in emits + +- :exclamation: ***This rule has not been released yet.*** +- :gear: This rule is included in . + +## :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-validator.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-emit-validator.js) diff --git a/lib/index.js b/lib/index.js index d38cff322..1379e284e 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-validator': require('./rules/require-emit-validator'), '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-validator.js b/lib/rules/require-emit-validator.js new file mode 100644 index 000000000..c85ee42ad --- /dev/null +++ b/lib/rules/require-emit-validator.js @@ -0,0 +1,90 @@ +/** + * @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: [], + url: 'https://eslint.vuejs.org/rules/require-emit-validator.html' + }, + fixable: null, + messages: { + 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: [] + }, + /** @param {RuleContext} context */ + create(context) { + // ---------------------------------------------------------------------- + // Helpers + // ---------------------------------------------------------------------- + + /** + * @param {ComponentArrayEmit|ComponentObjectEmit} emit + */ + function checker({ value, node, emitName }) { + const hasType = + !!value && + (value.type === 'ArrowFunctionExpression' || + value.type === 'FunctionExpression' || + // validator may from outer scope + value.type === 'Identifier') + + if (!hasType) { + const name = + emitName || + (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', + data: { name } + }) + } + } + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + return utils.executeOnVue(context, (obj) => { + utils.getComponentEmits(obj).forEach(checker) + }) + } +} diff --git a/tests/lib/rules/require-emit-validator.js b/tests/lib/rules/require-emit-validator.js new file mode 100644 index 000000000..6c1170e95 --- /dev/null +++ b/tests/lib/rules/require-emit-validator.js @@ -0,0 +1,330 @@ +/** + * @fileoverview Emit definitions should be detailed + * @author Pig Fang + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/require-emit-validator') + +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester() +ruleTester.run('require-emit-validator', 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') + }, + { + 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' } + } + ], + + 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: '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: 'skipped', + data: { name: 'foo' }, + line: 4, + suggestions: [ + { + messageId: 'emptyValidation', + output: ` + export default { + emits: { + foo: () => true, + bar: (payload) => {} + } + }` + } + ] + } + ] + }, + { + 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 + } + ] + } + ] +})