Skip to content

Commit

Permalink
feat: add --report-unused-inline-configs (#19201)
Browse files Browse the repository at this point in the history
* feat: add --report-unused-inline-configs / linterOptions.reportUnusedInlineConfigs

* fix: unused severity normalization refactor

* Update lib/linter/linter.js

* Review feedback: reverting when in eslintrc mode

* Apply suggestions from code review

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

* Review feedback: removing boolean support

* Review feedback: explain 'unused'

* fix: getDirectiveComments call

* Update unit tests to put logic within FlatConfigArray

* Finish updating tests: cli.js

* fix: any array size is a difference

* fix: any object key mismatch is a difference too

* git checkout main -- docs/src/use/configure/migration-guide.md

* fix: remove migration notice from inline configs note in eslintrc-incompat.js

* git checkout main -- messages/eslintrc-incompat.js

* Update docs/src/integrate/nodejs-api.md

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

* Apply suggestions from code review

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

* rules.md touchups

* Handle nullish values in containsDifferentProperty

* Revert lib/shared/severity.js whitespace change

* Apply suggestions from code review

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

* Fix option-utils, and collectively update tests

* Report already-disabled rules too

* Ah, on second thought, one more unit test...

* Update lib/linter/linter.js

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

* Updated tests too

* Apply suggestions from code review

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

* Update lib/linter/linter.js

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

---------

Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com>
Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
  • Loading branch information
3 people authored Jan 16, 2025
1 parent e39d3f2 commit 1637b8e
Show file tree
Hide file tree
Showing 14 changed files with 593 additions and 12 deletions.
21 changes: 21 additions & 0 deletions docs/src/use/command-line-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Inline configuration comments:
--report-unused-disable-directives Adds reported errors for unused eslint-disable and eslint-enable directives
--report-unused-disable-directives-severity String Chooses severity level for reporting unused eslint-disable and
eslint-enable directives - either: off, warn, error, 0, 1, or 2
--report-unused-inline-configs String Adds reported errors for unused eslint inline config comments - either: off, warn, error, 0, 1, or 2
Caching:
--cache Only check changed files - default: false
Expand Down Expand Up @@ -759,6 +760,26 @@ Same as [`--report-unused-disable-directives`](#--report-unused-disable-directiv
args: ["--report-unused-disable-directives-severity", "warn", "file.js"]
}) }}

#### `--report-unused-inline-configs`

This option causes ESLint to report inline config comments like `/* eslint rule-name: "error" */` whose rule severity and any options match what's already been configured.

* **Argument Type**: String. One of the following values:
1. `off` (or `0`)
1. `warn` (or `1`)
1. `error` (or `2`)
* **Multiple Arguments**: No
* **Default Value**: By default, `linterOptions.reportUnusedInlineConfigs` configuration setting is used (which defaults to `"off"`).

This can be useful to keep files clean and devoid of misleading clutter.
Inline config comments are meant to change ESLint's behavior in some way: if they change nothing, there is no reason to leave them in.

##### `--report-unused-inline-configs` example

```shell
npx eslint --report-unused-inline-configs error file.js
```

### Caching

#### `--cache`
Expand Down
21 changes: 21 additions & 0 deletions docs/src/use/configure/configuration-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Each configuration object contains all of the information ESLint needs to execut
* `linterOptions` - An object containing settings related to the linting process.
* `noInlineConfig` - A Boolean value indicating if inline configuration is allowed.
* `reportUnusedDisableDirectives` - A severity string indicating if and how unused disable and enable directives should be tracked and reported. For legacy compatibility, `true` is equivalent to `"warn"` and `false` is equivalent to `"off"`. (default: `"warn"`).
* `reportUnusedInlineConfigs` - A severity string indicating if and how unused inline configs should be tracked and reported. (default: `"off"`)
* `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 @@ -301,6 +302,26 @@ You can override this setting using the [`--report-unused-disable-directives`](.

For legacy compatibility, `true` is equivalent to `"warn"` and `false` is equivalent to `"off"`.

#### Reporting Unused Inline Configs

Inline config comments such as `/* eslint rule-name: "error" */` are used to change ESLint rule severity and/or options around certain portions of code.
As a project's ESLint configuration file changes, it's possible for these directives to no longer be different from what was already set.
You can enable reporting of these unused inline config comments by setting the `reportUnusedInlineConfigs` option to a severity string, as in this example:

```js
// eslint.config.js
export default [
{
files: ["**/*.js"],
linterOptions: {
reportUnusedInlineConfigs: "error"
}
}
];
```

You can override this setting using the [`--report-unused-inline-configs`](../command-line-interface#--report-unused-inline-configs) command line option.

### Configuring Rules

You can configure any number of rules in a configuration object by add a `rules` property containing an object with your rule configurations. The names in this object are the names of the rules and the values are the configurations for each of those rules. Here's an example:
Expand Down
21 changes: 20 additions & 1 deletion docs/src/use/configure/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,25 @@ Configuration comments can include descriptions to explain why the comment is ne
*/
```

