Skip to content

Commit 898740e

Browse files
authored
Add vue/no-restricted-static-attribute rule (#1192)
* Add `vue/no-restricted-static-attribute` rule * fix
1 parent ba230cd commit 898740e

File tree

5 files changed

+417
-1
lines changed

5 files changed

+417
-1
lines changed

docs/rules/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -287,14 +287,15 @@ 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-static-attribute](./no-restricted-static-attribute.md) | disallow specific attribute | |
290291
| [vue/no-restricted-v-bind](./no-restricted-v-bind.md) | disallow specific argument in `v-bind` | |
291292
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
292293
| [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | |
293294
| [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | |
294295
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
295296
| [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties | |
296-
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: |
297297
| [vue/no-useless-mustaches](./no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
298+
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: |
298299
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
299300
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
300301
| [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` | |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-restricted-static-attribute
5+
description: disallow specific attribute
6+
---
7+
# vue/no-restricted-static-attribute
8+
> disallow specific attribute
9+
10+
## :book: Rule Details
11+
12+
This rule allows you to specify attribute 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 attribute name or pattern to be restricted:
17+
18+
```json
19+
{
20+
"vue/no-restricted-static-attribute": ["error", "foo", "bar"]
21+
}
22+
```
23+
24+
<eslint-code-block :rules="{'vue/no-restricted-static-attribute': ['error', 'foo', 'bar']}">
25+
26+
```vue
27+
<template>
28+
<!-- ✘ BAD -->
29+
<div foo="x" />
30+
<div bar />
31+
</template>
32+
```
33+
34+
</eslint-code-block>
35+
36+
Alternatively, the rule also accepts objects.
37+
38+
```json
39+
{
40+
"vue/no-restricted-static-attribute": ["error",
41+
{
42+
"key": "stlye",
43+
"message": "Using \"stlye\" is not allowed. Use \"style\" instead."
44+
}
45+
]
46+
}
47+
```
48+
49+
The following properties can be specified for the object.
50+
51+
- `key` ... Specify the attribute key name or pattern.
52+
- `value` ... Specify the value text or pattern or `true`. If specified, it will only be reported if the specified value is used. If `true`, it will only be reported if there is no value or if the value and key are same.
53+
- `element` ... Specify the element name or pattern. If specified, it will only be reported if used on the specified element.
54+
- `message` ... Specify an optional custom message.
55+
56+
### `{ "key": "foo", "value": "bar" }`
57+
58+
<eslint-code-block :rules="{'vue/no-restricted-static-attribute': ['error', { key: 'foo', value: 'bar' }]}">
59+
60+
```vue
61+
<template>
62+
<!-- ✓ GOOD -->
63+
<div foo="foo" />
64+
65+
<!-- ✘ BAD -->
66+
<div foo="bar" />
67+
</template>
68+
```
69+
70+
</eslint-code-block>
71+
72+
### `{ "key": "foo", "element": "MyButton" }`
73+
74+
<eslint-code-block :rules="{'vue/no-restricted-static-attribute': ['error', { key: 'foo', element: 'MyButton' }]}">
75+
76+
```vue
77+
<template>
78+
<!-- ✓ GOOD -->
79+
<CoolButton foo="x" />
80+
81+
<!-- ✘ BAD -->
82+
<MyButton foo="x" />
83+
</template>
84+
```
85+
86+
</eslint-code-block>
87+
88+
## :couple: Related rules
89+
90+
- [vue/no-restricted-v-bind]
91+
92+
[vue/no-restricted-v-bind]: ./no-restricted-v-bind.md
93+
94+
## :mag: Implementation
95+
96+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-static-attribute.js)
97+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-static-attribute.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ module.exports = {
8080
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
8181
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
8282
'no-reserved-keys': require('./rules/no-reserved-keys'),
83+
'no-restricted-static-attribute': require('./rules/no-restricted-static-attribute'),
8384
'no-restricted-syntax': require('./rules/no-restricted-syntax'),
8485
'no-restricted-v-bind': require('./rules/no-restricted-v-bind'),
8586
'no-setup-props-destructure': require('./rules/no-setup-props-destructure'),
+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/**
2+
* @author Yosuke Ota
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 {import('vue-eslint-parser').AST.VAttribute} VAttribute
12+
*/
13+
/**
14+
* @typedef {object} ParsedOption
15+
* @property { (key: VAttribute) => boolean } test
16+
* @property {boolean} [useValue]
17+
* @property {boolean} [useElement]
18+
* @property {string} [message]
19+
*/
20+
21+
/**
22+
* @param {string} str
23+
* @returns {(str: string) => boolean}
24+
*/
25+
function buildMatcher(str) {
26+
if (regexp.isRegExp(str)) {
27+
const re = regexp.toRegExp(str)
28+
return (s) => {
29+
re.lastIndex = 0
30+
return re.test(s)
31+
}
32+
}
33+
return (s) => s === str
34+
}
35+
/**
36+
* @param {any} option
37+
* @returns {ParsedOption}
38+
*/
39+
function parseOption(option) {
40+
if (typeof option === 'string') {
41+
const matcher = buildMatcher(option)
42+
return {
43+
test({ key }) {
44+
return matcher(key.rawName)
45+
}
46+
}
47+
}
48+
const parsed = parseOption(option.key)
49+
if (option.value) {
50+
const keyTest = parsed.test
51+
if (option.value === true) {
52+
parsed.test = (node) => {
53+
if (!keyTest(node)) {
54+
return false
55+
}
56+
return node.value == null || node.value.value === node.key.rawName
57+
}
58+
} else {
59+
const valueMatcher = buildMatcher(option.value)
60+
parsed.test = (node) => {
61+
if (!keyTest(node)) {
62+
return false
63+
}
64+
return node.value != null && valueMatcher(node.value.value)
65+
}
66+
}
67+
parsed.useValue = true
68+
}
69+
if (option.element) {
70+
const argTest = parsed.test
71+
const tagMatcher = buildMatcher(option.element)
72+
parsed.test = (node) => {
73+
if (!argTest(node)) {
74+
return false
75+
}
76+
const element = node.parent.parent
77+
return tagMatcher(element.rawName)
78+
}
79+
parsed.useElement = true
80+
}
81+
parsed.message = option.message
82+
return parsed
83+
}
84+
85+
module.exports = {
86+
meta: {
87+
type: 'suggestion',
88+
docs: {
89+
description: 'disallow specific attribute',
90+
categories: undefined,
91+
url: 'https://eslint.vuejs.org/rules/no-restricted-static-attribute.html'
92+
},
93+
fixable: null,
94+
schema: {
95+
type: 'array',
96+
items: {
97+
oneOf: [
98+
{ type: 'string' },
99+
{
100+
type: 'object',
101+
properties: {
102+
key: { type: 'string' },
103+
value: { anyOf: [{ type: 'string' }, { enum: [true] }] },
104+
element: { type: 'string' },
105+
message: { type: 'string', minLength: 1 }
106+
},
107+
required: ['key'],
108+
additionalProperties: false
109+
}
110+
]
111+
},
112+
uniqueItems: true,
113+
minItems: 0
114+
},
115+
116+
messages: {
117+
// eslint-disable-next-line eslint-plugin/report-message-format
118+
restrictedAttr: '{{message}}'
119+
}
120+
},
121+
create(context) {
122+
if (!context.options.length) {
123+
return {}
124+
}
125+
/** @type {ParsedOption[]} */
126+
const options = context.options.map(parseOption)
127+
128+
return utils.defineTemplateBodyVisitor(context, {
129+
/**
130+
* @param {VAttribute} node
131+
*/
132+
'VAttribute[directive=false]'(node) {
133+
for (const option of options) {
134+
if (option.test(node)) {
135+
const message = option.message || defaultMessage(node, option)
136+
context.report({
137+
node,
138+
messageId: 'restrictedAttr',
139+
data: { message }
140+
})
141+
return
142+
}
143+
}
144+
}
145+
})
146+
147+
/**
148+
* @param {VAttribute} node
149+
* @param {ParsedOption} option
150+
*/
151+
function defaultMessage(node, option) {
152+
const key = node.key.rawName
153+
const value = !option.useValue
154+
? ''
155+
: node.value == null
156+
? '` set to `true'
157+
: `="${node.value.value}"`
158+
159+
let on = ''
160+
if (option.useElement) {
161+
on = ` on \`<${node.parent.parent.rawName}>\``
162+
}
163+
return `Using \`${key + value}\`${on} is not allowed.`
164+
}
165+
}
166+
}

0 commit comments

Comments
 (0)