Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #3147 #4237

Merged
merged 4 commits into from Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 10 additions & 5 deletions lib/__tests__/postcssPlugin.test.js
Expand Up @@ -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"
})
);
});
});

Expand Down
62 changes: 62 additions & 0 deletions lib/__tests__/reportUnknownRuleNames.test.js
@@ -0,0 +1,62 @@
"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. 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. Did 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. Did you mean function-whitelist?"
});
expect(linted.results[0].warnings).toContainEqual(
expect.objectContaining({
severity: "error",
rule: "color-hex-case"
})
);
});
});
15 changes: 8 additions & 7 deletions lib/augmentConfig.js
Expand Up @@ -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;
Expand Down
12 changes: 10 additions & 2 deletions lib/lintSource.js
Expand Up @@ -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");

Expand Down Expand Up @@ -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]);
Expand Down
71 changes: 71 additions & 0 deletions lib/reportUnknownRuleNames.js
@@ -0,0 +1,71 @@
"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 ? ` Did you mean ${suggestions.join(", ")}?` : "")
);
}

const cache = new Map();

module.exports = function reportUnknownRuleNames(
unknownRuleName,
postcssRoot,
postcssResult
) {
const suggestions = cache.has(unknownRuleName)
? cache.get(unknownRuleName)
: extractSuggestions(unknownRuleName);
const warningProperties = {
severity: "error",
rule: unknownRuleName,
node: postcssRoot,
index: 0
};

cache.set(unknownRuleName, suggestions);
postcssResult.warn(
rejectMessage(unknownRuleName, suggestions),
warningProperties
);
};
36 changes: 36 additions & 0 deletions lib/utils/__tests__/levenshteinDistance.test.js
@@ -0,0 +1,36 @@
"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("", "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);
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);
});
51 changes: 51 additions & 0 deletions 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(
hudochenkov marked this conversation as resolved.
Show resolved Hide resolved
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];
};