Skip to content

Commit

Permalink
New Add vue/no-potential-property-typo rule (#1072)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
IWANABETHATGUY committed May 20, 2020
1 parent 7f39dc7 commit d2c94a9
Show file tree
Hide file tree
Showing 8 changed files with 776 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -286,6 +286,7 @@ For example:
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
| [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | |
| [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | |
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
Expand Down
121 changes: 121 additions & 0 deletions docs/rules/no-potential-component-option-typo.md
@@ -0,0 +1,121 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/no-potential-component-option-typo
description: disallow a potential typo in your component property
---
# vue/no-potential-component-option-typo
> disallow a potential typo in your component property
## :book: Rule Details

This Rule disallow a potential typo in your component options

**Here is the config**
```js
{'vue/no-potential-component-option-typo': ['error', {presets: ['all'], custom: ['test']}]}
```

<eslint-code-block :rules="{'vue/no-potential-component-option-typo': ['error', {presets: ['all'], custom: ['test']}]}">

```vue
<script>
export default {
/* ✓ GOOD */
props: {
},
/* × BAD */
method: {
},
/* ✓ GOOD */
data: {
},
/* × BAD */
beforeRouteEnteR() {
},
/* × BAD due to custom option 'test'*/
testt: {
}
}
</script>
```

</eslint-code-block>

> we use editdistance to compare two string similarity, threshold is an option to control upper bound of editdistance to report
**Here is the another example about config option `threshold`**
```js
{'vue/no-potential-component-option-typo': ['error', {presets: ['vue', 'nuxt'], threshold: 5}]}
```

<eslint-code-block :rules="{'vue/no-potential-component-option-typo': ['error', {presets: ['vue', 'nuxt'], threshold: 5}]}">

```vue
<script>
export default {
/* ✓ BAD, due to threshold is 5 */
props: {
},
/* ✓ BAD, due to threshold is 5 */
method: {
},
/* ✓ BAD, due to threshold is 5 */
data: {
},
/* × GOOD, due to we don't choose vue-router preset or add a custom option */
beforeRouteEnteR() {
}
}
</script>
```

</eslint-code-block>

## :wrench: Options
```js
{
"vue/no-unsed-vars": [{
presets: {
type: 'array',
items: {
type: 'string',
enum: ['all', 'vue', 'vue-router', 'nuxt']
},
uniqueItems: true,
minItems: 0
},
custom: {
type: 'array',
minItems: 0,
items: { type: 'string' },
uniqueItems: true
},
threshold: {
type: 'number',
'minimum': 1
}
}]
}
```
- `presets` ... `enum type`, contains several common vue component option set, `['all']` is the same as `['vue', 'vue-router', 'nuxt']`. **default** `[]`
- `custom` ... `array type`, a list store your custom component option want to detect. **default** `[]`
- `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`
## :rocket: Suggestion
- 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

## :books: Further reading
- [Edit_distance](https://en.wikipedia.org/wiki/Edit_distance)
## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-potential-component-option-typo.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-potential-component-option-typo.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -67,6 +67,7 @@ module.exports = {
'no-multi-spaces': require('./rules/no-multi-spaces'),
'no-multiple-template-root': require('./rules/no-multiple-template-root'),
'no-parsing-error': require('./rules/no-parsing-error'),
'no-potential-component-option-typo': require('./rules/no-potential-component-option-typo'),
'no-ref-as-operand': require('./rules/no-ref-as-operand'),
'no-reserved-component-names': require('./rules/no-reserved-component-names'),
'no-reserved-keys': require('./rules/no-reserved-keys'),
Expand Down
108 changes: 108 additions & 0 deletions lib/rules/no-potential-component-option-typo.js
@@ -0,0 +1,108 @@
/**
* @fileoverview detect if there is a potential typo in your component property
* @author IWANABETHATGUY
*/
'use strict'

const utils = require('../utils')
const vueComponentOptions = require('../utils/vue-component-options.json')
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow a potential typo in your component property',
categories: undefined,
recommended: false,
url: 'https://eslint.vuejs.org/rules/no-potential-component-option-typo.html'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
presets: {
type: 'array',
items: {
type: 'string',
enum: ['all', 'vue', 'vue-router', 'nuxt']
},
uniqueItems: true,
minItems: 0
},
custom: {
type: 'array',
minItems: 0,
items: { type: 'string' },
uniqueItems: true
},
threshold: {
type: 'number',
'minimum': 1
}
}
}
]
},

