Skip to content

Commit

Permalink
feat: add meta.defaultOptions (#17656)
Browse files Browse the repository at this point in the history
* [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 <nicholas@humanwhocodes.com>

* 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 <milos.djermanovic@gmail.com>

* 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 <milos.djermanovic@gmail.com>

* 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 <milos.djermanovic@gmail.com>

* Update docs/src/extend/custom-rules.md

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

* Update docs/src/extend/custom-rules.md

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

* 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 <milos.djermanovic@gmail.com>

* Apply suggestions from code review

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

* 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 <milos.djermanovic@gmail.com>

* Update package.json

---------

Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com>
Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
  • Loading branch information
3 people authored Nov 15, 2024
1 parent fd33f13 commit 2edc0e2
Show file tree
Hide file tree
Showing 86 changed files with 1,186 additions and 548 deletions.
47 changes: 47 additions & 0 deletions docs/src/extend/custom-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ The source file for a rule exports an object with the following properties. Both

* `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.

* `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.

* `deprecated`: (`boolean`) Indicates whether the rule has been deprecated. You may omit the `deprecated` property if the rule has not been deprecated.

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

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.

### Option Defaults

Rules may specify a `meta.defaultOptions` array of default values for any options.
When the rule is enabled in a user configuration, ESLint will recursively merge any user-provided option elements on top of the default elements.

For example, given the following defaults:

```js
export default {
meta: {
defaultOptions: [{
alias: "basic",
}],
schema: [{
type: "object",
properties: {
alias: {
type: "string"
}
},
additionalProperties: false
}]
},
create(context) {
const [{ alias }] = context.options;

return { /* ... */ };
}
}
```

The rule would have a runtime `alias` value of `"basic"` unless the user configuration specifies a different value, such as with `["error", { alias: "complex" }]`.

Each element of the options array is merged according to the following rules:

* Any missing value or explicit user-provided `undefined` will fall back to a default option
* User-provided arrays and primitive values other than `undefined` override a default option
* User-provided objects will merge into a default option object and replace a non-object default otherwise

Option defaults will also be validated against the rule's `meta.schema`.

**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.
Both user-provided and `meta.defaultOptions` options will override any defaults specified in a rule's schema.
ESLint may disable Ajv's `useDefaults` in a future major version.

### Accessing Shebangs

[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()`.
Expand Down
59 changes: 35 additions & 24 deletions lib/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
// Requirements
//-----------------------------------------------------------------------------

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

//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -119,28 +121,6 @@ function languageOptionsToJSON(languageOptions, objectKey = "languageOptions") {
return result;
}

/**
* Normalizes the rules configuration. Ensure that each rule config is
* an array and that the severity is a number. This function modifies the
* rulesConfig.
* @param {Record<string, any>} rulesConfig The rules configuration to normalize.
* @returns {void}
*/
function normalizeRulesConfig(rulesConfig) {

for (const [ruleId, ruleConfig] of Object.entries(rulesConfig)) {

// ensure rule config is an array
if (!Array.isArray(ruleConfig)) {
rulesConfig[ruleId] = [ruleConfig];
}

// normalize severity
rulesConfig[ruleId][0] = severities.get(rulesConfig[ruleId][0]);
}

}


//-----------------------------------------------------------------------------
// Exports
Expand Down Expand Up @@ -239,7 +219,7 @@ class Config {

// Process the rules
if (this.rules) {
normalizeRulesConfig(this.rules);
this.#normalizeRulesConfig();
ruleValidator.validate(this);
}
}
Expand Down Expand Up @@ -276,6 +256,37 @@ class Config {
processor: this.#processorName
};
}

/**
* Normalizes the rules configuration. Ensures that each rule config is
* an array and that the severity is a number. Applies meta.defaultOptions.
* This function modifies `this.rules`.
* @returns {void}
*/
#normalizeRulesConfig() {
for (const [ruleId, originalConfig] of Object.entries(this.rules)) {

// ensure rule config is an array
let ruleConfig = Array.isArray(originalConfig)
? originalConfig
: [originalConfig];

// normalize severity
ruleConfig[0] = severities.get(ruleConfig[0]);

const rule = getRuleFromConfig(ruleId, this);

// apply meta.defaultOptions
const slicedOptions = ruleConfig.slice(1);
const mergedOptions = deepMergeArrays(rule?.meta?.defaultOptions, slicedOptions);

if (mergedOptions.length) {
ruleConfig = [ruleConfig[0], ...mergedOptions];
}

this.rules[ruleId] = ruleConfig;
}
}
}

module.exports = { Config };
43 changes: 35 additions & 8 deletions lib/linter/linter.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ 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 { deepMergeArrays } = require("../shared/deep-merge-arrays");
const jslang = require("../languages/js");
const { activeFlags, inactiveFlags } = require("../shared/flags");
const debug = require("debug")("eslint:linter");
Expand Down Expand Up @@ -892,14 +893,14 @@ function storeTime(time, timeOpts, slots) {
/**
* Get the options for a rule (not including severity), if any
* @param {RuleConfig} ruleConfig rule configuration
* @param {Object|undefined} defaultOptions rule.meta.defaultOptions
* @returns {Array} of rule options, empty Array if none
*/
function getRuleOptions(ruleConfig) {
function getRuleOptions(ruleConfig, defaultOptions) {
if (Array.isArray(ruleConfig)) {
return ruleConfig.slice(1);
return deepMergeArrays(defaultOptions, ruleConfig.slice(1));
}
return [];

return defaultOptions ?? [];
}

/**
Expand Down Expand Up @@ -957,6 +958,7 @@ function createRuleListeners(rule, ruleContext) {
* @param {LanguageOptions} languageOptions The options for parsing the code.
* @param {Object} settings The settings that were enabled in the config
* @param {string} filename The reported filename of the code
* @param {boolean} applyDefaultOptions If true, apply rules' meta.defaultOptions in computing their config options.
* @param {boolean} disableFixes If true, it doesn't make `fix` properties.
* @param {string | undefined} cwd cwd of the cli
* @param {string} physicalFilename The full path of the file on disk without any code block information
Expand All @@ -967,9 +969,21 @@ function createRuleListeners(rule, ruleContext) {
* @throws {Error} If traversal into a node fails.
*/
function runRules(
sourceCode, configuredRules, ruleMapper, parserName, language, languageOptions,
settings, filename, disableFixes, cwd, physicalFilename, ruleFilter,
stats, slots
sourceCode,
configuredRules,
ruleMapper,
parserName,
language,
languageOptions,
settings,
filename,
applyDefaultOptions,
disableFixes,
cwd,
physicalFilename,
ruleFilter,
stats,
slots
) {
const emitter = createEmitter();

Expand Down Expand Up @@ -1022,7 +1036,7 @@ function runRules(
Object.create(sharedTraversalContext),
{
id: ruleId,
options: getRuleOptions(configuredRules[ruleId]),
options: getRuleOptions(configuredRules[ruleId], applyDefaultOptions ? rule.meta?.defaultOptions : void 0),
report(...args) {

/*
Expand Down Expand Up @@ -1419,6 +1433,7 @@ class Linter {
languageOptions,
settings,
options.filename,
true,
options.disableFixes,
slots.cwd,
providedOptions.physicalFilename,
Expand Down Expand Up @@ -1848,6 +1863,17 @@ class Linter {
if (config.rules[ruleId][0] > 0) {
shouldValidateOptions = false;
}
} else {

/**
* Since we know the user provided options, apply defaults on top of them
*/
const slicedOptions = ruleOptions.slice(1);
const mergedOptions = deepMergeArrays(rule.meta?.defaultOptions, slicedOptions);

if (mergedOptions.length) {
ruleOptions = [ruleOptions[0], ...mergedOptions];
}
}

if (shouldValidateOptions) {
Expand Down Expand Up @@ -1917,6 +1943,7 @@ class Linter {
languageOptions,
settings,
options.filename,
false,
options.disableFixes,
slots.cwd,
providedOptions.physicalFilename,
Expand Down
17 changes: 10 additions & 7 deletions lib/rules/accessor-pairs.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ module.exports = {
meta: {
type: "suggestion",

defaultOptions: [{
enforceForClassMembers: true,
getWithoutSet: false,
setWithoutGet: true
}],

docs: {
description: "Enforce getter and setter pairs in objects and classes",
recommended: false,
Expand All @@ -149,16 +155,13 @@ module.exports = {
type: "object",
properties: {
getWithoutSet: {
type: "boolean",
default: false
type: "boolean"
},
setWithoutGet: {
type: "boolean",
default: true
type: "boolean"
},
enforceForClassMembers: {
type: "boolean",
default: true
type: "boolean"
}
},
additionalProperties: false
Expand All @@ -174,7 +177,7 @@ module.exports = {
}
},
create(context) {
const config = context.options[0] || {};
const [config] = context.options;
const checkGetWithoutSet = config.getWithoutSet === true;
const checkSetWithoutGet = config.setWithoutGet !== false;
const enforceForClassMembers = config.enforceForClassMembers !== false;
Expand Down
18 changes: 10 additions & 8 deletions lib/rules/array-callback-return.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ module.exports = {
meta: {
type: "problem",

defaultOptions: [{
allowImplicit: false,
checkForEach: false,
allowVoid: false
}],

docs: {
description: "Enforce `return` statements in callbacks of array methods",
recommended: false,
Expand All @@ -229,16 +235,13 @@ module.exports = {
type: "object",
properties: {
allowImplicit: {
type: "boolean",
default: false
type: "boolean"
},
checkForEach: {
type: "boolean",
default: false
type: "boolean"
},
allowVoid: {
type: "boolean",
default: false
type: "boolean"
}
},
additionalProperties: false
Expand All @@ -256,8 +259,7 @@ module.exports = {
},

create(context) {

const options = context.options[0] || { allowImplicit: false, checkForEach: false, allowVoid: false };
const [options] = context.options;
const sourceCode = context.sourceCode;

let funcInfo = {
Expand Down
4 changes: 3 additions & 1 deletion lib/rules/arrow-body-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ module.exports = {
meta: {
type: "suggestion",

defaultOptions: ["as-needed"],

docs: {
description: "Require braces around arrow function bodies",
recommended: false,
Expand Down Expand Up @@ -71,7 +73,7 @@ module.exports = {
create(context) {
const options = context.options;
const always = options[0] === "always";
const asNeeded = !options[0] || options[0] === "as-needed";
const asNeeded = options[0] === "as-needed";
const never = options[0] === "never";
const requireReturnForObjectLiteral = options[1] && options[1].requireReturnForObjectLiteral;
const sourceCode = context.sourceCode;
Expand Down
Loading

0 comments on commit 2edc0e2

Please sign in to comment.