Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d2c94a9

Browse files
New Add vue/no-potential-property-typo rule (#1072)
* feat(utils/index.js): add util lib * feat(tests/lib/utils/index.js): add unit test * feat: change test, complete rule * feat: add test, add preset, custom option * feat: add testcase * test: add test, 100% test cover * test: menual indentation * style: remove todo comment that have been done🚀 * fix: change rule name -> no-unknown-component-options * feat: rename `no-unknow-component-options` -> `no-potential-component-options-typo` * feat: remove unnecessary readme * feat: revert lib/utils/index.js * docs: update readme * feat: set categories to undefined * test: add test case * test: add test case * test: add vue preset as default preset, abcde and abcd test case * test: udpate test * test: all valid options * improvement: comment * test: inline test
1 parent 7f39dc7 commit d2c94a9

File tree

8 files changed

+776
-0
lines changed

8 files changed

+776
-0
lines changed

‎docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ For example:
286286
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
287287
| [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | |
288288
| [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | |
289+
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
289290
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
290291
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
291292
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-potential-component-option-typo
5+
description: disallow a potential typo in your component property
6+
---
7+
# vue/no-potential-component-option-typo
8+
> disallow a potential typo in your component property
9+
10+
## :book: Rule Details
11+
12+
This Rule disallow a potential typo in your component options
13+
14+
**Here is the config**
15+
```js
16+
{'vue/no-potential-component-option-typo': ['error', {presets: ['all'], custom: ['test']}]}
17+
```
18+
19+
<eslint-code-block :rules="{'vue/no-potential-component-option-typo': ['error', {presets: ['all'], custom: ['test']}]}">
20+
21+
```vue
22+
<script>
23+
export default {
24+
/* ✓ GOOD */
25+
props: {
26+
27+
},
28+
/* ×ばつ BAD */
29+
method: {
30+
31+
},
32+
/* ✓ GOOD */
33+
data: {
34+
35+
},
36+
/* ×ばつ BAD */
37+
beforeRouteEnteR() {
38+
39+
},
40+
/* ×ばつ BAD due to custom option 'test'*/
41+
testt: {
42+
43+
}
44+
}
45+
</script>
46+
```
47+
48+
</eslint-code-block>
49+
50+
> we use editdistance to compare two string similarity, threshold is an option to control upper bound of editdistance to report
51+
52+
**Here is the another example about config option `threshold`**
53+
```js
54+
{'vue/no-potential-component-option-typo': ['error', {presets: ['vue', 'nuxt'], threshold: 5}]}
55+
```
56+
57+
<eslint-code-block :rules="{'vue/no-potential-component-option-typo': ['error', {presets: ['vue', 'nuxt'], threshold: 5}]}">
58+
59+
```vue
60+
<script>
61+
export default {
62+
/* ✓ BAD, due to threshold is 5 */
63+
props: {
64+
65+
},
66+
/* ✓ BAD, due to threshold is 5 */
67+
method: {
68+
69+
},
70+
/* ✓ BAD, due to threshold is 5 */
71+
data: {
72+
73+
},
74+
/* ×ばつ GOOD, due to we don't choose vue-router preset or add a custom option */
75+
beforeRouteEnteR() {
76+
77+
}
78+
}
79+
</script>
80+
```
81+
82+
</eslint-code-block>
83+
84+
## :wrench: Options
85+
```js
86+
{
87+
"vue/no-unsed-vars": [{
88+
presets: {
89+
type: 'array',
90+
items: {
91+
type: 'string',
92+
enum: ['all', 'vue', 'vue-router', 'nuxt']
93+
},
94+
uniqueItems: true,
95+
minItems: 0
96+
},
97+
custom: {
98+
type: 'array',
99+
minItems: 0,
100+
items: { type: 'string' },
101+
uniqueItems: true
102+
},
103+
threshold: {
104+
type: 'number',
105+
'minimum': 1
106+
}
107+
}]
108+
}
109+
```
110+
- `presets` ... `enum type`, contains several common vue component option set, `['all']` is the same as `['vue', 'vue-router', 'nuxt']`. **default** `[]`
111+
- `custom` ... `array type`, a list store your custom component option want to detect. **default** `[]`
112+
- `threshold` ... `number type`, a number used to control the upper limit of the reported editing distance, we recommend don't change this config option, even if it is required, not bigger than `2`. **default** `1`
113+
## :rocket: Suggestion
114+
- We provide all the possible component option that editdistance between your vue component option and configuration options is greater than 0 and lessEqual than threshold
115+
116+
## :books: Further reading
117+
- [Edit_distance](https://en.wikipedia.org/wiki/Edit_distance)
118+
## :mag: Implementation
119+
120+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-potential-component-option-typo.js)
121+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-potential-component-option-typo.js)

‎lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ module.exports = {
6767
'no-multi-spaces': require('./rules/no-multi-spaces'),
6868
'no-multiple-template-root': require('./rules/no-multiple-template-root'),
6969
'no-parsing-error': require('./rules/no-parsing-error'),
70+
'no-potential-component-option-typo': require('./rules/no-potential-component-option-typo'),
7071
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
7172
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
7273
'no-reserved-keys': require('./rules/no-reserved-keys'),
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* @fileoverview detect if there is a potential typo in your component property
3+
* @author IWANABETHATGUY
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
const vueComponentOptions = require('../utils/vue-component-options.json')
9+
// ------------------------------------------------------------------------------
10+
// Rule Definition
11+
// ------------------------------------------------------------------------------
12+
13+
module.exports = {
14+
meta: {
15+
type: 'suggestion',
16+
docs: {
17+
description: 'disallow a potential typo in your component property',
18+
categories: undefined,
19+
recommended: false,
20+
url: 'https://eslint.vuejs.org/rules/no-potential-component-option-typo.html'
21+
},
22+
fixable: null,
23+
schema: [
24+
{
25+
type: 'object',
26+
properties: {
27+
presets: {
28+
type: 'array',
29+
items: {
30+
type: 'string',
31+
enum: ['all', 'vue', 'vue-router', 'nuxt']
32+
},
33+
uniqueItems: true,
34+
minItems: 0
35+
},
36+
custom: {
37+
type: 'array',
38+
minItems: 0,
39+
items: { type: 'string' },
40+
uniqueItems: true
41+
},
42+
threshold: {
43+
type: 'number',
44+
'minimum': 1
45+
}
46+
}
47+
}
48+
]
49+
},
50+
51+
create: function (context) {
52+
const option = context.options[0] || {}
53+
const custom = option['custom'] || []
54+
const presets = option['presets'] || ['vue']
55+
const threshold = option['threshold'] || 1
56+
let candidateOptions
57+
if (presets.includes('all')) {
58+
candidateOptions = Object.keys(vueComponentOptions).reduce((pre, cur) => {
59+
return [...pre, ...vueComponentOptions[cur]]
60+
}, [])
61+
} else {
62+
candidateOptions = presets.reduce((pre, cur) => {
63+
return [...pre, ...vueComponentOptions[cur]]
64+
}, [])
65+
}
66+
const candidateOptionSet = new Set([...candidateOptions, ...custom])
67+
const candidateOptionList = [...candidateOptionSet]
68+
if (!candidateOptionList.length) {
69+
return {}
70+
}
71+
return utils.executeOnVue(context, obj => {
72+
const componentInstanceOptions = obj.properties.filter(
73+
p => p.type === 'Property' && p.key.type === 'Identifier'
74+
)
75+
if (!componentInstanceOptions.length) {
76+
return {}
77+
}
78+
componentInstanceOptions.forEach(option => {
79+
const id = option.key
80+
const name = id.name
81+
if (candidateOptionSet.has(name)) {
82+
return
83+
}
84+
const potentialTypoList = candidateOptionList
85+
.map(o => ({ option: o, distance: utils.editDistance(o, name) }))
86+
.filter(({ distance, option }) => distance <= threshold && distance > 0)
87+
.sort((a, b) => a.distance - b.distance)
88+
if (potentialTypoList.length) {
89+
context.report({
90+
node: id,
91+
loc: id.loc,
92+
message: `'{{name}}' may be a typo, which is similar to option [{{option}}].`,
93+
data: {
94+
name,
95+
option: potentialTypoList.map(({ option }) => option).join(',')
96+
},
97+
suggest: potentialTypoList.map(({ option }) => ({
98+
desc: `Replace property '${name}' to '${option}'`,
99+
fix (fixer) {
100+
return fixer.replaceText(id, option)
101+
}
102+
}))
103+
})
104+
}
105+
})
106+
})
107+
}
108+
}

‎lib/utils/index.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,38 @@ module.exports = {
914914
return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
915915
},
916916

917+
/**
918+
* return two string editdistance
919+
* @param {string} a string a to compare
920+
* @param {string} b string b to compare
921+
* @returns {number}
922+
*/
923+
editDistance (a, b) {
924+
if (a === b) {
925+
return 0
926+
}
927+
const alen = a.length
928+
const blen = b.length
929+
const dp = Array.from({ length: alen + 1 }).map(_ =>
930+
Array.from({ length: blen + 1 }).fill(0)
931+
)
932+
for (let i = 0; i <= alen; i++) {
933+
dp[i][0] = i
934+
}
935+
for (let j = 0; j <= blen; j++) {
936+
dp[0][j] = j
937+
}
938+
for (let i = 1; i <= alen; i++) {
939+
for (let j = 1; j <= blen; j++) {
940+
if (a[i - 1] === b[j - 1]) {
941+
dp[i][j] = dp[i - 1][j - 1]
942+
} else {
943+
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
944+
}
945+
}
946+
}
947+
return dp[alen][blen]
948+
},
917949
/**
918950
* Unwrap typescript types like "X as F"
919951
* @template T

‎lib/utils/vue-component-options.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"nuxt": ["asyncData", "fetch", "head", "key", "layout", "loading", "middleware", "scrollToTop", "transition", "validate", "watchQuery"],
3+
"vue-router": [
4+
"beforeRouteEnter",
5+
"beforeRouteUpdate",
6+
"beforeRouteLeave"
7+
],
8+
"vue": [
9+
"data",
10+
"props",
11+
"propsData",
12+
"computed",
13+
"methods",
14+
"watch",
15+
"el",
16+
"template",
17+
"render",
18+
"renderError",
19+
"staticRenderFns",
20+
"beforeCreate",
21+
"created",
22+
"beforeDestroy",
23+
"destroyed",
24+
"beforeMount",
25+
"mounted",
26+
"beforeUpdate",
27+
"updated",
28+
"activated",
29+
"deactivated",
30+
"errorCaptured",
31+
"serverPrefetch",
32+
"directives",
33+
"components",
34+
"transitions",
35+
"filters",
36+
"provide",
37+
"inject",
38+
"model",
39+
"parent",
40+
"mixins",
41+
"name",
42+
"extends",
43+
"delimiters",
44+
"comments",
45+
"inheritAttrs"
46+
]
47+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /