From a4224cf219b5c68a48fc0ccda8a192961563803e Mon Sep 17 00:00:00 2001 From: thesheppard Date: Fri, 12 Jan 2024 19:35:30 +0200 Subject: [PATCH 01/15] docs: Add no-restricted-v-on rule --- docs/rules/no-restricted-v-on.md | 125 +++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 docs/rules/no-restricted-v-on.md diff --git a/docs/rules/no-restricted-v-on.md b/docs/rules/no-restricted-v-on.md new file mode 100644 index 000000000..67d41697f --- /dev/null +++ b/docs/rules/no-restricted-v-on.md @@ -0,0 +1,125 @@ +--- +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 wantt 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", "/^v-/", "foo", "bar"] +} +``` + + + +```vue + +``` + + + +By default, `'/^v-/'` is set. This prevents mistakes intended to be directives. + + + +```vue + +``` + + + +Alternatively, the rule also accepts objects. + +```json +{ + "vue/no-restricted-v-on": ["error", + { + "argument": "/^v-/", + "message": "Using `@v-xxx` is not allowed. Instead, remove `@` and use it as directive." + }, + { + "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-static-attribute]: ./no-restricted-static-attribute.md + +- [vue/no-restricted-v-bind] + +[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) \ No newline at end of file From 6453856879087f9cb714644a728811e524dbd0fd Mon Sep 17 00:00:00 2001 From: thesheppard Date: Fri, 12 Jan 2024 19:36:11 +0200 Subject: [PATCH 02/15] feat: Add tests + impl. of no-restricted-v-on rule --- lib/rules/no-restricted-v-on.js | 192 ++++++++++++++++++++++++++ tests/lib/rules/no-restricted-v-on.js | 169 +++++++++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 lib/rules/no-restricted-v-on.js create mode 100644 tests/lib/rules/no-restricted-v-on.js diff --git a/lib/rules/no-restricted-v-on.js b/lib/rules/no-restricted-v-on.js new file mode 100644 index 000000000..e73c1d99e --- /dev/null +++ b/lib/rules/no-restricted-v-on.js @@ -0,0 +1,192 @@ +/** + * @author Kamogelo Moalusi + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') +const regexp = require('../utils/regexp') + +const DEFAULT_OPTIONS = [ + { + argument: '/^v-/', + message: + 'Using `@v-xxx` is not allowed. Instead, remove `@` and use it as directive.' + } +] +/** + * @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) + ) + }, + modifiers: [] + } + } + if (option === null) { + return { + test(key) { + return key.argument === null + }, + modifiers: [] + } + } + 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.argument.rawName}` + : '' + const mod = + 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' }, + element: { type: 'string' }, + message: { type: 'string', minLength: 1 }, + modifiers: { + type: 'array', + items: { + type: 'string', + enum: [ + 'prevent', + 'stop', + 'capture', + 'self', + 'once', + 'passive' + ] + }, + uniqueItems: true + } + }, + required: ['argument'], + additionalProperties: false + } + ] + }, + uniqueItems: true, + minItems: 0 + }, + messages: { + // eslint-disable-next-line eslint-plugin/report-message-format + restrictedVOn: '{{message}}' + } + }, + + /** @param {RuleContext} context */ + create(context) { + /** @type {ParsedOption[]} */ + const options = ( + context.options.length === 0 ? DEFAULT_OPTIONS : context.options + ).map(parseOption) + + // const options = DEFAULT_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..8d27952ed --- /dev/null +++ b/tests/lib/rules/no-restricted-v-on.js @@ -0,0 +1,169 @@ +/** + * @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: '', + errors: [ + { + message: + 'Using `@v-xxx` is not allowed. Instead, remove `@` and use it as directive.', + line: 1, + column: 16 + } + ] + }, + { + filename: 'test.vue', + code: '', + errors: [ + { + message: + 'Using `@v-xxx` is not allowed. Instead, remove `@` and use it as directive.', + 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: ['/^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
.' + } + ] + } + ] +}) From 75a54430a36dd73f3e388c5077ecff4e9362c3f4 Mon Sep 17 00:00:00 2001 From: thesheppard Date: Fri, 12 Jan 2024 19:53:53 +0200 Subject: [PATCH 03/15] fix: linting issues --- docs/rules/index.md | 1 + docs/rules/no-restricted-v-on.md | 6 ++++-- lib/index.js | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) 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-v-on.md b/docs/rules/no-restricted-v-on.md index 67d41697f..fa19a71e8 100644 --- a/docs/rules/no-restricted-v-on.md +++ b/docs/rules/no-restricted-v-on.md @@ -54,7 +54,8 @@ Alternatively, the rule also accepts objects. ```json { - "vue/no-restricted-v-on": ["error", + "vue/no-restricted-v-on": [ + "error", { "argument": "/^v-/", "message": "Using `@v-xxx` is not allowed. Instead, remove `@` and use it as directive." @@ -91,6 +92,7 @@ The following properties can be specified for the object.
``` + ### `{ "argument": "foo", "element": "MyButton" }` @@ -122,4 +124,4 @@ The following properties can be specified for the object. ## :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) \ No newline at end of file +- [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'), From 6b736cdedb03c58fd2c63c2277c9ec2abfb07f61 Mon Sep 17 00:00:00 2001 From: KG Date: Fri, 12 Jan 2024 20:14:33 +0200 Subject: [PATCH 04/15] Update docs/rules/no-restricted-v-on.md Co-authored-by: Flo Edelmann --- docs/rules/no-restricted-v-on.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-restricted-v-on.md b/docs/rules/no-restricted-v-on.md index fa19a71e8..50441606e 100644 --- a/docs/rules/no-restricted-v-on.md +++ b/docs/rules/no-restricted-v-on.md @@ -12,7 +12,7 @@ description: disallow specific argument in `v-on` ## :book: Rule Details -This rule allows you to specify `v-on` argument names that you don't wantt to use in your application. +This rule allows you to specify `v-on` argument names that you don't want to use in your application. ## :wrench: Options From e6655af038ea40a78f9bb894704361309d6b1382 Mon Sep 17 00:00:00 2001 From: KG Date: Fri, 12 Jan 2024 20:16:35 +0200 Subject: [PATCH 05/15] Update lib/rules/no-restricted-v-on.js Co-authored-by: Flo Edelmann --- lib/rules/no-restricted-v-on.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/no-restricted-v-on.js b/lib/rules/no-restricted-v-on.js index e73c1d99e..a7622d922 100644 --- a/lib/rules/no-restricted-v-on.js +++ b/lib/rules/no-restricted-v-on.js @@ -153,7 +153,7 @@ module.exports = { ] }, uniqueItems: true, - minItems: 0 + minItems: 1 }, messages: { // eslint-disable-next-line eslint-plugin/report-message-format From 25de99b03d5559aed7e31da7e6ce0860d46215cd Mon Sep 17 00:00:00 2001 From: KG Date: Fri, 12 Jan 2024 20:16:52 +0200 Subject: [PATCH 06/15] Update lib/rules/no-restricted-v-on.js Co-authored-by: Flo Edelmann --- lib/rules/no-restricted-v-on.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/rules/no-restricted-v-on.js b/lib/rules/no-restricted-v-on.js index a7622d922..c90a99124 100644 --- a/lib/rules/no-restricted-v-on.js +++ b/lib/rules/no-restricted-v-on.js @@ -144,7 +144,8 @@ module.exports = { 'passive' ] }, - uniqueItems: true + uniqueItems: true, + minItems: 1 } }, required: ['argument'], From 572aabbe62cbd622b3cb1446aa03c215cb4a7543 Mon Sep 17 00:00:00 2001 From: KG Date: Fri, 12 Jan 2024 20:17:03 +0200 Subject: [PATCH 07/15] Update lib/rules/no-restricted-v-on.js Co-authored-by: Flo Edelmann --- lib/rules/no-restricted-v-on.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/rules/no-restricted-v-on.js b/lib/rules/no-restricted-v-on.js index c90a99124..0d1ebcdff 100644 --- a/lib/rules/no-restricted-v-on.js +++ b/lib/rules/no-restricted-v-on.js @@ -169,8 +169,6 @@ module.exports = { context.options.length === 0 ? DEFAULT_OPTIONS : context.options ).map(parseOption) - // const options = DEFAULT_OPTIONS.map(parseOption) - return utils.defineTemplateBodyVisitor(context, { /** * @param {VDirectiveKey} node From 82d3a9d2d6ecceb6b2a1425fab59a44b1cfd62ae Mon Sep 17 00:00:00 2001 From: thesheppard Date: Fri, 12 Jan 2024 20:41:12 +0200 Subject: [PATCH 08/15] Address PR comments --- lib/rules/no-restricted-v-on.js | 31 +++++++++++---------------- tests/lib/rules/no-restricted-v-on.js | 25 ++++++--------------- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/lib/rules/no-restricted-v-on.js b/lib/rules/no-restricted-v-on.js index 0d1ebcdff..b3afe2de8 100644 --- a/lib/rules/no-restricted-v-on.js +++ b/lib/rules/no-restricted-v-on.js @@ -7,17 +7,10 @@ const utils = require('../utils') const regexp = require('../utils/regexp') -const DEFAULT_OPTIONS = [ - { - argument: '/^v-/', - message: - 'Using `@v-xxx` is not allowed. Instead, remove `@` and use it as directive.' - } -] /** * @typedef {object} ParsedOption * @property { (key: VDirectiveKey) => boolean } test - * @property {string[]} modifiers + * @property {string[]} [modifiers] * @property {boolean} [useElement] * @property {string} [message] */ @@ -51,16 +44,14 @@ function parseOption(option) { key.argument.type === 'VIdentifier' && matcher(key.argument.rawName) ) - }, - modifiers: [] + } } } if (option === null) { return { test(key) { return key.argument === null - }, - modifiers: [] + } } } const parsed = parseOption(option.argument) @@ -103,7 +94,9 @@ function defaultMessage(key, option) { ? `@${key.argument.rawName}` : '' const mod = - option.modifiers.length > 0 ? `.${option.modifiers.join('.')}` : '' + option.modifiers != null && option.modifiers.length > 0 + ? `.${option.modifiers.join('.')}` + : '' let element = 'element' if (option.useElement) { element = `<${key.parent.parent.parent.rawName}>` @@ -124,7 +117,7 @@ module.exports = { type: 'array', items: { oneOf: [ - { type: ['string', 'null'] }, + { type: 'string' }, { type: 'object', properties: { @@ -153,8 +146,7 @@ module.exports = { } ] }, - uniqueItems: true, - minItems: 1 + uniqueItems: true }, messages: { // eslint-disable-next-line eslint-plugin/report-message-format @@ -164,10 +156,11 @@ module.exports = { /** @param {RuleContext} context */ create(context) { + if (context.options.length === 0) { + return {} + } /** @type {ParsedOption[]} */ - const options = ( - context.options.length === 0 ? DEFAULT_OPTIONS : context.options - ).map(parseOption) + const options = context.options.map(parseOption) return utils.defineTemplateBodyVisitor(context, { /** diff --git a/tests/lib/rules/no-restricted-v-on.js b/tests/lib/rules/no-restricted-v-on.js index 8d27952ed..aa73a19fa 100644 --- a/tests/lib/rules/no-restricted-v-on.js +++ b/tests/lib/rules/no-restricted-v-on.js @@ -62,23 +62,11 @@ tester.run('no-restricted-v-on', rule, { invalid: [ { filename: 'test.vue', - code: '', - errors: [ - { - message: - 'Using `@v-xxx` is not allowed. Instead, remove `@` and use it as directive.', - line: 1, - column: 16 - } - ] - }, - { - filename: 'test.vue', - code: '', + code: '', + options: ['test'], errors: [ { - message: - 'Using `@v-xxx` is not allowed. Instead, remove `@` and use it as directive.', + message: 'Using `@test` is not allowed on this element.', line: 1, column: 16 } @@ -116,12 +104,11 @@ tester.run('no-restricted-v-on', rule, { }, { filename: 'test.vue', - code: '', - options: ['/^v-/', { argument: 'foo', modifiers: ['prevent'] }, null], + code: '', + options: ['/^v-/', { argument: 'foo', modifiers: ['prevent'] }], 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.' + 'Using `@foo.prevent` is not allowed on this element.' ] }, { From cb52c0f81a5d552be7bfe4a6eeff718c3627bc9b Mon Sep 17 00:00:00 2001 From: thesheppard Date: Fri, 12 Jan 2024 20:44:27 +0200 Subject: [PATCH 09/15] docs: Address minor PR comments --- docs/rules/no-restricted-v-on.md | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/docs/rules/no-restricted-v-on.md b/docs/rules/no-restricted-v-on.md index 50441606e..ee077d11f 100644 --- a/docs/rules/no-restricted-v-on.md +++ b/docs/rules/no-restricted-v-on.md @@ -20,11 +20,11 @@ This rule takes a list of strings, where each string is a argument name or patte ```json { - "vue/no-restricted-v-on": ["error", "/^v-/", "foo", "bar"] + "vue/no-restricted-v-on": ["error", "foo", "bar"] } ``` - + ```vue