Skip to content

Commit 7d13ce3

Browse files
Add new vue/no-restricted-v-on rule (#2367)
Co-authored-by: Flo Edelmann <git@flo-edelmann.de>
1 parent 634f38d commit 7d13ce3

7 files changed

+480
-2
lines changed

docs/rules/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ For example:
244244
| [vue/no-restricted-props](./no-restricted-props.md) | disallow specific props | :bulb: | :hammer: |
245245
| [vue/no-restricted-static-attribute](./no-restricted-static-attribute.md) | disallow specific attribute | | :hammer: |
246246
| [vue/no-restricted-v-bind](./no-restricted-v-bind.md) | disallow specific argument in `v-bind` | | :hammer: |
247+
| [vue/no-restricted-v-on](./no-restricted-v-on.md) | disallow specific argument in `v-on` | | :hammer: |
247248
| [vue/no-root-v-if](./no-root-v-if.md) | disallow `v-if` directives on root element | | :hammer: |
248249
| [vue/no-setup-props-reactivity-loss](./no-setup-props-reactivity-loss.md) | disallow usages that lose the reactivity of `props` passed to `setup` | | :hammer: |
249250
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | | :hammer: |

docs/rules/no-restricted-static-attribute.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ title: vue/no-restricted-static-attribute
55
description: disallow specific attribute
66
since: v7.0.0
77
---
8+
89
# vue/no-restricted-static-attribute
910

1011
> disallow specific attribute
@@ -39,7 +40,8 @@ Alternatively, the rule also accepts objects.
3940

4041
```json
4142
{
42-
"vue/no-restricted-static-attribute": ["error",
43+
"vue/no-restricted-static-attribute": [
44+
"error",
4345
{
4446
"key": "stlye",
4547
"message": "Using \"stlye\" is not allowed. Use \"style\" instead."
@@ -95,8 +97,10 @@ The following properties can be specified for the object.
9597
## :couple: Related Rules
9698

9799
- [vue/no-restricted-v-bind]
100+
- [vue/no-restricted-v-on]
98101

99102
[vue/no-restricted-v-bind]: ./no-restricted-v-bind.md
103+
[vue/no-restricted-v-on]: ./no-restricted-v-on.md
100104

101105
## :rocket: Version
102106

docs/rules/no-restricted-v-bind.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ title: vue/no-restricted-v-bind
55
description: disallow specific argument in `v-bind`
66
since: v7.0.0
77
---
8+
89
# vue/no-restricted-v-bind
910

1011
> disallow specific argument in `v-bind`
@@ -53,7 +54,8 @@ Alternatively, the rule also accepts objects.
5354

5455
```json
5556
{
56-
"vue/no-restricted-v-bind": ["error",
57+
"vue/no-restricted-v-bind": [
58+
"error",
5759
{
5860
"argument": "/^v-/",
5961
"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.
112114
## :couple: Related Rules
113115

114116
- [vue/no-restricted-static-attribute]
117+
- [vue/no-restricted-v-on]
115118

116119
[vue/no-restricted-static-attribute]: ./no-restricted-static-attribute.md
120+
[vue/no-restricted-v-on]: ./no-restricted-v-on.md
117121

118122
## :rocket: Version
119123

docs/rules/no-restricted-v-on.md

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-restricted-v-on
5+
description: disallow specific argument in `v-on`
6+
---
7+
# vue/no-restricted-v-on
8+
9+
> disallow specific argument in `v-on`
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
## :book: Rule Details
14+
15+
This rule allows you to specify `v-on` argument names that you don't want to use in your application.
16+
17+
## :wrench: Options
18+
19+
This rule takes a list of strings, where each string is a argument name or pattern to be restricted:
20+
21+
```json
22+
{
23+
"vue/no-restricted-v-on": ["error", "foo", "/^bar/"]
24+
}
25+
```
26+
27+
<eslint-code-block :rules="{'vue/no-restricted-v-on': ['error', 'foo', '/^bar/']}">
28+
29+
```vue
30+
<template>
31+
<!-- ✓ GOOD -->
32+
<div v-on:click="x" />
33+
<div @tap="x" />
34+
35+
<!-- ✗ BAD -->
36+
<div v-on:foo="x" />
37+
<div @bar-baz="x" />
38+
</template>
39+
```
40+
41+
</eslint-code-block>
42+
43+
Alternatively, the rule also accepts objects.
44+
45+
```json
46+
{
47+
"vue/no-restricted-v-on": [
48+
"error",
49+
{
50+
"argument": "foo",
51+
"message": "Use \"v-on:x\" instead."
52+
},
53+
{
54+
"argument": "bar",
55+
"message": "\"@bar\" is deprecated."
56+
}
57+
]
58+
}
59+
```
60+
61+
The following properties can be specified for the object.
62+
63+
- `argument` ... Specify the argument name or pattern or `null`. If `null` is specified, it matches `v-on=`.
64+
- `modifiers` ... Specifies an array of the modifier names. If specified, it will only be reported if the specified modifier is used.
65+
- `element` ... Specify the element name or pattern. If specified, it will only be reported if used on the specified element.
66+
- `message` ... Specify an optional custom message.
67+
68+
### `{ "argument": "foo", "modifiers": ["prevent"] }`
69+
70+
<eslint-code-block :rules="{'vue/no-restricted-v-on': ['error', { argument: 'foo', modifiers: ['prevent'] }]}">
71+
72+
```vue
73+
<template>
74+
<!-- ✓ GOOD -->
75+
<div @foo="x" />
76+
77+
<!-- ✗ BAD -->
78+
<div @foo.prevent="x" />
79+
</template>
80+
```
81+
82+
</eslint-code-block>
83+
84+
### `{ "argument": "foo", "element": "MyButton" }`
85+
86+
<eslint-code-block :rules="{'vue/no-restricted-v-on': ['error', { argument: 'foo', element: 'MyButton' }]}">
87+
88+
```vue
89+
<template>
90+
<!-- ✓ GOOD -->
91+
<CoolButton @foo="x" />
92+
93+
<!-- ✗ BAD -->
94+
<MyButton @foo="x" />
95+
</template>
96+
```
97+
98+
</eslint-code-block>
99+
100+
## :couple: Related Rules
101+
102+
- [vue/no-restricted-static-attribute]
103+
- [vue/no-restricted-v-bind]
104+
105+
[vue/no-restricted-static-attribute]: ./no-restricted-static-attribute.md
106+
[vue/no-restricted-v-bind]: ./no-restricted-v-bind.md
107+
108+
## :mag: Implementation
109+
110+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-v-on.js)
111+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-v-on.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ module.exports = {
131131
'no-restricted-static-attribute': require('./rules/no-restricted-static-attribute'),
132132
'no-restricted-syntax': require('./rules/no-restricted-syntax'),
133133
'no-restricted-v-bind': require('./rules/no-restricted-v-bind'),
134+
'no-restricted-v-on': require('./rules/no-restricted-v-on'),
134135
'no-root-v-if': require('./rules/no-root-v-if'),
135136
'no-setup-props-destructure': require('./rules/no-setup-props-destructure'),
136137
'no-setup-props-reactivity-loss': require('./rules/no-setup-props-reactivity-loss'),

lib/rules/no-restricted-v-on.js

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* @author Kamogelo Moalusi <github.com/thesheppard>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
const regexp = require('../utils/regexp')
9+
10+
/**
11+
* @typedef {object} ParsedOption
12+
* @property { (key: VDirectiveKey) => boolean } test
13+
* @property {string[]} [modifiers]
14+
* @property {boolean} [useElement]
15+
* @property {string} [message]
16+
*/
17+
18+
/**
19+
* @param {string} str
20+
* @returns {(str: string) => boolean}
21+
*/
22+
function buildMatcher(str) {
23+
if (regexp.isRegExp(str)) {
24+
const re = regexp.toRegExp(str)
25+
return (s) => {
26+
re.lastIndex = 0
27+
return re.test(s)
28+
}
29+
}
30+
return (s) => s === str
31+
}
32+
33+
/**
34+
* @param {any} option
35+
* @returns {ParsedOption}
36+
*/
37+
function parseOption(option) {
38+
if (typeof option === 'string') {
39+
const matcher = buildMatcher(option)
40+
return {
41+
test(key) {
42+
return Boolean(
43+
key.argument &&
44+
key.argument.type === 'VIdentifier' &&
45+
matcher(key.argument.rawName)
46+
)
47+
}
48+
}
49+
}
50+
if (option === null) {
51+
return {
52+
test(key) {
53+
return key.argument === null
54+
}
55+
}
56+
}
57+
const parsed = parseOption(option.argument)
58+
59+
if (option.modifiers) {
60+
const argTest = parsed.test
61+
parsed.test = (key) => {
62+
if (!argTest(key)) {
63+
return false
64+
}
65+
return /** @type {string[]} */ (option.modifiers).every((modName) =>
66+
key.modifiers.some((mid) => mid.name === modName)
67+
)
68+
}
69+
parsed.modifiers = option.modifiers
70+
}
71+
if (option.element) {
72+
const argTest = parsed.test
73+
const tagMatcher = buildMatcher(option.element)
74+
parsed.test = (key) => {
75+
if (!argTest(key)) {
76+
return false
77+
}
78+
return tagMatcher(key.parent.parent.parent.rawName)
79+
}
80+
parsed.useElement = true
81+
}
82+
parsed.message = option.message
83+
return parsed
84+
}
85+
86+
/**
87+
* @param {VDirectiveKey} key
88+
* @param {ParsedOption} option
89+
*/
90+
function defaultMessage(key, option) {
91+
const von = key.name.rawName === '@' ? '' : 'v-on'
92+
const arg =
93+
key.argument != null && key.argument.type === 'VIdentifier'
94+
? `${key.name.rawName === '@' ? '@' : ':'}${key.argument.rawName}`
95+
: ''
96+
const mod =
97+
option.modifiers != null && option.modifiers.length > 0
98+
? `.${option.modifiers.join('.')}`
99+
: ''
100+
let element = 'element'
101+
if (option.useElement) {
102+
element = `<${key.parent.parent.parent.rawName}>`
103+
}
104+
return `Using \`${von + arg + mod}\` is not allowed on this ${element}.`
105+
}
106+
107+
module.exports = {
108+
meta: {
109+
type: 'suggestion',
110+
docs: {
111+
description: 'disallow specific argument in `v-on`',
112+
categories: undefined,
113+
url: 'https://eslint.vuejs.org/rules/no-restricted-v-on.html'
114+
},
115+
fixable: null,
116+
schema: {
117+
type: 'array',
118+
items: {
119+
oneOf: [
120+
{ type: ['string', 'null'] },
121+
{
122+
type: 'object',
123+
properties: {
124+
argument: { type: ['string', 'null'] },
125+
element: { type: 'string' },
126+
message: { type: 'string', minLength: 1 },
127+
modifiers: {
128+
type: 'array',
129+
items: {
130+
type: 'string',
131+
enum: [
132+
'prevent',
133+
'stop',
134+
'capture',
135+
'self',
136+
'once',
137+
'passive'
138+
]
139+
},
140+
uniqueItems: true,
141+
minItems: 1
142+
}
143+
},
144+
required: ['argument'],
145+
additionalProperties: false
146+
}
147+
]
148+
},
149+
uniqueItems: true
150+
},
151+
messages: {
152+
// eslint-disable-next-line eslint-plugin/report-message-format
153+
restrictedVOn: '{{message}}'
154+
}
155+
},
156+
157+
/** @param {RuleContext} context */
158+
create(context) {
159+
if (context.options.length === 0) {
160+
return {}
161+
}
162+
/** @type {ParsedOption[]} */
163+
const options = context.options.map(parseOption)
164+
165+
return utils.defineTemplateBodyVisitor(context, {
166+
/**
167+
* @param {VDirectiveKey} node
168+
*/
169+
"VAttribute[directive=true][key.name.name='on'] > VDirectiveKey"(node) {
170+
for (const option of options) {
171+
if (option.test(node)) {
172+
const message = option.message || defaultMessage(node, option)
173+
context.report({
174+
node,
175+
messageId: 'restrictedVOn',
176+
data: { message }
177+
})
178+
return
179+
}
180+
}
181+
}
182+
})
183+
}
184+
}

0 commit comments

Comments
 (0)