Skip to content

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

8 files changed

+776
-0
lines changed

Diff for: docs/rules/README.md

+1
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 | |

Diff for: docs/rules/no-potential-component-option-typo.md

+121
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)

Diff for: lib/index.js

+1
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'),

Diff for: lib/rules/no-potential-component-option-typo.js

+108
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+
}

Diff for: lib/utils/index.js

+32
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

Diff for: lib/utils/vue-component-options.json

+47
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)