Skip to content

Commit

Permalink
feat: report-unused-disable-directive to report unused eslint-enable (#…
Browse files Browse the repository at this point in the history
…17611)

* feat: report-unused-disable-directive to report unused eslint-enable

* test: fix test case

* chore: refactor

* test: add test cases

* chore: update order

* chore: fix test case order

* fix: revert test case

* docs: update docs

* chore: minor refactor

* fix: false positive

* chore: update comment

* Update lib/linter/apply-disable-directives.js

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

* chore: refactored collectUsedEnableDirectives to return a Set

* Update lib/linter/apply-disable-directives.js

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

* Update lib/linter/apply-disable-directives.js

Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com>

* fix: doc comments

* test: fix test cases

* fix: remove withoutRuleIdKey and fix jsdoc

* chore: refactor jsdoc

* Update lib/linter/apply-disable-directives.js

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

---------

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com>
  • Loading branch information
3 people committed Oct 17, 2023
1 parent dcfe573 commit 70648ee
Show file tree
Hide file tree
Showing 7 changed files with 2,847 additions and 368 deletions.
4 changes: 2 additions & 2 deletions docs/src/integrate/nodejs-api.md
Expand Up @@ -152,7 +152,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob
* `options.plugins` (`Record<string, Plugin> | null`)<br>
Default is `null`. The plugin implementations that ESLint uses for the `plugins` setting of your configuration. This is a map-like object. Those keys are plugin IDs and each value is implementation.
* `options.reportUnusedDisableDirectives` (`"error" | "warn" | "off" | null`)<br>
Default is `null`. The severity to report unused eslint-disable directives. If this option is a severity, it overrides the `reportUnusedDisableDirectives` setting in your configurations.
Default is `null`. The severity to report unused eslint-disable and eslint-enable directives. If this option is a severity, it overrides the `reportUnusedDisableDirectives` setting in your configurations.
* `options.resolvePluginsRelativeTo` (`string` | `null`)<br>
Default is `null`. The path to a directory where plugins should be resolved from. If `null` is present, ESLint loads plugins from the location of the configuration file that contains the plugin setting. If a path is present, ESLint loads all plugins from there.
* `options.rulePaths` (`string[]`)<br>
Expand Down Expand Up @@ -537,7 +537,7 @@ The most important method on `Linter` is `verify()`, which initiates linting of
* `filterCodeBlock` - (optional) A function that decides which code blocks the linter should adopt. The function receives two arguments. The first argument is the virtual filename of a code block. The second argument is the text of the code block. If the function returned `true` then the linter adopts the code block. If the function was omitted, the linter adopts only `*.js` code blocks. If you provided a `filterCodeBlock` function, it overrides this default behavior, so the linter doesn't adopt `*.js` code blocks automatically.
* `disableFixes` - (optional) when set to `true`, the linter doesn't make either the `fix` or `suggestions` property of the lint result.
* `allowInlineConfig` - (optional) set to `false` to disable inline comments from changing ESLint rules.
* `reportUnusedDisableDirectives` - (optional) when set to `true`, adds reported errors for unused `eslint-disable` directives when no problems would be reported in the disabled area anyway.
* `reportUnusedDisableDirectives` - (optional) when set to `true`, adds reported errors for unused `eslint-disable` and `eslint-enable` directives when no problems would be reported in the disabled area anyway.

If the third argument is a string, it is interpreted as the `filename`.

