Skip to content

Commit dbba30d

Browse files
authored
Add vue/no-restricted-component-options rule (#1213)
1 parent 04f83ba commit dbba30d

File tree

5 files changed

+658
-0
lines changed

5 files changed

+658
-0
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ For example:
288288
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
289289
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
290290
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
291+
| [vue/no-restricted-component-options](./no-restricted-component-options.md) | disallow specific component option | |
291292
| [vue/no-restricted-static-attribute](./no-restricted-static-attribute.md) | disallow specific attribute | |
292293
| [vue/no-restricted-v-bind](./no-restricted-v-bind.md) | disallow specific argument in `v-bind` | |
293294
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |

Diff for: docs/rules/no-restricted-component-options.md

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-restricted-component-options
5+
description: disallow specific component option
6+
---
7+
# vue/no-restricted-component-options
8+
> disallow specific component option
9+
10+
## :book: Rule Details
11+
12+
This rule allows you to specify component options 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 component option name or pattern to be restricted:
17+
18+
```json
19+
{
20+
"vue/no-restricted-component-options": ["error", "init", "beforeCompile", "compiled", "activate", "ready", "/^(?:at|de)tached$/"]
21+
}
22+
```
23+
24+
<eslint-code-block :rules="{'vue/no-restricted-component-options': ['error', 'init', 'beforeCompile', 'compiled', 'activate', 'ready', '/^(?:at|de)tached$/']}">
25+
26+
```vue
27+
<script>
28+
export default {
29+
/* ✗ BAD */
30+
init: function () {},
31+
beforeCompile: function () {},
32+
compiled: function () {},
33+
activate: function () {},
34+
ready: function () {},
35+
attached: function () {},
36+
detached: function () {},
37+
38+
/* ✓ GOOD */
39+
beforeCreate: function () {},
40+
activated: function () {},
41+
mounted: function () {},
42+
}
43+
</script>
44+
```
45+
46+
</eslint-code-block>
47+
48+
Also, you can use an array to specify the path of object properties.
49+
50+
e.g. `[ "error", ["props", "/.*/", "twoWay"] ]`
51+
52+
<eslint-code-block :rules="{'vue/no-restricted-component-options': ['error' , ['props', '/.*/', 'twoWay'] ]}">
53+
54+
```vue
55+
<script>
56+
export default {
57+
props: {
58+
size: Number,
59+
name: {
60+
type: String,
61+
required: true,
62+
/* ✗ BAD */
63+
twoWay: true
64+
}
65+
}
66+
}
67+
</script>
68+
```
69+
70+
</eslint-code-block>
71+
72+
You can use `"*"` to match all properties, including computed keys.
73+
74+
e.g. `[ "error", ["props", "*", "twoWay"] ]`
75+
76+
<eslint-code-block :rules="{'vue/no-restricted-component-options': ['error' , ['props', '*', 'twoWay'] ]}">
77+
78+
```vue
79+
<script>
80+
export default {
81+
props: {
82+
[foo + bar]: {
83+
type: String,
84+
required: true,
85+
/* ✗ BAD */
86+
twoWay: true
87+
}
88+
}
89+
}
90+
</script>
91+
```
92+
93+
</eslint-code-block>
94+
95+
Alternatively, the rule also accepts objects.
96+
97+
```json
98+
{
99+
"vue/no-restricted-component-options": ["error",
100+
{
101+
"name": "init",
102+
"message": "Use \"beforeCreate\" instead."
103+
},
104+
{
105+
"name": "/^(?:at|de)tached$/",
106+
"message": "\"attached\" and \"detached\" is deprecated."
107+
},
108+
{
109+
"name": ["props", "/.*/", "twoWay"],
110+
"message": "\"props.*.twoWay\" cannot be used."
111+
}
112+
]
113+
}
114+
```
115+
116+
The following properties can be specified for the object.
117+
118+
- `name` ... Specify the component option name or pattern, or the path by its array.
119+
- `message` ... Specify an optional custom message.
120+
121+
## :mag: Implementation
122+
123+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-component-options.js)
124+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-component-options.js)

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ module.exports = {
8282
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
8383
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
8484
'no-reserved-keys': require('./rules/no-reserved-keys'),
85+
'no-restricted-component-options': require('./rules/no-restricted-component-options'),
8586
'no-restricted-static-attribute': require('./rules/no-restricted-static-attribute'),
8687
'no-restricted-syntax': require('./rules/no-restricted-syntax'),
8788
'no-restricted-v-bind': require('./rules/no-restricted-v-bind'),

Diff for: lib/rules/no-restricted-component-options.js

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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 {object} ParsedOption
12+
* @property {Tester} test
13+
* @property {string|undefined} [message]
14+
*/
15+
/**
16+
* @typedef {object} MatchResult
17+
* @property {Tester | undefined} [next]
18+
* @property {boolean} [wildcard]
19+
* @property {string} keyName
20+
*/
21+
/**
22+
* @typedef { (name: string) => boolean } Matcher
23+
* @typedef { (node: Property | SpreadElement) => (MatchResult | null) } Tester
24+
*/
25+
26+
/**
27+
* @param {string} str
28+
* @returns {Matcher}
29+
*/
30+
function buildMatcher(str) {
31+
if (regexp.isRegExp(str)) {
32+
const re = regexp.toRegExp(str)
33+
return (s) => {
34+
re.lastIndex = 0
35+
return re.test(s)
36+
}
37+
}
38+
return (s) => s === str
39+
}
40+
41+
/**
42+
* @param {string | string[] | { name: string | string[], message?: string } } option
43+
* @returns {ParsedOption}
44+
*/
45+
function parseOption(option) {
46+
if (typeof option === 'string' || Array.isArray(option)) {
47+
return parseOption({
48+
name: option
49+
})
50+
}
51+
52+
/**
53+
* @typedef {object} Step
54+
* @property {Matcher} [test]
55+
* @property {boolean} [wildcard]
56+
*/
57+
58+
/** @type {Step[]} */
59+
const steps = []
60+
for (const name of Array.isArray(option.name) ? option.name : [option.name]) {
61+
if (name === '*') {
62+
steps.push({ wildcard: true })
63+
} else {
64+
steps.push({ test: buildMatcher(name) })
65+
}
66+
}
67+
const message = option.message
68+
69+
return {
70+
test: buildTester(0),
71+
message
72+
}
73+
74+
/**
75+
* @param {number} index
76+
* @returns {Tester}
77+
*/
78+
function buildTester(index) {
79+
const { wildcard, test } = steps[index]
80+
const next = index + 1
81+
const needNext = steps.length > next
82+
return (node) => {
83+
/** @type {string} */
84+
let keyName
85+
if (wildcard) {
86+
keyName = '*'
87+
} else {
88+
if (node.type !== 'Property') {
89+
return null
90+
}
91+
const name = utils.getStaticPropertyName(node)
92+
if (!name || !test(name)) {
93+
return null
94+
}
95+
keyName = name
96+
}
97+
98+
return {
99+
next: needNext ? buildTester(next) : undefined,
100+
wildcard,
101+
keyName
102+
}
103+
}
104+
}
105+
}
106+
107+
module.exports = {
108+
meta: {
109+
type: 'suggestion',
110+
docs: {
111+
description: 'disallow specific component option',
112+
categories: undefined,
113+
url: 'https://eslint.vuejs.org/rules/no-restricted-component-options.html'
114+
},
115+
fixable: null,
116+
schema: {
117+
type: 'array',
118+
items: {
119+
oneOf: [
120+
{ type: 'string' },
121+
{
122+
type: 'array',
123+
items: {
124+
type: 'string'
125+
}
126+
},
127+
{
128+
type: 'object',
129+
properties: {
130+
name: {
131+
anyOf: [
132+
{ type: 'string' },
133+
{
134+
type: 'array',
135+
items: {
136+
type: 'string'
137+
}
138+
}
139+
]
140+
},
141+
message: { type: 'string', minLength: 1 }
142+
},
143+
required: ['name'],
144+
additionalProperties: false
145+
}
146+
]
147+
},
148+
uniqueItems: true,
149+
minItems: 0
150+
},
151+
152+
messages: {
153+
// eslint-disable-next-line eslint-plugin/report-message-format
154+
restrictedOption: '{{message}}'
155+
}
156+
},
157+
/** @param {RuleContext} context */
158+
create(context) {
159+
if (!context.options || context.options.length === 0) {
160+
return {}
161+
}
162+
/** @type {ParsedOption[]} */
163+
const options = context.options.map(parseOption)
164+
165+
return utils.defineVueVisitor(context, {
166+
onVueObjectEnter(node) {
167+
for (const option of options) {
168+
verify(node, option.test, option.message)
169+
}
170+
}
171+
})
172+
173+
/**
174+
* @param {ObjectExpression} node
175+
* @param {Tester} test
176+
* @param {string | undefined} customMessage
177+
* @param {string[]} path
178+
*/
179+
function verify(node, test, customMessage, path = []) {
180+
for (const prop of node.properties) {
181+
const result = test(prop)
182+
if (!result) {
183+
continue
184+
}
185+
if (result.next) {
186+
if (
187+
prop.type !== 'Property' ||
188+
prop.value.type !== 'ObjectExpression'
189+
) {
190+
continue
191+
}
192+
verify(prop.value, result.next, customMessage, [
193+
...path,
194+
result.keyName
195+
])
196+
} else {
197+
const message =
198+
customMessage || defaultMessage([...path, result.keyName])
199+
context.report({
200+
node: prop.type === 'Property' ? prop.key : prop,
201+
messageId: 'restrictedOption',
202+
data: { message }
203+
})
204+
}
205+
}
206+
}
207+
208+
/**
209+
* @param {string[]} path
210+
*/
211+
function defaultMessage(path) {
212+
return `Using \`${path.join('.')}\` is not allowed.`
213+
}
214+
}
215+
}

0 commit comments

Comments
 (0)