create: function (context) {
const option = context.options[0] || {}
const custom = option['custom'] || []
const presets = option['presets'] || ['vue']
const threshold = option['threshold'] || 1
let candidateOptions
if (presets.includes('all')) {
candidateOptions = Object.keys(vueComponentOptions).reduce((pre, cur) => {
return [...pre, ...vueComponentOptions[cur]]
}, [])
} else {
candidateOptions = presets.reduce((pre, cur) => {
return [...pre, ...vueComponentOptions[cur]]
}, [])
}
const candidateOptionSet = new Set([...candidateOptions, ...custom])
const candidateOptionList = [...candidateOptionSet]
if (!candidateOptionList.length) {
return {}
}
return utils.executeOnVue(context, obj => {
const componentInstanceOptions = obj.properties.filter(
p => p.type === 'Property' && p.key.type === 'Identifier'
)
if (!componentInstanceOptions.length) {
return {}
}
componentInstanceOptions.forEach(option => {
const id = option.key
const name = id.name
if (candidateOptionSet.has(name)) {
return
}
const potentialTypoList = candidateOptionList
.map(o => ({ option: o, distance: utils.editDistance(o, name) }))
.filter(({ distance, option }) => distance <= threshold && distance > 0)
.sort((a, b) => a.distance - b.distance)
if (potentialTypoList.length) {
context.report({
node: id,
loc: id.loc,
message: `'{{name}}' may be a typo, which is similar to option [{{option}}].`,
data: {
name,
option: potentialTypoList.map(({ option }) => option).join(',')
},
suggest: potentialTypoList.map(({ option }) => ({
desc: `Replace property '${name}' to '${option}'`,
fix (fixer) {
return fixer.replaceText(id, option)
}
}))
})
}
})
})
}
}
32 changes: 32 additions & 0 deletions lib/utils/index.js
Expand Up @@ -914,6 +914,38 @@ module.exports = {
return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
},

/**
* return two string editdistance
* @param {string} a string a to compare
* @param {string} b string b to compare
* @returns {number}
*/
editDistance (a, b) {
if (a === b) {
return 0
}
const alen = a.length
const blen = b.length
const dp = Array.from({ length: alen + 1 }).map(_ =>
Array.from({ length: blen + 1 }).fill(0)
)
for (let i = 0; i <= alen; i++) {
dp[i][0] = i
}
for (let j = 0; j <= blen; j++) {
dp[0][j] = j
}
for (let i = 1; i <= alen; i++) {
for (let j = 1; j <= blen; j++) {
if (a[i - 1] === b[j - 1]) {
dp[i][j] = dp[i - 1][j - 1]
} else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
}
}
}
return dp[alen][blen]
},
/**
* Unwrap typescript types like "X as F"
* @template T
Expand Down
47 changes: 47 additions & 0 deletions lib/utils/vue-component-options.json
@@ -0,0 +1,47 @@
{
"nuxt": ["asyncData", "fetch", "head", "key", "layout", "loading", "middleware", "scrollToTop", "transition", "validate", "watchQuery"],
"vue-router": [
"beforeRouteEnter",
"beforeRouteUpdate",
"beforeRouteLeave"
],
"vue": [
"data",
"props",
"propsData",
"computed",
"methods",
"watch",
"el",
"template",
"render",
"renderError",
"staticRenderFns",
"beforeCreate",
"created",
"beforeDestroy",
"destroyed",
"beforeMount",
"mounted",
"beforeUpdate",
"updated",
"activated",
"deactivated",
"errorCaptured",
"serverPrefetch",
"directives",
"components",
"transitions",
"filters",
"provide",
"inject",
"model",
"parent",
"mixins",
"name",
"extends",
"delimiters",
"comments",
"inheritAttrs"
]
}

0 comments on commit d2c94a9

Please sign in to comment.