From 9c0b56bf99a8b89c71d9a372f8908a1401dedb05 Mon Sep 17 00:00:00 2001 From: Ivan Kopeykin Date: Sat, 31 Aug 2019 16:05:55 +0300 Subject: [PATCH 1/4] add levenshteinDistance util function --- .../__tests__/levenshteinDistance.test.js | 34 +++++++++++++ lib/utils/levenshteinDistance.js | 51 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 lib/utils/__tests__/levenshteinDistance.test.js create mode 100644 lib/utils/levenshteinDistance.js diff --git a/lib/utils/__tests__/levenshteinDistance.test.js b/lib/utils/__tests__/levenshteinDistance.test.js new file mode 100644 index 0000000000..2d167557d0 --- /dev/null +++ b/lib/utils/__tests__/levenshteinDistance.test.js @@ -0,0 +1,34 @@ +"use strict"; + +const levenshteinDistance = require("../levenshteinDistance"); + +it("return -1 if max distance exited", () => { + expect(levenshteinDistance("aaaaa aaaaa", "aaaaa", 5)).toBe(-1); + expect(levenshteinDistance("a", "a", 0)).toBe(0); + expect(levenshteinDistance("a", "aa", 0)).toBe(-1); +}); + +it("same strings return 0", () => { + expect(levenshteinDistance("", "")).toBe(0); + expect(levenshteinDistance("a", "a")).toBe(0); + expect(levenshteinDistance("aaaa", "aaaa")).toBe(0); + expect(levenshteinDistance("aa aa", "aa aa")).toBe(0); +}); + +it("common test cases", () => { + expect(levenshteinDistance("funcsion", "function")).toBe(1); + expect(levenshteinDistance("funtion", "function")).toBe(1); + expect(levenshteinDistance("function-a", "a-function")).toBe(4); + expect(levenshteinDistance("allow", "allows")).toBe(1); + expect(levenshteinDistance("block-no-empty", "block-emty-no")).toBe(7); + expect(levenshteinDistance("comments", "coment")).toBe(2); +}); + +it("max distance works properly", () => { + expect(levenshteinDistance("blabla", "albalb", 4)).toBe(4); + expect(levenshteinDistance("comments", "comment", 1)).toBe(1); + expect(levenshteinDistance("comments", "coment", 1)).toBe(-1); + expect( + levenshteinDistance("duplicate-no-sorce", "no-duplicate-source", 3) + ).toBe(-1); +}); diff --git a/lib/utils/levenshteinDistance.js b/lib/utils/levenshteinDistance.js new file mode 100644 index 0000000000..e49557ee2e --- /dev/null +++ b/lib/utils/levenshteinDistance.js @@ -0,0 +1,51 @@ +/* @flow */ +"use strict"; + +/** + * Compute levenshtein distance, see https://en.wikipedia.org/wiki/Levenshtein_distance for more info + * @param {string} firstStr first string + * @param {string} secondStr second string + * @param {number} [maxDistance=99] maximum distance, if distance is greater then returns -1 + * @returns {number} computed distance + */ +module.exports = function levenshteinDistance( + firstStr /*: string*/, + secondStr /*: string*/, + maxDistance /*: number*/ = 99 +) /*: number*/ { + if (Math.abs(firstStr.length - secondStr.length) > maxDistance) return -1; + + if (firstStr.length === 0 || secondStr.length === 0) + return firstStr.length || secondStr.length; + + let line = new Uint32Array(firstStr.length + 1); + + for (let i = 0; i < line.length; i++) { + line[i] = i; + } + + for (let i = 0; i < secondStr.length; i++) { + const newLine = line.slice(); + + newLine[0] = i + 1; + let min = newLine[0]; + + for (let j = 1; j < line.length; j++) { + newLine[j] = Math.min( + line[j - 1] + (secondStr[i] === firstStr[j - 1] ? 0 : 1), + line[j] + 1, + newLine[j - 1] + 1 + ); + + if (newLine[j] < min) min = newLine[j]; + } + + if (min > maxDistance) { + return -1; + } + + line = newLine; + } + + return line[line.length - 1] > maxDistance ? -1 : line[line.length - 1]; +}; From 1cb3b1d52bd8ade5acc4ad3d5ba68a3cb50b36a2 Mon Sep 17 00:00:00 2001 From: Ivan Kopeykin Date: Sat, 31 Aug 2019 17:43:52 +0300 Subject: [PATCH 2/4] feat(reportUnknownRuleNames): --- lib/__tests__/postcssPlugin.test.js | 15 +++-- lib/__tests__/reportUnknownRuleNames.test.js | 64 +++++++++++++++++++ lib/augmentConfig.js | 15 +++-- lib/lintSource.js | 12 +++- lib/reportUnknownRuleNames.js | 67 ++++++++++++++++++++ 5 files changed, 159 insertions(+), 14 deletions(-) create mode 100644 lib/__tests__/reportUnknownRuleNames.test.js create mode 100644 lib/reportUnknownRuleNames.js diff --git a/lib/__tests__/postcssPlugin.test.js b/lib/__tests__/postcssPlugin.test.js index 6207e4dfc1..f7af3c3959 100644 --- a/lib/__tests__/postcssPlugin.test.js +++ b/lib/__tests__/postcssPlugin.test.js @@ -68,11 +68,16 @@ it("`configFile` option with undefined rule", () => { return postcssPlugin .process("a {}", { from: undefined }, config) - .then(() => { - throw new Error("should not have succeeded"); - }) - .catch(err => { - expect(err).toEqual(configurationError(`Undefined rule ${ruleName}`)); + .then(result => { + expect(result.messages).toContainEqual( + expect.objectContaining({ + line: 1, + column: 1, + rule: ruleName, + text: `Unknown rule ${ruleName}.`, + severity: "error" + }) + ); }); }); diff --git a/lib/__tests__/reportUnknownRuleNames.test.js b/lib/__tests__/reportUnknownRuleNames.test.js new file mode 100644 index 0000000000..f9455395a0 --- /dev/null +++ b/lib/__tests__/reportUnknownRuleNames.test.js @@ -0,0 +1,64 @@ +"use strict"; + +const standalone = require("../standalone"); + +it("test case (1)", () => { + const config = { + rules: { + "color-hex-cas": ["upper"], + "function-whitelst": ["scale"] + } + }; + + return standalone({ + config, + code: "a {}" + }).then(linted => { + expect(linted.results[0].warnings).toHaveLength(2); + expect(linted.results[0].warnings).toContainEqual({ + line: 1, + column: 1, + severity: "error", + rule: "color-hex-cas", + text: "Unknown rule color-hex-cas. May be you mean color-hex-case." + }); + expect(linted.results[0].warnings).toContainEqual({ + line: 1, + column: 1, + severity: "error", + rule: "function-whitelst", + text: + "Unknown rule function-whitelst. May be you mean function-whitelist." + }); + }); +}); + +it("test case (2)", () => { + const config = { + rules: { + "color-hex-case": ["upper"], + "function-whitelst": ["rgb"] + } + }; + + return standalone({ + config, + code: "a { color: #fff; transform: scale(0.7); }" + }).then(linted => { + expect(linted.results[0].warnings).toHaveLength(2); + expect(linted.results[0].warnings).toContainEqual({ + line: 1, + column: 1, + severity: "error", + rule: "function-whitelst", + text: + "Unknown rule function-whitelst. May be you mean function-whitelist." + }); + expect(linted.results[0].warnings).toContainEqual( + expect.objectContaining({ + severity: "error", + rule: "color-hex-case" + }) + ); + }); +}); diff --git a/lib/augmentConfig.js b/lib/augmentConfig.js index 76c657bc47..2260d504c9 100644 --- a/lib/augmentConfig.js +++ b/lib/augmentConfig.js @@ -320,15 +320,16 @@ function normalizeAllRuleSettings( requireRule(ruleName) || _.get(config, ["pluginFunctions", ruleName]); if (!rule) { - throw configurationError(`Undefined rule ${ruleName}`); + normalizedRules[ruleName] = []; + } else { + normalizedRules[ruleName] = normalizeRuleSettings( + rawRuleSettings, + ruleName, + _.get(rule, "primaryOptionArray") + ); } - - normalizedRules[ruleName] = normalizeRuleSettings( - rawRuleSettings, - ruleName, - _.get(rule, "primaryOptionArray") - ); }); + config.rules = normalizedRules; return config; diff --git a/lib/lintSource.js b/lib/lintSource.js index 548a217dfa..6bf37a67aa 100644 --- a/lib/lintSource.js +++ b/lib/lintSource.js @@ -3,9 +3,9 @@ const _ = require("lodash"); const assignDisabledRanges = require("./assignDisabledRanges"); -const configurationError = require("./utils/configurationError"); const getOsEol = require("./utils/getOsEol"); const path = require("path"); +const reportUnknownRuleNames = require("./reportUnknownRuleNames"); const requireRule = require("./requireRule"); const rulesOrder = require("./rules"); @@ -205,7 +205,15 @@ function lintPostcssResult( requireRule(ruleName) || _.get(config, ["pluginFunctions", ruleName]); if (ruleFunction === undefined) { - throw configurationError(`Undefined rule ${ruleName}`); + performRules.push( + Promise.all( + postcssRoots.map(postcssRoot => + reportUnknownRuleNames([ruleName], postcssRoot, postcssResult) + ) + ) + ); + + return; } const ruleSettings = _.get(config, ["rules", ruleName]); diff --git a/lib/reportUnknownRuleNames.js b/lib/reportUnknownRuleNames.js new file mode 100644 index 0000000000..b7283a9efd --- /dev/null +++ b/lib/reportUnknownRuleNames.js @@ -0,0 +1,67 @@ +"use strict"; + +const levenshteinDistance = require("./utils/levenshteinDistance"); +const rules = require("./rules"); + +const MAX_LEVENSHTEIN_DISTANCE = 6; + +function extractSuggestions(ruleName) { + const suggestions = new Array(MAX_LEVENSHTEIN_DISTANCE); + + for (let i = 0; i < suggestions.length; i++) { + suggestions[i] = []; + } + + rules.forEach(existRuleName => { + const distance = levenshteinDistance( + existRuleName, + ruleName, + MAX_LEVENSHTEIN_DISTANCE + ); + + if (distance > 0) { + suggestions[distance - 1].push(existRuleName); + } + }); + + let result = []; + + for (let i = 0; i < suggestions.length; i++) { + if (suggestions[i].length > 0) { + if (i < 3) { + return suggestions[i].slice(0, 3); + } + + result = result.concat(suggestions[i]); + } + } + + return result.slice(0, 3); +} + +function rejectMessage(ruleName, suggestions = []) { + return ( + `Unknown rule ${ruleName}.` + + (suggestions.length > 0 + ? ` May be you mean ${suggestions.join(", ")}.` + : "") + ); +} + +module.exports = function reportUnknownRuleNames( + unknownRules, + postcssRoot, + postcssResult +) { + unknownRules.forEach(ruleName => { + const suggestions = extractSuggestions(ruleName); + const warningProperties /*: Object*/ = { + severity: "error", + rule: ruleName, + node: postcssRoot, + index: 0 + }; + + postcssResult.warn(rejectMessage(ruleName, suggestions), warningProperties); + }); +}; From 7ef4043e884e5434e1f9d0d8c0710a16c9e95961 Mon Sep 17 00:00:00 2001 From: Ivan Kopeykin Date: Sun, 1 Sep 2019 14:01:03 +0300 Subject: [PATCH 3/4] feat(reportUnknownRuleNames): using cache for suggestions --- lib/reportUnknownRuleNames.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/reportUnknownRuleNames.js b/lib/reportUnknownRuleNames.js index b7283a9efd..703195e6d3 100644 --- a/lib/reportUnknownRuleNames.js +++ b/lib/reportUnknownRuleNames.js @@ -48,13 +48,17 @@ function rejectMessage(ruleName, suggestions = []) { ); } +const cache = new Map(); + module.exports = function reportUnknownRuleNames( unknownRules, postcssRoot, postcssResult ) { unknownRules.forEach(ruleName => { - const suggestions = extractSuggestions(ruleName); + const suggestions = cache.has(ruleName) + ? cache.get(ruleName) + : extractSuggestions(ruleName); const warningProperties /*: Object*/ = { severity: "error", rule: ruleName, @@ -62,6 +66,7 @@ module.exports = function reportUnknownRuleNames( index: 0 }; + cache.set(ruleName, suggestions); postcssResult.warn(rejectMessage(ruleName, suggestions), warningProperties); }); }; From 1aaddea0d26fd77f545d0475c09f297f0c033ba9 Mon Sep 17 00:00:00 2001 From: Ivan Kopeykin Date: Wed, 4 Sep 2019 17:58:33 +0300 Subject: [PATCH 4/4] apply discussions --- lib/__tests__/reportUnknownRuleNames.test.js | 8 ++--- lib/lintSource.js | 2 +- lib/reportUnknownRuleNames.js | 33 +++++++++---------- .../__tests__/levenshteinDistance.test.js | 2 ++ 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/lib/__tests__/reportUnknownRuleNames.test.js b/lib/__tests__/reportUnknownRuleNames.test.js index f9455395a0..b3199865dd 100644 --- a/lib/__tests__/reportUnknownRuleNames.test.js +++ b/lib/__tests__/reportUnknownRuleNames.test.js @@ -20,15 +20,14 @@ it("test case (1)", () => { column: 1, severity: "error", rule: "color-hex-cas", - text: "Unknown rule color-hex-cas. May be you mean color-hex-case." + text: "Unknown rule color-hex-cas. Did you mean color-hex-case?" }); expect(linted.results[0].warnings).toContainEqual({ line: 1, column: 1, severity: "error", rule: "function-whitelst", - text: - "Unknown rule function-whitelst. May be you mean function-whitelist." + text: "Unknown rule function-whitelst. Did you mean function-whitelist?" }); }); }); @@ -51,8 +50,7 @@ it("test case (2)", () => { column: 1, severity: "error", rule: "function-whitelst", - text: - "Unknown rule function-whitelst. May be you mean function-whitelist." + text: "Unknown rule function-whitelst. Did you mean function-whitelist?" }); expect(linted.results[0].warnings).toContainEqual( expect.objectContaining({ diff --git a/lib/lintSource.js b/lib/lintSource.js index 6bf37a67aa..46345898d6 100644 --- a/lib/lintSource.js +++ b/lib/lintSource.js @@ -208,7 +208,7 @@ function lintPostcssResult( performRules.push( Promise.all( postcssRoots.map(postcssRoot => - reportUnknownRuleNames([ruleName], postcssRoot, postcssResult) + reportUnknownRuleNames(ruleName, postcssRoot, postcssResult) ) ) ); diff --git a/lib/reportUnknownRuleNames.js b/lib/reportUnknownRuleNames.js index 703195e6d3..dd6f86a4e1 100644 --- a/lib/reportUnknownRuleNames.js +++ b/lib/reportUnknownRuleNames.js @@ -42,31 +42,30 @@ function extractSuggestions(ruleName) { function rejectMessage(ruleName, suggestions = []) { return ( `Unknown rule ${ruleName}.` + - (suggestions.length > 0 - ? ` May be you mean ${suggestions.join(", ")}.` - : "") + (suggestions.length > 0 ? ` Did you mean ${suggestions.join(", ")}?` : "") ); } const cache = new Map(); module.exports = function reportUnknownRuleNames( - unknownRules, + unknownRuleName, postcssRoot, postcssResult ) { - unknownRules.forEach(ruleName => { - const suggestions = cache.has(ruleName) - ? cache.get(ruleName) - : extractSuggestions(ruleName); - const warningProperties /*: Object*/ = { - severity: "error", - rule: ruleName, - node: postcssRoot, - index: 0 - }; + const suggestions = cache.has(unknownRuleName) + ? cache.get(unknownRuleName) + : extractSuggestions(unknownRuleName); + const warningProperties = { + severity: "error", + rule: unknownRuleName, + node: postcssRoot, + index: 0 + }; - cache.set(ruleName, suggestions); - postcssResult.warn(rejectMessage(ruleName, suggestions), warningProperties); - }); + cache.set(unknownRuleName, suggestions); + postcssResult.warn( + rejectMessage(unknownRuleName, suggestions), + warningProperties + ); }; diff --git a/lib/utils/__tests__/levenshteinDistance.test.js b/lib/utils/__tests__/levenshteinDistance.test.js index 2d167557d0..e8318f7112 100644 --- a/lib/utils/__tests__/levenshteinDistance.test.js +++ b/lib/utils/__tests__/levenshteinDistance.test.js @@ -16,6 +16,8 @@ it("same strings return 0", () => { }); it("common test cases", () => { + expect(levenshteinDistance("", "function")).toBe(8); + expect(levenshteinDistance("function", "")).toBe(8); expect(levenshteinDistance("funcsion", "function")).toBe(1); expect(levenshteinDistance("funtion", "function")).toBe(1); expect(levenshteinDistance("function-a", "a-function")).toBe(4);