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

feat: implement rfc 2021-suppression-support #15459

Merged
merged 14 commits into from Jan 27, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link is broken as there's no getsuppressedmessages section in the document.

* [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.
Yiwei-Ding marked this conversation as resolved.
Show resolved Hide resolved

```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
Yiwei-Ding marked this conversation as resolved.
Show resolved Hide resolved
}
}, { filename: "foo.js" });
const suppressedMessages = linter.getSuppressedMessages();

console.log(suppressedMessages.suppressions); // [{ "kind": "directive", "justification": "Need to suppress }]
Yiwei-Ding marked this conversation as resolved.
Show resolved Hide resolved
```

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.
Yiwei-Ding marked this conversation as resolved.
Show resolved Hide resolved
* @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
102 changes: 76 additions & 26 deletions lib/linter/apply-disable-directives.js
Expand Up @@ -197,11 +197,10 @@ function processUnusedDisableDirectives(allDirectives) {
* for the exported function, except that `reportUnusedDisableDirectives` is not supported
* (this function always reports unused disable directives).
* @returns {{problems: Problem[], unusedDisableDirectives: Problem[]}} An object with a list
* of filtered problems and unused eslint-disable directives
* of problems (including suppressed ones) and unused eslint-disable directives
*/
Yiwei-Ding marked this conversation as resolved.
Show resolved Hide resolved
function applyDirectives(options) {
const problems = [];
let nextDirectiveIndex = 0;
let currentGlobalDisableDirective = null;
const disabledRuleMap = new Map();

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

for (const problem of options.problems) {
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved
let nextDirectiveIndex = 0;
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved

// isProblemSuppressedBefore indicates whether the problem is suppressed by block directives.
const isProblemSuppressedBefore = problem.suppressions ? true : false; // eslint-disable-line no-unneeded-ternary -- problem.suppressions might be changed afterwards.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to add location information to the suppression to show where the directive is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good idea. The location info can be leveraged for auditing purposes as well. I also find a location property in suppressions in the SARIF format.

But I'd like to add location info to the suppressions in another PR because:

  1. The type location in suppressions is needed to be discussed. Shall we use the line and the column in unprocessedDirective directly or we use the similar structure in LintMessage (line/column/endLine/endColumn)?
  2. This PR is huge enough due to a lot of modification in core logic and tests.


switch (directive.type) {
case "disable":
if (directive.ruleId === null) {
currentGlobalDisableDirective = directive;
disabledRuleMap.clear();
enabledRules.clear();
} else if (currentGlobalDisableDirective) {
enabledRules.delete(directive.ruleId);
disabledRuleMap.set(directive.ruleId, directive);
} else {
disabledRuleMap.set(directive.ruleId, directive);
if (!isProblemSuppressedBefore) {
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved
if (directive.ruleId === null) {
currentGlobalDisableDirective = directive;
disabledRuleMap.clear();
enabledRules.clear();
} else if (currentGlobalDisableDirective) {
enabledRules.delete(directive.ruleId);
disabledRuleMap.set(directive.ruleId, directive);
} else {
disabledRuleMap.set(directive.ruleId, directive);
}
}

if (directive.ruleId === null || directive.ruleId === problem.ruleId) {
Yiwei-Ding marked this conversation as resolved.
Show resolved Hide resolved
if (problem.suppressions) {
problem.suppressions.push(suppression);
} else {
problem.suppressions = [suppression];
}
}
break;

case "enable":
if (directive.ruleId === null) {
currentGlobalDisableDirective = null;
disabledRuleMap.clear();
} else if (currentGlobalDisableDirective) {
enabledRules.add(directive.ruleId);
disabledRuleMap.delete(directive.ruleId);
} else {
disabledRuleMap.delete(directive.ruleId);
if (!isProblemSuppressedBefore) {
if (directive.ruleId === null) {
currentGlobalDisableDirective = null;
disabledRuleMap.clear();
} else if (currentGlobalDisableDirective) {
enabledRules.add(directive.ruleId);
disabledRuleMap.delete(directive.ruleId);
} 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 = [];
}
}
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved
break;

Expand All @@ -250,9 +275,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 Down Expand Up @@ -280,24 +305,47 @@ 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.
* of reported problems, determines whether problems should be reported or suppressed.
* @param {Object} options Information about directives and problems
* @param {{
* 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 @@ -341,10 +389,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);
};