Skip to content

Commit 2edc0e2

Browse files
JoshuaKGoldbergnzakasmdjermanovic
authored
feat: add meta.defaultOptions (#17656)
* [Reference] feat: add meta.defaultOptions * Removed optionsRaw * computed-property-spacing: defaultOptions * fix: handle object type mismatches in merging * Validate arrays in flat-config-array * Fix rule defaultOptions typos * Put back getRuleOptions as before * Apply deep merging in config-validator and rule-validator * Converted remaining rules. Note: inline comments still need to have defaults applied. * Fixes around inline comments * Extract to a getRuleOptionsInline * nit: new extra line * Test fix: meta.defaultOptions in a comment * Refactor-level review feedback * Used a recommended rule in linter.js test * Added custom-rules.md docs * Update docs/src/extend/custom-rules.md Co-authored-by: Nicholas C. Zakas <[email protected]> * Clarified undefined point * Adjusted for edge cases per review * Refactored per review * Removed lint disable in source * Added Linter test for meta.defaultOptions * Documented useDefaults from Ajv * Set up meta+schema merging unit tests for flat (passing) and legacy (failing) * Potential solution: boolean applyDefaultOptions param for runRules * chore: node:assert * Update lib/shared/deep-merge-arrays.js Co-authored-by: Milos Djermanovic <[email protected]> * Made tests more explicit on defaulting behavior * Handled defaultOptions and option-less inline comments * Added explicit tests for mismatched comment options and comment options with schema: false * Try out configToValidate approach * Add in unit tests Co-authored-by: Milos Djermanovic <[email protected]> * Always apply defaultOptions, even with meta.schema: false * Filled in some falsy values * Fix a few lint complaints * That's right, Infinity is not allowed * Update lib/config/rule-validator.js Co-authored-by: Milos Djermanovic <[email protected]> * Update docs/src/extend/custom-rules.md Co-authored-by: Milos Djermanovic <[email protected]> * Update docs/src/extend/custom-rules.md Co-authored-by: Milos Djermanovic <[email protected]> * Revert deprecated rules * Bring in eslintrc#factor-in-default-options * Add index.d.ts types * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic <[email protected]> * Apply suggestions from code review Co-authored-by: Milos Djermanovic <[email protected]> * Moved config changing outside of validation * git checkout main -- lib/config/rule-validator.js * linter.js touchups and revert * Update lib/config/config.js Co-authored-by: Milos Djermanovic <[email protected]> * Update package.json --------- Co-authored-by: Nicholas C. Zakas <[email protected]> Co-authored-by: Milos Djermanovic <[email protected]>
1 parent fd33f13 commit 2edc0e2

86 files changed

Lines changed: 1186 additions & 548 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/src/extend/custom-rules.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ The source file for a rule exports an object with the following properties. Both
6060

6161
* `schema`: (`object | array | false`) Specifies the [options](#options-schemas) so ESLint can prevent invalid [rule configurations](../use/configure/rules). Mandatory when the rule has options.
6262

63+
* `defaultOptions`: (`array`) Specifies [default options](#option-defaults) for the rule. If present, any user-provided options in their config will be merged on top of them recursively.
64+
6365
* `deprecated`: (`boolean`) Indicates whether the rule has been deprecated. You may omit the `deprecated` property if the rule has not been deprecated.
6466

6567
* `replacedBy`: (`array`) In the case of a deprecated rule, specify replacement rule(s).
@@ -800,6 +802,51 @@ module.exports = {
800802

801803
To learn more about JSON Schema, we recommend looking at some examples on the [JSON Schema website](https://json-schema.org/learn/miscellaneous-examples), or reading the free [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/) ebook.
802804

805+
### Option Defaults
806+
807+
Rules may specify a `meta.defaultOptions` array of default values for any options.
808+
When the rule is enabled in a user configuration, ESLint will recursively merge any user-provided option elements on top of the default elements.
809+
810+
For example, given the following defaults:
811+
812+
```js
813+
export default {
814+
meta: {
815+
defaultOptions: [{
816+
alias: "basic",
817+
}],
818+
schema: [{
819+
type: "object",
820+
properties: {
821+
alias: {
822+
type: "string"
823+
}
824+
},
825+
additionalProperties: false
826+
}]
827+
},
828+
create(context) {
829+
const [{ alias }] = context.options;
830+
831+
return { /* ... */ };
832+
}
833+
}
834+
```
835+
836+
The rule would have a runtime `alias` value of `"basic"` unless the user configuration specifies a different value, such as with `["error", { alias: "complex" }]`.
837+
838+
Each element of the options array is merged according to the following rules:
839+
840+
* Any missing value or explicit user-provided `undefined` will fall back to a default option
841+
* User-provided arrays and primitive values other than `undefined` override a default option
842+
* User-provided objects will merge into a default option object and replace a non-object default otherwise
843+
844+
Option defaults will also be validated against the rule's `meta.schema`.
845+
846+
**Note:** ESLint internally uses [Ajv](https://ajv.js.org) for schema validation with its [`useDefaults` option](https://ajv.js.org/guide/modifying-data.html#assigning-defaults) enabled.
847+
Both user-provided and `meta.defaultOptions` options will override any defaults specified in a rule's schema.
848+
ESLint may disable Ajv's `useDefaults` in a future major version.
849+
803850
### Accessing Shebangs
804851

805852
[Shebangs (#!)](https://en.wikipedia.org/wiki/Shebang_(Unix)) are represented by the unique tokens of type `"Shebang"`. They are treated as comments and can be accessed by the methods outlined in the [Accessing Comments](#accessing-comments) section, such as `sourceCode.getAllComments()`.

lib/config/config.js

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
// Requirements
1010
//-----------------------------------------------------------------------------
1111

12-
const { RuleValidator } = require("./rule-validator");
12+
const { deepMergeArrays } = require("../shared/deep-merge-arrays");
13+
const { getRuleFromConfig } = require("./flat-config-helpers");
1314
const { flatConfigSchema, hasMethod } = require("./flat-config-schema");
15+
const { RuleValidator } = require("./rule-validator");
1416
const { ObjectSchema } = require("@eslint/config-array");
1517

1618
//-----------------------------------------------------------------------------
@@ -119,28 +121,6 @@ function languageOptionsToJSON(languageOptions, objectKey = "languageOptions") {
119121
return result;
120122
}
121123

122-
/**
123-
* Normalizes the rules configuration. Ensure that each rule config is
124-
* an array and that the severity is a number. This function modifies the
125-
* rulesConfig.
126-
* @param {Record<string, any>} rulesConfig The rules configuration to normalize.
127-
* @returns {void}
128-
*/
129-
function normalizeRulesConfig(rulesConfig) {
130-
131-
for (const [ruleId, ruleConfig] of Object.entries(rulesConfig)) {
132-
133-
// ensure rule config is an array
134-
if (!Array.isArray(ruleConfig)) {
135-
rulesConfig[ruleId] = [ruleConfig];
136-
}
137-
138-
// normalize severity
139-
rulesConfig[ruleId][0] = severities.get(rulesConfig[ruleId][0]);
140-
}
141-
142-
}
143-
144124

145125
//-----------------------------------------------------------------------------
146126
// Exports
@@ -239,7 +219,7 @@ class Config {
239219

240220
// Process the rules
241221
if (this.rules) {
242-
normalizeRulesConfig(this.rules);
222+
this.#normalizeRulesConfig();
243223
ruleValidator.validate(this);
244224
}
245225
}
@@ -276,6 +256,37 @@ class Config {
276256
processor: this.#processorName
277257
};
278258
}
259+
260+
/**
261+
* Normalizes the rules configuration. Ensures that each rule config is
262+
* an array and that the severity is a number. Applies meta.defaultOptions.
263+
* This function modifies `this.rules`.
264+
* @returns {void}
265+
*/
266+
#normalizeRulesConfig() {
267+
for (const [ruleId, originalConfig] of Object.entries(this.rules)) {
268+
269+
// ensure rule config is an array
270+
let ruleConfig = Array.isArray(originalConfig)
271+
? originalConfig
272+
: [originalConfig];
273+
274+
// normalize severity
275+
ruleConfig[0] = severities.get(ruleConfig[0]);
276+
277+
const rule = getRuleFromConfig(ruleId, this);
278+
279+
// apply meta.defaultOptions
280+
const slicedOptions = ruleConfig.slice(1);
281+
const mergedOptions = deepMergeArrays(rule?.meta?.defaultOptions, slicedOptions);
282+
283+
if (mergedOptions.length) {
284+
ruleConfig = [ruleConfig[0], ...mergedOptions];
285+
}
286+
287+
this.rules[ruleId] = ruleConfig;
288+
}
289+
}
279290
}
280291

281292
module.exports = { Config };

lib/linter/linter.js

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const { startTime, endTime } = require("../shared/stats");
4141
const { RuleValidator } = require("../config/rule-validator");
4242
const { assertIsRuleSeverity } = require("../config/flat-config-schema");
4343
const { normalizeSeverityToString } = require("../shared/severity");
44+
const { deepMergeArrays } = require("../shared/deep-merge-arrays");
4445
const jslang = require("../languages/js");
4546
const { activeFlags, inactiveFlags } = require("../shared/flags");
4647
const debug = require("debug")("eslint:linter");
@@ -892,14 +893,14 @@ function storeTime(time, timeOpts, slots) {
892893
/**
893894
* Get the options for a rule (not including severity), if any
894895
* @param {RuleConfig} ruleConfig rule configuration
896+
* @param {Object|undefined} defaultOptions rule.meta.defaultOptions
895897
* @returns {Array} of rule options, empty Array if none
896898
*/
897-
function getRuleOptions(ruleConfig) {
899+
function getRuleOptions(ruleConfig, defaultOptions) {
898900
if (Array.isArray(ruleConfig)) {
899-
return ruleConfig.slice(1);
901+
return deepMergeArrays(defaultOptions, ruleConfig.slice(1));
900902
}
901-
return [];
902-
903+
return defaultOptions ?? [];
903904
}
904905

905906
/**
@@ -957,6 +958,7 @@ function createRuleListeners(rule, ruleContext) {
957958
* @param {LanguageOptions} languageOptions The options for parsing the code.
958959
* @param {Object} settings The settings that were enabled in the config
959960
* @param {string} filename The reported filename of the code
961+
* @param {boolean} applyDefaultOptions If true, apply rules' meta.defaultOptions in computing their config options.
960962
* @param {boolean} disableFixes If true, it doesn't make `fix` properties.
961963
* @param {string | undefined} cwd cwd of the cli
962964
* @param {string} physicalFilename The full path of the file on disk without any code block information
@@ -967,9 +969,21 @@ function createRuleListeners(rule, ruleContext) {
967969
* @throws {Error} If traversal into a node fails.
968970
*/
969971
function runRules(
970-
sourceCode, configuredRules, ruleMapper, parserName, language, languageOptions,
971-
settings, filename, disableFixes, cwd, physicalFilename, ruleFilter,
972-
stats, slots
972+
sourceCode,
973+
configuredRules,
974+
ruleMapper,
975+
parserName,
976+
language,
977+
languageOptions,
978+
settings,
979+
filename,
980+
applyDefaultOptions,
981+
disableFixes,
982+
cwd,
983+
physicalFilename,
984+
ruleFilter,
985+
stats,
986+
slots
973987
) {
974988
const emitter = createEmitter();
975989

@@ -1022,7 +1036,7 @@ function runRules(
10221036
Object.create(sharedTraversalContext),
10231037
{
10241038
id: ruleId,
1025-
options: getRuleOptions(configuredRules[ruleId]),
1039+
options: getRuleOptions(configuredRules[ruleId], applyDefaultOptions ? rule.meta?.defaultOptions : void 0),
10261040
report(...args) {
10271041

10281042
/*
@@ -1419,6 +1433,7 @@ class Linter {
14191433
languageOptions,
14201434
settings,
14211435
options.filename,
1436+
true,
14221437
options.disableFixes,
14231438
slots.cwd,
14241439
providedOptions.physicalFilename,
@@ -1848,6 +1863,17 @@ class Linter {
18481863
if (config.rules[ruleId][0] > 0) {
18491864
shouldValidateOptions = false;
18501865
}
1866+
} else {
1867+
1868+
/**
1869+
* Since we know the user provided options, apply defaults on top of them
1870+
*/
1871+
const slicedOptions = ruleOptions.slice(1);
1872+
const mergedOptions = deepMergeArrays(rule.meta?.defaultOptions, slicedOptions);
1873+
1874+
if (mergedOptions.length) {
1875+
ruleOptions = [ruleOptions[0], ...mergedOptions];
1876+
}
18511877
}
18521878

18531879
if (shouldValidateOptions) {
@@ -1917,6 +1943,7 @@ class Linter {
19171943
languageOptions,
19181944
settings,
19191945
options.filename,
1946+
false,
19201947
options.disableFixes,
19211948
slots.cwd,
19221949
providedOptions.physicalFilename,

lib/rules/accessor-pairs.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ module.exports = {
139139
meta: {
140140
type: "suggestion",
141141

142+
defaultOptions: [{
143+
enforceForClassMembers: true,
144+
getWithoutSet: false,
145+
setWithoutGet: true
146+
}],
147+
142148
docs: {
143149
description: "Enforce getter and setter pairs in objects and classes",
144150
recommended: false,
@@ -149,16 +155,13 @@ module.exports = {
149155
type: "object",
150156
properties: {
151157
getWithoutSet: {
152-
type: "boolean",
153-
default: false
158+
type: "boolean"
154159
},
155160
setWithoutGet: {
156-
type: "boolean",
157-
default: true
161+
type: "boolean"
158162
},
159163
enforceForClassMembers: {
160-
type: "boolean",
161-
default: true
164+
type: "boolean"
162165
}
163166
},
164167
additionalProperties: false
@@ -174,7 +177,7 @@ module.exports = {
174177
}
175178
},
176179
create(context) {
177-
const config = context.options[0] || {};
180+
const [config] = context.options;
178181
const checkGetWithoutSet = config.getWithoutSet === true;
179182
const checkSetWithoutGet = config.setWithoutGet !== false;
180183
const enforceForClassMembers = config.enforceForClassMembers !== false;

lib/rules/array-callback-return.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ module.exports = {
215215
meta: {
216216
type: "problem",
217217

218+
defaultOptions: [{
219+
allowImplicit: false,
220+
checkForEach: false,
221+
allowVoid: false
222+
}],
223+
218224
docs: {
219225
description: "Enforce `return` statements in callbacks of array methods",
220226
recommended: false,
@@ -229,16 +235,13 @@ module.exports = {
229235
type: "object",
230236
properties: {
231237
allowImplicit: {
232-
type: "boolean",
233-
default: false
238+
type: "boolean"
234239
},
235240
checkForEach: {
236-
type: "boolean",
237-
default: false
241+
type: "boolean"
238242
},
239243
allowVoid: {
240-
type: "boolean",
241-
default: false
244+
type: "boolean"
242245
}
243246
},
244247
additionalProperties: false
@@ -256,8 +259,7 @@ module.exports = {
256259
},
257260

258261
create(context) {
259-
260-
const options = context.options[0] || { allowImplicit: false, checkForEach: false, allowVoid: false };
262+
const [options] = context.options;
261263
const sourceCode = context.sourceCode;
262264

263265
let funcInfo = {

lib/rules/arrow-body-style.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ module.exports = {
1919
meta: {
2020
type: "suggestion",
2121

22+
defaultOptions: ["as-needed"],
23+
2224
docs: {
2325
description: "Require braces around arrow function bodies",
2426
recommended: false,
@@ -71,7 +73,7 @@ module.exports = {
7173
create(context) {
7274
const options = context.options;
7375
const always = options[0] === "always";
74-
const asNeeded = !options[0] || options[0] === "as-needed";
76+
const asNeeded = options[0] === "as-needed";
7577
const never = options[0] === "never";
7678
const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral;
7779
const sourceCode = context.sourceCode;

0 commit comments

Comments
 (0)