diff --git a/docs/rules/index.md b/docs/rules/index.md index 6e4c2fe68..1367f797e 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -244,6 +244,7 @@ For example: | [vue/no-restricted-props](./no-restricted-props.md) | disallow specific props | :bulb: | :hammer: | | [vue/no-restricted-static-attribute](./no-restricted-static-attribute.md) | disallow specific attribute | | :hammer: | | [vue/no-restricted-v-bind](./no-restricted-v-bind.md) | disallow specific argument in `v-bind` | | :hammer: | +| [vue/no-restricted-v-on](./no-restricted-v-on.md) | disallow specific argument in `v-on` | | :hammer: | | [vue/no-root-v-if](./no-root-v-if.md) | disallow `v-if` directives on root element | | :hammer: | | [vue/no-setup-props-reactivity-loss](./no-setup-props-reactivity-loss.md) | disallow usages that lose the reactivity of `props` passed to `setup` | | :hammer: | | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | | :hammer: | diff --git a/docs/rules/no-restricted-static-attribute.md b/docs/rules/no-restricted-static-attribute.md index 428590506..a0534f563 100644 --- a/docs/rules/no-restricted-static-attribute.md +++ b/docs/rules/no-restricted-static-attribute.md @@ -5,6 +5,7 @@ title: vue/no-restricted-static-attribute description: disallow specific attribute since: v7.0.0 --- + # vue/no-restricted-static-attribute > disallow specific attribute @@ -39,7 +40,8 @@ Alternatively, the rule also accepts objects. ```json { - "vue/no-restricted-static-attribute": ["error", + "vue/no-restricted-static-attribute": [ + "error", { "key": "stlye", "message": "Using \"stlye\" is not allowed. Use \"style\" instead." @@ -95,8 +97,10 @@ The following properties can be specified for the object. ## :couple: Related Rules - [vue/no-restricted-v-bind] +- [vue/no-restricted-v-on] [vue/no-restricted-v-bind]: ./no-restricted-v-bind.md +[vue/no-restricted-v-on]: ./no-restricted-v-on.md ## :rocket: Version diff --git a/docs/rules/no-restricted-v-bind.md b/docs/rules/no-restricted-v-bind.md index 8b9332f6e..4c1495f4e 100644 --- a/docs/rules/no-restricted-v-bind.md +++ b/docs/rules/no-restricted-v-bind.md @@ -5,6 +5,7 @@ title: vue/no-restricted-v-bind description: disallow specific argument in `v-bind` since: v7.0.0 --- + # vue/no-restricted-v-bind > disallow specific argument in `v-bind` @@ -53,7 +54,8 @@ Alternatively, the rule also accepts objects. ```json { - "vue/no-restricted-v-bind": ["error", + "vue/no-restricted-v-bind": [ + "error", { "argument": "/^v-/", "message": "Using `:v-xxx` is not allowed. Instead, remove `:` and use it as directive." @@ -112,8 +114,10 @@ The following properties can be specified for the object. ## :couple: Related Rules - [vue/no-restricted-static-attribute] +- [vue/no-restricted-v-on] [vue/no-restricted-static-attribute]: ./no-restricted-static-attribute.md +[vue/no-restricted-v-on]: ./no-restricted-v-on.md ## :rocket: Version diff --git a/docs/rules/no-restricted-v-on.md b/docs/rules/no-restricted-v-on.md new file mode 100644 index 000000000..1b73bd57d --- /dev/null +++ b/docs/rules/no-restricted-v-on.md @@ -0,0 +1,111 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-restricted-v-on +description: disallow specific argument in `v-on` +--- +# vue/no-restricted-v-on + +> disallow specific argument in `v-on` + +- :exclamation: ***This rule has not been released yet.*** + +## :book: Rule Details + +This rule allows you to specify `v-on` argument names that you don't want to use in your application. + +## :wrench: Options + +This rule takes a list of strings, where each string is a argument name or pattern to be restricted: + +```json +{ + "vue/no-restricted-v-on": ["error", "foo", "/^bar/"] +} +``` + + + +```vue + + + + + + + + + +``` + + + +Alternatively, the rule also accepts objects. + +```json +{ + "vue/no-restricted-v-on": [ + "error", + { + "argument": "foo", + "message": "Use \"v-on:x\" instead." + }, + { + "argument": "bar", + "message": "\"@bar\" is deprecated." + } + ] +} +``` + +The following properties can be specified for the object. + +- `argument` ... Specify the argument name or pattern or `null`. If `null` is specified, it matches `v-on=`. +- `modifiers` ... Specifies an array of the modifier names. If specified, it will only be reported if the specified modifier is used. +- `element` ... Specify the element name or pattern. If specified, it will only be reported if used on the specified element. +- `message` ... Specify an optional custom message. + +### `{ "argument": "foo", "modifiers": ["prevent"] }` + + + +```vue + + + + + + + +``` + + + +### `{ "argument": "foo", "element": "MyButton" }` + + + +```vue + + + + + + + +``` + + + +## :couple: Related Rules + +- [vue/no-restricted-static-attribute] +- [vue/no-restricted-v-bind] + +[vue/no-restricted-static-attribute]: ./no-restricted-static-attribute.md +[vue/no-restricted-v-bind]: ./no-restricted-v-bind.md + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-v-on.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-v-on.js) diff --git a/lib/index.js b/lib/index.js index 3eb5208ce..d9022cabf 100644 --- a/lib/index.js +++ b/lib/index.js @@ -131,6 +131,7 @@ module.exports = { 'no-restricted-static-attribute': require('./rules/no-restricted-static-attribute'), 'no-restricted-syntax': require('./rules/no-restricted-syntax'), 'no-restricted-v-bind': require('./rules/no-restricted-v-bind'), + 'no-restricted-v-on': require('./rules/no-restricted-v-on'), 'no-root-v-if': require('./rules/no-root-v-if'), 'no-setup-props-destructure': require('./rules/no-setup-props-destructure'), 'no-setup-props-reactivity-loss': require('./rules/no-setup-props-reactivity-loss'), diff --git a/lib/rules/no-restricted-v-on.js b/lib/rules/no-restricted-v-on.js new file mode 100644 index 000000000..2379df349 --- /dev/null +++ b/lib/rules/no-restricted-v-on.js @@ -0,0 +1,184 @@ +/** + * @author Kamogelo Moalusi + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') +const regexp = require('../utils/regexp') + +/** + * @typedef {object} ParsedOption + * @property { (key: VDirectiveKey) => boolean } test + * @property {string[]} [modifiers] + * @property {boolean} [useElement] + * @property {string} [message] + */ + +/** + * @param {string} str + * @returns {(str: string) => boolean} + */ +function buildMatcher(str) { + if (regexp.isRegExp(str)) { + const re = regexp.toRegExp(str) + return (s) => { + re.lastIndex = 0 + return re.test(s) + } + } + return (s) => s === str +} + +/** + * @param {any} option + * @returns {ParsedOption} + */ +function parseOption(option) { + if (typeof option === 'string') { + const matcher = buildMatcher(option) + return { + test(key) { + return Boolean( + key.argument && + key.argument.type === 'VIdentifier' && + matcher(key.argument.rawName) + ) + } + } + } + if (option === null) { + return { + test(key) { + return key.argument === null + } + } + } + const parsed = parseOption(option.argument) + + if (option.modifiers) { + const argTest = parsed.test + parsed.test = (key) => { + if (!argTest(key)) { + return false + } + return /** @type {string[]} */ (option.modifiers).every((modName) => + key.modifiers.some((mid) => mid.name === modName) + ) + } + parsed.modifiers = option.modifiers + } + if (option.element) { + const argTest = parsed.test + const tagMatcher = buildMatcher(option.element) + parsed.test = (key) => { + if (!argTest(key)) { + return false + } + return tagMatcher(key.parent.parent.parent.rawName) + } + parsed.useElement = true + } + parsed.message = option.message + return parsed +} + +/** + * @param {VDirectiveKey} key + * @param {ParsedOption} option + */ +function defaultMessage(key, option) { + const von = key.name.rawName === '@' ? '' : 'v-on' + const arg = + key.argument != null && key.argument.type === 'VIdentifier' + ? `${key.name.rawName === '@' ? '@' : ':'}${key.argument.rawName}` + : '' + const mod = + option.modifiers != null && option.modifiers.length > 0 + ? `.${option.modifiers.join('.')}` + : '' + let element = 'element' + if (option.useElement) { + element = `<${key.parent.parent.parent.rawName}>` + } + return `Using \`${von + arg + mod}\` is not allowed on this ${element}.` +} + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow specific argument in `v-on`', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/no-restricted-v-on.html' + }, + fixable: null, + schema: { + type: 'array', + items: { + oneOf: [ + { type: ['string', 'null'] }, + { + type: 'object', + properties: { + argument: { type: ['string', 'null'] }, + element: { type: 'string' }, + message: { type: 'string', minLength: 1 }, + modifiers: { + type: 'array', + items: { + type: 'string', + enum: [ + 'prevent', + 'stop', + 'capture', + 'self', + 'once', + 'passive' + ] + }, + uniqueItems: true, + minItems: 1 + } + }, + required: ['argument'], + additionalProperties: false + } + ] + }, + uniqueItems: true + }, + messages: { + // eslint-disable-next-line eslint-plugin/report-message-format + restrictedVOn: '{{message}}' + } + }, + + /** @param {RuleContext} context */ + create(context) { + if (context.options.length === 0) { + return {} + } + /** @type {ParsedOption[]} */ + const options = context.options.map(parseOption) + + return utils.defineTemplateBodyVisitor(context, { + /** + * @param {VDirectiveKey} node + */ + "VAttribute[directive=true][key.name.name='on'] > VDirectiveKey"(node) { + for (const option of options) { + if (option.test(node)) { + const message = option.message || defaultMessage(node, option) + context.report({ + node, + messageId: 'restrictedVOn', + data: { message } + }) + return + } + } + } + }) + } +} diff --git a/tests/lib/rules/no-restricted-v-on.js b/tests/lib/rules/no-restricted-v-on.js new file mode 100644 index 000000000..1d73d5c15 --- /dev/null +++ b/tests/lib/rules/no-restricted-v-on.js @@ -0,0 +1,173 @@ +/** + * @author Kamogelo Moalusi + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-restricted-v-on') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('no-restricted-v-on', rule, { + valid: [ + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '', + options: ['foo'] + }, + { + filename: 'test.vue', + code: '', + options: ['foo'] + }, + { + filename: 'test.vue', + code: '', + options: ['foo'] + }, + { + filename: 'test.vue', + code: '', + options: [{ argument: 'foo', modifiers: ['prevent'] }] + }, + { + filename: 'test.vue', + code: '', + options: [{ argument: 'foo', element: 'input' }] + } + ], + invalid: [ + { + filename: 'test.vue', + code: '', + options: ['test'], + errors: [ + { + message: 'Using `@test` is not allowed on this element.', + line: 1, + column: 16 + } + ] + }, + { + filename: 'test.vue', + code: '', + options: ['foo'], + errors: ['Using `@foo` is not allowed on this element.'] + }, + { + filename: 'test.vue', + code: '', + options: ['foo', 'bar'], + errors: [ + 'Using `@foo` is not allowed on this element.', + 'Using `@bar` is not allowed on this element.' + ] + }, + { + filename: 'test.vue', + code: '', + options: [{ argument: '/^(foo|bar)$/' }], + errors: [ + 'Using `@foo` is not allowed on this element.', + 'Using `@bar` is not allowed on this element.' + ] + }, + { + filename: 'test.vue', + code: '', + options: [{ argument: 'foo', modifiers: ['once'] }], + errors: ['Using `@foo.once` is not allowed on this element.'] + }, + { + filename: 'test.vue', + code: '', + options: ['/^v-/', { argument: 'foo', modifiers: ['prevent'] }, null], + errors: [ + 'Using `@v-on` is not allowed on this element.', + 'Using `@foo.prevent` is not allowed on this element.', + 'Using `v-on` is not allowed on this element.' + ] + }, + { + filename: 'test.vue', + code: '', + options: [null], + errors: ['Using `v-on` is not allowed on this element.'] + }, + { + filename: 'test.vue', + code: ` + + + + `, + options: ['/^v-/', { argument: 'foo', element: `/^My/` }], + errors: [ + 'Using `@v-on` is not allowed on this element.', + 'Using `@foo` is not allowed on this .' + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + options: ['/^f/', { argument: 'foo' }], + errors: ['Using `@foo` is not allowed on this element.'] + }, + { + filename: 'test.vue', + code: ` + + + `, + options: [{ argument: 'foo', message: 'foo' }], + errors: ['foo'] + }, + { + filename: 'test.vue', + code: '', + options: [{ argument: 'foo', element: 'div' }], + errors: [ + { + message: 'Using `@foo` is not allowed on this .' + } + ] + }, + { + filename: 'test.vue', + code: '', + options: ['foo'], + errors: [ + { + message: 'Using `v-on:foo` is not allowed on this element.' + } + ] + } + ] +})