Skip to content

Commit ba230cd

Browse files
authored
Add vue/no-restricted-v-bind rule (#1191)
* Add `vue/no-restricted-v-bind` rule * Fix * Add testcase
1 parent bf7d219 commit ba230cd

File tree

5 files changed

+463
-0
lines changed

5 files changed

+463
-0
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ For example:
287287
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
288288
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
289289
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
290+
| [vue/no-restricted-v-bind](./no-restricted-v-bind.md) | disallow specific argument in `v-bind` | |
290291
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
291292
| [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | |
292293
| [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | |

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

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

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ module.exports = {
8181
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
8282
'no-reserved-keys': require('./rules/no-reserved-keys'),
8383
'no-restricted-syntax': require('./rules/no-restricted-syntax'),
84+
'no-restricted-v-bind': require('./rules/no-restricted-v-bind'),
8485
'no-setup-props-destructure': require('./rules/no-setup-props-destructure'),
8586
'no-shared-component-data': require('./rules/no-shared-component-data'),
8687
'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'),

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

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

0 commit comments

Comments
 (0)