From 69393bd0edd1e9ad64a6028a2660a3a1dc958f2b Mon Sep 17 00:00:00 2001 From: IWANABETHATGUY <974153916@qq.com> Date: Mon, 9 Mar 2020 13:27:25 +0800 Subject: [PATCH 01/21] feat(utils/index.js): add util lib --- lib/utils/index.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/lib/utils/index.js b/lib/utils/index.js index 011ec6571..35d1132e1 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -867,5 +867,44 @@ module.exports = { */ unwrapTypes (node) { return node.type === 'TSAsExpression' ? node.expression : node + }, + + /** + * + * + * @param {string} a string a to compare + * @param {any} b string b to compare + * @param {number} [threshold=1] + * @returns if these two string editdistance lessEqual than threshold, + * usually if the answer is true, like `method` `methods`, we + * could say this is a potential typo. + */ + isEditDistanceLessEqualThanThreshold (a, b, threshold = 1) { + 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++) { + let min = threshold + 1 + 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 + } + min = Math.min(min, dp[i][j]) + } + if (min === threshold + 1) { + return false + } + } + return dp[alen][blen] <= threshold } } From 2773283bbad9bcdbb2b56246b1f9e633ac2248a3 Mon Sep 17 00:00:00 2001 From: IWANABETHATGUY <974153916@qq.com> Date: Mon, 9 Mar 2020 13:38:54 +0800 Subject: [PATCH 02/21] feat(tests/lib/utils/index.js): add unit test --- tests/lib/utils/index.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/lib/utils/index.js b/tests/lib/utils/index.js index f698a98ef..6e1fadb78 100644 --- a/tests/lib/utils/index.js +++ b/tests/lib/utils/index.js @@ -367,3 +367,20 @@ describe('getComponentProps', () => { assert.notOk(props[3].value) }) }) + +describe('isEditDistanceLessEqualThanThreshold', () => { + const isEditDistanceLessEqualThanThreshold = utils.isEditDistanceLessEqualThanThreshold + it('should return false if two string editdistance greater than threshold', () => { + assert.equal(isEditDistanceLessEqualThanThreshold('book', 'back'), false) + assert.equal(isEditDistanceLessEqualThanThreshold('methods', 'metho'), false) + assert.equal(isEditDistanceLessEqualThanThreshold('methods', 'metds'), false) + assert.equal(isEditDistanceLessEqualThanThreshold('computed', 'comput'), false) + }) + + it('should return true if two string editdistance lessEqual than threshold', () => { + assert.equal(isEditDistanceLessEqualThanThreshold('book', 'back', 2), true) + assert.equal(isEditDistanceLessEqualThanThreshold('methods', 'method'), true) + assert.equal(isEditDistanceLessEqualThanThreshold('methods', 'methds'), true) + assert.equal(isEditDistanceLessEqualThanThreshold('computed', 'computd'), true) + }) +}) From 9d9724e2a56c2d8e75c14576ae6abd4ac9a65378 Mon Sep 17 00:00:00 2001 From: IWANABETHATGUY <974153916@qq.com> Date: Mon, 9 Mar 2020 22:43:01 +0800 Subject: [PATCH 03/21] feat: change test, complete rule --- docs/rules/no-potential-property-typo.md | 36 +++++++++ lib/rules/no-potential-property-typo.js | 76 +++++++++++++++++++ lib/utils/index.js | 22 ++---- lib/utils/vue-component-options.json | 39 ++++++++++ tests/lib/rules/no-potential-property-typo.js | 29 +++++++ tests/lib/utils/index.js | 25 +++--- 6 files changed, 199 insertions(+), 28 deletions(-) create mode 100644 docs/rules/no-potential-property-typo.md create mode 100644 lib/rules/no-potential-property-typo.js create mode 100644 lib/utils/vue-component-options.json create mode 100644 tests/lib/rules/no-potential-property-typo.js diff --git a/docs/rules/no-potential-property-typo.md b/docs/rules/no-potential-property-typo.md new file mode 100644 index 000000000..fc997a862 --- /dev/null +++ b/docs/rules/no-potential-property-typo.md @@ -0,0 +1,36 @@ +# detect if there is a potential typo in your component property (no-potential-property-typo) + +Please describe the origin of the rule here. + + +## Rule Details + +This rule aims to... + +Examples of **incorrect** code for this rule: + +```js + +// fill me in + +``` + +Examples of **correct** code for this rule: + +```js + +// fill me in + +``` + +### Options + +If there are any options, describe them here. Otherwise, delete this section. + +## When Not To Use It + +Give a short description of when it would be appropriate to turn off this rule. + +## Further Reading + +If there are other links that describe the issue this rule addresses, please include them here in a bulleted list. diff --git a/lib/rules/no-potential-property-typo.js b/lib/rules/no-potential-property-typo.js new file mode 100644 index 000000000..60e3f130b --- /dev/null +++ b/lib/rules/no-potential-property-typo.js @@ -0,0 +1,76 @@ +/** + * @fileoverview detect if there is a potential typo in your component property + * @author IWANABETHATGUY + */ +'use strict' + +const { executeOnVue, editDistance } = 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', + category: 'essential', + recommended: false, + url: 'https://eslint.vuejs.org/rules/no-potential-property-typo.html' + }, + fixable: null, + schema: [ + // fill in your schema + ] + }, + + create: function (context) { + // variables should be defined here + + // ---------------------------------------------------------------------- + // Helpers + // ---------------------------------------------------------------------- + + // any helper functions should go here or else delete this section + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + return executeOnVue(context, obj => { + // TODO: threshold is an option + const threshold = 1 + const componentInstanceOptions = obj.properties.filter( + p => p.type === 'Property' && p.key.type === 'Identifier' + ) + if (!componentInstanceOptions.length) { + return + } + componentInstanceOptions.forEach(option => { + const name = option.key.name + const potentialTypoList = vueComponentOptions + .map(o => ({ option: o, distance: editDistance(o, name) })) + .filter(({ distance }) => distance > threshold || distance === 0) + .sort((a, b) => a.distance - b.distance) + if (potentialTypoList.length) { + context.report({ + node: option.key, + loc: option.key.loc, + message: `'{{name}}' may be a typo, which is similar to vue component option {{option}}.`, + data: { + name, + option: potentialTypoList.map(({ option }) => option).join(',') + }, + suggestion: potentialTypoList.map(({ option }) => ({ + desc: `Replace property ${name} to ${option}`, + fix (fixer) { + return fixer.replaceText(option.key, option) + } + })) + }) + } + }) + }) + } +} diff --git a/lib/utils/index.js b/lib/utils/index.js index 35d1132e1..0db1cbe34 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -870,16 +870,15 @@ module.exports = { }, /** - * - * + * return two string editdistance * @param {string} a string a to compare - * @param {any} b string b to compare - * @param {number} [threshold=1] - * @returns if these two string editdistance lessEqual than threshold, - * usually if the answer is true, like `method` `methods`, we - * could say this is a potential typo. + * @param {string} b string b to compare + * @returns {number} */ - isEditDistanceLessEqualThanThreshold (a, b, threshold = 1) { + editDistance (a, b) { + if (a === b) { + return 0 + } const alen = a.length const blen = b.length const dp = Array.from({ length: alen + 1 }).map(_ => @@ -892,19 +891,14 @@ module.exports = { dp[0][j] = j } for (let i = 1; i <= alen; i++) { - let min = threshold + 1 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 } - min = Math.min(min, dp[i][j]) - } - if (min === threshold + 1) { - return false } } - return dp[alen][blen] <= threshold + return dp[alen][blen] } } diff --git a/lib/utils/vue-component-options.json b/lib/utils/vue-component-options.json new file mode 100644 index 000000000..21cecbfb3 --- /dev/null +++ b/lib/utils/vue-component-options.json @@ -0,0 +1,39 @@ +[ + "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-property-typo.js b/tests/lib/rules/no-potential-property-typo.js new file mode 100644 index 000000000..ed0058d28 --- /dev/null +++ b/tests/lib/rules/no-potential-property-typo.js @@ -0,0 +1,29 @@ +/** + * @fileoverview detect if there is a potential typo in your component property + * @author IWANABETHATGUY + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +var rule = require('../../../lib/rules/no-potential-property-typo') + +var RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +var ruleTester = new RuleTester() +ruleTester.run('no-potential-property-typo', rule, { + + valid: [ + + // give me some code that won't trigger a warning + ], + + invalid: [ + ] +}) diff --git a/tests/lib/utils/index.js b/tests/lib/utils/index.js index 6e1fadb78..219c94d35 100644 --- a/tests/lib/utils/index.js +++ b/tests/lib/utils/index.js @@ -368,19 +368,16 @@ describe('getComponentProps', () => { }) }) -describe('isEditDistanceLessEqualThanThreshold', () => { - const isEditDistanceLessEqualThanThreshold = utils.isEditDistanceLessEqualThanThreshold - it('should return false if two string editdistance greater than threshold', () => { - assert.equal(isEditDistanceLessEqualThanThreshold('book', 'back'), false) - assert.equal(isEditDistanceLessEqualThanThreshold('methods', 'metho'), false) - assert.equal(isEditDistanceLessEqualThanThreshold('methods', 'metds'), false) - assert.equal(isEditDistanceLessEqualThanThreshold('computed', 'comput'), false) - }) - - it('should return true if two string editdistance lessEqual than threshold', () => { - assert.equal(isEditDistanceLessEqualThanThreshold('book', 'back', 2), true) - assert.equal(isEditDistanceLessEqualThanThreshold('methods', 'method'), true) - assert.equal(isEditDistanceLessEqualThanThreshold('methods', 'methds'), true) - assert.equal(isEditDistanceLessEqualThanThreshold('computed', 'computd'), true) +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) }) }) From b39cff983173489c3b46d0912446f8167e2aecbd Mon Sep 17 00:00:00 2001 From: IWANABETHATGUY <974153916@qq.com> Date: Tue, 10 Mar 2020 14:55:40 +0800 Subject: [PATCH 04/21] feat: add test, add preset, custom option --- lib/rules/no-potential-property-typo.js | 73 +++++++++++----- lib/utils/vue-component-options.json | 86 ++++++++++--------- tests/lib/rules/no-potential-property-typo.js | 64 ++++++++++++-- 3 files changed, 157 insertions(+), 66 deletions(-) diff --git a/lib/rules/no-potential-property-typo.js b/lib/rules/no-potential-property-typo.js index 60e3f130b..c08d6e4c6 100644 --- a/lib/rules/no-potential-property-typo.js +++ b/lib/rules/no-potential-property-typo.js @@ -4,7 +4,7 @@ */ 'use strict' -const { executeOnVue, editDistance } = require('../utils') +const utils = require('../utils') const vueComponentOptions = require('../utils/vue-component-options.json') // ------------------------------------------------------------------------------ // Rule Definition @@ -21,49 +21,80 @@ module.exports = { }, fixable: null, schema: [ - // fill in your 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) { - // variables should be defined here - - // ---------------------------------------------------------------------- - // Helpers - // ---------------------------------------------------------------------- - - // any helper functions should go here or else delete this section - - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- - - return executeOnVue(context, obj => { + const option = context.options[0] || {} + const custom = option['custom'] || [] + const presets = option['presets'] || [] + 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) => { + if (vueComponentOptions[cur]) { + return [...pre, ...vueComponentOptions[cur]] + } + return pre + }, []) + } + candidateOptions = [...new Set(candidateOptions.concat(custom))] + if (!candidateOptions.length) { + return {} + } + return utils.executeOnVue(context, obj => { // TODO: threshold is an option - const threshold = 1 const componentInstanceOptions = obj.properties.filter( p => p.type === 'Property' && p.key.type === 'Identifier' ) if (!componentInstanceOptions.length) { - return + return {} } componentInstanceOptions.forEach(option => { const name = option.key.name - const potentialTypoList = vueComponentOptions - .map(o => ({ option: o, distance: editDistance(o, name) })) + const potentialTypoList = candidateOptions + .map(o => ({ option: o, distance: utils.editDistance(o, name) })) .filter(({ distance }) => distance > threshold || distance === 0) .sort((a, b) => a.distance - b.distance) if (potentialTypoList.length) { context.report({ node: option.key, loc: option.key.loc, - message: `'{{name}}' may be a typo, which is similar to vue component option {{option}}.`, + message: `'{{name}}' may be a typo, which is similar to vue component option '{{option}}'.`, data: { name, option: potentialTypoList.map(({ option }) => option).join(',') }, suggestion: potentialTypoList.map(({ option }) => ({ - desc: `Replace property ${name} to ${option}`, + desc: `Replace property '${name}' to '${option}'`, fix (fixer) { return fixer.replaceText(option.key, option) } diff --git a/lib/utils/vue-component-options.json b/lib/utils/vue-component-options.json index 21cecbfb3..643e1299e 100644 --- a/lib/utils/vue-component-options.json +++ b/lib/utils/vue-component-options.json @@ -1,39 +1,47 @@ -[ - "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 +{ + "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-property-typo.js b/tests/lib/rules/no-potential-property-typo.js index ed0058d28..ca67d6fe0 100644 --- a/tests/lib/rules/no-potential-property-typo.js +++ b/tests/lib/rules/no-potential-property-typo.js @@ -8,22 +8,74 @@ // Requirements // ------------------------------------------------------------------------------ -var rule = require('../../../lib/rules/no-potential-property-typo') +const rule = require('../../../lib/rules/no-potential-property-typo') -var RuleTester = require('eslint').RuleTester +const RuleTester = require('eslint').RuleTester // ------------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------------ -var ruleTester = new RuleTester() -ruleTester.run('no-potential-property-typo', rule, { - +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2017, sourceType: 'module' } +}) +tester.run('no-potential-property-typo', rule, { valid: [ - + { + filename: 'test.vue', + code: ` + export default { + dat: {}, + method: {} + } + ` + } // give me some code that won't trigger a warning ], invalid: [ + { + filename: 'test.vue', + code: ` + export default { + dat: {}, + method: {} + } + `, + options: [{ custom: ['data', 'methods'] }], + errors: [ + `'dat' may be a typo, which is similar to vue component option 'data'.`, + `'method' may be a typo, which is similar to vue component option 'methods'.` + // { + // message: `'dat' may be a typo, which is similar to vue component option 'data'.` + // // suggestions: [ + // // { + // // desc: `Replace property 'dat' to 'data'`, + // // output: ` + // // export default { + // // data: {}, + // // method: {} + // // } + // // ` + // // } + // // ] + // }, + // { + // message: `'method' may be a typo, which is similar to vue component option 'methods'.` + // // suggestions: [ + // // { + // // desc: `Replace property 'method' to 'methods'`, + // // output: ` + // // export default { + // // dat: {}, + // // methods: {} + // // } + // // ` + // // } + // // ] + // } + ] + } ] }) From d16d7cde9072e48dfe579b1b985b99b9bc137af0 Mon Sep 17 00:00:00 2001 From: IWANABETHATGUY <974153916@qq.com> Date: Tue, 10 Mar 2020 16:33:06 +0800 Subject: [PATCH 05/21] feat: add testcase --- docs/rules/README.md | 1 + docs/rules/no-potential-property-typo.md | 16 +- lib/configs/essential.js | 1 + lib/index.js | 1 + lib/rules/no-potential-property-typo.js | 17 +- tests/lib/rules/no-potential-property-typo.js | 190 ++++++++++++++---- 6 files changed, 179 insertions(+), 47 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index ab1f5b220..ce00a14ce 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -42,6 +42,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | [vue/no-dupe-keys](./no-dupe-keys.md) | disallow duplication of field names | | | [vue/no-duplicate-attributes](./no-duplicate-attributes.md) | disallow duplication of attributes | | | [vue/no-parsing-error](./no-parsing-error.md) | disallow parsing errors in `