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 10 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
79 changes: 35 additions & 44 deletions lib/linter/apply-disable-directives.js
Expand Up @@ -197,62 +197,52 @@ 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();

// enabledRules is only used when there is a current global disable directive.
const enabledRules = new Set();
const usedDisableDirectives = new Set();

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

while (
nextDirectiveIndex < options.directives.length &&
compareLocations(options.directives[nextDirectiveIndex], problem) <= 0
) {
const directive = options.directives[nextDirectiveIndex++];

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);
}
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);
}
break;

// no default
if (directive.ruleId === null || directive.ruleId === problem.ruleId) {
switch (directive.type) {
case "disable":
disableDirectivesForProblem.push(directive);
break;

case "enable":
disableDirectivesForProblem = [];
break;

// no default
}
}
}

if (disabledRuleMap.has(problem.ruleId)) {
usedDisableDirectives.add(disabledRuleMap.get(problem.ruleId));
} else if (currentGlobalDisableDirective && !enabledRules.has(problem.ruleId)) {
usedDisableDirectives.add(currentGlobalDisableDirective);
} else {
problems.push(problem);
if (disableDirectivesForProblem.length > 0) {
const suppressions = disableDirectivesForProblem.map(directive => ({
kind: "directive",
justification: directive.unprocessedDirective.justification
}));

if (problem.suppressions) {
problem.suppressions = problem.suppressions.concat(suppressions);
} else {
problem.suppressions = suppressions;
usedDisableDirectives.add(disableDirectivesForProblem[disableDirectivesForProblem.length - 1]);
}
}

problems.push(problem);
}

const unusedDisableDirectivesToReport = options.directives
Expand Down Expand Up @@ -282,22 +272,23 @@ function applyDirectives(options) {

/**
* 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, adds the suppression information to the problems.
* @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 {{ruleId: (string|null), line: number, column: number, suppressions?: {kind: string, justification: string}}[]}
* An object with a list of reported problems, the suppressed of which contain the suppression information.
*/
module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirectives = "off" }) => {
const blockDirectives = directives
Expand Down