Skip to content

Commit

Permalink
feat: implement rfc 2021-suppression-support
Browse files Browse the repository at this point in the history
  • Loading branch information
Yiwei-Ding committed Dec 28, 2021
1 parent 51c37b1 commit 88707fa
Show file tree
Hide file tree
Showing 10 changed files with 2,290 additions and 504 deletions.
48 changes: 48 additions & 0 deletions docs/developer-guide/nodejs-api.md
Expand Up @@ -19,13 +19,15 @@ While ESLint is designed to be run on the command line, it's possible to use ESL
* [static getErrorResults()][eslint-geterrorresults]
* [LintResult type][lintresult]
* [LintMessage type][lintmessage]
* [SuppressedLintMessage type][suppressedlintmessage]
* [EditInfo type][editinfo]
* [Formatter type][formatter]
* [SourceCode](#sourcecode)
* [splitLines()](#sourcecodesplitlines)
* [Linter](#linter)
* [verify()](#linterverify)
* [verifyAndFix()](#linterverifyandfix)
* [getSuppressedMessages()](#getsuppressedmessages)
* [defineRule()](#linterdefinerule)
* [defineRules()](#linterdefinerules)
* [getRules()](#lintergetrules)
Expand Down Expand Up @@ -347,6 +349,8 @@ The `LintResult` value is the information of the linting result of each file. Th
The absolute path to the file of this result. This is the string `"<text>"` if the file path is unknown (when you didn't pass the `options.filePath` option to the [`eslint.lintText()`][eslint-linttext] method).
* `messages` (`LintMessage[]`)<br>
The array of [LintMessage] objects.
* `suppressedMessages` (`SuppressedLintMessage[]`)<br>
The array of [SuppressedLintMessage] objects.
* `fixableErrorCount` (`number`)<br>
The number of errors that can be fixed automatically by the `fix` constructor option.
* `fixableWarningCount` (`number`)<br>
Expand Down Expand Up @@ -389,6 +393,33 @@ The `LintMessage` value is the information of each linting error. The `messages`
* `suggestions` (`{ desc: string; fix: EditInfo }[] | undefined`)<br>
The list of suggestions. Each suggestion is the pair of a description and an [EditInfo] object to fix code. API users such as editor integrations can choose one of them to fix the problem of this message. This property is undefined if this message doesn't have any suggestions.

### ◆ SuppressedLintMessage type

The `SuppressedLintMessage` value is the information of each suppressed linting error. The `suppressedMessages` property of the [LintResult] type contains it. It has the following properties:

* `ruleId` (`string` | `null`)<br>
Same as `ruleId` in [LintMessage] type.
* `severity` (`1 | 2`)<br>
Same as `severity` in [LintMessage] type.
* `fatal` (`boolean | undefined`)<br>
Same as `fatal` in [LintMessage] type.
* `message` (`string`)<br>
Same as `message` in [LintMessage] type.
* `line` (`number | undefined`)<br>
Same as `line` in [LintMessage] type.
* `column` (`number | undefined`)<br>
Same as `column` in [LintMessage] type.
* `endLine` (`number | undefined`)<br>
Same as `endLine` in [LintMessage] type.
* `endColumn` (`number | undefined`)<br>
Same as `endColumn` in [LintMessage] type.
* `fix` (`EditInfo | undefined`)<br>
Same as `fix` in [LintMessage] type.
* `suggestions` (`{ desc: string; fix: EditInfo }[] | undefined`)<br>
Same as `suggestions` in [LintMessage] type.
* `suppressions` (`{ kind: string; justification: string}[]`)<br>
The list of suppressions. Each suppression is the pair of a kind and a justification.

### ◆ EditInfo type

The `EditInfo` value is information to edit text. The `fix` and `suggestions` properties of [LintMessage] type contain it. It has following properties:
Expand Down Expand Up @@ -551,6 +582,22 @@ The information available for each linting message is:
* `fix` - an object describing the fix for the problem (this property is omitted if no fix is available).
* `suggestions` - an array of objects describing possible lint fixes for editors to programmatically enable (see details in the [Working with Rules docs](./working-with-rules.md#providing-suggestions)).

You can get the suppressed messages from the pervious run by `getSuppressedMessages()` method. If there is not a previous run, `getSuppressedMessage()` will return an empty list.

```js
const Linter = require("eslint").Linter;
const linter = new Linter();

const messages = linter.verify("var foo = bar; // eslint-disable-line -- Need to suppress", {
rules: {
semi: 2
}
}, { filename: "foo.js" });
const suppressedMessages = linter.getSuppressedMessages();

console.log(suppressedMessages.suppressions); // [{ "kind": "directive", "justification": "Need to suppress }]
```

Linting message objects have a deprecated `source` property. This property **will be removed** from linting messages in an upcoming breaking release. If you depend on this property, you should now use the `SourceCode` instance provided by the linter.

You can also get an instance of the `SourceCode` object used inside of `linter` by using the `getSourceCode()` method:
Expand Down Expand Up @@ -912,6 +959,7 @@ ruleTester.run("my-rule", myRule, {
[eslint-geterrorresults]: #-eslintgeterrorresultsresults
[lintresult]: #-lintresult-type
[lintmessage]: #-lintmessage-type
[suppressedlintmessage]: #-suppressedlintmessage-type
[editinfo]: #-editinfo-type
[formatter]: #-formatter-type
[linter]: #linter
4 changes: 4 additions & 0 deletions lib/cli-engine/cli-engine.js
Expand Up @@ -51,6 +51,7 @@ const validFixTypes = new Set(["directive", "problem", "suggestion", "layout"]);
/** @typedef {import("../shared/types").ConfigData} ConfigData */
/** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */
/** @typedef {import("../shared/types").LintMessage} LintMessage */
/** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */
/** @typedef {import("../shared/types").ParserOptions} ParserOptions */
/** @typedef {import("../shared/types").Plugin} Plugin */
/** @typedef {import("../shared/types").RuleConf} RuleConf */
Expand Down Expand Up @@ -91,6 +92,7 @@ const validFixTypes = new Set(["directive", "problem", "suggestion", "layout"]);
* @typedef {Object} LintResult
* @property {string} filePath The path to the file that was linted.
* @property {LintMessage[]} messages All of the messages for the result.
* @property {SuppressedLintMessage[]} suppressedMessages All of the suppressed messages for the result.
* @property {number} errorCount Number of errors for the result.
* @property {number} warningCount Number of warnings for the result.
* @property {number} fixableErrorCount Number of fixable errors for the result.
Expand Down Expand Up @@ -261,6 +263,7 @@ function verifyText({
const result = {
filePath,
messages,
suppressedMessages: linter.getSuppressedMessages(),
...calculateStatsPerFile(messages)
};

Expand Down Expand Up @@ -307,6 +310,7 @@ function createIgnoreResult(filePath, baseDir) {
message
}
],
suppressedMessages: [],
errorCount: 0,
warningCount: 1,
fixableErrorCount: 0,
Expand Down
57 changes: 50 additions & 7 deletions lib/linter/apply-disable-directives.js
Expand Up @@ -201,7 +201,6 @@ function processUnusedDisableDirectives(allDirectives) {
*/
function applyDirectives(options) {
const problems = [];
let nextDirectiveIndex = 0;
let currentGlobalDisableDirective = null;
const disabledRuleMap = new Map();

Expand All @@ -210,11 +209,14 @@ function applyDirectives(options) {
const usedDisableDirectives = new Set();

for (const problem of options.problems) {
let nextDirectiveIndex = 0;

while (
nextDirectiveIndex < options.directives.length &&
compareLocations(options.directives[nextDirectiveIndex], problem) <= 0
) {
const directive = options.directives[nextDirectiveIndex++];
const suppression = { kind: "directive", justification: directive.unprocessedDirective.justification };

switch (directive.type) {
case "disable":
Expand All @@ -228,6 +230,14 @@ function applyDirectives(options) {
} else {
disabledRuleMap.set(directive.ruleId, directive);
}

if (directive.ruleId === null || directive.ruleId === problem.ruleId) {
if (problem.suppressions) {
problem.suppressions.push(suppression);
} else {
problem.suppressions = [suppression];
}
}
break;

case "enable":
Expand All @@ -240,6 +250,14 @@ function applyDirectives(options) {
} else {
disabledRuleMap.delete(directive.ruleId);
}

if (directive.ruleId === null || directive.ruleId === problem.ruleId) {
if (problem.suppressions && directive.unprocessedDirective.type === "disable-line" && compareLocations(directive.unprocessedDirective, problem) <= 0) {
problem.suppressions.pop();
} else {
problem.suppressions = [];
}
}
break;

// no default
Expand All @@ -250,9 +268,9 @@ function applyDirectives(options) {
usedDisableDirectives.add(disabledRuleMap.get(problem.ruleId));
} else if (currentGlobalDisableDirective && !enabledRules.has(problem.ruleId)) {
usedDisableDirectives.add(currentGlobalDisableDirective);
} else {
problems.push(problem);
}

problems.push(problem);
}

const unusedDisableDirectivesToReport = options.directives
Expand All @@ -276,6 +294,28 @@ function applyDirectives(options) {
return { problems, unusedDisableDirectives };
}

/**
* Given a list of reported problems, distinguish problems between normal messages and suppressed messages.
* @param {Problem[]} problems A list of reported problems.
* @returns {{messages: LintMessage[], suppressedMessages: SuppressedLintMessage[]}}
* An object with a list of LintMessage and a list of SuppressLintMessage.
*/
function distinguishSuppressedMessages(problems) {
const messages = [];
const suppressedMessages = [];

for (const problem of problems) {
if (typeof problem.suppressions === "undefined" || problem.suppressions.length === 0) {
delete problem.suppressions;
messages.push(problem);
} else {
suppressedMessages.push(problem);
}
}

return { messages, suppressedMessages };
}

/**
* Given a list of directive comments (i.e. metadata about eslint-disable and eslint-enable comments) and a list
* of reported problems, determines which problems should be reported.
Expand All @@ -284,16 +324,17 @@ function applyDirectives(options) {
* type: ("disable"|"enable"|"disable-line"|"disable-next-line"),
* ruleId: (string|null),
* line: number,
* column: number
* column: number,
* justification: string
* }} options.directives Directive comments found in the file, with one-based columns.
* Two directive comments can only have the same location if they also have the same type (e.g. a single eslint-disable
* comment for two different rules is represented as two directives).
* @param {{ruleId: (string|null), line: number, column: number}[]} options.problems
* A list of problems reported by rules, sorted by increasing location in the file, with one-based columns.
* @param {"off" | "warn" | "error"} options.reportUnusedDisableDirectives If `"warn"` or `"error"`, adds additional problems for unused directives
* @param {boolean} options.disableFixes If true, it doesn't make `fix` properties.
* @returns {{ruleId: (string|null), line: number, column: number}[]}
* A list of reported problems that were not disabled by the directive comments.
* @returns {{messages: LintMessage[], suppressedMessages: SuppressedLintMessage[]}}
* An object with a list of LintMessage and a list of SuppressLintMessage.
*/
module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirectives = "off" }) => {
const blockDirectives = directives
Expand Down Expand Up @@ -337,10 +378,12 @@ module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirec
reportUnusedDisableDirectives
});

return reportUnusedDisableDirectives !== "off"
const reportedProblems = reportUnusedDisableDirectives !== "off"
? lineDirectivesResult.problems
.concat(blockDirectivesResult.unusedDisableDirectives)
.concat(lineDirectivesResult.unusedDisableDirectives)
.sort(compareLocations)
: lineDirectivesResult.problems;

return distinguishSuppressedMessages(reportedProblems);
};

0 comments on commit 88707fa

Please sign in to comment.