Expand Down
4 changes: 2 additions & 2 deletions docs/src/use/command-line-interface.md
Expand Up @@ -97,7 +97,7 @@ Output:
Inline configuration comments:
--no-inline-config Prevent comments from changing config or rules
--report-unused-disable-directives Adds reported errors for unused eslint-disable directives
--report-unused-disable-directives Adds reported errors for unused eslint-disable and eslint-enable directives
Caching:
--cache Only check changed files - default: false
Expand Down Expand Up @@ -577,7 +577,7 @@ This option causes ESLint to report directive comments like `// eslint-disable-l

* **Argument Type**: No argument.

This can be useful to prevent future errors from unexpectedly being suppressed, by cleaning up old `eslint-disable` comments which are no longer applicable.
This can be useful to prevent future errors from unexpectedly being suppressed, by cleaning up old `eslint-disable` and `eslint-enable` comments which are no longer applicable.

::: warning
When using this option, it is possible that new errors start being reported whenever ESLint or custom rules are upgraded.
Expand Down
6 changes: 3 additions & 3 deletions docs/src/use/configure/configuration-files-new.md
Expand Up @@ -76,7 +76,7 @@ Each configuration object contains all of the information ESLint needs to execut
* `parserOptions` - An object specifying additional options that are passed directly to the `parse()` or `parseForESLint()` method on the parser. The available options are parser-dependent.
* `linterOptions` - An object containing settings related to the linting process.
* `noInlineConfig` - A Boolean value indicating if inline configuration is allowed.
* `reportUnusedDisableDirectives` - A Boolean value indicating if unused disable directives should be tracked and reported.
* `reportUnusedDisableDirectives` - A Boolean value indicating if unused disable and enable directives should be tracked and reported.
* `processor` - Either an object containing `preprocess()` and `postprocess()` methods or a string indicating the name of a processor inside of a plugin (i.e., `"pluginName/processorName"`).
* `plugins` - An object containing a name-value mapping of plugin names to plugin objects. When `files` is specified, these plugins are only available to the matching files.
* `rules` - An object containing the configured rules. When `files` or `ignores` are specified, these rule configurations are only available to the matching files.
Expand Down Expand Up @@ -244,7 +244,7 @@ export default [

#### Reporting unused disable directives

Disable directives such as `/*eslint-disable*/` and `/*eslint-disable-next-line*/` are used to disable ESLint rules around certain portions of code. As code changes, it's possible for these directives to no longer be needed because the code has changed in such a way that the rule is no longer triggered. You can enable reporting of these unused disable directives by setting the `reportUnusedDisableDirectives` option to `true`, as in this example:
Disable and enable directives such as `/*eslint-disable*/`, `/*eslint-enable*/` and `/*eslint-disable-next-line*/` are used to disable ESLint rules around certain portions of code. As code changes, it's possible for these directives to no longer be needed because the code has changed in such a way that the rule is no longer triggered. You can enable reporting of these unused disable directives by setting the `reportUnusedDisableDirectives` option to `true`, as in this example:

```js
export default [
Expand All @@ -257,7 +257,7 @@ export default [
];
```

By default, unused disable directives are reported as warnings. You can change this setting using the `--report-unused-disable-directives` command line option.
By default, unused disable and enable directives are reported as warnings. You can change this setting using the `--report-unused-disable-directives` command line option.

### Configuring language options

Expand Down
138 changes: 126 additions & 12 deletions lib/linter/apply-disable-directives.js
Expand Up @@ -30,7 +30,7 @@ function compareLocations(itemA, itemB) {

/**
* Groups a set of directives into sub-arrays by their parent comment.
* @param {Directive[]} directives Unused directives to be removed.
* @param {Iterable<Directive>} directives Unused directives to be removed.
* @returns {Directive[][]} Directives grouped by their parent comment.
*/
function groupByParentComment(directives) {
Expand Down Expand Up @@ -177,10 +177,10 @@ function createCommentRemoval(directives, commentToken) {

/**
* Parses details from directives to create output Problems.
* @param {Directive[]} allDirectives Unused directives to be removed.
* @param {Iterable<Directive>} allDirectives Unused directives to be removed.
* @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
*/
function processUnusedDisableDirectives(allDirectives) {
function processUnusedDirectives(allDirectives) {
const directiveGroups = groupByParentComment(allDirectives);

return directiveGroups.flatMap(
Expand All @@ -199,14 +199,103 @@ function processUnusedDisableDirectives(allDirectives) {
);
}

/**
* Collect eslint-enable comments that are removing suppressions by eslint-disable comments.
* @param {Directive[]} directives The directives to check.
* @returns {Set<Directive>} The used eslint-enable comments
*/
function collectUsedEnableDirectives(directives) {

/**
* A Map of `eslint-enable` keyed by ruleIds that may be marked as used.
* If `eslint-enable` does not have a ruleId, the key will be `null`.
* @type {Map<string|null, Directive>}
*/
const enabledRules = new Map();

/**
* A Set of `eslint-enable` marked as used.
* It is also the return value of `collectUsedEnableDirectives` function.
* @type {Set<Directive>}
*/
const usedEnableDirectives = new Set();

/*
* Checks the directives backwards to see if the encountered `eslint-enable` is used by the previous `eslint-disable`,
* and if so, stores the `eslint-enable` in `usedEnableDirectives`.
*/
for (let index = directives.length - 1; index >= 0; index--) {
const directive = directives[index];

if (directive.type === "disable") {
if (enabledRules.size === 0) {
continue;
}
if (directive.ruleId === null) {

// If encounter `eslint-disable` without ruleId,
// mark all `eslint-enable` currently held in enabledRules as used.
// e.g.
// /* eslint-disable */ <- current directive
// /* eslint-enable rule-id1 */ <- used
// /* eslint-enable rule-id2 */ <- used
// /* eslint-enable */ <- used
for (const enableDirective of enabledRules.values()) {
usedEnableDirectives.add(enableDirective);
}
enabledRules.clear();
} else {
const enableDirective = enabledRules.get(directive.ruleId);

if (enableDirective) {

// If encounter `eslint-disable` with ruleId, and there is an `eslint-enable` with the same ruleId in enabledRules,
// mark `eslint-enable` with ruleId as used.
// e.g.
// /* eslint-disable rule-id */ <- current directive
// /* eslint-enable rule-id */ <- used
usedEnableDirectives.add(enableDirective);
} else {
const enabledDirectiveWithoutRuleId = enabledRules.get(null);

if (enabledDirectiveWithoutRuleId) {

// If encounter `eslint-disable` with ruleId, and there is no `eslint-enable` with the same ruleId in enabledRules,
// mark `eslint-enable` without ruleId as used.
// e.g.
// /* eslint-disable rule-id */ <- current directive
// /* eslint-enable */ <- used
usedEnableDirectives.add(enabledDirectiveWithoutRuleId);
}
}
}
} else if (directive.type === "enable") {
if (directive.ruleId === null) {

// If encounter `eslint-enable` without ruleId, the `eslint-enable` that follows it are unused.
// So clear enabledRules.
// e.g.
// /* eslint-enable */ <- current directive
// /* eslint-enable rule-id *// <- unused
// /* eslint-enable */ <- unused
enabledRules.clear();
enabledRules.set(null, directive);
} else {
enabledRules.set(directive.ruleId, directive);
}
}
}
return usedEnableDirectives;
}

/**
* This is the same as the exported function, except that it
* doesn't handle disable-line and disable-next-line directives, and it always reports unused
* disable directives.
* @param {Object} options options for applying directives. This is the same as the options
* for the exported function, except that `reportUnusedDisableDirectives` is not supported
* (this function always reports unused disable directives).
* @returns {{problems: LintMessage[], unusedDisableDirectives: LintMessage[]}} An object with a list
* @returns {{problems: LintMessage[], unusedDirectives: LintMessage[]}} An object with a list
* of problems (including suppressed ones) and unused eslint-disable directives
*/
function applyDirectives(options) {
Expand Down Expand Up @@ -258,17 +347,42 @@ function applyDirectives(options) {
const unusedDisableDirectivesToReport = options.directives
.filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive));

const processed = processUnusedDisableDirectives(unusedDisableDirectivesToReport);

const unusedDisableDirectives = processed
const unusedEnableDirectivesToReport = new Set(
options.directives.filter(directive => directive.unprocessedDirective.type === "enable")
);

/*
* If directives has the eslint-enable directive,
* check whether the eslint-enable comment is used.
*/
if (unusedEnableDirectivesToReport.size > 0) {
for (const directive of collectUsedEnableDirectives(options.directives)) {
unusedEnableDirectivesToReport.delete(directive);
}
}

const processed = processUnusedDirectives(unusedDisableDirectivesToReport)
.concat(processUnusedDirectives(unusedEnableDirectivesToReport));

const unusedDirectives = processed
.map(({ description, fix, unprocessedDirective }) => {
const { parentComment, type, line, column } = unprocessedDirective;

let message;

if (type === "enable") {
message = description
? `Unused eslint-enable directive (no matching eslint-disable directives were found for ${description}).`
: "Unused eslint-enable directive (no matching eslint-disable directives were found).";
} else {
message = description
? `Unused eslint-disable directive (no problems were reported from ${description}).`
: "Unused eslint-disable directive (no problems were reported).";
}
return {
ruleId: null,
message: description
? `Unused eslint-disable directive (no problems were reported from ${description}).`
: "Unused eslint-disable directive (no problems were reported).",
message,
line: type === "disable-next-line" ? parentComment.commentToken.loc.start.line : line,
column: type === "disable-next-line" ? parentComment.commentToken.loc.start.column + 1 : column,
severity: options.reportUnusedDisableDirectives === "warn" ? 1 : 2,
Expand All @@ -277,7 +391,7 @@ function applyDirectives(options) {
};
});

return { problems, unusedDisableDirectives };
return { problems, unusedDirectives };
}

/**
Expand Down Expand Up @@ -344,8 +458,8 @@ module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirec

return reportUnusedDisableDirectives !== "off"
? lineDirectivesResult.problems
.concat(blockDirectivesResult.unusedDisableDirectives)
.concat(lineDirectivesResult.unusedDisableDirectives)
.concat(blockDirectivesResult.unusedDirectives)
.concat(lineDirectivesResult.unusedDirectives)
.sort(compareLocations)
: lineDirectivesResult.problems;
};
4 changes: 2 additions & 2 deletions lib/options.js
Expand Up @@ -47,7 +47,7 @@ const optionator = require("optionator");
* @property {Object} [parserOptions] Specify parser options
* @property {string[]} [plugin] Specify plugins
* @property {string} [printConfig] Print the configuration for the given file
* @property {boolean | undefined} reportUnusedDisableDirectives Adds reported errors for unused eslint-disable directives
* @property {boolean | undefined} reportUnusedDisableDirectives Adds reported errors for unused eslint-disable and eslint-enable directives
* @property {string} [resolvePluginsRelativeTo] A folder where plugins should be resolved from, CWD by default
* @property {Object} [rule] Specify rules
* @property {string[]} [rulesdir] Load additional rules from this directory. Deprecated: Use rules from plugins
Expand Down Expand Up @@ -304,7 +304,7 @@ module.exports = function(usingFlatConfig) {
option: "report-unused-disable-directives",
type: "Boolean",
default: void 0,
description: "Adds reported errors for unused eslint-disable directives"
description: "Adds reported errors for unused eslint-disable and eslint-enable directives"
},
{
heading: "Caching"
Expand Down

0 comments on commit 70648ee

Please sign in to comment.