diff --git a/docs/rules/README.md b/docs/rules/README.md
index e184873e5..45fad1ea6 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -269,6 +269,7 @@ For example:
| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
| [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 | |
diff --git a/docs/rules/no-potential-component-option-typo.md b/docs/rules/no-potential-component-option-typo.md
new file mode 100644
index 000000000..91256a7d0
--- /dev/null
+++ b/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']}]}
+```
+
+
+
+```vue
+
+```
+
+
+
+> 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}]}
+```
+
+
+
+```vue
+
+```
+
+
+
+## :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)
diff --git a/lib/index.js b/lib/index.js
index 10e2e8766..565e4277a 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -53,6 +53,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'),
diff --git a/lib/rules/no-potential-component-option-typo.js b/lib/rules/no-potential-component-option-typo.js
new file mode 100644
index 000000000..13d18f57e
--- /dev/null
+++ b/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)
+ }
+ }))
+ })
+ }
+ })
+ })
+ }
+}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index e784f2ab3..574c02f0d 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -868,6 +868,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
diff --git a/lib/utils/vue-component-options.json b/lib/utils/vue-component-options.json
new file mode 100644
index 000000000..643e1299e
--- /dev/null
+++ b/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"
+ ]
+}
\ No newline at end of file
diff --git a/tests/lib/rules/no-potential-component-option-typo.js b/tests/lib/rules/no-potential-component-option-typo.js
new file mode 100644
index 000000000..12f8ed04e
--- /dev/null
+++ b/tests/lib/rules/no-potential-component-option-typo.js
@@ -0,0 +1,452 @@
+/**
+ * @fileoverview detect if there is a potential typo in your component property
+ * @author IWANABETHATGUY
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-potential-component-option-typo')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+})
+
+tester.run('no-potential-component-option-typo', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ // because vue preset is include by default, set the presets to empty
+ options: [{ presets: [] }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ presets: ['vue'] }]
+ },
+ // test if give preset and the potentialTypoList length is zero, just for 100% test cover
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ presets: ['vue'] }]
+ },
+ // multi preset that won't report
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ presets: ['vue', 'vue-router'] }]
+ },
+ // test custom option that is not available in the presets
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ custom: ['custom', 'foo'] }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ custom: ['abcde', 'abcd'] }]
+ },
+ // valid test case set custom and threshold
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ custom: ['custom', 'foo'], threshold: 2 }]
+ },
+ // test all valid vue options
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [{ presets: ['all'] }]
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+ `,
+ errors: [
+ {
+ message: "'dat' may be a typo, which is similar to option [data].",
+ line: 4,
+ column: 9,
+ suggestions: [
+ {
+ desc: `Replace property 'dat' to 'data'`,
+ output: `
+ `
+ }
+ ]
+ },
+ {
+ message: `'method' may be a typo, which is similar to option [methods].`,
+ line: 5,
+ column: 9,
+ suggestions: [
+ {
+ desc: `Replace property 'method' to 'methods'`,
+ output: `
+ `
+ }
+ ]
+ }
+ ],
+ options: [{ custom: ['data', 'methods'] }]
+ },
+ // test if user define custom rule is duplicate with presets
+ // test custom option that is not available in the presets
+ {
+ filename: 'test.vue',
+ code: `
+ `,
+ errors: [
+ {
+ message: "'dat' may be a typo, which is similar to option [data].",
+ line: 4,
+ column: 9,
+ suggestions: [
+ {
+ desc: `Replace property 'dat' to 'data'`,
+ output: `
+ `
+ }
+ ]
+ },
+ {
+ message: `'method' may be a typo, which is similar to option [methods].`,
+ line: 5,
+ column: 9,
+ suggestions: [
+ {
+ desc: `Replace property 'method' to 'methods'`,
+ output: `
+ `
+ }
+ ]
+ },
+ {
+ message: `'custo' may be a typo, which is similar to option [custom].`,
+ line: 6,
+ column: 9,
+ suggestions: [
+ {
+ desc: `Replace property 'custo' to 'custom'`,
+ output: `
+ `
+ }
+ ]
+ }
+ ],
+ options: [
+ { custom: ['data', 'methods', 'custom', 'foo'], presets: ['all'] }
+ ]
+ },
+ // test if report correctly, only have preset option
+ {
+ filename: 'test.vue',
+ code: `
+ `,
+ errors: [
+ {
+ message: "'dat' may be a typo, which is similar to option [data].",
+ line: 4,
+ column: 9,
+ suggestions: [
+ {
+ desc: `Replace property 'dat' to 'data'`,
+ output: `
+ `
+ }
+ ]
+ },
+ {
+ message: `'method' may be a typo, which is similar to option [methods].`,
+ line: 5,
+ column: 9,
+ suggestions: [
+ {
+ desc: `Replace property 'method' to 'methods'`,
+ output: `
+ `
+ }
+ ]
+ }
+ ],
+ options: [{ presets: ['vue'] }]
+ },
+ // multi preset report typo
+ {
+ filename: 'test.vue',
+ code: `
+ `,
+ errors: [
+ {
+ message: "'dat' may be a typo, which is similar to option [data].",
+ line: 4,
+ column: 9,
+ suggestions: [
+ {
+ desc: `Replace property 'dat' to 'data'`,
+ output: `
+ `
+ }
+ ]
+ },
+ {
+ message:
+ "'beforeRouteEntr' may be a typo, which is similar to option [beforeRouteEnter].",
+ line: 5,
+ column: 9,
+ suggestions: [
+ {
+ desc: `Replace property 'beforeRouteEntr' to 'beforeRouteEnter'`,
+ output: `
+ `
+ }
+ ]
+ },
+ {
+ message: `'method' may be a typo, which is similar to option [methods].`,
+ line: 6,
+ column: 9,
+ suggestions: [
+ {
+ desc: `Replace property 'method' to 'methods'`,
+ output: `
+ `
+ }
+ ]
+ }
+ ],
+ options: [{ presets: ['vue', 'vue-router'] }]
+ },
+ // test multi suggestion
+ {
+ filename: 'test.vue',
+ code: `
+ `,
+ errors: [
+ {
+ message: `'method' may be a typo, which is similar to option [methods,data].`,
+ line: 4,
+ column: 9,
+ suggestions: [
+ {
+ desc: `Replace property 'method' to 'methods'`,
+ output: `
+ `
+ },
+ {
+ desc: `Replace property 'method' to 'data'`,
+ output: `
+ `
+ }
+ ]
+ }
+ ],
+ options: [{ custom: ['data', 'methods'], threshold: 10, presets: [] }]
+ }
+ ]
+})
diff --git a/tests/lib/utils/index.js b/tests/lib/utils/index.js
index f698a98ef..219c94d35 100644
--- a/tests/lib/utils/index.js
+++ b/tests/lib/utils/index.js
@@ -367,3 +367,17 @@ describe('getComponentProps', () => {
assert.notOk(props[3].value)
})
})
+
+describe('editdistance', () => {
+ const editDistance = utils.editDistance
+ it('should return editDistance beteen two string', () => {
+ assert.equal(editDistance('book', 'back'), 2)
+ assert.equal(editDistance('methods', 'metho'), 2)
+ assert.equal(editDistance('methods', 'metds'), 2)
+ assert.equal(editDistance('computed', 'comput'), 2)
+ assert.equal(editDistance('book', 'back'), 2)
+ assert.equal(editDistance('methods', 'method'), 1)
+ assert.equal(editDistance('methods', 'methds'), 1)
+ assert.equal(editDistance('computed', 'computd'), 1)
+ })
+})