#### Report unused `eslint` inline config comments

To report unused `eslint` inline config comments (those that don't change anything from what was already configured), use the `reportUnusedInlineConfigs` setting. For example:

```js
// eslint.config.js
export default [
{
linterOptions: {
reportUnusedInlineConfigs: "error"
}
}
];
```

This setting defaults to `"off"`.

This setting is similar to the [`--report-unused-inline-configs`](../command-line-interface#--report-unused-inline-configs) CLI option.

### Using Configuration Files

To configure rules inside of a [configuration file](./configuration-files#configuration-file), use the `rules` key along with an error level and any options you want to use. For example:
Expand Down Expand Up @@ -345,7 +364,7 @@ You can also use the [`--no-inline-config`](../command-line-interface#--no-inlin

#### Report unused `eslint-disable` comments

To report unused `eslint-disable` comments, use the `reportUnusedDisableDirectives` setting. For example:
To report unused `eslint-disable` comments (those that disable rules which would not report on the disabled line), use the `reportUnusedDisableDirectives` setting. For example:

```js
// eslint.config.js
Expand Down
8 changes: 8 additions & 0 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ async function translateOptions({
quiet,
reportUnusedDisableDirectives,
reportUnusedDisableDirectivesSeverity,
reportUnusedInlineConfigs,
resolvePluginsRelativeTo,
rule,
rulesdir,
Expand Down Expand Up @@ -180,6 +181,13 @@ async function translateOptions({
};
}

if (reportUnusedInlineConfigs !== void 0) {
overrideConfig[0].linterOptions = {
...overrideConfig[0].linterOptions,
reportUnusedInlineConfigs: normalizeSeverityToString(reportUnusedInlineConfigs)
};
}

if (plugin) {
overrideConfig[0].plugins = await loadPlugins(importer, plugin);
}
Expand Down
17 changes: 16 additions & 1 deletion lib/config/flat-config-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,20 @@ const disableDirectiveSeveritySchema = {
}
};

/** @type {ObjectPropertySchema} */
const unusedInlineConfigsSeveritySchema = {
merge(first, second) {
const value = second === void 0 ? first : second;

return normalizeSeverityToNumber(value);
},
validate(value) {
if (!ALLOWED_SEVERITIES.has(value)) {
throw new TypeError("Expected one of: \"error\", \"warn\", \"off\", 0, 1, or 2.");
}
}
};

/** @type {ObjectPropertySchema} */
const deepObjectAssignSchema = {
merge(first = {}, second = {}) {
Expand Down Expand Up @@ -555,7 +569,8 @@ const flatConfigSchema = {
linterOptions: {
schema: {
noInlineConfig: booleanSchema,
reportUnusedDisableDirectives: disableDirectiveSeveritySchema
reportUnusedDisableDirectives: disableDirectiveSeveritySchema,
reportUnusedInlineConfigs: unusedInlineConfigsSeveritySchema
}
},
language: languageSchema,
Expand Down
79 changes: 74 additions & 5 deletions lib/linter/linter.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const { FlatConfigArray } = require("../config/flat-config-array");
const { startTime, endTime } = require("../shared/stats");
const { RuleValidator } = require("../config/rule-validator");
const { assertIsRuleSeverity } = require("../config/flat-config-schema");
const { normalizeSeverityToString } = require("../shared/severity");
const { normalizeSeverityToString, normalizeSeverityToNumber } = require("../shared/severity");
const { deepMergeArrays } = require("../shared/deep-merge-arrays");
const jslang = require("../languages/js");
const { activeFlags, inactiveFlags } = require("../shared/flags");
Expand All @@ -56,6 +56,7 @@ const { VFile } = require("./vfile");
const { ParserService } = require("../services/parser-service");
const { FileContext } = require("./file-context");
const { ProcessorService } = require("../services/processor-service");
const { containsDifferentProperty } = require("../shared/option-utils");
const STEP_KIND_VISIT = 1;
const STEP_KIND_CALL = 2;

Expand All @@ -76,6 +77,7 @@ const STEP_KIND_CALL = 2;
/** @typedef {import("@eslint/core").Language} Language */
/** @typedef {import("@eslint/core").RuleSeverity} RuleSeverity */
/** @typedef {import("@eslint/core").RuleConfig} RuleConfig */
/** @typedef {import("../types").Linter.StringSeverity} StringSeverity */


/* eslint-disable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */
Expand Down Expand Up @@ -141,7 +143,8 @@ const STEP_KIND_CALL = 2;
/**
* @typedef {Object} InternalOptions
* @property {string | null} warnInlineConfig The config name what `noInlineConfig` setting came from. If `noInlineConfig` setting didn't exist, this is null. If this is a config name, then the linter warns directive comments.
* @property {"off" | "warn" | "error"} reportUnusedDisableDirectives (boolean values were normalized)
* @property {StringSeverity} reportUnusedDisableDirectives Severity to report unused disable directives, if not "off" (boolean values were normalized).
* @property {StringSeverity} reportUnusedInlineConfigs Severity to report unused inline configs, if not "off".
*/

//------------------------------------------------------------------------------
Expand Down Expand Up @@ -314,6 +317,62 @@ function createLintingProblem(options) {
};
}

/**
* Wraps the value in an Array if it isn't already one.
* @template T
* @param {T|T[]} value Value to be wrapped.
* @returns {Array} The value as an array.
*/
function asArray(value) {
return Array.isArray(value) ? value : [value];
}

/**
* Pushes a problem to inlineConfigProblems if ruleOptions are redundant.
* @param {ConfigData} config Provided config.
* @param {Object} loc A line/column location
* @param {Array} problems Problems that may be added to.
* @param {string} ruleId The rule ID.
* @param {Array} ruleOptions The rule options, merged with the config's.
* @param {Array} ruleOptionsInline The rule options from the comment.
* @param {"error"|"warn"} severity The severity to report.
* @returns {void}
*/
function addProblemIfSameSeverityAndOptions(config, loc, problems, ruleId, ruleOptions, ruleOptionsInline, severity) {
const existingConfigRaw = config.rules?.[ruleId];
const existingConfig = existingConfigRaw ? asArray(existingConfigRaw) : ["off"];
const existingSeverity = normalizeSeverityToString(existingConfig[0]);
const inlineSeverity = normalizeSeverityToString(ruleOptions[0]);
const sameSeverity = existingSeverity === inlineSeverity;

if (!sameSeverity) {
return;
}

const alreadyConfigured = existingConfigRaw
? `is already configured to '${existingSeverity}'`
: "is not enabled so can't be turned off";
let message;

if ((existingConfig.length === 1 && ruleOptions.length === 1) || existingSeverity === "off") {
message = `Unused inline config ('${ruleId}' ${alreadyConfigured}).`;
} else if (!containsDifferentProperty(ruleOptions.slice(1), existingConfig.slice(1))) {
message = ruleOptionsInline.length === 1
? `Unused inline config ('${ruleId}' ${alreadyConfigured}).`
: `Unused inline config ('${ruleId}' ${alreadyConfigured} with the same options).`;
}

if (message) {
problems.push(createLintingProblem({
ruleId: null,
message,
loc,
language: config.language,
severity: normalizeSeverityToNumber(severity)
}));
}
}

/**
* Creates a collection of disable directives from a comment
* @param {Object} options to create disable directives
Expand Down Expand Up @@ -517,7 +576,7 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig, config)
return;
}

let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
let ruleOptions = asArray(ruleValue);

/*
* If the rule was already configured, inline rule configuration that
Expand Down Expand Up @@ -555,7 +614,7 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig, config)
*/
ruleOptions = [
ruleOptions[0], // severity from the inline config
...Array.isArray(config.rules[name]) ? config.rules[name].slice(1) : [] // options from the provided config
...asArray(config.rules[name]).slice(1) // options from the provided config
];
}

Expand Down Expand Up @@ -774,6 +833,8 @@ function normalizeVerifyOptions(providedOptions, config) {
}
}

const reportUnusedInlineConfigs = linterOptions.reportUnusedInlineConfigs === void 0 ? "off" : normalizeSeverityToString(linterOptions.reportUnusedInlineConfigs);

let ruleFilter = providedOptions.ruleFilter;

if (typeof ruleFilter !== "function") {
Expand All @@ -787,6 +848,7 @@ function normalizeVerifyOptions(providedOptions, config) {
? `your config${configNameOfNoInlineConfig}`
: null,
reportUnusedDisableDirectives,
reportUnusedInlineConfigs,
disableFixes: Boolean(providedOptions.disableFixes),
stats: providedOptions.stats,
ruleFilter
Expand Down Expand Up @@ -1787,7 +1849,8 @@ class Linter {

try {

let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
const ruleOptionsInline = asArray(ruleValue);
let ruleOptions = ruleOptionsInline;

assertIsRuleSeverity(ruleId, ruleOptions[0]);

Expand Down Expand Up @@ -1850,6 +1913,12 @@ class Linter {
}
}

if (options.reportUnusedInlineConfigs !== "off") {
addProblemIfSameSeverityAndOptions(
config, loc, inlineConfigProblems, ruleId, ruleOptions, ruleOptionsInline, options.reportUnusedInlineConfigs
);
}

if (shouldValidateOptions) {
ruleValidator.validate({
plugins: config.plugins,
Expand Down
13 changes: 13 additions & 0 deletions lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,18 @@ module.exports = function(usingFlatConfig) {
};
}

let reportUnusedInlineConfigsFlag;

if (usingFlatConfig) {
reportUnusedInlineConfigsFlag = {
option: "report-unused-inline-configs",
type: "String",
default: void 0,
description: "Adds reported errors for unused eslint inline config comments",
enum: ["off", "warn", "error", "0", "1", "2"]
};
}

return optionator({
prepend: "eslint [options] file.js [file.js] [dir]",
defaults: {
Expand Down Expand Up @@ -350,6 +362,7 @@ module.exports = function(usingFlatConfig) {
description: "Chooses severity level for reporting unused eslint-disable and eslint-enable directives",
enum: ["off", "warn", "error", "0", "1", "2"]
},
reportUnusedInlineConfigsFlag,
{
heading: "Caching"
},
Expand Down
56 changes: 56 additions & 0 deletions lib/shared/option-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @fileoverview Utilities to operate on option objects.
* @author Josh Goldberg
*/

"use strict";

/**
* Determines whether any of input's properties are different
* from values that already exist in original.
* @template T
* @param {Partial<T>} input New value.
* @param {T} original Original value.
* @returns {boolean} Whether input includes an explicit difference.
*/
function containsDifferentProperty(input, original) {
if (input === original) {
return false;
}

if (
typeof input !== typeof original ||
Array.isArray(input) !== Array.isArray(original)
) {
return true;
}

if (Array.isArray(input)) {
return (
input.length !== original.length ||
input.some((value, i) =>
containsDifferentProperty(value, original[i]))
);
}

if (typeof input === "object") {
if (input === null || original === null) {
return true;
}

const inputKeys = Object.keys(input);
const originalKeys = Object.keys(original);

return inputKeys.length !== originalKeys.length || inputKeys.some(
inputKey =>
!Object.hasOwn(original, inputKey) ||
containsDifferentProperty(input[inputKey], original[inputKey])
);
}

return true;
}

module.exports = {
containsDifferentProperty
};
Loading

0 comments on commit 1637b8e

Please sign in to comment.