From 9b70e84a66b802040776fcd5d1028cc66b813826 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 16 Oct 2023 12:41:12 -0400 Subject: [PATCH 01/26] [Reference] feat: add meta.defaultOptions --- lib/linter/deep-merge.js | 61 ++++++++++++++++++++++++++ lib/linter/get-rule-options.js | 60 +++++++++++++++++++++++++ lib/linter/linter.js | 17 ++----- lib/rules/accessor-pairs.js | 17 ++++--- lib/rules/array-bracket-newline.js | 4 +- lib/rules/array-bracket-spacing.js | 3 ++ lib/rules/array-callback-return.js | 17 ++++--- lib/rules/array-element-newline.js | 2 + lib/rules/arrow-body-style.js | 4 +- lib/rules/arrow-parens.js | 5 ++- lib/rules/arrow-spacing.js | 23 +++++----- lib/rules/block-spacing.js | 2 + lib/rules/brace-style.js | 9 ++-- lib/rules/camelcase.js | 30 ++++++++----- lib/rules/capitalized-comments.js | 10 ++++- lib/rules/class-methods-use-this.js | 14 +++--- lib/rules/comma-dangle.js | 27 ++++-------- lib/rules/comma-spacing.js | 8 ++-- lib/rules/comma-style.js | 33 ++++++-------- lib/rules/complexity.js | 14 +++--- lib/rules/computed-property-spacing.js | 7 +-- lib/rules/consistent-return.js | 9 ++-- lib/rules/consistent-this.js | 10 ++--- lib/rules/curly.js | 2 + lib/rules/generator-star-spacing.js | 5 +++ lib/rules/indent.js | 12 ++--- lib/rules/lines-around-comment.js | 35 +++++++-------- lib/rules/max-len.js | 6 +++ lib/rules/new-cap.js | 22 +++++----- lib/rules/no-implicit-coercion.js | 37 ++++++---------- lib/rules/no-multi-spaces.js | 9 ++-- lib/rules/no-sequences.js | 14 +++--- lib/rules/no-unused-expressions.js | 30 +++++++------ lib/rules/no-use-before-define.js | 27 ++++++------ lib/rules/object-curly-newline.js | 4 +- lib/rules/operator-linebreak.js | 13 ++++-- lib/rules/use-isnan.js | 11 +++-- lib/shared/types.js | 1 + tests/lib/linter/deep-merge.js | 61 ++++++++++++++++++++++++++ 39 files changed, 439 insertions(+), 236 deletions(-) create mode 100644 lib/linter/deep-merge.js create mode 100644 lib/linter/get-rule-options.js create mode 100644 tests/lib/linter/deep-merge.js diff --git a/lib/linter/deep-merge.js b/lib/linter/deep-merge.js new file mode 100644 index 00000000000..ef5a533d9fb --- /dev/null +++ b/lib/linter/deep-merge.js @@ -0,0 +1,61 @@ +/* eslint-disable eqeqeq, no-undefined -- `null` and `undefined` are different in options */ +/** + * @fileoverview Applies default rule options + * @author JoshuaKGoldberg + */ + +"use strict"; + +/** + * Check if the variable contains an object strictly rejecting arrays + * @param {unknown} obj an object + * @returns {boolean} Whether obj is an object + */ +function isObjectNotArray(obj) { + return typeof obj === "object" && obj != null && !Array.isArray(obj); +} + +/** + * Deeply merges second on top of first, creating a new {} object if needed. + * @param {T} first Base, default value. + * @param {U} second User-specified value. + * @returns {T | U | (T & U)} Merged equivalent of second on top of first. + */ +function deepMerge(first, second) { + if (second === null || (second !== undefined && typeof second !== "object")) { + return second; + } + + if (typeof first !== "object" && typeof second === "object" && second !== null) { + return second; + } + + if (first === null || Array.isArray(first) || second === undefined) { + return first; + } + + const keysUnion = new Set(Object.keys(first).concat(Object.keys(second))); + + return Array.from(keysUnion).reduce((acc, key) => { + const firstValue = first[key]; + const secondValue = second[key]; + + if (firstValue !== undefined && secondValue !== undefined) { + if (isObjectNotArray(firstValue) && isObjectNotArray(secondValue)) { + acc[key] = deepMerge(firstValue, secondValue); + } else { + acc[key] = secondValue; + } + } else if (firstValue !== undefined) { + acc[key] = firstValue; + } else { + acc[key] = secondValue; + } + + return acc; + }, {}); +} + +module.exports = { deepMerge }; + +/* eslint-enable eqeqeq, no-undefined -- `null` and `undefined` are different in options */ diff --git a/lib/linter/get-rule-options.js b/lib/linter/get-rule-options.js new file mode 100644 index 00000000000..9b1747a33dd --- /dev/null +++ b/lib/linter/get-rule-options.js @@ -0,0 +1,60 @@ +/** + * @fileoverview Applies default rule options + * @author JoshuaKGoldberg + */ + +"use strict"; + +const { deepMerge } = require("./deep-merge"); + +/** + * Creates rule options by merging a user config on top of any default options. + * @param {Array|undefined} defaultOptions Default options from a rule's meta. + * @param {Array} ruleConfig User-specified rule configuration. + * @returns {Array} Rule options, factoring in user config and any defaults. + */ +function getMergedRuleOptions(defaultOptions, ruleConfig) { + if (!defaultOptions) { + return ruleConfig; + } + + const options = []; + const sharedLength = Math.min(defaultOptions.length, ruleConfig.length); + let i; + + for (i = 0; i < sharedLength; i += 1) { + options.push(deepMerge(defaultOptions[i], ruleConfig[i])); + } + + options.push(...defaultOptions.slice(i)); + options.push(...ruleConfig.slice(i)); + + return options; +} + +/** + * Get the options for a rule (not including severity), if any, factoring in defaults + * @param {Array|undefined} defaultOptions default options from rule's meta. + * @param {Array|number} ruleConfig rule configuration + * @returns {Array} of rule options, empty Array if none + */ +function getRuleOptions(defaultOptions, ruleConfig) { + if (Array.isArray(ruleConfig)) { + return getMergedRuleOptions(defaultOptions, ruleConfig.slice(1)); + } + return defaultOptions || []; +} + +/** + * Get the raw (non-defaulted) options for a rule (not including severity), if any + * @param {Array|number} ruleConfig rule configuration + * @returns {Array} of rule options, empty Array if none + */ +function getRuleOptionsRaw(ruleConfig) { + if (Array.isArray(ruleConfig)) { + return ruleConfig.slice(1); + } + return []; +} + +module.exports = { getRuleOptions, getRuleOptionsRaw }; diff --git a/lib/linter/linter.js b/lib/linter/linter.js index e195812e513..37c6b59316e 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -33,6 +33,7 @@ const CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"), applyDisableDirectives = require("./apply-disable-directives"), ConfigCommentParser = require("./config-comment-parser"), + { getRuleOptions, getRuleOptionsRaw } = require("./get-rule-options"), NodeEventGenerator = require("./node-event-generator"), createReportTranslator = require("./report-translator"), Rules = require("./rules"), @@ -772,19 +773,6 @@ function stripUnicodeBOM(text) { return text; } -/** - * Get the options for a rule (not including severity), if any - * @param {Array|number} ruleConfig rule configuration - * @returns {Array} of rule options, empty Array if none - */ -function getRuleOptions(ruleConfig) { - if (Array.isArray(ruleConfig)) { - return ruleConfig.slice(1); - } - return []; - -} - /** * Analyze scope of the given AST. * @param {ASTNode} ast The `Program` node to analyze. @@ -1037,7 +1025,8 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO Object.create(sharedTraversalContext), { id: ruleId, - options: getRuleOptions(configuredRules[ruleId]), + options: getRuleOptions(rule.meta && rule.meta.defaultOptions, configuredRules[ruleId]), + optionsRaw: getRuleOptionsRaw(configuredRules[ruleId]), report(...args) { /* diff --git a/lib/rules/accessor-pairs.js b/lib/rules/accessor-pairs.js index f97032895df..5e39b540bed 100644 --- a/lib/rules/accessor-pairs.js +++ b/lib/rules/accessor-pairs.js @@ -139,6 +139,12 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ + enforceForClassMembers: true, + getWithoutGet: false, + setWithoutGet: true + }], + docs: { description: "Enforce getter and setter pairs in objects and classes", recommended: false, @@ -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 @@ -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; diff --git a/lib/rules/array-bracket-newline.js b/lib/rules/array-bracket-newline.js index c3676bf4dfa..6b90955a648 100644 --- a/lib/rules/array-bracket-newline.js +++ b/lib/rules/array-bracket-newline.js @@ -16,6 +16,8 @@ module.exports = { meta: { type: "layout", + defaultOptions: [{ minItems: null, multiline: true }], + docs: { description: "Enforce linebreaks after opening and before closing array brackets", recommended: false, @@ -192,7 +194,7 @@ module.exports = { */ function check(node) { const elements = node.elements; - const normalizedOptions = normalizeOptions(context.options[0]); + const normalizedOptions = normalizeOptions(context.optionsRaw[0]); const options = normalizedOptions[node.type]; const openBracket = sourceCode.getFirstToken(node); const closeBracket = sourceCode.getLastToken(node); diff --git a/lib/rules/array-bracket-spacing.js b/lib/rules/array-bracket-spacing.js index e3a46d82214..db08ee04b67 100644 --- a/lib/rules/array-bracket-spacing.js +++ b/lib/rules/array-bracket-spacing.js @@ -15,6 +15,8 @@ module.exports = { meta: { type: "layout", + defaultOptions: ["never"], + docs: { description: "Enforce consistent spacing inside array brackets", recommended: false, @@ -55,6 +57,7 @@ module.exports = { const spaced = context.options[0] === "always", sourceCode = context.sourceCode; + /** * Determines whether an option is set, relative to the spacing option. * If spaced is "always", then check whether option is set to false. diff --git a/lib/rules/array-callback-return.js b/lib/rules/array-callback-return.js index 6d8f258fa14..1008a35252f 100644 --- a/lib/rules/array-callback-return.js +++ b/lib/rules/array-callback-return.js @@ -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, @@ -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 @@ -257,7 +260,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 = { diff --git a/lib/rules/array-element-newline.js b/lib/rules/array-element-newline.js index 0c806ef3a82..8aeb11caab8 100644 --- a/lib/rules/array-element-newline.js +++ b/lib/rules/array-element-newline.js @@ -16,6 +16,8 @@ module.exports = { meta: { type: "layout", + defaultOptions: ["always"], + docs: { description: "Enforce line breaks after each array element", recommended: false, diff --git a/lib/rules/arrow-body-style.js b/lib/rules/arrow-body-style.js index 759070454c4..a5947e500c2 100644 --- a/lib/rules/arrow-body-style.js +++ b/lib/rules/arrow-body-style.js @@ -19,6 +19,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: ["as-needed"], + docs: { description: "Require braces around arrow function bodies", recommended: false, @@ -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; diff --git a/lib/rules/arrow-parens.js b/lib/rules/arrow-parens.js index 0463323176e..2d957b17723 100644 --- a/lib/rules/arrow-parens.js +++ b/lib/rules/arrow-parens.js @@ -32,6 +32,8 @@ module.exports = { meta: { type: "layout", + defaultOptions: ["always"], + docs: { description: "Require parentheses around arrow function arguments", recommended: false, @@ -48,8 +50,7 @@ module.exports = { type: "object", properties: { requireForBlockBody: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false diff --git a/lib/rules/arrow-spacing.js b/lib/rules/arrow-spacing.js index fb74d2cb272..175c49dc9d8 100644 --- a/lib/rules/arrow-spacing.js +++ b/lib/rules/arrow-spacing.js @@ -19,6 +19,11 @@ module.exports = { meta: { type: "layout", + defaultOptions: [{ + after: true, + before: true + }], + docs: { description: "Enforce consistent spacing before and after the arrow in arrow functions", recommended: false, @@ -32,12 +37,10 @@ module.exports = { type: "object", properties: { before: { - type: "boolean", - default: true + type: "boolean" }, after: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false @@ -54,13 +57,7 @@ module.exports = { }, create(context) { - - // merge rules with default - const rule = Object.assign({}, context.options[0]); - - rule.before = rule.before !== false; - rule.after = rule.after !== false; - + const [options] = context.options; const sourceCode = context.sourceCode; /** @@ -101,7 +98,7 @@ module.exports = { const tokens = getTokens(node); const countSpace = countSpaces(tokens); - if (rule.before) { + if (options.before) { // should be space(s) before arrow if (countSpace.before === 0) { @@ -127,7 +124,7 @@ module.exports = { } } - if (rule.after) { + if (options.after) { // should be space(s) after arrow if (countSpace.after === 0) { diff --git a/lib/rules/block-spacing.js b/lib/rules/block-spacing.js index dd4851c6843..c4b005a023e 100644 --- a/lib/rules/block-spacing.js +++ b/lib/rules/block-spacing.js @@ -16,6 +16,8 @@ module.exports = { meta: { type: "layout", + defaultOptions: ["always"], + docs: { description: "Disallow or enforce spaces inside of blocks after opening block and before closing block", recommended: false, diff --git a/lib/rules/brace-style.js b/lib/rules/brace-style.js index 59758c90925..9b3c1e2b340 100644 --- a/lib/rules/brace-style.js +++ b/lib/rules/brace-style.js @@ -16,6 +16,8 @@ module.exports = { meta: { type: "layout", + defaultOptions: ["1tbs", { allowSingleLine: false }], + docs: { description: "Enforce consistent brace style for blocks", recommended: false, @@ -30,8 +32,7 @@ module.exports = { type: "object", properties: { allowSingleLine: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -51,8 +52,8 @@ module.exports = { }, create(context) { - const style = context.options[0] || "1tbs", - params = context.options[1] || {}, + const style = context.options[0], + params = context.options[1], sourceCode = context.sourceCode; //-------------------------------------------------------------------------- diff --git a/lib/rules/camelcase.js b/lib/rules/camelcase.js index 51bb4122df0..7ffb48ee762 100644 --- a/lib/rules/camelcase.js +++ b/lib/rules/camelcase.js @@ -20,6 +20,14 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ + allow: [], + ignoreDestructuring: false, + ignoreGlobals: false, + ignoreIMports: false, + properties: "always" + }], + docs: { description: "Enforce camelcase naming convention", recommended: false, @@ -31,16 +39,13 @@ module.exports = { type: "object", properties: { ignoreDestructuring: { - type: "boolean", - default: false + type: "boolean" }, ignoreImports: { - type: "boolean", - default: false + type: "boolean" }, ignoreGlobals: { - type: "boolean", - default: false + type: "boolean" }, properties: { enum: ["always", "never"] @@ -67,12 +72,13 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; - const properties = options.properties === "never" ? "never" : "always"; - const ignoreDestructuring = options.ignoreDestructuring; - const ignoreImports = options.ignoreImports; - const ignoreGlobals = options.ignoreGlobals; - const allow = options.allow || []; + const [{ + allow, + ignoreDestructuring, + ignoreGlobals, + ignoreImports, + properties + }] = context.options; const sourceCode = context.sourceCode; //-------------------------------------------------------------------------- diff --git a/lib/rules/capitalized-comments.js b/lib/rules/capitalized-comments.js index 3a17b056620..645fb37493c 100644 --- a/lib/rules/capitalized-comments.js +++ b/lib/rules/capitalized-comments.js @@ -68,7 +68,7 @@ function getNormalizedOptions(rawOptions, which) { * @returns {Object} An object with "Line" and "Block" keys and corresponding * normalized options objects. */ -function getAllNormalizedOptions(rawOptions = {}) { +function getAllNormalizedOptions(rawOptions) { return { Line: getNormalizedOptions(rawOptions, "line"), Block: getNormalizedOptions(rawOptions, "block") @@ -104,6 +104,12 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: ["always", { + ignorePattern: "", + ignoreInlineComments: false, + ignoreConsecutiveComments: false + }], + docs: { description: "Enforce or disallow capitalization of the first letter of a comment", recommended: false, @@ -137,7 +143,7 @@ module.exports = { create(context) { - const capitalize = context.options[0] || "always", + const capitalize = context.options[0], normalizedOptions = getAllNormalizedOptions(context.options[1]), sourceCode = context.sourceCode; diff --git a/lib/rules/class-methods-use-this.js b/lib/rules/class-methods-use-this.js index 9cf8a1b8a86..11054630328 100644 --- a/lib/rules/class-methods-use-this.js +++ b/lib/rules/class-methods-use-this.js @@ -20,6 +20,11 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ + enforceForClassFields: true, + exceptMethods: [] + }], + docs: { description: "Enforce that class methods utilize `this`", recommended: false, @@ -36,8 +41,7 @@ module.exports = { } }, enforceForClassFields: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false @@ -48,9 +52,9 @@ module.exports = { } }, create(context) { - const config = Object.assign({}, context.options[0]); - const enforceForClassFields = config.enforceForClassFields !== false; - const exceptMethods = new Set(config.exceptMethods || []); + const [options] = context.options; + const { enforceForClassFields } = options; + const exceptMethods = new Set(options.exceptMethods); const stack = []; diff --git a/lib/rules/comma-dangle.js b/lib/rules/comma-dangle.js index e49983b722e..eaca024eadf 100644 --- a/lib/rules/comma-dangle.js +++ b/lib/rules/comma-dangle.js @@ -15,14 +15,6 @@ const astUtils = require("./utils/ast-utils"); // Helpers //------------------------------------------------------------------------------ -const DEFAULT_OPTIONS = Object.freeze({ - arrays: "never", - objects: "never", - imports: "never", - exports: "never", - functions: "never" -}); - /** * Checks whether or not a trailing comma is allowed in a given node. * If the `lastItem` is `RestElement` or `RestProperty`, it disallows trailing commas. @@ -53,17 +45,8 @@ function normalizeOptions(optionValue, ecmaVersion) { functions: ecmaVersion < 2017 ? "ignore" : optionValue }; } - if (typeof optionValue === "object" && optionValue !== null) { - return { - arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays, - objects: optionValue.objects || DEFAULT_OPTIONS.objects, - imports: optionValue.imports || DEFAULT_OPTIONS.imports, - exports: optionValue.exports || DEFAULT_OPTIONS.exports, - functions: optionValue.functions || DEFAULT_OPTIONS.functions - }; - } - return DEFAULT_OPTIONS; + return optionValue; } //------------------------------------------------------------------------------ @@ -75,6 +58,14 @@ module.exports = { meta: { type: "layout", + defaultOptions: [{ + arrays: "never", + objects: "never", + imports: "never", + exports: "never", + functions: "never" + }], + docs: { description: "Require or disallow trailing commas", recommended: false, diff --git a/lib/rules/comma-spacing.js b/lib/rules/comma-spacing.js index 96015ef6779..4b2653c0185 100644 --- a/lib/rules/comma-spacing.js +++ b/lib/rules/comma-spacing.js @@ -15,6 +15,8 @@ module.exports = { meta: { type: "layout", + defaultOptions: [{ before: false, after: true }], + docs: { description: "Enforce consistent spacing before and after commas", recommended: false, @@ -50,11 +52,7 @@ module.exports = { const sourceCode = context.sourceCode; const tokensAndComments = sourceCode.tokensAndComments; - - const options = { - before: context.options[0] ? context.options[0].before : false, - after: context.options[0] ? context.options[0].after : true - }; + const [options] = context.options; //-------------------------------------------------------------------------- // Helpers diff --git a/lib/rules/comma-style.js b/lib/rules/comma-style.js index bc69de4698d..8c21e14011d 100644 --- a/lib/rules/comma-style.js +++ b/lib/rules/comma-style.js @@ -16,6 +16,19 @@ module.exports = { meta: { type: "layout", + defaultOptions: ["last", { + exceptions: { + ArrayPattern: true, + ArrowFunctionExpression: true, + CallExpression: true, + FunctionDeclaration: true, + FunctionExpression: true, + ImportDeclaration: true, + ObjectPattern: true, + NewExpression: true + } + }], + docs: { description: "Enforce consistent comma style", recommended: false, @@ -50,26 +63,8 @@ module.exports = { }, create(context) { - const style = context.options[0] || "last", + const [style, { exceptions }] = context.options, sourceCode = context.sourceCode; - const exceptions = { - ArrayPattern: true, - ArrowFunctionExpression: true, - CallExpression: true, - FunctionDeclaration: true, - FunctionExpression: true, - ImportDeclaration: true, - ObjectPattern: true, - NewExpression: true - }; - - if (context.options.length === 2 && Object.prototype.hasOwnProperty.call(context.options[1], "exceptions")) { - const keys = Object.keys(context.options[1].exceptions); - - for (let i = 0; i < keys.length; i++) { - exceptions[keys[i]] = context.options[1].exceptions[keys[i]]; - } - } //-------------------------------------------------------------------------- // Helpers diff --git a/lib/rules/complexity.js b/lib/rules/complexity.js index b7925074db4..aecda90d721 100644 --- a/lib/rules/complexity.js +++ b/lib/rules/complexity.js @@ -17,11 +17,15 @@ const { upperCaseFirst } = require("../shared/string-utils"); // Rule Definition //------------------------------------------------------------------------------ +const THRESHOLD_DEFAULT = 20; + /** @type {import('../shared/types').Rule} */ module.exports = { meta: { type: "suggestion", + defaultOptions: [THRESHOLD_DEFAULT], + docs: { description: "Enforce a maximum cyclomatic complexity allowed in a program", recommended: false, @@ -60,15 +64,15 @@ module.exports = { create(context) { const option = context.options[0]; - let THRESHOLD = 20; + let threshold = THRESHOLD_DEFAULT; if ( typeof option === "object" && (Object.prototype.hasOwnProperty.call(option, "maximum") || Object.prototype.hasOwnProperty.call(option, "max")) ) { - THRESHOLD = option.maximum || option.max; + threshold = option.maximum || option.max; } else if (typeof option === "number") { - THRESHOLD = option; + threshold = option; } //-------------------------------------------------------------------------- @@ -137,7 +141,7 @@ module.exports = { return; } - if (complexity > THRESHOLD) { + if (complexity > threshold) { let name; if (codePath.origin === "class-field-initializer") { @@ -154,7 +158,7 @@ module.exports = { data: { name: upperCaseFirst(name), complexity, - max: THRESHOLD + max: threshold } }); } diff --git a/lib/rules/computed-property-spacing.js b/lib/rules/computed-property-spacing.js index 1e4e17c6c71..c2464c7153a 100644 --- a/lib/rules/computed-property-spacing.js +++ b/lib/rules/computed-property-spacing.js @@ -31,14 +31,15 @@ module.exports = { type: "object", properties: { enforceForClassMembers: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false } ], + defaultOptions: ["never"], + messages: { unexpectedSpaceBefore: "There should be no space before '{{tokenValue}}'.", unexpectedSpaceAfter: "There should be no space after '{{tokenValue}}'.", @@ -51,7 +52,7 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; const propertyNameMustBeSpaced = context.options[0] === "always"; // default is "never" - const enforceForClassMembers = !context.options[1] || context.options[1].enforceForClassMembers; + const enforceForClassMembers = !context.optionsRaw[1] || context.optionsRaw[1].enforceForClassMembers !== false; //-------------------------------------------------------------------------- // Helpers diff --git a/lib/rules/consistent-return.js b/lib/rules/consistent-return.js index 304e924b14a..7a77c60f342 100644 --- a/lib/rules/consistent-return.js +++ b/lib/rules/consistent-return.js @@ -52,6 +52,7 @@ module.exports = { meta: { type: "suggestion", + docs: { description: "Require `return` statements to either always or never specify values", recommended: false, @@ -62,13 +63,14 @@ module.exports = { type: "object", properties: { treatUndefinedAsUnspecified: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false }], + defaultOptions: [{ treatUndefinedAsUnspecified: false }], + messages: { missingReturn: "Expected to return a value at the end of {{name}}.", missingReturnValue: "{{name}} expected a return value.", @@ -77,8 +79,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; - const treatUndefinedAsUnspecified = options.treatUndefinedAsUnspecified === true; + const [{ treatUndefinedAsUnspecified }] = context.options; let funcInfo = null; /** diff --git a/lib/rules/consistent-this.js b/lib/rules/consistent-this.js index 658957ae25b..9a76e7a49a8 100644 --- a/lib/rules/consistent-this.js +++ b/lib/rules/consistent-this.js @@ -28,6 +28,8 @@ module.exports = { uniqueItems: true }, + defaultOptions: ["that"], + messages: { aliasNotAssignedToThis: "Designated alias '{{name}}' is not assigned to 'this'.", unexpectedAlias: "Unexpected alias '{{name}}' for 'this'." @@ -35,15 +37,9 @@ module.exports = { }, create(context) { - let aliases = []; + const aliases = context.options; const sourceCode = context.sourceCode; - if (context.options.length === 0) { - aliases.push("that"); - } else { - aliases = context.options; - } - /** * Reports that a variable declarator or assignment expression is assigning * a non-'this' value to the specified alias. diff --git a/lib/rules/curly.js b/lib/rules/curly.js index 35408247a19..536954c1c32 100644 --- a/lib/rules/curly.js +++ b/lib/rules/curly.js @@ -53,6 +53,8 @@ module.exports = { ] }, + defaultOptions: ["all"], + fixable: "code", messages: { diff --git a/lib/rules/generator-star-spacing.js b/lib/rules/generator-star-spacing.js index 81c0b61059a..67849a08515 100644 --- a/lib/rules/generator-star-spacing.js +++ b/lib/rules/generator-star-spacing.js @@ -59,6 +59,11 @@ module.exports = { } ], + defaultOptions: [{ + after: false, + before: true + }], + messages: { missingBefore: "Missing space before *.", missingAfter: "Missing space after *.", diff --git a/lib/rules/indent.js b/lib/rules/indent.js index 7ea4b3f86c3..70f86461351 100644 --- a/lib/rules/indent.js +++ b/lib/rules/indent.js @@ -602,12 +602,10 @@ module.exports = { ObjectExpression: ELEMENT_LIST_SCHEMA, ImportDeclaration: ELEMENT_LIST_SCHEMA, flatTernaryExpressions: { - type: "boolean", - default: false + type: "boolean" }, offsetTernaryExpressions: { - type: "boolean", - default: false + type: "boolean" }, ignoredNodes: { type: "array", @@ -619,13 +617,15 @@ module.exports = { } }, ignoreComments: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false } ], + + defaultOptions: [4], + messages: { wrongIndentation: "Expected indentation of {{expected}} but found {{actual}}." } diff --git a/lib/rules/lines-around-comment.js b/lib/rules/lines-around-comment.js index 10aeba3cbc1..e282bb6ebcf 100644 --- a/lib/rules/lines-around-comment.js +++ b/lib/rules/lines-around-comment.js @@ -67,28 +67,22 @@ module.exports = { type: "object", properties: { beforeBlockComment: { - type: "boolean", - default: true + type: "boolean" }, afterBlockComment: { - type: "boolean", - default: false + type: "boolean" }, beforeLineComment: { - type: "boolean", - default: false + type: "boolean" }, afterLineComment: { - type: "boolean", - default: false + type: "boolean" }, allowBlockStart: { - type: "boolean", - default: false + type: "boolean" }, allowBlockEnd: { - type: "boolean", - default: false + type: "boolean" }, allowClassStart: { type: "boolean" @@ -115,13 +109,18 @@ module.exports = { type: "boolean" }, afterHashbangComment: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false } ], + + defaultOptions: [{ + applyDefaultIgnorePatterns: true, + beforeBlockComment: true + }], + messages: { after: "Expected line after comment.", before: "Expected line before comment." @@ -129,14 +128,10 @@ module.exports = { }, create(context) { - - const options = Object.assign({}, context.options[0]); - const ignorePattern = options.ignorePattern; + const [options] = context.options; + const { applyDefaultIgnorePatterns, ignorePattern } = options; const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN; const customIgnoreRegExp = new RegExp(ignorePattern, "u"); - const applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false; - - options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true; const sourceCode = context.sourceCode; diff --git a/lib/rules/max-len.js b/lib/rules/max-len.js index 53ad5310799..fa5017b70ac 100644 --- a/lib/rules/max-len.js +++ b/lib/rules/max-len.js @@ -79,6 +79,12 @@ module.exports = { OPTIONS_OR_INTEGER_SCHEMA, OPTIONS_SCHEMA ], + + defaultOptions: [{ + code: 80, + tabWidth: 4 + }], + messages: { max: "This line has a length of {{lineLength}}. Maximum allowed is {{maxLength}}.", maxComment: "This line has a comment length of {{lineLength}}. Maximum allowed is {{maxCommentLength}}." diff --git a/lib/rules/new-cap.js b/lib/rules/new-cap.js index f81e42fd0c8..650cdff093f 100644 --- a/lib/rules/new-cap.js +++ b/lib/rules/new-cap.js @@ -92,12 +92,10 @@ module.exports = { type: "object", properties: { newIsCap: { - type: "boolean", - default: true + type: "boolean" }, capIsNew: { - type: "boolean", - default: true + type: "boolean" }, newIsCapExceptions: { type: "array", @@ -118,13 +116,19 @@ module.exports = { type: "string" }, properties: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false } ], + + defaultOptions: [{ + capIsNew: true, + newIsCap: true, + properties: true + }], + messages: { upper: "A function with a name starting with an uppercase letter should only be used as a constructor.", lower: "A constructor name should not start with a lowercase letter." @@ -132,11 +136,7 @@ module.exports = { }, create(context) { - - const config = Object.assign({}, context.options[0]); - - config.newIsCap = config.newIsCap !== false; - config.capIsNew = config.capIsNew !== false; + const [config] = context.options; const skipProperties = config.properties === false; const newIsCapExceptions = checkArray(config, "newIsCapExceptions", []).reduce(invert, {}); diff --git a/lib/rules/no-implicit-coercion.js b/lib/rules/no-implicit-coercion.js index 36baad3835e..c693e754d25 100644 --- a/lib/rules/no-implicit-coercion.js +++ b/lib/rules/no-implicit-coercion.js @@ -14,21 +14,6 @@ const astUtils = require("./utils/ast-utils"); const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/u; const ALLOWABLE_OPERATORS = ["~", "!!", "+", "*"]; -/** - * Parses and normalizes an option object. - * @param {Object} options An option object to parse. - * @returns {Object} The parsed and normalized option object. - */ -function parseOptions(options) { - return { - boolean: "boolean" in options ? options.boolean : true, - number: "number" in options ? options.number : true, - string: "string" in options ? options.string : true, - disallowTemplateShorthand: "disallowTemplateShorthand" in options ? options.disallowTemplateShorthand : false, - allow: options.allow || [] - }; -} - /** * Checks whether or not a node is a double logical negating. * @param {ASTNode} node An UnaryExpression node to check. @@ -202,20 +187,16 @@ module.exports = { type: "object", properties: { boolean: { - type: "boolean", - default: true + type: "boolean" }, number: { - type: "boolean", - default: true + type: "boolean" }, string: { - type: "boolean", - default: true + type: "boolean" }, disallowTemplateShorthand: { - type: "boolean", - default: false + type: "boolean" }, allow: { type: "array", @@ -228,13 +209,21 @@ module.exports = { additionalProperties: false }], + defaultOptions: [{ + allow: [], + boolean: true, + disallowTemplateShorthand: false, + number: true, + string: true + }], + messages: { useRecommendation: "use `{{recommendation}}` instead." } }, create(context) { - const options = parseOptions(context.options[0] || {}); + const [options] = context.options; const sourceCode = context.sourceCode; /** diff --git a/lib/rules/no-multi-spaces.js b/lib/rules/no-multi-spaces.js index 62074e657ae..4ad9a470e73 100644 --- a/lib/rules/no-multi-spaces.js +++ b/lib/rules/no-multi-spaces.js @@ -38,14 +38,15 @@ module.exports = { additionalProperties: false }, ignoreEOLComments: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false } ], + defaultOptions: [{ ignoreEOLComments: false, exceptions: { Property: true } }], + messages: { multipleSpaces: "Multiple spaces found before '{{displayValue}}'." } @@ -53,9 +54,7 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; - const options = context.options[0] || {}; - const ignoreEOLComments = options.ignoreEOLComments; - const exceptions = Object.assign({ Property: true }, options.exceptions); + const [{ exceptions, ignoreEOLComments }] = context.options; const hasExceptions = Object.keys(exceptions).some(key => exceptions[key]); /** diff --git a/lib/rules/no-sequences.js b/lib/rules/no-sequences.js index cd21fc7842c..96184b94f9e 100644 --- a/lib/rules/no-sequences.js +++ b/lib/rules/no-sequences.js @@ -15,9 +15,6 @@ const astUtils = require("./utils/ast-utils"); // Helpers //------------------------------------------------------------------------------ -const DEFAULT_OPTIONS = { - allowInParentheses: true -}; //------------------------------------------------------------------------------ // Rule Definition @@ -37,20 +34,23 @@ module.exports = { schema: [{ properties: { allowInParentheses: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false }], + defaultOptions: [{ + allowInParentheses: true + }], + messages: { unexpectedCommaExpression: "Unexpected use of comma operator." } }, create(context) { - const options = Object.assign({}, DEFAULT_OPTIONS, context.options[0]); + const [{ allowInParentheses }] = context.options; const sourceCode = context.sourceCode; /** @@ -116,7 +116,7 @@ module.exports = { } // Wrapping a sequence in extra parens indicates intent - if (options.allowInParentheses) { + if (allowInParentheses) { if (requiresExtraParens(node)) { if (isParenthesisedTwice(node)) { return; diff --git a/lib/rules/no-unused-expressions.js b/lib/rules/no-unused-expressions.js index 46bb7baac22..fd1437c1606 100644 --- a/lib/rules/no-unused-expressions.js +++ b/lib/rules/no-unused-expressions.js @@ -42,37 +42,41 @@ module.exports = { type: "object", properties: { allowShortCircuit: { - type: "boolean", - default: false + type: "boolean" }, allowTernary: { - type: "boolean", - default: false + type: "boolean" }, allowTaggedTemplates: { - type: "boolean", - default: false + type: "boolean" }, enforceForJSX: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false } ], + defaultOptions: [{ + allowShortCircuit: false, + allowTernary: false, + allowTaggedTemplates: false, + enforceForJSX: false + }], + messages: { unusedExpression: "Expected an assignment or function call and instead saw an expression." } }, create(context) { - const config = context.options[0] || {}, - allowShortCircuit = config.allowShortCircuit || false, - allowTernary = config.allowTernary || false, - allowTaggedTemplates = config.allowTaggedTemplates || false, - enforceForJSX = config.enforceForJSX || false; + const [{ + allowShortCircuit, + allowTernary, + allowTaggedTemplates, + enforceForJSX + }] = context.options; /** * Has AST suggesting a directive. diff --git a/lib/rules/no-use-before-define.js b/lib/rules/no-use-before-define.js index 9d6b043404a..6e4510d2df0 100644 --- a/lib/rules/no-use-before-define.js +++ b/lib/rules/no-use-before-define.js @@ -18,21 +18,16 @@ const FOR_IN_OF_TYPE = /^For(?:In|Of)Statement$/u; * @returns {Object} The parsed options. */ function parseOptions(options) { - let functions = true; - let classes = true; - let variables = true; - let allowNamedExports = false; - - if (typeof options === "string") { - functions = (options !== "nofunc"); - } else if (typeof options === "object" && options !== null) { - functions = options.functions !== false; - classes = options.classes !== false; - variables = options.variables !== false; - allowNamedExports = !!options.allowNamedExports; + if (typeof options === "object" && options !== null) { + return options; } - return { functions, classes, variables, allowNamedExports }; + const functions = + typeof options === "string" + ? options !== "nofunc" + : true; + + return { functions, classes: true, variables: true, allowNamedExports: false }; } /** @@ -251,6 +246,12 @@ module.exports = { } ], + defaultOptions: [{ + classes: true, + functions: true, + variables: true + }], + messages: { usedBeforeDefined: "'{{name}}' was used before it was defined." } diff --git a/lib/rules/object-curly-newline.js b/lib/rules/object-curly-newline.js index caf1982312a..f2173a65ec6 100644 --- a/lib/rules/object-curly-newline.js +++ b/lib/rules/object-curly-newline.js @@ -176,6 +176,8 @@ module.exports = { } ], + defaultOptions: [{ consistent: true }], + messages: { unexpectedLinebreakBeforeClosingBrace: "Unexpected line break before this closing brace.", unexpectedLinebreakAfterOpeningBrace: "Unexpected line break after this opening brace.", @@ -186,7 +188,7 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; - const normalizedOptions = normalizeOptions(context.options[0]); + const normalizedOptions = normalizeOptions(context.optionsRaw[0]); /** * Reports a given node if it violated this rule. diff --git a/lib/rules/operator-linebreak.js b/lib/rules/operator-linebreak.js index 2b609f63576..1b5ed0f6eb1 100644 --- a/lib/rules/operator-linebreak.js +++ b/lib/rules/operator-linebreak.js @@ -44,6 +44,13 @@ module.exports = { } ], + defaultOptions: ["after", { + overrides: { + "?": "before", + ":": "before" + } + }], + fixable: "code", messages: { @@ -56,9 +63,9 @@ module.exports = { create(context) { - const usedDefaultGlobal = !context.options[0]; - const globalStyle = context.options[0] || "after"; - const options = context.options[1] || {}; + const usedDefaultGlobal = !context.optionsRaw[0]; + const globalStyle = context.options[0]; + const options = context.optionsRaw[1] || {}; const styleOverrides = options.overrides ? Object.assign({}, options.overrides) : {}; if (usedDefaultGlobal && !styleOverrides["?"]) { diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index 21dc3952902..ba6f39aec47 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -47,18 +47,21 @@ module.exports = { type: "object", properties: { enforceForSwitchCase: { - type: "boolean", - default: true + type: "boolean" }, enforceForIndexOf: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false } ], + defaultOptions: [{ + enforceForIndexOf: false, + enforceForSwitchCase: true + }], + messages: { comparisonWithNaN: "Use the isNaN function to compare with NaN.", switchNaN: "'switch(NaN)' can never match a case clause. Use Number.isNaN instead of the switch.", diff --git a/lib/shared/types.js b/lib/shared/types.js index e3a40bc986b..5225c66a428 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -148,6 +148,7 @@ module.exports = {}; /** * @typedef {Object} RuleMeta * @property {boolean} [deprecated] If `true` then the rule has been deprecated. + * @property {Array} [defaultOptions] Default options for the rule. * @property {RuleMetaDocs} docs The document information of the rule. * @property {"code"|"whitespace"} [fixable] The autofix type. * @property {boolean} [hasSuggestions] If `true` then the rule provides suggestions. diff --git a/tests/lib/linter/deep-merge.js b/tests/lib/linter/deep-merge.js new file mode 100644 index 00000000000..f8dbd248c5d --- /dev/null +++ b/tests/lib/linter/deep-merge.js @@ -0,0 +1,61 @@ +/* eslint-disable no-undefined -- `null` and `undefined` are different in options */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const assert = require("assert"); +const { deepMerge } = require("../../../lib/linter/deep-merge"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +/** + * Turns a value into its string equivalent for a test name. + * @param {unknown} value Value to be stringified. + * @returns {string} String equivalent of the value. + */ +function stringify(value) { + return typeof value === "object" ? JSON.stringify(value) : `${value}`; +} + +describe("deepMerge", () => { + for (const [first, second, result] of [ + [undefined, undefined, undefined], + [undefined, null, null], + [null, undefined, null], + [null, null, null], + [{}, null, null], + [null, {}, null], + [null, "abc", "abc"], + [null, 123, 123], + [{ a: undefined }, { a: 0 }, { a: 0 }], + [{ a: null }, { a: 0 }, { a: 0 }], + [{ a: 0 }, { a: 1 }, { a: 1 }], + [{ a: 0 }, { a: null }, { a: null }], + [{ a: 0 }, { a: undefined }, { a: 0 }], + [{ a: ["b"] }, { a: ["c"] }, { a: ["c"] }], + [{ a: [{ b: "c" }] }, { a: [{ d: "e" }] }, { a: [{ d: "e" }] }], + [{ a: 0 }, "abc", "abc"], + [{ a: 0 }, 123, 123], + [123, undefined, 123], + [123, null, null], + [123, { a: 0 }, { a: 0 }], + ["abc", undefined, "abc"], + ["abc", null, null], + ["abc", { a: 0 }, { a: 0 }], + [["abc"], undefined, ["abc"]], + [["abc"], null, null], + [[], ["def"], []], + [["abc"], ["def"], ["abc"]] + ]) { + it(`${stringify(first)}, ${stringify(second)}`, () => { + assert.deepStrictEqual(deepMerge(first, second), result); + }); + } +}); + +/* eslint-enable no-undefined -- `null` and `undefined` are different in options */ From 90504b72c604a91ea541ff03b80d70ed570ade22 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 18 Oct 2023 23:29:05 -0400 Subject: [PATCH 02/26] Removed optionsRaw --- lib/linter/get-rule-options.js | 14 +------------- lib/linter/linter.js | 3 +-- lib/rules/array-bracket-newline.js | 4 +--- lib/rules/computed-property-spacing.js | 7 +++---- lib/rules/object-curly-newline.js | 4 +--- lib/rules/operator-linebreak.js | 13 +++---------- 6 files changed, 10 insertions(+), 35 deletions(-) diff --git a/lib/linter/get-rule-options.js b/lib/linter/get-rule-options.js index 9b1747a33dd..d4ccb781d54 100644 --- a/lib/linter/get-rule-options.js +++ b/lib/linter/get-rule-options.js @@ -45,16 +45,4 @@ function getRuleOptions(defaultOptions, ruleConfig) { return defaultOptions || []; } -/** - * Get the raw (non-defaulted) options for a rule (not including severity), if any - * @param {Array|number} ruleConfig rule configuration - * @returns {Array} of rule options, empty Array if none - */ -function getRuleOptionsRaw(ruleConfig) { - if (Array.isArray(ruleConfig)) { - return ruleConfig.slice(1); - } - return []; -} - -module.exports = { getRuleOptions, getRuleOptionsRaw }; +module.exports = { getRuleOptions }; diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 37c6b59316e..e7a532380e2 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -33,7 +33,7 @@ const CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"), applyDisableDirectives = require("./apply-disable-directives"), ConfigCommentParser = require("./config-comment-parser"), - { getRuleOptions, getRuleOptionsRaw } = require("./get-rule-options"), + { getRuleOptions } = require("./get-rule-options"), NodeEventGenerator = require("./node-event-generator"), createReportTranslator = require("./report-translator"), Rules = require("./rules"), @@ -1026,7 +1026,6 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO { id: ruleId, options: getRuleOptions(rule.meta && rule.meta.defaultOptions, configuredRules[ruleId]), - optionsRaw: getRuleOptionsRaw(configuredRules[ruleId]), report(...args) { /* diff --git a/lib/rules/array-bracket-newline.js b/lib/rules/array-bracket-newline.js index 6b90955a648..c3676bf4dfa 100644 --- a/lib/rules/array-bracket-newline.js +++ b/lib/rules/array-bracket-newline.js @@ -16,8 +16,6 @@ module.exports = { meta: { type: "layout", - defaultOptions: [{ minItems: null, multiline: true }], - docs: { description: "Enforce linebreaks after opening and before closing array brackets", recommended: false, @@ -194,7 +192,7 @@ module.exports = { */ function check(node) { const elements = node.elements; - const normalizedOptions = normalizeOptions(context.optionsRaw[0]); + const normalizedOptions = normalizeOptions(context.options[0]); const options = normalizedOptions[node.type]; const openBracket = sourceCode.getFirstToken(node); const closeBracket = sourceCode.getLastToken(node); diff --git a/lib/rules/computed-property-spacing.js b/lib/rules/computed-property-spacing.js index c2464c7153a..1e4e17c6c71 100644 --- a/lib/rules/computed-property-spacing.js +++ b/lib/rules/computed-property-spacing.js @@ -31,15 +31,14 @@ module.exports = { type: "object", properties: { enforceForClassMembers: { - type: "boolean" + type: "boolean", + default: true } }, additionalProperties: false } ], - defaultOptions: ["never"], - messages: { unexpectedSpaceBefore: "There should be no space before '{{tokenValue}}'.", unexpectedSpaceAfter: "There should be no space after '{{tokenValue}}'.", @@ -52,7 +51,7 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; const propertyNameMustBeSpaced = context.options[0] === "always"; // default is "never" - const enforceForClassMembers = !context.optionsRaw[1] || context.optionsRaw[1].enforceForClassMembers !== false; + const enforceForClassMembers = !context.options[1] || context.options[1].enforceForClassMembers; //-------------------------------------------------------------------------- // Helpers diff --git a/lib/rules/object-curly-newline.js b/lib/rules/object-curly-newline.js index f2173a65ec6..caf1982312a 100644 --- a/lib/rules/object-curly-newline.js +++ b/lib/rules/object-curly-newline.js @@ -176,8 +176,6 @@ module.exports = { } ], - defaultOptions: [{ consistent: true }], - messages: { unexpectedLinebreakBeforeClosingBrace: "Unexpected line break before this closing brace.", unexpectedLinebreakAfterOpeningBrace: "Unexpected line break after this opening brace.", @@ -188,7 +186,7 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; - const normalizedOptions = normalizeOptions(context.optionsRaw[0]); + const normalizedOptions = normalizeOptions(context.options[0]); /** * Reports a given node if it violated this rule. diff --git a/lib/rules/operator-linebreak.js b/lib/rules/operator-linebreak.js index 1b5ed0f6eb1..2b609f63576 100644 --- a/lib/rules/operator-linebreak.js +++ b/lib/rules/operator-linebreak.js @@ -44,13 +44,6 @@ module.exports = { } ], - defaultOptions: ["after", { - overrides: { - "?": "before", - ":": "before" - } - }], - fixable: "code", messages: { @@ -63,9 +56,9 @@ module.exports = { create(context) { - const usedDefaultGlobal = !context.optionsRaw[0]; - const globalStyle = context.options[0]; - const options = context.optionsRaw[1] || {}; + const usedDefaultGlobal = !context.options[0]; + const globalStyle = context.options[0] || "after"; + const options = context.options[1] || {}; const styleOverrides = options.overrides ? Object.assign({}, options.overrides) : {}; if (usedDefaultGlobal && !styleOverrides["?"]) { From 7a61675c10a779809a863ebf0903ba7482674220 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 26 Oct 2023 13:06:33 +0300 Subject: [PATCH 03/26] computed-property-spacing: defaultOptions --- lib/rules/computed-property-spacing.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/rules/computed-property-spacing.js b/lib/rules/computed-property-spacing.js index 1e4e17c6c71..3aefc2d2365 100644 --- a/lib/rules/computed-property-spacing.js +++ b/lib/rules/computed-property-spacing.js @@ -39,6 +39,8 @@ module.exports = { } ], + defaultOptions: ["never", { enforceForClassMembers: true }], + messages: { unexpectedSpaceBefore: "There should be no space before '{{tokenValue}}'.", unexpectedSpaceAfter: "There should be no space after '{{tokenValue}}'.", From e4933116d6e153539bdc9f667873644e1ed436b6 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 21 Nov 2023 03:57:52 +0100 Subject: [PATCH 04/26] fix: handle object type mismatches in merging --- lib/linter/deep-merge.js | 16 +++++++++++----- lib/rules/consistent-return.js | 1 - tests/lib/linter/deep-merge.js | 10 +++++++--- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/linter/deep-merge.js b/lib/linter/deep-merge.js index ef5a533d9fb..6f477dcda68 100644 --- a/lib/linter/deep-merge.js +++ b/lib/linter/deep-merge.js @@ -22,16 +22,22 @@ function isObjectNotArray(obj) { * @returns {T | U | (T & U)} Merged equivalent of second on top of first. */ function deepMerge(first, second) { - if (second === null || (second !== undefined && typeof second !== "object")) { + if ( + second === null || + (second !== undefined && typeof second !== "object") || + Array.isArray(second) || + (typeof first !== "object" && typeof second === "object" && second !== null) || + first === null && second !== undefined + ) { return second; } - if (typeof first !== "object" && typeof second === "object" && second !== null) { - return second; + if (first === null || second === undefined) { + return first; } - if (first === null || Array.isArray(first) || second === undefined) { - return first; + if (Array.isArray(first) !== Array.isArray(second)) { + return second; } const keysUnion = new Set(Object.keys(first).concat(Object.keys(second))); diff --git a/lib/rules/consistent-return.js b/lib/rules/consistent-return.js index 7a77c60f342..f31c2e10025 100644 --- a/lib/rules/consistent-return.js +++ b/lib/rules/consistent-return.js @@ -52,7 +52,6 @@ module.exports = { meta: { type: "suggestion", - docs: { description: "Require `return` statements to either always or never specify values", recommended: false, diff --git a/tests/lib/linter/deep-merge.js b/tests/lib/linter/deep-merge.js index f8dbd248c5d..59b4c7ac489 100644 --- a/tests/lib/linter/deep-merge.js +++ b/tests/lib/linter/deep-merge.js @@ -29,9 +29,11 @@ describe("deepMerge", () => { [null, undefined, null], [null, null, null], [{}, null, null], - [null, {}, null], + [null, {}, {}], [null, "abc", "abc"], + [null, { abc: true }, { abc: true }], [null, 123, 123], + [null, [123], [123]], [{ a: undefined }, { a: 0 }, { a: 0 }], [{ a: null }, { a: 0 }, { a: 0 }], [{ a: 0 }, { a: 1 }, { a: 1 }], @@ -49,8 +51,10 @@ describe("deepMerge", () => { ["abc", { a: 0 }, { a: 0 }], [["abc"], undefined, ["abc"]], [["abc"], null, null], - [[], ["def"], []], - [["abc"], ["def"], ["abc"]] + [[], ["def"], ["def"]], + [["abc"], ["def"], ["def"]], + [["abc"], { def: 0 }, { def: 0 }], + [{ abc: true }, ["def"], ["def"]] ]) { it(`${stringify(first)}, ${stringify(second)}`, () => { assert.deepStrictEqual(deepMerge(first, second), result); From 1bb2568917aded7bcac8fe477022f87e3476a99a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 21 Nov 2023 04:57:46 +0100 Subject: [PATCH 05/26] Validate arrays in flat-config-array --- lib/config/rule-validator.js | 10 +- .../{deep-merge.js => deep-merge-arrays.js} | 37 +++++-- lib/linter/get-rule-options.js | 29 +---- tests/lib/config/flat-config-array.js | 7 +- tests/lib/linter/deep-merge-arrays.js | 103 ++++++++++++++++++ tests/lib/linter/deep-merge.js | 65 ----------- 6 files changed, 145 insertions(+), 106 deletions(-) rename lib/linter/{deep-merge.js => deep-merge-arrays.js} (66%) create mode 100644 tests/lib/linter/deep-merge-arrays.js delete mode 100644 tests/lib/linter/deep-merge.js diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js index eee5b40bd07..1db255e5de5 100644 --- a/lib/config/rule-validator.js +++ b/lib/config/rule-validator.js @@ -16,6 +16,7 @@ const { getRuleFromConfig, getRuleOptionsSchema } = require("./flat-config-helpers"); +const { deepMergeArrays } = require("../linter/deep-merge-arrays"); const ruleReplacements = require("../../conf/replacements.json"); //----------------------------------------------------------------------------- @@ -141,7 +142,10 @@ class RuleValidator { if (validateRule) { - validateRule(ruleOptions.slice(1)); + const slicedOptions = ruleOptions.slice(1); + const mergedOptions = deepMergeArrays(rule.meta.defaultOptions, slicedOptions); + + validateRule(mergedOptions); if (validateRule.errors) { throw new Error(`Key "rules": Key "${ruleId}": ${ @@ -150,6 +154,10 @@ class RuleValidator { ).join("") }`); } + + if (mergedOptions.length) { + config.rules[ruleId] = [ruleOptions[0], ...mergedOptions]; + } } } } diff --git a/lib/linter/deep-merge.js b/lib/linter/deep-merge-arrays.js similarity index 66% rename from lib/linter/deep-merge.js rename to lib/linter/deep-merge-arrays.js index 6f477dcda68..ad22a28b70b 100644 --- a/lib/linter/deep-merge.js +++ b/lib/linter/deep-merge-arrays.js @@ -21,14 +21,8 @@ function isObjectNotArray(obj) { * @param {U} second User-specified value. * @returns {T | U | (T & U)} Merged equivalent of second on top of first. */ -function deepMerge(first, second) { - if ( - second === null || - (second !== undefined && typeof second !== "object") || - Array.isArray(second) || - (typeof first !== "object" && typeof second === "object" && second !== null) || - first === null && second !== undefined - ) { +function deepMergeElements(first, second) { + if (second === null || (second !== undefined && typeof second !== "object")) { return second; } @@ -36,7 +30,7 @@ function deepMerge(first, second) { return first; } - if (Array.isArray(first) !== Array.isArray(second)) { + if (Array.isArray(first)) { return second; } @@ -48,7 +42,7 @@ function deepMerge(first, second) { if (firstValue !== undefined && secondValue !== undefined) { if (isObjectNotArray(firstValue) && isObjectNotArray(secondValue)) { - acc[key] = deepMerge(firstValue, secondValue); + acc[key] = deepMergeElements(firstValue, secondValue); } else { acc[key] = secondValue; } @@ -62,6 +56,27 @@ function deepMerge(first, second) { }, {}); } -module.exports = { deepMerge }; +/** + * Deeply merges second on top of first, creating a new [] array if needed. + * @param {T[]} first Base, default values. + * @param {U[]} second User-specified values. + * @returns {(T | U | (T & U))[]} Merged equivalent of second on top of first. + */ +function deepMergeArrays(first, second) { + if (!first) { + return second; + } + + if (!second) { + return first; + } + + return [ + ...first.map((value, i) => deepMergeElements(value, second[i])), + ...second.slice(first.length) + ]; +} + +module.exports = { deepMergeArrays }; /* eslint-enable eqeqeq, no-undefined -- `null` and `undefined` are different in options */ diff --git a/lib/linter/get-rule-options.js b/lib/linter/get-rule-options.js index d4ccb781d54..82e4a2bbd83 100644 --- a/lib/linter/get-rule-options.js +++ b/lib/linter/get-rule-options.js @@ -5,32 +5,7 @@ "use strict"; -const { deepMerge } = require("./deep-merge"); - -/** - * Creates rule options by merging a user config on top of any default options. - * @param {Array|undefined} defaultOptions Default options from a rule's meta. - * @param {Array} ruleConfig User-specified rule configuration. - * @returns {Array} Rule options, factoring in user config and any defaults. - */ -function getMergedRuleOptions(defaultOptions, ruleConfig) { - if (!defaultOptions) { - return ruleConfig; - } - - const options = []; - const sharedLength = Math.min(defaultOptions.length, ruleConfig.length); - let i; - - for (i = 0; i < sharedLength; i += 1) { - options.push(deepMerge(defaultOptions[i], ruleConfig[i])); - } - - options.push(...defaultOptions.slice(i)); - options.push(...ruleConfig.slice(i)); - - return options; -} +const { deepMergeArrays } = require("./deep-merge-arrays"); /** * Get the options for a rule (not including severity), if any, factoring in defaults @@ -40,7 +15,7 @@ function getMergedRuleOptions(defaultOptions, ruleConfig) { */ function getRuleOptions(defaultOptions, ruleConfig) { if (Array.isArray(ruleConfig)) { - return getMergedRuleOptions(defaultOptions, ruleConfig.slice(1)); + return deepMergeArrays(defaultOptions, ruleConfig.slice(1)); } return defaultOptions || []; } diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index b0dbfec93f7..8bc46ca5707 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -17,6 +17,7 @@ const { } = require("@eslint/js").configs; const stringify = require("json-stable-stringify-without-jsonify"); const espree = require("espree"); +const rules = require("../../../lib/rules"); //----------------------------------------------------------------------------- // Helpers @@ -139,7 +140,7 @@ function normalizeRuleConfig(rulesConfig) { }; for (const ruleId of Object.keys(rulesConfigCopy)) { - rulesConfigCopy[ruleId] = [2]; + rulesConfigCopy[ruleId] = [2, ...(rules.get(ruleId).meta.defaultOptions || [])]; } return rulesConfigCopy; @@ -2008,9 +2009,11 @@ describe("FlatConfigArray", () => { assert.deepStrictEqual(config.rules, { camelcase: [2, { + allow: [], ignoreDestructuring: false, ignoreGlobals: false, - ignoreImports: false + ignoreImports: false, + properties: "always" }], "default-case": [2, {}] }); diff --git a/tests/lib/linter/deep-merge-arrays.js b/tests/lib/linter/deep-merge-arrays.js new file mode 100644 index 00000000000..4aefb11f19c --- /dev/null +++ b/tests/lib/linter/deep-merge-arrays.js @@ -0,0 +1,103 @@ +/* eslint-disable no-undefined -- `null` and `undefined` are different in options */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const assert = require("assert"); +const { deepMergeArrays } = require("../../../lib/linter/deep-merge-arrays"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +/** + * Turns a value into its string equivalent for a test name. + * @param {unknown} value Value to be stringified. + * @returns {string} String equivalent of the value. + */ +function stringify(value) { + return typeof value === "object" ? JSON.stringify(value) : `${value}`; +} + +describe("deepMerge", () => { + for (const [first, second, result] of [ + [["abc"], undefined, ["abc"]], + [undefined, ["abc"], ["abc"]], + [[], ["abc"], ["abc"]], + [[undefined], ["abc"], ["abc"]], + [[undefined, undefined], ["abc"], ["abc", undefined]], + [[undefined, undefined], ["abc", "def"], ["abc", "def"]], + [[undefined, null], ["abc"], ["abc", null]], + [[undefined, null], ["abc", "def"], ["abc", "def"]], + [[null], ["abc"], ["abc"]], + [[123], [undefined], [123]], + [[123], [null], [null]], + [[123], [{ a: 0 }], [{ a: 0 }]], + [["abc"], [undefined], ["abc"]], + [["abc"], [null], [null]], + [["abc"], ["def"], ["def"]], + [[["abc"]], [null], [null]], + [[["abc"]], ["def"], ["def"]], + [[["abc"]], [{ a: 0 }], [{ a: 0 }]], + [[{ abc: true }], ["def"], ["def"]], + [[{ a: undefined }], [{ a: 0 }], [{ a: 0 }]], + [[{ a: null }], [{ a: 0 }], [{ a: 0 }]], + [[{ a: 0 }], [{ a: 1 }], [{ a: 1 }]], + [[{ a: 0 }], [{ a: null }], [{ a: null }]], + [[{ a: 0 }], [{ a: undefined }], [{ a: 0 }]], + [[{ a: 0 }], ["abc"], ["abc"]], + [[{ a: 0 }], [123], [123]], + [[[{ a: 0 }]], [123], [123]], + [ + [{ a: ["b"] }], + [{ a: ["c"] }], + [{ a: ["c"] }] + ], + [ + [{ a: [{ b: "c" }] }], + [{ a: [{ d: "e" }] }], + [{ a: [{ d: "e" }] }] + ], + [ + [{ a: { b: "c" }, d: true }], + [{ a: { e: "f" } }], + [{ a: { b: "c", e: "f" }, d: true }] + ], + [ + [{ a: { b: "c" } }], + [{ a: { e: "f" }, d: true }], + [{ a: { b: "c", e: "f" }, d: true }] + ], + [ + [{ a: { b: "c" } }, { d: true }], + [{ a: { e: "f" } }, { f: 123 }], + [{ a: { b: "c", e: "f" } }, { d: true, f: 123 }] + ], + [ + [{ + allow: [], + ignoreDestructuring: false, + ignoreGlobals: false, + ignoreImports: false, + properties: "always" + }], + [], + [{ + allow: [], + ignoreDestructuring: false, + ignoreGlobals: false, + ignoreImports: false, + properties: "always" + }] + ] + ]) { + it(`${stringify(first)}, ${stringify(second)}`, () => { + assert.deepStrictEqual(deepMergeArrays(first, second), result); + }); + } +}); + +/* eslint-enable no-undefined -- `null` and `undefined` are different in options */ diff --git a/tests/lib/linter/deep-merge.js b/tests/lib/linter/deep-merge.js deleted file mode 100644 index 59b4c7ac489..00000000000 --- a/tests/lib/linter/deep-merge.js +++ /dev/null @@ -1,65 +0,0 @@ -/* eslint-disable no-undefined -- `null` and `undefined` are different in options */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("assert"); -const { deepMerge } = require("../../../lib/linter/deep-merge"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -/** - * Turns a value into its string equivalent for a test name. - * @param {unknown} value Value to be stringified. - * @returns {string} String equivalent of the value. - */ -function stringify(value) { - return typeof value === "object" ? JSON.stringify(value) : `${value}`; -} - -describe("deepMerge", () => { - for (const [first, second, result] of [ - [undefined, undefined, undefined], - [undefined, null, null], - [null, undefined, null], - [null, null, null], - [{}, null, null], - [null, {}, {}], - [null, "abc", "abc"], - [null, { abc: true }, { abc: true }], - [null, 123, 123], - [null, [123], [123]], - [{ a: undefined }, { a: 0 }, { a: 0 }], - [{ a: null }, { a: 0 }, { a: 0 }], - [{ a: 0 }, { a: 1 }, { a: 1 }], - [{ a: 0 }, { a: null }, { a: null }], - [{ a: 0 }, { a: undefined }, { a: 0 }], - [{ a: ["b"] }, { a: ["c"] }, { a: ["c"] }], - [{ a: [{ b: "c" }] }, { a: [{ d: "e" }] }, { a: [{ d: "e" }] }], - [{ a: 0 }, "abc", "abc"], - [{ a: 0 }, 123, 123], - [123, undefined, 123], - [123, null, null], - [123, { a: 0 }, { a: 0 }], - ["abc", undefined, "abc"], - ["abc", null, null], - ["abc", { a: 0 }, { a: 0 }], - [["abc"], undefined, ["abc"]], - [["abc"], null, null], - [[], ["def"], ["def"]], - [["abc"], ["def"], ["def"]], - [["abc"], { def: 0 }, { def: 0 }], - [{ abc: true }, ["def"], ["def"]] - ]) { - it(`${stringify(first)}, ${stringify(second)}`, () => { - assert.deepStrictEqual(deepMerge(first, second), result); - }); - } -}); - -/* eslint-enable no-undefined -- `null` and `undefined` are different in options */ From 629f691c76a8a5aaa6210328b6e29bb740925e00 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 21 Nov 2023 05:09:10 +0100 Subject: [PATCH 06/26] Fix rule defaultOptions typos --- lib/rules/accessor-pairs.js | 2 +- lib/rules/camelcase.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rules/accessor-pairs.js b/lib/rules/accessor-pairs.js index 5e39b540bed..90c71950f42 100644 --- a/lib/rules/accessor-pairs.js +++ b/lib/rules/accessor-pairs.js @@ -141,7 +141,7 @@ module.exports = { defaultOptions: [{ enforceForClassMembers: true, - getWithoutGet: false, + getWithoutSet: false, setWithoutGet: true }], diff --git a/lib/rules/camelcase.js b/lib/rules/camelcase.js index 7ffb48ee762..5e75df2e8f7 100644 --- a/lib/rules/camelcase.js +++ b/lib/rules/camelcase.js @@ -24,7 +24,7 @@ module.exports = { allow: [], ignoreDestructuring: false, ignoreGlobals: false, - ignoreIMports: false, + ignoreImports: false, properties: "always" }], From 6276b71e686a4cd8b7c2107636e491f341e4b9b0 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 21 Nov 2023 05:10:36 +0100 Subject: [PATCH 07/26] Put back getRuleOptions as before --- lib/linter/get-rule-options.js | 23 ----------------------- lib/linter/linter.js | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 25 deletions(-) delete mode 100644 lib/linter/get-rule-options.js diff --git a/lib/linter/get-rule-options.js b/lib/linter/get-rule-options.js deleted file mode 100644 index 82e4a2bbd83..00000000000 --- a/lib/linter/get-rule-options.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @fileoverview Applies default rule options - * @author JoshuaKGoldberg - */ - -"use strict"; - -const { deepMergeArrays } = require("./deep-merge-arrays"); - -/** - * Get the options for a rule (not including severity), if any, factoring in defaults - * @param {Array|undefined} defaultOptions default options from rule's meta. - * @param {Array|number} ruleConfig rule configuration - * @returns {Array} of rule options, empty Array if none - */ -function getRuleOptions(defaultOptions, ruleConfig) { - if (Array.isArray(ruleConfig)) { - return deepMergeArrays(defaultOptions, ruleConfig.slice(1)); - } - return defaultOptions || []; -} - -module.exports = { getRuleOptions }; diff --git a/lib/linter/linter.js b/lib/linter/linter.js index d0820524eea..9f29933cee4 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -33,7 +33,6 @@ const CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"), applyDisableDirectives = require("./apply-disable-directives"), ConfigCommentParser = require("./config-comment-parser"), - { getRuleOptions } = require("./get-rule-options"), NodeEventGenerator = require("./node-event-generator"), createReportTranslator = require("./report-translator"), Rules = require("./rules"), @@ -773,6 +772,19 @@ function stripUnicodeBOM(text) { return text; } +/** + * Get the options for a rule (not including severity), if any + * @param {Array|number} ruleConfig rule configuration + * @returns {Array} of rule options, empty Array if none + */ +function getRuleOptions(ruleConfig) { + if (Array.isArray(ruleConfig)) { + return ruleConfig.slice(1); + } + return []; + +} + /** * Analyze scope of the given AST. * @param {ASTNode} ast The `Program` node to analyze. @@ -1025,7 +1037,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO Object.create(sharedTraversalContext), { id: ruleId, - options: getRuleOptions(rule.meta && rule.meta.defaultOptions, configuredRules[ruleId]), + options: getRuleOptions(configuredRules[ruleId]), report(...args) { /* From 1f36e9331bc3c73a5ea4b4ed6c6f5d722a0ff158 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 21 Nov 2023 06:22:13 +0100 Subject: [PATCH 08/26] Apply deep merging in config-validator and rule-validator --- lib/config/rule-validator.js | 2 +- lib/shared/config-validator.js | 9 +++++++-- lib/{linter => shared}/deep-merge-arrays.js | 12 ++++-------- tests/lib/{linter => shared}/deep-merge-arrays.js | 4 +++- 4 files changed, 15 insertions(+), 12 deletions(-) rename lib/{linter => shared}/deep-merge-arrays.js (90%) rename tests/lib/{linter => shared}/deep-merge-arrays.js (96%) diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js index 1db255e5de5..32e8b116df2 100644 --- a/lib/config/rule-validator.js +++ b/lib/config/rule-validator.js @@ -10,13 +10,13 @@ //----------------------------------------------------------------------------- const ajvImport = require("../shared/ajv"); +const { deepMergeArrays } = require("../shared/deep-merge-arrays"); const ajv = ajvImport(); const { parseRuleId, getRuleFromConfig, getRuleOptionsSchema } = require("./flat-config-helpers"); -const { deepMergeArrays } = require("../linter/deep-merge-arrays"); const ruleReplacements = require("../../conf/replacements.json"); //----------------------------------------------------------------------------- diff --git a/lib/shared/config-validator.js b/lib/shared/config-validator.js index 47353ac4814..a1d77057ffe 100644 --- a/lib/shared/config-validator.js +++ b/lib/shared/config-validator.js @@ -32,6 +32,7 @@ const } } = require("@eslint/eslintrc"), { emitDeprecationWarning } = require("./deprecation-warnings"); +const { deepMergeArrays } = require("./deep-merge-arrays"); const ajv = require("./ajv")(); const ruleValidators = new WeakMap(); @@ -140,10 +141,14 @@ function validateRuleSchema(rule, localOptions) { function validateRuleOptions(rule, ruleId, options, source = null) { try { const severity = validateRuleSeverity(options); + const ruleOptions = Array.isArray(options) ? options.slice(1) : []; + const ruleOptionsWithDefaults = rule && rule.meta ? deepMergeArrays(rule.meta.defaultOptions, ruleOptions) : ruleOptions; if (severity !== 0) { - validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []); + validateRuleSchema(rule, ruleOptionsWithDefaults); } + + return [severity, ...ruleOptionsWithDefaults]; } catch (err) { const enhancedMessage = `Configuration for rule "${ruleId}" is invalid:\n${err.message}`; @@ -203,7 +208,7 @@ function validateRules( Object.keys(rulesConfig).forEach(id => { const rule = getAdditionalRule(id) || BuiltInRules.get(id) || null; - validateRuleOptions(rule, id, rulesConfig[id], source); + rulesConfig[id] = validateRuleOptions(rule, id, rulesConfig[id], source); }); } diff --git a/lib/linter/deep-merge-arrays.js b/lib/shared/deep-merge-arrays.js similarity index 90% rename from lib/linter/deep-merge-arrays.js rename to lib/shared/deep-merge-arrays.js index ad22a28b70b..b14b1aaa5d8 100644 --- a/lib/linter/deep-merge-arrays.js +++ b/lib/shared/deep-merge-arrays.js @@ -30,7 +30,7 @@ function deepMergeElements(first, second) { return first; } - if (Array.isArray(first)) { + if (Array.isArray(first) || typeof first !== "object") { return second; } @@ -63,16 +63,12 @@ function deepMergeElements(first, second) { * @returns {(T | U | (T & U))[]} Merged equivalent of second on top of first. */ function deepMergeArrays(first, second) { - if (!first) { - return second; - } - - if (!second) { - return first; + if (!first || !second) { + return second || first || []; } return [ - ...first.map((value, i) => deepMergeElements(value, second[i])), + ...first.map((value, i) => deepMergeElements(value, i < second.length ? second[i] : undefined)), ...second.slice(first.length) ]; } diff --git a/tests/lib/linter/deep-merge-arrays.js b/tests/lib/shared/deep-merge-arrays.js similarity index 96% rename from tests/lib/linter/deep-merge-arrays.js rename to tests/lib/shared/deep-merge-arrays.js index 4aefb11f19c..50e736fbd47 100644 --- a/tests/lib/linter/deep-merge-arrays.js +++ b/tests/lib/shared/deep-merge-arrays.js @@ -7,7 +7,7 @@ //------------------------------------------------------------------------------ const assert = require("assert"); -const { deepMergeArrays } = require("../../../lib/linter/deep-merge-arrays"); +const { deepMergeArrays } = require("../../../lib/shared/deep-merge-arrays"); //------------------------------------------------------------------------------ // Tests @@ -24,6 +24,7 @@ function stringify(value) { describe("deepMerge", () => { for (const [first, second, result] of [ + [[], undefined, []], [["abc"], undefined, ["abc"]], [undefined, ["abc"], ["abc"]], [[], ["abc"], ["abc"]], @@ -39,6 +40,7 @@ describe("deepMerge", () => { [["abc"], [undefined], ["abc"]], [["abc"], [null], [null]], [["abc"], ["def"], ["def"]], + [["abc"], [{ a: 0 }], [{ a: 0 }]], [[["abc"]], [null], [null]], [[["abc"]], ["def"], ["def"]], [[["abc"]], [{ a: 0 }], [{ a: 0 }]], From ec021b5443722c496a985e9953cf4b4e3fa932bb Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 18 Dec 2023 14:29:27 -0500 Subject: [PATCH 09/26] Converted remaining rules. Note: inline comments still need to have defaults applied. --- lib/rules/array-callback-return.js | 1 - lib/rules/capitalized-comments.js | 10 +---- lib/rules/curly.js | 1 - lib/rules/default-case.js | 4 +- lib/rules/dot-notation.js | 4 +- lib/rules/func-names.js | 6 +-- lib/rules/func-style.js | 13 +++--- lib/rules/getter-return.js | 10 ++--- lib/rules/grouped-accessor-pairs.js | 4 +- lib/rules/id-denylist.js | 3 +- lib/rules/id-length.js | 17 ++++--- lib/rules/id-match.js | 29 ++++++------ lib/rules/no-bitwise.js | 9 ++-- lib/rules/no-cond-assign.js | 6 +-- lib/rules/no-console.js | 5 ++- lib/rules/no-constant-condition.js | 10 ++--- lib/rules/no-constructor-return.js | 2 +- lib/rules/no-duplicate-imports.js | 9 ++-- lib/rules/no-else-return.js | 9 ++-- lib/rules/no-empty-function.js | 8 ++-- lib/rules/no-empty-pattern.js | 8 ++-- lib/rules/no-empty.js | 9 ++-- lib/rules/no-eval.js | 9 ++-- lib/rules/no-extend-native.js | 6 +-- lib/rules/no-extra-boolean-cast.js | 8 ++-- lib/rules/no-fallthrough.js | 20 ++++----- lib/rules/no-global-assign.js | 5 ++- lib/rules/no-implicit-globals.js | 8 ++-- lib/rules/no-inline-comments.js | 10 ++--- lib/rules/no-inner-declarations.js | 6 ++- lib/rules/no-invalid-regexp.js | 17 +++---- lib/rules/no-invalid-this.js | 8 ++-- lib/rules/no-irregular-whitespace.js | 38 +++++++--------- lib/rules/no-labels.js | 12 +++-- lib/rules/no-multi-assign.js | 15 +++---- lib/rules/no-plusplus.js | 13 ++---- lib/rules/no-promise-executor-return.js | 10 ++--- lib/rules/no-redeclare.js | 13 +++--- lib/rules/no-return-assign.js | 4 +- lib/rules/no-self-assign.js | 7 +-- lib/rules/no-shadow.js | 32 +++++++------ lib/rules/no-undef.js | 8 ++-- lib/rules/no-underscore-dangle.js | 55 +++++++++++------------ lib/rules/no-unneeded-ternary.js | 8 ++-- lib/rules/no-unreachable-loop.js | 6 ++- lib/rules/no-unsafe-negation.js | 8 ++-- lib/rules/no-unsafe-optional-chaining.js | 8 ++-- lib/rules/no-useless-computed-key.js | 7 +-- lib/rules/no-useless-rename.js | 15 +++---- lib/rules/no-void.js | 8 ++-- lib/rules/no-warning-comments.js | 16 ++++--- lib/rules/operator-assignment.js | 6 ++- lib/rules/prefer-arrow-callback.js | 13 +++--- lib/rules/prefer-const.js | 8 ++-- lib/rules/prefer-promise-reject-errors.js | 8 ++-- lib/rules/prefer-regex-literals.js | 7 +-- lib/rules/radix.js | 4 +- lib/rules/require-atomic-updates.js | 7 +-- lib/rules/sort-imports.js | 32 ++++++------- lib/rules/sort-keys.js | 24 ++++------ lib/rules/sort-vars.js | 8 ++-- lib/rules/strict.js | 5 ++- lib/rules/unicode-bom.js | 6 ++- lib/rules/use-isnan.js | 4 +- lib/rules/valid-typeof.js | 5 ++- lib/rules/yoda.js | 18 +++----- 66 files changed, 344 insertions(+), 368 deletions(-) diff --git a/lib/rules/array-callback-return.js b/lib/rules/array-callback-return.js index 1008a35252f..974fea8cdd1 100644 --- a/lib/rules/array-callback-return.js +++ b/lib/rules/array-callback-return.js @@ -259,7 +259,6 @@ module.exports = { }, create(context) { - const [options] = context.options; const sourceCode = context.sourceCode; diff --git a/lib/rules/capitalized-comments.js b/lib/rules/capitalized-comments.js index 645fb37493c..3a17b056620 100644 --- a/lib/rules/capitalized-comments.js +++ b/lib/rules/capitalized-comments.js @@ -68,7 +68,7 @@ function getNormalizedOptions(rawOptions, which) { * @returns {Object} An object with "Line" and "Block" keys and corresponding * normalized options objects. */ -function getAllNormalizedOptions(rawOptions) { +function getAllNormalizedOptions(rawOptions = {}) { return { Line: getNormalizedOptions(rawOptions, "line"), Block: getNormalizedOptions(rawOptions, "block") @@ -104,12 +104,6 @@ module.exports = { meta: { type: "suggestion", - defaultOptions: ["always", { - ignorePattern: "", - ignoreInlineComments: false, - ignoreConsecutiveComments: false - }], - docs: { description: "Enforce or disallow capitalization of the first letter of a comment", recommended: false, @@ -143,7 +137,7 @@ module.exports = { create(context) { - const capitalize = context.options[0], + const capitalize = context.options[0] || "always", normalizedOptions = getAllNormalizedOptions(context.options[1]), sourceCode = context.sourceCode; diff --git a/lib/rules/curly.js b/lib/rules/curly.js index 536954c1c32..f0c1b215d8e 100644 --- a/lib/rules/curly.js +++ b/lib/rules/curly.js @@ -66,7 +66,6 @@ module.exports = { }, create(context) { - const multiOnly = (context.options[0] === "multi"); const multiLine = (context.options[0] === "multi-line"); const multiOrNest = (context.options[0] === "multi-or-nest"); diff --git a/lib/rules/default-case.js b/lib/rules/default-case.js index 4f2fad0c4f8..c3d01acd03c 100644 --- a/lib/rules/default-case.js +++ b/lib/rules/default-case.js @@ -15,6 +15,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Require `default` cases in `switch` statements", recommended: false, @@ -37,7 +39,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; + const [options] = context.options; const commentPattern = options.commentPattern ? new RegExp(options.commentPattern, "u") : DEFAULT_COMMENT_PATTERN; diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js index 21cba54e2a5..e3e3a9f601a 100644 --- a/lib/rules/dot-notation.js +++ b/lib/rules/dot-notation.js @@ -25,6 +25,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Enforce dot notation whenever possible", recommended: false, @@ -57,7 +59,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; + const [options] = context.options; const allowKeywords = options.allowKeywords === void 0 || options.allowKeywords; const sourceCode = context.sourceCode; diff --git a/lib/rules/func-names.js b/lib/rules/func-names.js index b180580e114..6990f0c7ba5 100644 --- a/lib/rules/func-names.js +++ b/lib/rules/func-names.js @@ -29,6 +29,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: ["always", {}], + docs: { description: "Require or disallow named `function` expressions", recommended: false, @@ -68,7 +70,6 @@ module.exports = { }, create(context) { - const sourceCode = context.sourceCode; /** @@ -79,13 +80,12 @@ module.exports = { function getConfigForNode(node) { if ( node.generator && - context.options.length > 1 && context.options[1].generators ) { return context.options[1].generators; } - return context.options[0] || "always"; + return context.options[0]; } /** diff --git a/lib/rules/func-style.js b/lib/rules/func-style.js index ab83772ef5f..6bb0ff07ccb 100644 --- a/lib/rules/func-style.js +++ b/lib/rules/func-style.js @@ -13,6 +13,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: ["expression", {}], + docs: { description: "Enforce the consistent use of either `function` declarations or expressions", recommended: false, @@ -27,8 +29,7 @@ module.exports = { type: "object", properties: { allowArrowFunctions: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -42,11 +43,9 @@ module.exports = { }, create(context) { - - const style = context.options[0], - allowArrowFunctions = context.options[1] && context.options[1].allowArrowFunctions, - enforceDeclarations = (style === "declaration"), - stack = []; + const [style, { allowArrowFunctions }] = context.options; + const enforceDeclarations = (style === "declaration"); + const stack = []; const nodesToCheck = { FunctionDeclaration(node) { diff --git a/lib/rules/getter-return.js b/lib/rules/getter-return.js index 79ebf3e0902..843dfa2d4d6 100644 --- a/lib/rules/getter-return.js +++ b/lib/rules/getter-return.js @@ -42,6 +42,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{}], + docs: { description: "Enforce `return` statements in getters", recommended: true, @@ -55,8 +57,7 @@ module.exports = { type: "object", properties: { allowImplicit: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -70,8 +71,7 @@ module.exports = { }, create(context) { - - const options = context.options[0] || { allowImplicit: false }; + const [{ allowImplicit }] = context.options; const sourceCode = context.sourceCode; let funcInfo = { @@ -184,7 +184,7 @@ module.exports = { funcInfo.hasReturn = true; // if allowImplicit: false, should also check node.argument - if (!options.allowImplicit && !node.argument) { + if (!allowImplicit && !node.argument) { context.report({ node, messageId: "expected", diff --git a/lib/rules/grouped-accessor-pairs.js b/lib/rules/grouped-accessor-pairs.js index 9556f475682..cd627b479ba 100644 --- a/lib/rules/grouped-accessor-pairs.js +++ b/lib/rules/grouped-accessor-pairs.js @@ -95,6 +95,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: ["anyOrder"], + docs: { description: "Require grouped accessor pairs in object literals and classes", recommended: false, @@ -114,7 +116,7 @@ module.exports = { }, create(context) { - const order = context.options[0] || "anyOrder"; + const [order] = context.options; const sourceCode = context.sourceCode; /** diff --git a/lib/rules/id-denylist.js b/lib/rules/id-denylist.js index baaa65fe01a..747192da569 100644 --- a/lib/rules/id-denylist.js +++ b/lib/rules/id-denylist.js @@ -98,6 +98,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [], + docs: { description: "Disallow specified identifiers", recommended: false, @@ -118,7 +120,6 @@ module.exports = { }, create(context) { - const denyList = new Set(context.options); const reportedNodes = new Set(); const sourceCode = context.sourceCode; diff --git a/lib/rules/id-length.js b/lib/rules/id-length.js index 97bc0e43006..3771f7b0f51 100644 --- a/lib/rules/id-length.js +++ b/lib/rules/id-length.js @@ -21,6 +21,13 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ + exceptionPatterns: [], + exceptions: [], + max: Infinity, + min: 2 + }], + docs: { description: "Enforce minimum and maximum identifier lengths", recommended: false, @@ -32,8 +39,7 @@ module.exports = { type: "object", properties: { min: { - type: "integer", - default: 2 + type: "integer" }, max: { type: "integer" @@ -68,12 +74,11 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; - const minLength = typeof options.min !== "undefined" ? options.min : 2; - const maxLength = typeof options.max !== "undefined" ? options.max : Infinity; + const [options] = context.options; + const { max: maxLength, min: minLength } = options; const properties = options.properties !== "never"; const exceptions = new Set(options.exceptions); - const exceptionPatterns = (options.exceptionPatterns || []).map(pattern => new RegExp(pattern, "u")); + const exceptionPatterns = options.exceptionPatterns.map(pattern => new RegExp(pattern, "u")); const reportedNodes = new Set(); /** diff --git a/lib/rules/id-match.js b/lib/rules/id-match.js index e225454e771..820429abce8 100644 --- a/lib/rules/id-match.js +++ b/lib/rules/id-match.js @@ -14,6 +14,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: ["^.+$", {}], + docs: { description: "Require identifiers to match a specified regular expression", recommended: false, @@ -28,20 +30,16 @@ module.exports = { type: "object", properties: { properties: { - type: "boolean", - default: false + type: "boolean" }, classFields: { - type: "boolean", - default: false + type: "boolean" }, onlyDeclarations: { - type: "boolean", - default: false + type: "boolean" }, ignoreDestructuring: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -58,14 +56,13 @@ module.exports = { //-------------------------------------------------------------------------- // Options //-------------------------------------------------------------------------- - const pattern = context.options[0] || "^.+$", - regexp = new RegExp(pattern, "u"); - - const options = context.options[1] || {}, - checkProperties = !!options.properties, - checkClassFields = !!options.classFields, - onlyDeclarations = !!options.onlyDeclarations, - ignoreDestructuring = !!options.ignoreDestructuring; + const [pattern, { + classFields: checkClassFields, + ignoreDestructuring, + onlyDeclarations, + properties: checkProperties + }] = context.options; + const regexp = new RegExp(pattern, "u"); const sourceCode = context.sourceCode; let globalScope; diff --git a/lib/rules/no-bitwise.js b/lib/rules/no-bitwise.js index d90992b2064..f904f994cfb 100644 --- a/lib/rules/no-bitwise.js +++ b/lib/rules/no-bitwise.js @@ -25,6 +25,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ allow: [] }], + docs: { description: "Disallow bitwise operators", recommended: false, @@ -43,8 +45,7 @@ module.exports = { uniqueItems: true }, int32Hint: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -57,9 +58,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; - const allowed = options.allow || []; - const int32Hint = options.int32Hint === true; + const [{ allow: allowed, int32Hint }] = context.options; /** * Reports an unexpected use of a bitwise operator. diff --git a/lib/rules/no-cond-assign.js b/lib/rules/no-cond-assign.js index 952920215aa..6d4135d08ec 100644 --- a/lib/rules/no-cond-assign.js +++ b/lib/rules/no-cond-assign.js @@ -33,6 +33,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: ["except-parens"], + docs: { description: "Disallow assignment operators in conditional expressions", recommended: true, @@ -54,9 +56,7 @@ module.exports = { }, create(context) { - - const prohibitAssign = (context.options[0] || "except-parens"); - + const [prohibitAssign] = context.options; const sourceCode = context.sourceCode; /** diff --git a/lib/rules/no-console.js b/lib/rules/no-console.js index d20477c5d9a..62dc1693836 100644 --- a/lib/rules/no-console.js +++ b/lib/rules/no-console.js @@ -20,6 +20,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow the use of `console`", recommended: false, @@ -52,8 +54,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; - const allowed = options.allow || []; + const [{ allow: allowed = [] }] = context.options; const sourceCode = context.sourceCode; /** diff --git a/lib/rules/no-constant-condition.js b/lib/rules/no-constant-condition.js index 24abe363280..f6570d32352 100644 --- a/lib/rules/no-constant-condition.js +++ b/lib/rules/no-constant-condition.js @@ -20,6 +20,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{ checkLoops: true }], + docs: { description: "Disallow constant expressions in conditions", recommended: true, @@ -31,8 +33,7 @@ module.exports = { type: "object", properties: { checkLoops: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false @@ -45,9 +46,8 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}, - checkLoops = options.checkLoops !== false, - loopSetStack = []; + const [{ checkLoops }] = context.options; + const loopSetStack = []; const sourceCode = context.sourceCode; let loopsInCurrentScope = new Set(); diff --git a/lib/rules/no-constructor-return.js b/lib/rules/no-constructor-return.js index d7d98939b9a..075ec918571 100644 --- a/lib/rules/no-constructor-return.js +++ b/lib/rules/no-constructor-return.js @@ -20,7 +20,7 @@ module.exports = { url: "https://eslint.org/docs/latest/rules/no-constructor-return" }, - schema: {}, + schema: [], fixable: null, diff --git a/lib/rules/no-duplicate-imports.js b/lib/rules/no-duplicate-imports.js index 25c07b7500d..7d584e22b42 100644 --- a/lib/rules/no-duplicate-imports.js +++ b/lib/rules/no-duplicate-imports.js @@ -232,6 +232,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{}], + docs: { description: "Disallow duplicate module imports", recommended: false, @@ -243,8 +245,7 @@ module.exports = { type: "object", properties: { includeExports: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -260,8 +261,8 @@ module.exports = { }, create(context) { - const includeExports = (context.options[0] || {}).includeExports, - modules = new Map(); + const [{ includeExports }] = context.options; + const modules = new Map(); const handlers = { ImportDeclaration: handleImportsExports( context, diff --git a/lib/rules/no-else-return.js b/lib/rules/no-else-return.js index 9dbf569651c..1a7bb45a98b 100644 --- a/lib/rules/no-else-return.js +++ b/lib/rules/no-else-return.js @@ -21,6 +21,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ allowElseIf: true }], + docs: { description: "Disallow `else` blocks after `return` statements in `if` statements", recommended: false, @@ -31,8 +33,7 @@ module.exports = { type: "object", properties: { allowElseIf: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false @@ -46,7 +47,7 @@ module.exports = { }, create(context) { - + const [{ allowElseIf }] = context.options; const sourceCode = context.sourceCode; //-------------------------------------------------------------------------- @@ -389,8 +390,6 @@ module.exports = { } } - const allowElseIf = !(context.options[0] && context.options[0].allowElseIf === false); - //-------------------------------------------------------------------------- // Public API //-------------------------------------------------------------------------- diff --git a/lib/rules/no-empty-function.js b/lib/rules/no-empty-function.js index 2fcb75534ac..aaf0ff3043c 100644 --- a/lib/rules/no-empty-function.js +++ b/lib/rules/no-empty-function.js @@ -94,6 +94,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ allow: [] }], + docs: { description: "Disallow empty functions", recommended: false, @@ -120,9 +122,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; - const allowed = options.allow || []; - + const [{ allow }] = context.options; const sourceCode = context.sourceCode; /** @@ -144,7 +144,7 @@ module.exports = { filter: astUtils.isCommentToken }); - if (!allowed.includes(kind) && + if (!allow.includes(kind) && node.body.type === "BlockStatement" && node.body.body.length === 0 && innerComments.length === 0 diff --git a/lib/rules/no-empty-pattern.js b/lib/rules/no-empty-pattern.js index fb75f6d25b3..a9756ef5f0e 100644 --- a/lib/rules/no-empty-pattern.js +++ b/lib/rules/no-empty-pattern.js @@ -15,6 +15,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{}], + docs: { description: "Disallow empty destructuring patterns", recommended: true, @@ -26,8 +28,7 @@ module.exports = { type: "object", properties: { allowObjectPatternsAsParameters: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -40,8 +41,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}, - allowObjectPatternsAsParameters = options.allowObjectPatternsAsParameters || false; + const [{ allowObjectPatternsAsParameters }] = context.options; return { ObjectPattern(node) { diff --git a/lib/rules/no-empty.js b/lib/rules/no-empty.js index 1c157963e9d..01119cfb86b 100644 --- a/lib/rules/no-empty.js +++ b/lib/rules/no-empty.js @@ -20,6 +20,8 @@ module.exports = { hasSuggestions: true, type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow empty block statements", recommended: true, @@ -31,8 +33,7 @@ module.exports = { type: "object", properties: { allowEmptyCatch: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -46,9 +47,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}, - allowEmptyCatch = options.allowEmptyCatch || false; - + const [{ allowEmptyCatch }] = context.options; const sourceCode = context.sourceCode; return { diff --git a/lib/rules/no-eval.js b/lib/rules/no-eval.js index a059526a68b..30e3860931f 100644 --- a/lib/rules/no-eval.js +++ b/lib/rules/no-eval.js @@ -42,6 +42,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow the use of `eval()`", recommended: false, @@ -52,7 +54,7 @@ module.exports = { { type: "object", properties: { - allowIndirect: { type: "boolean", default: false } + allowIndirect: { type: "boolean" } }, additionalProperties: false } @@ -64,10 +66,7 @@ module.exports = { }, create(context) { - const allowIndirect = Boolean( - context.options[0] && - context.options[0].allowIndirect - ); + const [{ allowIndirect }] = context.options; const sourceCode = context.sourceCode; let funcInfo = null; diff --git a/lib/rules/no-extend-native.js b/lib/rules/no-extend-native.js index fcbb3855725..1d87125e8b9 100644 --- a/lib/rules/no-extend-native.js +++ b/lib/rules/no-extend-native.js @@ -21,6 +21,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ exceptions: [] }], + docs: { description: "Disallow extending native types", recommended: false, @@ -49,10 +51,8 @@ module.exports = { }, create(context) { - - const config = context.options[0] || {}; const sourceCode = context.sourceCode; - const exceptions = new Set(config.exceptions || []); + const exceptions = new Set(context.options[0].exceptions); const modifiedBuiltins = new Set( Object.keys(globals.builtin) .filter(builtin => builtin[0].toUpperCase() === builtin[0]) diff --git a/lib/rules/no-extra-boolean-cast.js b/lib/rules/no-extra-boolean-cast.js index f342533bfc9..dacaa0eecb8 100644 --- a/lib/rules/no-extra-boolean-cast.js +++ b/lib/rules/no-extra-boolean-cast.js @@ -23,6 +23,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow unnecessary boolean casts", recommended: true, @@ -33,8 +35,7 @@ module.exports = { type: "object", properties: { enforceForLogicalOperands: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -49,6 +50,7 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; + const [{ enforceForLogicalOperands }] = context.options; // Node types which have a test which will coerce values to booleans. const BOOLEAN_NODE_TYPES = new Set([ @@ -80,7 +82,7 @@ module.exports = { function isLogicalContext(node) { return node.type === "LogicalExpression" && (node.operator === "||" || node.operator === "&&") && - (context.options.length && context.options[0].enforceForLogicalOperands === true); + (enforceForLogicalOperands === true); } diff --git a/lib/rules/no-fallthrough.js b/lib/rules/no-fallthrough.js index 91da1212022..7679ab6fc37 100644 --- a/lib/rules/no-fallthrough.js +++ b/lib/rules/no-fallthrough.js @@ -86,6 +86,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{}], + docs: { description: "Disallow fallthrough of `case` statements", recommended: true, @@ -97,12 +99,10 @@ module.exports = { type: "object", properties: { commentPattern: { - type: "string", - default: "" + type: "string" }, allowEmptyCase: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -115,24 +115,20 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; const codePathSegments = []; let currentCodePathSegments = new Set(); const sourceCode = context.sourceCode; - const allowEmptyCase = options.allowEmptyCase || false; + const [{ allowEmptyCase, commentPattern }] = context.options; + const fallthroughCommentPattern = commentPattern + ? new RegExp(commentPattern, "u") + : DEFAULT_FALLTHROUGH_COMMENT; /* * We need to use leading comments of the next SwitchCase node because * trailing comments is wrong if semicolons are omitted. */ let fallthroughCase = null; - let fallthroughCommentPattern = null; - if (options.commentPattern) { - fallthroughCommentPattern = new RegExp(options.commentPattern, "u"); - } else { - fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT; - } return { onCodePathStart() { diff --git a/lib/rules/no-global-assign.js b/lib/rules/no-global-assign.js index 99ae7a2ee5e..0a6f65eb56e 100644 --- a/lib/rules/no-global-assign.js +++ b/lib/rules/no-global-assign.js @@ -14,6 +14,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ exceptions: [] }], + docs: { description: "Disallow assignments to native objects or read-only global variables", recommended: true, @@ -40,9 +42,8 @@ module.exports = { }, create(context) { - const config = context.options[0]; const sourceCode = context.sourceCode; - const exceptions = (config && config.exceptions) || []; + const [{ exceptions }] = context.options; /** * Reports write references. diff --git a/lib/rules/no-implicit-globals.js b/lib/rules/no-implicit-globals.js index 2a182477c0e..a4efa0eb56c 100644 --- a/lib/rules/no-implicit-globals.js +++ b/lib/rules/no-implicit-globals.js @@ -14,6 +14,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow declarations in the global scope", recommended: false, @@ -24,8 +26,7 @@ module.exports = { type: "object", properties: { lexicalBindings: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -41,8 +42,7 @@ module.exports = { }, create(context) { - - const checkLexicalBindings = context.options[0] && context.options[0].lexicalBindings === true; + const [{ lexicalBindings: checkLexicalBindings }] = context.options; const sourceCode = context.sourceCode; /** diff --git a/lib/rules/no-inline-comments.js b/lib/rules/no-inline-comments.js index d96e6472d13..439418c7b11 100644 --- a/lib/rules/no-inline-comments.js +++ b/lib/rules/no-inline-comments.js @@ -15,6 +15,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow inline comments after code", recommended: false, @@ -40,12 +42,8 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; - const options = context.options[0]; - let customIgnoreRegExp; - - if (options && options.ignorePattern) { - customIgnoreRegExp = new RegExp(options.ignorePattern, "u"); - } + const [{ ignorePattern }] = context.options; + const customIgnoreRegExp = ignorePattern && new RegExp(ignorePattern, "u"); /** * Will check that comments are not on lines starting with or ending with code diff --git a/lib/rules/no-inner-declarations.js b/lib/rules/no-inner-declarations.js index f4bae43e58d..5887da26ed5 100644 --- a/lib/rules/no-inner-declarations.js +++ b/lib/rules/no-inner-declarations.js @@ -47,6 +47,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: ["functions"], + docs: { description: "Disallow variable or `function` declarations in nested blocks", recommended: true, @@ -65,6 +67,7 @@ module.exports = { }, create(context) { + const both = context.options[0] === "both"; /** * Ensure that a given node is at a program or function body's root. @@ -94,12 +97,11 @@ module.exports = { }); } - return { FunctionDeclaration: check, VariableDeclaration(node) { - if (context.options[0] === "both" && node.kind === "var") { + if (both && node.kind === "var") { check(node); } } diff --git a/lib/rules/no-invalid-regexp.js b/lib/rules/no-invalid-regexp.js index 3c42a68e8a3..80608cba7c3 100644 --- a/lib/rules/no-invalid-regexp.js +++ b/lib/rules/no-invalid-regexp.js @@ -22,6 +22,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{}], + docs: { description: "Disallow invalid regular expression strings in `RegExp` constructors", recommended: true, @@ -47,17 +49,10 @@ module.exports = { }, create(context) { - - const options = context.options[0]; - let allowedFlags = null; - - if (options && options.allowConstructorFlags) { - const temp = options.allowConstructorFlags.join("").replace(validFlags, ""); - - if (temp) { - allowedFlags = new RegExp(`[${temp}]`, "giu"); - } - } + const [{ allowConstructorFlags }] = context.options; + const allowedFlags = allowConstructorFlags + ? new RegExp(`[${allowConstructorFlags.join("").replace(validFlags, "")}]`, "giu") + : null; /** * Reports error with the provided message. diff --git a/lib/rules/no-invalid-this.js b/lib/rules/no-invalid-this.js index 9e214035c33..15199cc4909 100644 --- a/lib/rules/no-invalid-this.js +++ b/lib/rules/no-invalid-this.js @@ -35,6 +35,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ capIsConstructor: true }], + docs: { description: "Disallow use of `this` in contexts where the value of `this` is `undefined`", recommended: false, @@ -46,8 +48,7 @@ module.exports = { type: "object", properties: { capIsConstructor: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false @@ -60,8 +61,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; - const capIsConstructor = options.capIsConstructor !== false; + const [{ capIsConstructor }] = context.options; const stack = [], sourceCode = context.sourceCode; diff --git a/lib/rules/no-irregular-whitespace.js b/lib/rules/no-irregular-whitespace.js index ab7ccac54e4..5a647a9b4ed 100644 --- a/lib/rules/no-irregular-whitespace.js +++ b/lib/rules/no-irregular-whitespace.js @@ -30,6 +30,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{ skipStrings: true }], + docs: { description: "Disallow irregular whitespace", recommended: true, @@ -41,24 +43,19 @@ module.exports = { type: "object", properties: { skipComments: { - type: "boolean", - default: false + type: "boolean" }, skipStrings: { - type: "boolean", - default: true + type: "boolean" }, skipTemplates: { - type: "boolean", - default: false + type: "boolean" }, skipRegExps: { - type: "boolean", - default: false + type: "boolean" }, skipJSXText: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -71,21 +68,20 @@ module.exports = { }, create(context) { - - // Module store of errors that we have found - let errors = []; - - // Lookup the `skipComments` option, which defaults to `false`. - const options = context.options[0] || {}; - const skipComments = !!options.skipComments; - const skipStrings = options.skipStrings !== false; - const skipRegExps = !!options.skipRegExps; - const skipTemplates = !!options.skipTemplates; - const skipJSXText = !!options.skipJSXText; + const [{ + skipComments, + skipStrings, + skipRegExps, + skipTemplates, + skipJSXText + }] = context.options; const sourceCode = context.sourceCode; const commentNodes = sourceCode.getAllComments(); + // Module store of errors that we have found + let errors = []; + /** * Removes errors that occur inside the given node * @param {ASTNode} node to check for matching errors. diff --git a/lib/rules/no-labels.js b/lib/rules/no-labels.js index d991a0a8062..cbd69a2b3bf 100644 --- a/lib/rules/no-labels.js +++ b/lib/rules/no-labels.js @@ -19,6 +19,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow labeled statements", recommended: false, @@ -30,12 +32,10 @@ module.exports = { type: "object", properties: { allowLoop: { - type: "boolean", - default: false + type: "boolean" }, allowSwitch: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -50,9 +50,7 @@ module.exports = { }, create(context) { - const options = context.options[0]; - const allowLoop = options && options.allowLoop; - const allowSwitch = options && options.allowSwitch; + const [{ allowLoop, allowSwitch }] = context.options; let scopeInfo = null; /** diff --git a/lib/rules/no-multi-assign.js b/lib/rules/no-multi-assign.js index a7a50c19495..32deab5596e 100644 --- a/lib/rules/no-multi-assign.js +++ b/lib/rules/no-multi-assign.js @@ -15,6 +15,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow use of chained assignment expressions", recommended: false, @@ -25,8 +27,7 @@ module.exports = { type: "object", properties: { ignoreNonDeclaration: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -38,19 +39,13 @@ module.exports = { }, create(context) { - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - const options = context.options[0] || { - ignoreNonDeclaration: false - }; + const [{ ignoreNonDeclaration }] = context.options; const selectors = [ "VariableDeclarator > AssignmentExpression.init", "PropertyDefinition > AssignmentExpression.value" ]; - if (!options.ignoreNonDeclaration) { + if (!ignoreNonDeclaration) { selectors.push("AssignmentExpression > AssignmentExpression.right"); } diff --git a/lib/rules/no-plusplus.js b/lib/rules/no-plusplus.js index 22a6fd01350..9d473ab2b35 100644 --- a/lib/rules/no-plusplus.js +++ b/lib/rules/no-plusplus.js @@ -50,6 +50,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow the unary operators `++` and `--`", recommended: false, @@ -61,8 +63,7 @@ module.exports = { type: "object", properties: { allowForLoopAfterthoughts: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -75,13 +76,7 @@ module.exports = { }, create(context) { - - const config = context.options[0]; - let allowForLoopAfterthoughts = false; - - if (typeof config === "object") { - allowForLoopAfterthoughts = config.allowForLoopAfterthoughts === true; - } + const [{ allowForLoopAfterthoughts }] = context.options; return { diff --git a/lib/rules/no-promise-executor-return.js b/lib/rules/no-promise-executor-return.js index b27e440729c..0b4714be85e 100644 --- a/lib/rules/no-promise-executor-return.js +++ b/lib/rules/no-promise-executor-return.js @@ -141,6 +141,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{}], + docs: { description: "Disallow returning values from Promise executor functions", recommended: false, @@ -153,8 +155,7 @@ module.exports = { type: "object", properties: { allowVoid: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -172,12 +173,9 @@ module.exports = { }, create(context) { - let funcInfo = null; const sourceCode = context.sourceCode; - const { - allowVoid = false - } = context.options[0] || {}; + const [{ allowVoid }] = context.options; return { diff --git a/lib/rules/no-redeclare.js b/lib/rules/no-redeclare.js index 8a4877e8a3c..94a3c212472 100644 --- a/lib/rules/no-redeclare.js +++ b/lib/rules/no-redeclare.js @@ -20,6 +20,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ builtinGlobals: true }], + docs: { description: "Disallow variable redeclaration", recommended: true, @@ -36,7 +38,7 @@ module.exports = { { type: "object", properties: { - builtinGlobals: { type: "boolean", default: true } + builtinGlobals: { type: "boolean" } }, additionalProperties: false } @@ -44,12 +46,7 @@ module.exports = { }, create(context) { - const options = { - builtinGlobals: Boolean( - context.options.length === 0 || - context.options[0].builtinGlobals - ) - }; + const [{ builtinGlobals }] = context.options; const sourceCode = context.sourceCode; /** @@ -58,7 +55,7 @@ module.exports = { * @returns {IterableIterator<{type:string,node:ASTNode,loc:SourceLocation}>} The declarations. */ function *iterateDeclarations(variable) { - if (options.builtinGlobals && ( + if (builtinGlobals && ( variable.eslintImplicitGlobalSetting === "readonly" || variable.eslintImplicitGlobalSetting === "writable" )) { diff --git a/lib/rules/no-return-assign.js b/lib/rules/no-return-assign.js index 73caf0e6bd3..7c1f00ec9a2 100644 --- a/lib/rules/no-return-assign.js +++ b/lib/rules/no-return-assign.js @@ -25,6 +25,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: ["except-parens"], + docs: { description: "Disallow assignment operators in `return` statements", recommended: false, @@ -44,7 +46,7 @@ module.exports = { }, create(context) { - const always = (context.options[0] || "except-parens") !== "except-parens"; + const always = context.options[0] !== "except-parens"; const sourceCode = context.sourceCode; return { diff --git a/lib/rules/no-self-assign.js b/lib/rules/no-self-assign.js index 33ac8fb5085..91b928ea112 100644 --- a/lib/rules/no-self-assign.js +++ b/lib/rules/no-self-assign.js @@ -129,6 +129,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{ props: true }], + docs: { description: "Disallow assignments where both sides are exactly the same", recommended: true, @@ -140,8 +142,7 @@ module.exports = { type: "object", properties: { props: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false @@ -155,7 +156,7 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; - const [{ props = true } = {}] = context.options; + const [{ props }] = context.options; /** * Reports a given node as self assignments. diff --git a/lib/rules/no-shadow.js b/lib/rules/no-shadow.js index 3e4d99822a8..b9e85845b80 100644 --- a/lib/rules/no-shadow.js +++ b/lib/rules/no-shadow.js @@ -29,6 +29,11 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ + allow: [], + hoist: "functions" + }], + docs: { description: "Disallow variable declarations from shadowing variables declared in the outer scope", recommended: false, @@ -39,7 +44,7 @@ module.exports = { { type: "object", properties: { - builtinGlobals: { type: "boolean", default: false }, + builtinGlobals: { type: "boolean" }, hoist: { enum: ["all", "functions", "never"], default: "functions" }, allow: { type: "array", @@ -47,7 +52,7 @@ module.exports = { type: "string" } }, - ignoreOnInitialization: { type: "boolean", default: false } + ignoreOnInitialization: { type: "boolean" } }, additionalProperties: false } @@ -60,13 +65,12 @@ module.exports = { }, create(context) { - - const options = { - builtinGlobals: context.options[0] && context.options[0].builtinGlobals, - hoist: (context.options[0] && context.options[0].hoist) || "functions", - allow: (context.options[0] && context.options[0].allow) || [], - ignoreOnInitialization: context.options[0] && context.options[0].ignoreOnInitialization - }; + const [{ + builtinGlobals, + hoist, + allow, + ignoreOnInitialization + }] = context.options; const sourceCode = context.sourceCode; /** @@ -174,7 +178,7 @@ module.exports = { * @returns {boolean} Whether or not the variable name is allowed. */ function isAllowed(variable) { - return options.allow.includes(variable.name); + return allow.includes(variable.name); } /** @@ -269,7 +273,7 @@ module.exports = { inner[1] < outer[0] && // Excepts FunctionDeclaration if is {"hoist":"function"}. - (options.hoist !== "functions" || !outerDef || outerDef.node.type !== "FunctionDeclaration") + (hoist !== "functions" || !outerDef || outerDef.node.type !== "FunctionDeclaration") ); } @@ -296,10 +300,10 @@ module.exports = { const shadowed = astUtils.getVariableByName(scope.upper, variable.name); if (shadowed && - (shadowed.identifiers.length > 0 || (options.builtinGlobals && "writeable" in shadowed)) && + (shadowed.identifiers.length > 0 || (builtinGlobals && "writeable" in shadowed)) && !isOnInitializer(variable, shadowed) && - !(options.ignoreOnInitialization && isInitPatternNode(variable, shadowed)) && - !(options.hoist !== "all" && isInTdz(variable, shadowed)) + !(ignoreOnInitialization && isInitPatternNode(variable, shadowed)) && + !(hoist !== "all" && isInTdz(variable, shadowed)) ) { const location = getDeclaredLocation(shadowed); const messageId = location.global ? "noShadowGlobal" : "noShadow"; diff --git a/lib/rules/no-undef.js b/lib/rules/no-undef.js index fe470286c04..e6ec5fbe14e 100644 --- a/lib/rules/no-undef.js +++ b/lib/rules/no-undef.js @@ -28,6 +28,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{}], + docs: { description: "Disallow the use of undeclared variables unless mentioned in `/*global */` comments", recommended: true, @@ -39,8 +41,7 @@ module.exports = { type: "object", properties: { typeof: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -52,8 +53,7 @@ module.exports = { }, create(context) { - const options = context.options[0]; - const considerTypeOf = options && options.typeof === true || false; + const [{ typeof: considerTypeOf }] = context.options; const sourceCode = context.sourceCode; return { diff --git a/lib/rules/no-underscore-dangle.js b/lib/rules/no-underscore-dangle.js index a0e05c6c1cc..da230792fa0 100644 --- a/lib/rules/no-underscore-dangle.js +++ b/lib/rules/no-underscore-dangle.js @@ -14,6 +14,13 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ + allow: [], + allowFunctionParams: true, + allowInArrayDestructuring: true, + allowInObjectDestructuring: true + }], + docs: { description: "Disallow dangling underscores in identifiers", recommended: false, @@ -31,36 +38,28 @@ module.exports = { } }, allowAfterThis: { - type: "boolean", - default: false + type: "boolean" }, allowAfterSuper: { - type: "boolean", - default: false + type: "boolean" }, allowAfterThisConstructor: { - type: "boolean", - default: false + type: "boolean" }, enforceInMethodNames: { - type: "boolean", - default: false + type: "boolean" }, allowFunctionParams: { - type: "boolean", - default: true + type: "boolean" }, enforceInClassFields: { - type: "boolean", - default: false + type: "boolean" }, allowInArrayDestructuring: { - type: "boolean", - default: true + type: "boolean" }, allowInObjectDestructuring: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false @@ -73,17 +72,17 @@ module.exports = { }, create(context) { - - const options = context.options[0] || {}; - const ALLOWED_VARIABLES = options.allow ? options.allow : []; - const allowAfterThis = typeof options.allowAfterThis !== "undefined" ? options.allowAfterThis : false; - const allowAfterSuper = typeof options.allowAfterSuper !== "undefined" ? options.allowAfterSuper : false; - const allowAfterThisConstructor = typeof options.allowAfterThisConstructor !== "undefined" ? options.allowAfterThisConstructor : false; - const enforceInMethodNames = typeof options.enforceInMethodNames !== "undefined" ? options.enforceInMethodNames : false; - const enforceInClassFields = typeof options.enforceInClassFields !== "undefined" ? options.enforceInClassFields : false; - const allowFunctionParams = typeof options.allowFunctionParams !== "undefined" ? options.allowFunctionParams : true; - const allowInArrayDestructuring = typeof options.allowInArrayDestructuring !== "undefined" ? options.allowInArrayDestructuring : true; - const allowInObjectDestructuring = typeof options.allowInObjectDestructuring !== "undefined" ? options.allowInObjectDestructuring : true; + const [{ + allow, + allowAfterSuper, + allowAfterThis, + allowAfterThisConstructor, + allowFunctionParams, + allowInArrayDestructuring, + allowInObjectDestructuring, + enforceInClassFields, + enforceInMethodNames + }] = context.options; const sourceCode = context.sourceCode; //------------------------------------------------------------------------- @@ -97,7 +96,7 @@ module.exports = { * @private */ function isAllowed(identifier) { - return ALLOWED_VARIABLES.includes(identifier); + return allow.includes(identifier); } /** diff --git a/lib/rules/no-unneeded-ternary.js b/lib/rules/no-unneeded-ternary.js index ab1bdc59cbf..8f87b8b0819 100644 --- a/lib/rules/no-unneeded-ternary.js +++ b/lib/rules/no-unneeded-ternary.js @@ -28,6 +28,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ defaultAssignment: true }], + docs: { description: "Disallow ternary operators when simpler alternatives exist", recommended: false, @@ -39,8 +41,7 @@ module.exports = { type: "object", properties: { defaultAssignment: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false @@ -56,8 +57,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; - const defaultAssignment = options.defaultAssignment !== false; + const [{ defaultAssignment }] = context.options; const sourceCode = context.sourceCode; /** diff --git a/lib/rules/no-unreachable-loop.js b/lib/rules/no-unreachable-loop.js index 577d39ac7c7..f5507ecc957 100644 --- a/lib/rules/no-unreachable-loop.js +++ b/lib/rules/no-unreachable-loop.js @@ -74,6 +74,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{ ignore: [] }], + docs: { description: "Disallow loops with a body that allows only one iteration", recommended: false, @@ -100,8 +102,8 @@ module.exports = { }, create(context) { - const ignoredLoopTypes = context.options[0] && context.options[0].ignore || [], - loopTypesToCheck = getDifference(allLoopTypes, ignoredLoopTypes), + const [{ ignore: ignoredLoopTypes }] = context.options; + const loopTypesToCheck = getDifference(allLoopTypes, ignoredLoopTypes), loopSelector = loopTypesToCheck.join(","), loopsByTargetSegments = new Map(), loopsToReport = new Set(); diff --git a/lib/rules/no-unsafe-negation.js b/lib/rules/no-unsafe-negation.js index cabd7e2ccc2..3bc824b2627 100644 --- a/lib/rules/no-unsafe-negation.js +++ b/lib/rules/no-unsafe-negation.js @@ -51,6 +51,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{}], + docs: { description: "Disallow negating the left operand of relational operators", recommended: true, @@ -64,8 +66,7 @@ module.exports = { type: "object", properties: { enforceForOrderingRelations: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -83,8 +84,7 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; - const options = context.options[0] || {}; - const enforceForOrderingRelations = options.enforceForOrderingRelations === true; + const [{ enforceForOrderingRelations }] = context.options; return { BinaryExpression(node) { diff --git a/lib/rules/no-unsafe-optional-chaining.js b/lib/rules/no-unsafe-optional-chaining.js index fe2bead856e..2e02b79d3e6 100644 --- a/lib/rules/no-unsafe-optional-chaining.js +++ b/lib/rules/no-unsafe-optional-chaining.js @@ -23,6 +23,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{}], + docs: { description: "Disallow use of optional chaining in contexts where the `undefined` value is not allowed", recommended: true, @@ -32,8 +34,7 @@ module.exports = { type: "object", properties: { disallowArithmeticOperators: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -46,8 +47,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; - const disallowArithmeticOperators = (options.disallowArithmeticOperators) || false; + const [{ disallowArithmeticOperators }] = context.options; /** * Reports unsafe usage of optional chaining diff --git a/lib/rules/no-useless-computed-key.js b/lib/rules/no-useless-computed-key.js index f2d9f3341f5..bda6950f8e6 100644 --- a/lib/rules/no-useless-computed-key.js +++ b/lib/rules/no-useless-computed-key.js @@ -90,6 +90,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow unnecessary computed property keys in objects and classes", recommended: false, @@ -100,8 +102,7 @@ module.exports = { type: "object", properties: { enforceForClassMembers: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -114,7 +115,7 @@ module.exports = { }, create(context) { const sourceCode = context.sourceCode; - const enforceForClassMembers = context.options[0] && context.options[0].enforceForClassMembers; + const [{ enforceForClassMembers }] = context.options; /** * Reports a given node if it violated this rule. diff --git a/lib/rules/no-useless-rename.js b/lib/rules/no-useless-rename.js index 0c818fb2c2f..4195947b7a5 100644 --- a/lib/rules/no-useless-rename.js +++ b/lib/rules/no-useless-rename.js @@ -20,6 +20,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow renaming import, export, and destructured assignments to the same name", recommended: false, @@ -32,9 +34,9 @@ module.exports = { { type: "object", properties: { - ignoreDestructuring: { type: "boolean", default: false }, - ignoreImport: { type: "boolean", default: false }, - ignoreExport: { type: "boolean", default: false } + ignoreDestructuring: { type: "boolean" }, + ignoreImport: { type: "boolean" }, + ignoreExport: { type: "boolean" } }, additionalProperties: false } @@ -46,11 +48,8 @@ module.exports = { }, create(context) { - const sourceCode = context.sourceCode, - options = context.options[0] || {}, - ignoreDestructuring = options.ignoreDestructuring === true, - ignoreImport = options.ignoreImport === true, - ignoreExport = options.ignoreExport === true; + const sourceCode = context.sourceCode; + const [{ ignoreDestructuring, ignoreImport, ignoreExport }] = context.options; //-------------------------------------------------------------------------- // Helpers diff --git a/lib/rules/no-void.js b/lib/rules/no-void.js index 9546d7a62c3..25a123dc6e7 100644 --- a/lib/rules/no-void.js +++ b/lib/rules/no-void.js @@ -13,6 +13,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow `void` operators", recommended: false, @@ -28,8 +30,7 @@ module.exports = { type: "object", properties: { allowAsStatement: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -38,8 +39,7 @@ module.exports = { }, create(context) { - const allowAsStatement = - context.options[0] && context.options[0].allowAsStatement; + const [{ allowAsStatement }] = context.options; //-------------------------------------------------------------------------- // Public diff --git a/lib/rules/no-warning-comments.js b/lib/rules/no-warning-comments.js index c415bee7a7b..628f5a2ac51 100644 --- a/lib/rules/no-warning-comments.js +++ b/lib/rules/no-warning-comments.js @@ -19,6 +19,11 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ + location: "start", + terms: ["todo", "fixme", "xxx"] + }], + docs: { description: "Disallow specified warning terms in comments", recommended: false, @@ -58,12 +63,10 @@ module.exports = { }, create(context) { - const sourceCode = context.sourceCode, - configuration = context.options[0] || {}, - warningTerms = configuration.terms || ["todo", "fixme", "xxx"], - location = configuration.location || "start", - decoration = [...configuration.decoration || []].join(""), - selfConfigRegEx = /\bno-warning-comments\b/u; + const sourceCode = context.sourceCode; + const [{ decoration, location, terms: warningTerms }] = context.options; + const escapedDecoration = escapeRegExp(decoration ? decoration.join("") : ""); + const selfConfigRegEx = /\bno-warning-comments\b/u; /** * Convert a warning term into a RegExp which will match a comment containing that whole word in the specified @@ -74,7 +77,6 @@ module.exports = { */ function convertToRegExp(term) { const escaped = escapeRegExp(term); - const escapedDecoration = escapeRegExp(decoration); /* * When matching at the start, ignore leading whitespace, and diff --git a/lib/rules/operator-assignment.js b/lib/rules/operator-assignment.js index f71d73be75d..412c97f66e0 100644 --- a/lib/rules/operator-assignment.js +++ b/lib/rules/operator-assignment.js @@ -62,6 +62,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: ["always"], + docs: { description: "Require or disallow assignment operator shorthand where possible", recommended: false, @@ -82,7 +84,7 @@ module.exports = { }, create(context) { - + const never = context.options[0] === "never"; const sourceCode = context.sourceCode; /** @@ -202,7 +204,7 @@ module.exports = { } return { - AssignmentExpression: context.options[0] !== "never" ? verify : prohibit + AssignmentExpression: !never ? verify : prohibit }; } diff --git a/lib/rules/prefer-arrow-callback.js b/lib/rules/prefer-arrow-callback.js index d22e508beb0..b2851c00781 100644 --- a/lib/rules/prefer-arrow-callback.js +++ b/lib/rules/prefer-arrow-callback.js @@ -150,6 +150,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ allowUnboundThis: true }], + docs: { description: "Require using arrow functions for callbacks", recommended: false, @@ -161,12 +163,10 @@ module.exports = { type: "object", properties: { allowNamedFunctions: { - type: "boolean", - default: false + type: "boolean" }, allowUnboundThis: { - type: "boolean", - default: true + type: "boolean" } }, additionalProperties: false @@ -181,10 +181,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; - - const allowUnboundThis = options.allowUnboundThis !== false; // default to true - const allowNamedFunctions = options.allowNamedFunctions; + const [{ allowNamedFunctions, allowUnboundThis }] = context.options; const sourceCode = context.sourceCode; /* diff --git a/lib/rules/prefer-const.js b/lib/rules/prefer-const.js index b43975e9bae..d86c79c39a8 100644 --- a/lib/rules/prefer-const.js +++ b/lib/rules/prefer-const.js @@ -331,6 +331,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ destructuring: "any" }], + docs: { description: "Require `const` declarations for variables that are never reassigned after declared", recommended: false, @@ -343,8 +345,8 @@ module.exports = { { type: "object", properties: { - destructuring: { enum: ["any", "all"], default: "any" }, - ignoreReadBeforeAssign: { type: "boolean", default: false } + destructuring: { enum: ["any", "all"] }, + ignoreReadBeforeAssign: { type: "boolean" } }, additionalProperties: false } @@ -355,7 +357,7 @@ module.exports = { }, create(context) { - const options = context.options[0] || {}; + const [options] = context.options; const sourceCode = context.sourceCode; const shouldMatchAnyDestructuredVariable = options.destructuring !== "all"; const ignoreReadBeforeAssign = options.ignoreReadBeforeAssign === true; diff --git a/lib/rules/prefer-promise-reject-errors.js b/lib/rules/prefer-promise-reject-errors.js index e990265e99f..9715d0af7b8 100644 --- a/lib/rules/prefer-promise-reject-errors.js +++ b/lib/rules/prefer-promise-reject-errors.js @@ -15,6 +15,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Require using Error objects as Promise rejection reasons", recommended: false, @@ -27,7 +29,7 @@ module.exports = { { type: "object", properties: { - allowEmptyReject: { type: "boolean", default: false } + allowEmptyReject: { type: "boolean" } }, additionalProperties: false } @@ -40,7 +42,7 @@ module.exports = { create(context) { - const ALLOW_EMPTY_REJECT = context.options.length && context.options[0].allowEmptyReject; + const [{ allowEmptyReject }] = context.options; const sourceCode = context.sourceCode; //---------------------------------------------------------------------- @@ -53,7 +55,7 @@ module.exports = { * @returns {void} */ function checkRejectCall(callExpression) { - if (!callExpression.arguments.length && ALLOW_EMPTY_REJECT) { + if (!callExpression.arguments.length && allowEmptyReject) { return; } if ( diff --git a/lib/rules/prefer-regex-literals.js b/lib/rules/prefer-regex-literals.js index ffaaeac3f27..c59dc273869 100644 --- a/lib/rules/prefer-regex-literals.js +++ b/lib/rules/prefer-regex-literals.js @@ -112,6 +112,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Disallow use of the `RegExp` constructor in favor of regular expression literals", recommended: false, @@ -125,8 +127,7 @@ module.exports = { type: "object", properties: { disallowRedundantWrapping: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -144,7 +145,7 @@ module.exports = { }, create(context) { - const [{ disallowRedundantWrapping = false } = {}] = context.options; + const [{ disallowRedundantWrapping }] = context.options; const sourceCode = context.sourceCode; /** diff --git a/lib/rules/radix.js b/lib/rules/radix.js index 7df97d98602..54d2a1f2a32 100644 --- a/lib/rules/radix.js +++ b/lib/rules/radix.js @@ -79,6 +79,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [MODE_ALWAYS], + docs: { description: "Enforce the consistent use of the radix argument when using `parseInt()`", recommended: false, @@ -103,7 +105,7 @@ module.exports = { }, create(context) { - const mode = context.options[0] || MODE_ALWAYS; + const [mode] = context.options; const sourceCode = context.sourceCode; /** diff --git a/lib/rules/require-atomic-updates.js b/lib/rules/require-atomic-updates.js index 7e397ceb1cf..92c33a6a27d 100644 --- a/lib/rules/require-atomic-updates.js +++ b/lib/rules/require-atomic-updates.js @@ -170,6 +170,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{}], + docs: { description: "Disallow assignments that can lead to race conditions due to usage of `await` or `yield`", recommended: false, @@ -182,8 +184,7 @@ module.exports = { type: "object", properties: { allowProperties: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -196,7 +197,7 @@ module.exports = { }, create(context) { - const allowProperties = !!context.options[0] && context.options[0].allowProperties; + const [{ allowProperties }] = context.options; const sourceCode = context.sourceCode; const assignmentReferences = new Map(); diff --git a/lib/rules/sort-imports.js b/lib/rules/sort-imports.js index 04814ed6fed..4a314a9b6d1 100644 --- a/lib/rules/sort-imports.js +++ b/lib/rules/sort-imports.js @@ -14,6 +14,10 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{ + memberSyntaxSortOrder: ["none", "all", "multiple", "single"] + }], + docs: { description: "Enforce sorted import declarations within modules", recommended: false, @@ -25,8 +29,7 @@ module.exports = { type: "object", properties: { ignoreCase: { - type: "boolean", - default: false + type: "boolean" }, memberSyntaxSortOrder: { type: "array", @@ -38,16 +41,13 @@ module.exports = { maxItems: 4 }, ignoreDeclarationSort: { - type: "boolean", - default: false + type: "boolean" }, ignoreMemberSort: { - type: "boolean", - default: false + type: "boolean" }, allowSeparatedGroups: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -64,14 +64,14 @@ module.exports = { }, create(context) { - - const configuration = context.options[0] || {}, - ignoreCase = configuration.ignoreCase || false, - ignoreDeclarationSort = configuration.ignoreDeclarationSort || false, - ignoreMemberSort = configuration.ignoreMemberSort || false, - memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"], - allowSeparatedGroups = configuration.allowSeparatedGroups || false, - sourceCode = context.sourceCode; + const [{ + ignoreCase, + ignoreDeclarationSort, + ignoreMemberSort, + memberSyntaxSortOrder, + allowSeparatedGroups + }] = context.options; + const sourceCode = context.sourceCode; let previousDeclaration = null; /** diff --git a/lib/rules/sort-keys.js b/lib/rules/sort-keys.js index 088b5890f30..c104df565c7 100644 --- a/lib/rules/sort-keys.js +++ b/lib/rules/sort-keys.js @@ -80,6 +80,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: ["asc", { caseSensitive: true, minKeys: 2 }], + docs: { description: "Require object keys to be sorted", recommended: false, @@ -94,21 +96,17 @@ module.exports = { type: "object", properties: { caseSensitive: { - type: "boolean", - default: true + type: "boolean" }, natural: { - type: "boolean", - default: false + type: "boolean" }, minKeys: { type: "integer", - minimum: 2, - default: 2 + minimum: 2 }, allowLineSeparatedGroups: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -121,14 +119,8 @@ module.exports = { }, create(context) { - - // Parse options. - const order = context.options[0] || "asc"; - const options = context.options[1]; - const insensitive = options && options.caseSensitive === false; - const natural = options && options.natural; - const minKeys = options && options.minKeys; - const allowLineSeparatedGroups = options && options.allowLineSeparatedGroups || false; + const [order, { caseSensitive, natural, minKeys, allowLineSeparatedGroups }] = context.options; + const insensitive = !caseSensitive; const isValidOrder = isValidOrders[ order + (insensitive ? "I" : "") + (natural ? "N" : "") ]; diff --git a/lib/rules/sort-vars.js b/lib/rules/sort-vars.js index 8fd723fd4e5..926bdd0d596 100644 --- a/lib/rules/sort-vars.js +++ b/lib/rules/sort-vars.js @@ -14,6 +14,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: [{}], + docs: { description: "Require variables within the same declaration block to be sorted", recommended: false, @@ -41,10 +43,8 @@ module.exports = { }, create(context) { - - const configuration = context.options[0] || {}, - ignoreCase = configuration.ignoreCase || false, - sourceCode = context.sourceCode; + const [{ ignoreCase }] = context.options; + const sourceCode = context.sourceCode; return { VariableDeclaration(node) { diff --git a/lib/rules/strict.js b/lib/rules/strict.js index f9dd7500be3..7482d51ce5d 100644 --- a/lib/rules/strict.js +++ b/lib/rules/strict.js @@ -68,6 +68,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: ["safe"], + docs: { description: "Require or disallow strict mode directives", recommended: false, @@ -96,11 +98,10 @@ module.exports = { }, create(context) { - const ecmaFeatures = context.parserOptions.ecmaFeatures || {}, scopes = [], classScopes = []; - let mode = context.options[0] || "safe"; + let [mode] = context.options; if (ecmaFeatures.impliedStrict) { mode = "implied"; diff --git a/lib/rules/unicode-bom.js b/lib/rules/unicode-bom.js index 09971d26e30..15c7ad77a51 100644 --- a/lib/rules/unicode-bom.js +++ b/lib/rules/unicode-bom.js @@ -13,6 +13,8 @@ module.exports = { meta: { type: "layout", + defaultOptions: ["never"], + docs: { description: "Require or disallow Unicode byte order mark (BOM)", recommended: false, @@ -43,8 +45,8 @@ module.exports = { Program: function checkUnicodeBOM(node) { const sourceCode = context.sourceCode, - location = { column: 0, line: 1 }, - requireBOM = context.options[0] || "never"; + location = { column: 0, line: 1 }; + const [requireBOM] = context.options; if (!sourceCode.hasBOM && (requireBOM === "always")) { context.report({ diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index ba6f39aec47..191f15a8a7e 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -71,9 +71,7 @@ module.exports = { }, create(context) { - - const enforceForSwitchCase = !context.options[0] || context.options[0].enforceForSwitchCase; - const enforceForIndexOf = context.options[0] && context.options[0].enforceForIndexOf; + const [{ enforceForIndexOf, enforceForSwitchCase }] = context.options; /** * Checks the given `BinaryExpression` node for `foo === NaN` and other comparisons. diff --git a/lib/rules/valid-typeof.js b/lib/rules/valid-typeof.js index 3818dafeae9..34d9228ff20 100644 --- a/lib/rules/valid-typeof.js +++ b/lib/rules/valid-typeof.js @@ -19,6 +19,8 @@ module.exports = { meta: { type: "problem", + defaultOptions: [{}], + docs: { description: "Enforce comparing `typeof` expressions against valid strings", recommended: true, @@ -47,11 +49,10 @@ module.exports = { }, create(context) { - const VALID_TYPES = new Set(["symbol", "undefined", "object", "boolean", "number", "string", "function", "bigint"]), OPERATORS = new Set(["==", "===", "!=", "!=="]); const sourceCode = context.sourceCode; - const requireStringLiterals = context.options[0] && context.options[0].requireStringLiterals; + const [{ requireStringLiterals }] = context.options; let globalScope; diff --git a/lib/rules/yoda.js b/lib/rules/yoda.js index af8f525182e..7f192b7fd7a 100644 --- a/lib/rules/yoda.js +++ b/lib/rules/yoda.js @@ -111,6 +111,8 @@ module.exports = { meta: { type: "suggestion", + defaultOptions: ["never", {}], + docs: { description: 'Require or disallow "Yoda" conditions', recommended: false, @@ -125,12 +127,10 @@ module.exports = { type: "object", properties: { exceptRange: { - type: "boolean", - default: false + type: "boolean" }, onlyEquality: { - type: "boolean", - default: false + type: "boolean" } }, additionalProperties: false @@ -145,14 +145,8 @@ module.exports = { }, create(context) { - - // Default to "never" (!always) if no option - const always = context.options[0] === "always"; - const exceptRange = - context.options[1] && context.options[1].exceptRange; - const onlyEquality = - context.options[1] && context.options[1].onlyEquality; - + const [when, { exceptRange, onlyEquality }] = context.options; + const always = when === "always"; const sourceCode = context.sourceCode; /** From f9b435f6f0348e5225e7e701092b680889a42a09 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 18 Dec 2023 20:20:22 -0500 Subject: [PATCH 10/26] Fixes around inline comments --- lib/linter/linter.js | 17 +++++++++++------ lib/rules/id-length.js | 3 +-- lib/rules/max-len.js | 6 ------ tests/lib/linter/linter.js | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index f74d0ecd13f..54f6846c4e8 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -45,6 +45,7 @@ const { FlatConfigArray } = require("../config/flat-config-array"); const { RuleValidator } = require("../config/rule-validator"); const { assertIsRuleOptions, assertIsRuleSeverity } = require("../config/flat-config-schema"); const { normalizeSeverityToString } = require("../shared/severity"); +const { deepMergeArrays } = require("../shared/deep-merge-arrays"); const debug = require("debug")("eslint:linter"); const MAX_AUTOFIX_PASSES = 10; const DEFAULT_PARSER_NAME = "espree"; @@ -759,14 +760,15 @@ function stripUnicodeBOM(text) { /** * Get the options for a rule (not including severity), if any + * @param {Object|undefined} defaultOptions rule.meta.defaultOptions * @param {Array|number} ruleConfig rule configuration * @returns {Array} of rule options, empty Array if none */ -function getRuleOptions(ruleConfig) { +function getRuleOptions(defaultOptions, ruleConfig) { if (Array.isArray(ruleConfig)) { - return ruleConfig.slice(1); + return deepMergeArrays(defaultOptions, ruleConfig.slice(1)); } - return []; + return defaultOptions || []; } @@ -1022,7 +1024,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO Object.create(sharedTraversalContext), { id: ruleId, - options: getRuleOptions(configuredRules[ruleId]), + options: getRuleOptions(rule.meta && rule.meta.defaultOptions, configuredRules[ruleId]), report(...args) { /* @@ -1684,13 +1686,16 @@ class Linter { assertIsRuleOptions(ruleId, ruleValue); assertIsRuleSeverity(ruleId, ruleOptions[0]); + const mergedOptions = deepMergeArrays(rule.meta && rule.meta.defaultOptions, ruleOptions.slice(1)); + const mergedValue = [ruleOptions[0], ...mergedOptions]; + ruleValidator.validate({ plugins: config.plugins, rules: { - [ruleId]: ruleOptions + [ruleId]: mergedValue } }); - mergedInlineConfig.rules[ruleId] = ruleValue; + mergedInlineConfig.rules[ruleId] = mergedValue; } catch (err) { let baseMessage = err.message.slice( diff --git a/lib/rules/id-length.js b/lib/rules/id-length.js index 3771f7b0f51..ee1d80528b3 100644 --- a/lib/rules/id-length.js +++ b/lib/rules/id-length.js @@ -24,7 +24,6 @@ module.exports = { defaultOptions: [{ exceptionPatterns: [], exceptions: [], - max: Infinity, min: 2 }], @@ -75,7 +74,7 @@ module.exports = { create(context) { const [options] = context.options; - const { max: maxLength, min: minLength } = options; + const { max: maxLength = Infinity, min: minLength } = options; const properties = options.properties !== "never"; const exceptions = new Set(options.exceptions); const exceptionPatterns = options.exceptionPatterns.map(pattern => new RegExp(pattern, "u")); diff --git a/lib/rules/max-len.js b/lib/rules/max-len.js index f914f0bc2bc..138a0f239fd 100644 --- a/lib/rules/max-len.js +++ b/lib/rules/max-len.js @@ -82,12 +82,6 @@ module.exports = { OPTIONS_OR_INTEGER_SCHEMA, OPTIONS_SCHEMA ], - - defaultOptions: [{ - code: 80, - tabWidth: 4 - }], - messages: { max: "This line has a length of {{lineLength}}. Maximum allowed is {{maxLength}}.", maxComment: "This line has a comment length of {{lineLength}}. Maximum allowed is {{maxCommentLength}}." diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index dec2aabb67d..cda652fa63a 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -13034,6 +13034,38 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(messages.length, 1); assert.strictEqual(suppressedMessages.length, 0); }); + + it("rules should apply meta.defaultOptions", () => { + const config = { + languageOptions: { + sourceType: "script" + }, + rules: {} + }; + const codeA = "/*eslint arrow-body-style: error */ var arrow = a => { return a; }"; + const messages = linter.verify(codeA, config, filename, false); + + assert.deepStrictEqual( + messages, + [ + { + severity: 2, + ruleId: "arrow-body-style", + message: "Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`.", + messageId: "unexpectedSingleBlock", + line: 1, + column: 54, + endLine: 1, + endColumn: 67, + fix: { + range: [53, 66], + text: "a" + }, + nodeType: "ArrowFunctionExpression" + } + ] + ); + }); }); describe("when evaluating code with invalid comments to enable rules", () => { From e04072fd5779e811f9885511e054d187dec6f0cd Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 18 Dec 2023 20:39:36 -0500 Subject: [PATCH 11/26] Extract to a getRuleOptionsInline --- lib/linter/linter.js | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 54f6846c4e8..9c265c1a29b 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -769,7 +769,23 @@ function getRuleOptions(defaultOptions, ruleConfig) { return deepMergeArrays(defaultOptions, ruleConfig.slice(1)); } return defaultOptions || []; +} + +/** + * Get the options for a rule's inline comment, including severity + * @param {string} ruleId Rule name being configured. + * @param {Object|undefined} defaultOptions rule.meta.defaultOptions + * @param {Array|number} ruleValue rule severity and options, if any + * @returns {Array} of rule options, empty Array if none + */ +function getRuleOptionsInline(ruleId, defaultOptions, ruleValue) { + assertIsRuleOptions(ruleId, ruleValue); + + const [ruleSeverity, ...ruleConfig] = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; + + assertIsRuleSeverity(ruleId, ruleSeverity); + return [ruleSeverity, ...deepMergeArrays(defaultOptions, ruleConfig)]; } /** @@ -1681,21 +1697,15 @@ class Linter { try { - const ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; - - assertIsRuleOptions(ruleId, ruleValue); - assertIsRuleSeverity(ruleId, ruleOptions[0]); - - const mergedOptions = deepMergeArrays(rule.meta && rule.meta.defaultOptions, ruleOptions.slice(1)); - const mergedValue = [ruleOptions[0], ...mergedOptions]; + const ruleOptions = getRuleOptionsInline(ruleId, rule.meta && rule.meta.defaultOptions, ruleValue); ruleValidator.validate({ plugins: config.plugins, rules: { - [ruleId]: mergedValue + [ruleId]: ruleOptions } }); - mergedInlineConfig.rules[ruleId] = mergedValue; + mergedInlineConfig.rules[ruleId] = ruleOptions; } catch (err) { let baseMessage = err.message.slice( From 3e4f310189eb4077db95ef5ee6066b2d0ad4ec9e Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 6 Jan 2024 18:25:00 -0500 Subject: [PATCH 12/26] nit: new extra line --- lib/rules/array-bracket-spacing.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/rules/array-bracket-spacing.js b/lib/rules/array-bracket-spacing.js index 13c4fe59bfd..c032de47f13 100644 --- a/lib/rules/array-bracket-spacing.js +++ b/lib/rules/array-bracket-spacing.js @@ -60,7 +60,6 @@ module.exports = { const spaced = context.options[0] === "always", sourceCode = context.sourceCode; - /** * Determines whether an option is set, relative to the spacing option. * If spaced is "always", then check whether option is set to false. From f5e284dd5b75fa5498f344f7a6ad83a52047049c Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 6 Jan 2024 18:34:18 -0500 Subject: [PATCH 13/26] Test fix: meta.defaultOptions in a comment --- tests/lib/config/flat-config-array.js | 1 - tests/lib/linter/linter.js | 27 +++++++++++---------------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index f52b5c966bd..98abc104872 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -13,7 +13,6 @@ const { FlatConfigArray } = require("../../../lib/config/flat-config-array"); const assert = require("chai").assert; const stringify = require("json-stable-stringify-without-jsonify"); const espree = require("espree"); -const rules = require("../../../lib/rules"); //----------------------------------------------------------------------------- // Helpers diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 50a677074c2..fefa8beda53 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -1457,13 +1457,8 @@ describe("Linter", () => { }); it("rules should apply meta.defaultOptions", () => { - const config = { - languageOptions: { - sourceType: "script" - }, - rules: {} - }; - const codeA = "/*eslint arrow-body-style: error */ var arrow = a => { return a; }"; + const config = { rules: {} }; + const codeA = "/*eslint block-spacing: error */ function run() {return true; }"; const messages = linter.verify(codeA, config, filename, false); assert.deepStrictEqual( @@ -1471,18 +1466,18 @@ describe("Linter", () => { [ { severity: 2, - ruleId: "arrow-body-style", - message: "Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`.", - messageId: "unexpectedSingleBlock", + ruleId: "block-spacing", + message: "Requires a space after '{'.", + messageId: "missing", + nodeType: "BlockStatement", line: 1, - column: 54, + column: 49, endLine: 1, - endColumn: 67, + endColumn: 50, fix: { - range: [53, 66], - text: "a" - }, - nodeType: "ArrowFunctionExpression" + range: [49, 49], + text: " " + } } ] ); From 2b2f99adab449678d1e7a218396a82b667943700 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 10 Jan 2024 16:58:50 -0500 Subject: [PATCH 14/26] Refactor-level review feedback --- lib/linter/linter.js | 14 +++++++------- lib/shared/deep-merge-arrays.js | 26 +++++++++++++------------- tests/lib/shared/deep-merge-arrays.js | 4 ++-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 91cf2cf1773..fe92a668ab4 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -777,25 +777,25 @@ function stripUnicodeBOM(text) { /** * Get the options for a rule (not including severity), if any - * @param {Object|undefined} defaultOptions rule.meta.defaultOptions * @param {Array|number} ruleConfig rule configuration + * @param {Object|undefined} defaultOptions rule.meta.defaultOptions * @returns {Array} of rule options, empty Array if none */ -function getRuleOptions(defaultOptions, ruleConfig) { +function getRuleOptions(ruleConfig, defaultOptions) { if (Array.isArray(ruleConfig)) { return deepMergeArrays(defaultOptions, ruleConfig.slice(1)); } - return defaultOptions || []; + return defaultOptions ?? []; } /** * Get the options for a rule's inline comment, including severity * @param {string} ruleId Rule name being configured. - * @param {Object|undefined} defaultOptions rule.meta.defaultOptions * @param {Array|number} ruleValue rule severity and options, if any + * @param {Object|undefined} defaultOptions rule.meta.defaultOptions * @returns {Array} of rule options, empty Array if none */ -function getRuleOptionsInline(ruleId, defaultOptions, ruleValue) { +function getRuleOptionsInline(ruleId, ruleValue, defaultOptions) { assertIsRuleOptions(ruleId, ruleValue); const [ruleSeverity, ...ruleConfig] = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; @@ -1023,7 +1023,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO Object.create(sharedTraversalContext), { id: ruleId, - options: getRuleOptions(rule.meta && rule.meta.defaultOptions, configuredRules[ruleId]), + options: getRuleOptions(configuredRules[ruleId], rule.meta?.defaultOptions), report(...args) { /* @@ -1730,7 +1730,7 @@ class Linter { try { - const ruleOptions = getRuleOptionsInline(ruleId, rule.meta && rule.meta.defaultOptions, ruleValue); + const ruleOptions = getRuleOptionsInline(ruleId, ruleValue, rule.meta?.defaultOptions); ruleValidator.validate({ plugins: config.plugins, diff --git a/lib/shared/deep-merge-arrays.js b/lib/shared/deep-merge-arrays.js index b14b1aaa5d8..1fcf9fa66ab 100644 --- a/lib/shared/deep-merge-arrays.js +++ b/lib/shared/deep-merge-arrays.js @@ -1,18 +1,18 @@ -/* eslint-disable eqeqeq, no-undefined -- `null` and `undefined` are different in options */ /** * @fileoverview Applies default rule options * @author JoshuaKGoldberg */ +/* eslint-disable eqeqeq, no-undefined -- `null` and `undefined` are different in options */ "use strict"; /** * Check if the variable contains an object strictly rejecting arrays - * @param {unknown} obj an object - * @returns {boolean} Whether obj is an object + * @param {unknown} value an object + * @returns {boolean} Whether value is an object */ -function isObjectNotArray(obj) { - return typeof obj === "object" && obj != null && !Array.isArray(obj); +function isObjectNotArray(value) { + return typeof value === "object" && value != null && !Array.isArray(value); } /** @@ -21,7 +21,7 @@ function isObjectNotArray(obj) { * @param {U} second User-specified value. * @returns {T | U | (T & U)} Merged equivalent of second on top of first. */ -function deepMergeElements(first, second) { +function deepMergeObjects(first, second) { if (second === null || (second !== undefined && typeof second !== "object")) { return second; } @@ -36,23 +36,23 @@ function deepMergeElements(first, second) { const keysUnion = new Set(Object.keys(first).concat(Object.keys(second))); - return Array.from(keysUnion).reduce((acc, key) => { + return Array.from(keysUnion).reduce((result, key) => { const firstValue = first[key]; const secondValue = second[key]; if (firstValue !== undefined && secondValue !== undefined) { if (isObjectNotArray(firstValue) && isObjectNotArray(secondValue)) { - acc[key] = deepMergeElements(firstValue, secondValue); + result[key] = deepMergeObjects(firstValue, secondValue); } else { - acc[key] = secondValue; + result[key] = secondValue; } } else if (firstValue !== undefined) { - acc[key] = firstValue; + result[key] = firstValue; } else { - acc[key] = secondValue; + result[key] = secondValue; } - return acc; + return result; }, {}); } @@ -68,7 +68,7 @@ function deepMergeArrays(first, second) { } return [ - ...first.map((value, i) => deepMergeElements(value, i < second.length ? second[i] : undefined)), + ...first.map((value, i) => deepMergeObjects(value, i < second.length ? second[i] : undefined)), ...second.slice(first.length) ]; } diff --git a/tests/lib/shared/deep-merge-arrays.js b/tests/lib/shared/deep-merge-arrays.js index 50e736fbd47..c58a8553b24 100644 --- a/tests/lib/shared/deep-merge-arrays.js +++ b/tests/lib/shared/deep-merge-arrays.js @@ -18,7 +18,7 @@ const { deepMergeArrays } = require("../../../lib/shared/deep-merge-arrays"); * @param {unknown} value Value to be stringified. * @returns {string} String equivalent of the value. */ -function stringify(value) { +function toTestCaseName(value) { return typeof value === "object" ? JSON.stringify(value) : `${value}`; } @@ -96,7 +96,7 @@ describe("deepMerge", () => { }] ] ]) { - it(`${stringify(first)}, ${stringify(second)}`, () => { + it(`${toTestCaseName(first)}, ${toTestCaseName(second)}`, () => { assert.deepStrictEqual(deepMergeArrays(first, second), result); }); } From 79d3eb056484a561c9c7a6fa04161f74f1d2a2ed Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 10 Jan 2024 17:02:04 -0500 Subject: [PATCH 15/26] Used a recommended rule in linter.js test --- tests/lib/linter/linter.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index fefa8beda53..b7da6292c58 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -1458,7 +1458,7 @@ describe("Linter", () => { it("rules should apply meta.defaultOptions", () => { const config = { rules: {} }; - const codeA = "/*eslint block-spacing: error */ function run() {return true; }"; + const codeA = "/*eslint no-constant-condition: error */ while (true) {}"; const messages = linter.verify(codeA, config, filename, false); assert.deepStrictEqual( @@ -1466,18 +1466,14 @@ describe("Linter", () => { [ { severity: 2, - ruleId: "block-spacing", - message: "Requires a space after '{'.", - messageId: "missing", - nodeType: "BlockStatement", + ruleId: "no-constant-condition", + message: "Unexpected constant condition.", + messageId: "unexpected", + nodeType: "Literal", line: 1, column: 49, endLine: 1, - endColumn: 50, - fix: { - range: [49, 49], - text: " " - } + endColumn: 53 } ] ); From efa307a403b0b2485af3e86cc13938eff31cf1bc Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 10 Jan 2024 17:28:49 -0500 Subject: [PATCH 16/26] Added custom-rules.md docs --- docs/src/extend/custom-rules.md | 43 +++++++++++++++++++++++++++ tests/lib/shared/deep-merge-arrays.js | 1 + 2 files changed, 44 insertions(+) diff --git a/docs/src/extend/custom-rules.md b/docs/src/extend/custom-rules.md index 9a2c0ada21f..33e506bc82c 100644 --- a/docs/src/extend/custom-rules.md +++ b/docs/src/extend/custom-rules.md @@ -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). @@ -787,6 +789,47 @@ module.exports = { To learn more about JSON Schema, we recommend looking at some examples on the [JSON Schema website](https://json-schema.org/learn/), 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 +module.exports = { + 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: + +* User-provided `undefined` uses any default option +* User-provided arrays and primitive values other than `undefined` override any 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`. + ### 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()`. diff --git a/tests/lib/shared/deep-merge-arrays.js b/tests/lib/shared/deep-merge-arrays.js index c58a8553b24..26bf7961263 100644 --- a/tests/lib/shared/deep-merge-arrays.js +++ b/tests/lib/shared/deep-merge-arrays.js @@ -47,6 +47,7 @@ describe("deepMerge", () => { [[{ abc: true }], ["def"], ["def"]], [[{ a: undefined }], [{ a: 0 }], [{ a: 0 }]], [[{ a: null }], [{ a: 0 }], [{ a: 0 }]], + [[{ a: null }], [{ a: { b: 0 } }], [{ a: { b: 0 } }]], [[{ a: 0 }], [{ a: 1 }], [{ a: 1 }]], [[{ a: 0 }], [{ a: null }], [{ a: null }]], [[{ a: 0 }], [{ a: undefined }], [{ a: 0 }]], From de245267a138aed4a29645c6c89ab53ceb51fa43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 11 Jan 2024 13:37:12 -0500 Subject: [PATCH 17/26] Update docs/src/extend/custom-rules.md Co-authored-by: Nicholas C. Zakas --- docs/src/extend/custom-rules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/extend/custom-rules.md b/docs/src/extend/custom-rules.md index 33e506bc82c..bd661ccbb69 100644 --- a/docs/src/extend/custom-rules.md +++ b/docs/src/extend/custom-rules.md @@ -797,7 +797,7 @@ When the rule is enabled in a user configuration, ESLint will recursively merge For example, given the following defaults: ```js -module.exports = { +export default { meta: { defaultOptions: [{ alias: "basic", From 6b18f8a96e1aef141324ed35ee18d629e1856580 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 11 Jan 2024 13:39:03 -0500 Subject: [PATCH 18/26] Clarified undefined point --- docs/src/extend/custom-rules.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/extend/custom-rules.md b/docs/src/extend/custom-rules.md index bd661ccbb69..96ca29abe81 100644 --- a/docs/src/extend/custom-rules.md +++ b/docs/src/extend/custom-rules.md @@ -824,8 +824,8 @@ The rule would have a runtime `alias` value of `"basic"` unless the user configu Each element of the options array is merged according to the following rules: -* User-provided `undefined` uses any default option -* User-provided arrays and primitive values other than `undefined` override any default option +* 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`. From 7f3e8081807c02140e2931000e874f19c46265a0 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 2 Feb 2024 14:01:42 -0500 Subject: [PATCH 19/26] Adjusted for edge cases per review --- lib/shared/deep-merge-arrays.js | 12 ++++++---- tests/lib/shared/deep-merge-arrays.js | 32 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/shared/deep-merge-arrays.js b/lib/shared/deep-merge-arrays.js index 1fcf9fa66ab..8fedf897b2b 100644 --- a/lib/shared/deep-merge-arrays.js +++ b/lib/shared/deep-merge-arrays.js @@ -22,23 +22,27 @@ function isObjectNotArray(value) { * @returns {T | U | (T & U)} Merged equivalent of second on top of first. */ function deepMergeObjects(first, second) { + if (first === null) { + return second ?? first; + } + if (second === null || (second !== undefined && typeof second !== "object")) { return second; } - if (first === null || second === undefined) { + if (second === undefined) { return first; } - if (Array.isArray(first) || typeof first !== "object") { + if (Array.isArray(first) || typeof first !== "object" || Array.isArray(second)) { return second; } const keysUnion = new Set(Object.keys(first).concat(Object.keys(second))); return Array.from(keysUnion).reduce((result, key) => { - const firstValue = first[key]; - const secondValue = second[key]; + const firstValue = Object.hasOwn(first, key) ? first[key] : undefined; + const secondValue = Object.hasOwn(second, key) ? second[key] : undefined; if (firstValue !== undefined && secondValue !== undefined) { if (isObjectNotArray(firstValue) && isObjectNotArray(secondValue)) { diff --git a/tests/lib/shared/deep-merge-arrays.js b/tests/lib/shared/deep-merge-arrays.js index 26bf7961263..c374ae7bf03 100644 --- a/tests/lib/shared/deep-merge-arrays.js +++ b/tests/lib/shared/deep-merge-arrays.js @@ -45,6 +45,8 @@ describe("deepMerge", () => { [[["abc"]], ["def"], ["def"]], [[["abc"]], [{ a: 0 }], [{ a: 0 }]], [[{ abc: true }], ["def"], ["def"]], + [[{ abc: true }], [["def"]], [["def"]]], + [[null], [{ abc: true }], [{ abc: true }]], [[{ a: undefined }], [{ a: 0 }], [{ a: 0 }]], [[{ a: null }], [{ a: 0 }], [{ a: 0 }]], [[{ a: null }], [{ a: { b: 0 } }], [{ a: { b: 0 } }]], @@ -79,6 +81,36 @@ describe("deepMerge", () => { [{ a: { e: "f" } }, { f: 123 }], [{ a: { b: "c", e: "f" } }, { d: true, f: 123 }] ], + [ + [{ hasOwnProperty: true }], + [{}], + [{ hasOwnProperty: true }] + ], + [ + [{ hasOwnProperty: false }], + [{}], + [{ hasOwnProperty: false }] + ], + [ + [{ hasOwnProperty: null }], + [{}], + [{ hasOwnProperty: null }] + ], + [ + [{ hasOwnProperty: undefined }], + [{}], + [{ hasOwnProperty: undefined }] + ], + [ + [{}], + [{ hasOwnProperty: null }], + [{ hasOwnProperty: null }] + ], + [ + [{}], + [{ hasOwnProperty: undefined }], + [{ hasOwnProperty: undefined }] + ], [ [{ allow: [], From 753bf8dbf75d7058ad0496650accf8c444a83c5c Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 2 Feb 2024 14:03:34 -0500 Subject: [PATCH 20/26] Refactored per review --- lib/shared/deep-merge-arrays.js | 35 ++++++++------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/lib/shared/deep-merge-arrays.js b/lib/shared/deep-merge-arrays.js index 8fedf897b2b..ab086b26f64 100644 --- a/lib/shared/deep-merge-arrays.js +++ b/lib/shared/deep-merge-arrays.js @@ -22,42 +22,23 @@ function isObjectNotArray(value) { * @returns {T | U | (T & U)} Merged equivalent of second on top of first. */ function deepMergeObjects(first, second) { - if (first === null) { - return second ?? first; - } - - if (second === null || (second !== undefined && typeof second !== "object")) { - return second; - } - - if (second === undefined) { + if (second === void 0) { return first; } - if (Array.isArray(first) || typeof first !== "object" || Array.isArray(second)) { + if (!isObjectNotArray(first) || !isObjectNotArray(second)) { return second; } - const keysUnion = new Set(Object.keys(first).concat(Object.keys(second))); - - return Array.from(keysUnion).reduce((result, key) => { - const firstValue = Object.hasOwn(first, key) ? first[key] : undefined; - const secondValue = Object.hasOwn(second, key) ? second[key] : undefined; + const result = { ...first, ...second }; - if (firstValue !== undefined && secondValue !== undefined) { - if (isObjectNotArray(firstValue) && isObjectNotArray(secondValue)) { - result[key] = deepMergeObjects(firstValue, secondValue); - } else { - result[key] = secondValue; - } - } else if (firstValue !== undefined) { - result[key] = firstValue; - } else { - result[key] = secondValue; + for (const key of Object.keys(second)) { + if (Object.prototype.propertyIsEnumerable.call(first, key)) { + result[key] = deepMergeObjects(first[key], second[key]); } + } - return result; - }, {}); + return result; } /** From 9bc927b643ecd23728a0a8765f90aba83a02ef30 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 2 Feb 2024 14:05:11 -0500 Subject: [PATCH 21/26] Removed lint disable in source --- lib/shared/deep-merge-arrays.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/shared/deep-merge-arrays.js b/lib/shared/deep-merge-arrays.js index ab086b26f64..a7a2deef338 100644 --- a/lib/shared/deep-merge-arrays.js +++ b/lib/shared/deep-merge-arrays.js @@ -2,7 +2,6 @@ * @fileoverview Applies default rule options * @author JoshuaKGoldberg */ -/* eslint-disable eqeqeq, no-undefined -- `null` and `undefined` are different in options */ "use strict"; @@ -12,7 +11,7 @@ * @returns {boolean} Whether value is an object */ function isObjectNotArray(value) { - return typeof value === "object" && value != null && !Array.isArray(value); + return typeof value === "object" && value !== null && value !== void 0 && !Array.isArray(value); } /** @@ -53,11 +52,9 @@ function deepMergeArrays(first, second) { } return [ - ...first.map((value, i) => deepMergeObjects(value, i < second.length ? second[i] : undefined)), + ...first.map((value, i) => deepMergeObjects(value, i < second.length ? second[i] : void 0)), ...second.slice(first.length) ]; } module.exports = { deepMergeArrays }; - -/* eslint-enable eqeqeq, no-undefined -- `null` and `undefined` are different in options */ From 7d25a7b3cf49bc590733c254c4e1952c3b715ef6 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 2 Feb 2024 14:28:08 -0500 Subject: [PATCH 22/26] Added Linter test for meta.defaultOptions --- tests/lib/linter/linter.js | 43 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index b7da6292c58..1f78086913d 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6760,6 +6760,49 @@ var a = "test2"; }); }); + describe("options", () => { + it("rules should apply meta.defaultOptions and ignore schema defaults", () => { + linter.defineRule("my-rule", { + meta: { + defaultOptions: [{ + inBoth: "from-default-options", + inDefaultOptions: "from-default-options" + }], + schema: { + type: "object", + properties: { + inBoth: { default: "from-schema", type: "string" }, + inDefaultOptions: { type: "string" }, + inSchema: { default: "from-schema", type: "string" } + }, + additionalProperties: false + } + }, + create(context) { + return { + Program(node) { + context.report({ node, message: JSON.stringify(context.options[0]) }); + } + }; + } + }); + + const config = { + rules: { + "my-rule": "error" + } + }; + + const code = ""; + const messages = linter.verify(code, config); + + assert.deepStrictEqual( + JSON.parse(messages[0].message), + { inBoth: "from-default-options", inDefaultOptions: "from-default-options" } + ); + }); + }); + describe("processors", () => { let receivedFilenames = []; let receivedPhysicalFilenames = []; From ce75fa28e2fd0f878ea5bf98b1046603aacdaba7 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 2 Feb 2024 14:35:56 -0500 Subject: [PATCH 23/26] Documented useDefaults from Ajv --- docs/src/extend/custom-rules.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/extend/custom-rules.md b/docs/src/extend/custom-rules.md index 96ca29abe81..cee68515142 100644 --- a/docs/src/extend/custom-rules.md +++ b/docs/src/extend/custom-rules.md @@ -830,6 +830,10 @@ Each element of the options array is merged according to the following rules: 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()`. From 0dc4810131a21a8d7c33faa1f48e7573aec40b2c Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 15 Feb 2024 18:01:13 -0500 Subject: [PATCH 24/26] Set up meta+schema merging unit tests for flat (passing) and legacy (failing) --- lib/config/rule-validator.js | 2 +- tests/lib/linter/linter.js | 59 ++++++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js index 9cdc7806fef..69e2da2fd27 100644 --- a/lib/config/rule-validator.js +++ b/lib/config/rule-validator.js @@ -166,7 +166,7 @@ class RuleValidator { if (validateRule) { const slicedOptions = ruleOptions.slice(1); - const mergedOptions = deepMergeArrays(rule.meta && rule.meta.defaultOptions, slicedOptions); + const mergedOptions = deepMergeArrays(rule.meta?.defaultOptions, slicedOptions); validateRule(mergedOptions); diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 3b6f822273a..ea5af53034a 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6763,7 +6763,7 @@ var a = "test2"; }); describe("options", () => { - it("rules should apply meta.defaultOptions and ignore schema defaults", () => { + it("rules should apply meta.defaultOptions on top of schema defaults", () => { linter.defineRule("my-rule", { meta: { defaultOptions: [{ @@ -6783,7 +6783,10 @@ var a = "test2"; create(context) { return { Program(node) { - context.report({ node, message: JSON.stringify(context.options[0]) }); + context.report({ + message: JSON.stringify(context.options[0]), + node + }); } }; } @@ -6800,7 +6803,7 @@ var a = "test2"; assert.deepStrictEqual( JSON.parse(messages[0].message), - { inBoth: "from-default-options", inDefaultOptions: "from-default-options" } + { inBoth: "from-default-options", inDefaultOptions: "from-default-options", inSchema: "from-schema" } ); }); }); @@ -15451,6 +15454,56 @@ var a = "test2"; }); }); + describe("options", () => { + it("rules should apply meta.defaultOptions on top of schema defaults", () => { + const config = { + plugins: { + test: { + rules: { + checker: { + meta: { + defaultOptions: [{ + inBoth: "from-default-options", + inDefaultOptions: "from-default-options" + }], + schema: [{ + type: "object", + properties: { + inBoth: { default: "from-schema", type: "string" }, + inDefaultOptions: { type: "string" }, + inSchema: { default: "from-schema", type: "string" } + }, + additionalProperties: false + }] + }, + create(context) { + return { + Program(node) { + context.report({ + message: JSON.stringify(context.options[0]), + node + }); + } + }; + } + } + } + } + }, + rules: { + "test/checker": "error" + } + }; + + const messages = linter.verify("foo", config, filename); + + assert.deepStrictEqual( + JSON.parse(messages[0].message), + { inBoth: "from-default-options", inDefaultOptions: "from-default-options", inSchema: "from-schema" } + ); + }); + }); + describe("processors", () => { let receivedFilenames = []; let receivedPhysicalFilenames = []; From adb7972d90ea8edbea23600ef1cece700ee03879 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 15 Feb 2024 18:40:37 -0500 Subject: [PATCH 25/26] Potential solution: boolean applyDefaultOptions param for runRules --- lib/linter/linter.js | 20 ++++++++++++++++++-- tests/lib/linter/linter.js | 4 ++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 4ef6b1ff001..81726ca8aeb 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -950,13 +950,27 @@ 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 * @param {Function} ruleFilter A predicate function to filter which rules should be executed. * @returns {LintMessage[]} An array of reported problems */ -function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename, ruleFilter) { +function runRules( + sourceCode, + configuredRules, + ruleMapper, + parserName, + languageOptions, + settings, + filename, + applyDefaultOptions, + disableFixes, + cwd, + physicalFilename, + ruleFilter +) { const emitter = createEmitter(); const nodeQueue = []; let currentNode = sourceCode.ast; @@ -1024,7 +1038,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO Object.create(sharedTraversalContext), { id: ruleId, - options: getRuleOptions(configuredRules[ruleId], rule.meta?.defaultOptions), + options: getRuleOptions(configuredRules[ruleId], applyDefaultOptions && rule.meta?.defaultOptions), report(...args) { /* @@ -1411,6 +1425,7 @@ class Linter { languageOptions, settings, options.filename, + true, options.disableFixes, slots.cwd, providedOptions.physicalFilename, @@ -1843,6 +1858,7 @@ class Linter { languageOptions, settings, options.filename, + false, options.disableFixes, slots.cwd, providedOptions.physicalFilename, diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index ea5af53034a..11faac439c8 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -6763,7 +6763,7 @@ var a = "test2"; }); describe("options", () => { - it("rules should apply meta.defaultOptions on top of schema defaults", () => { + it("rules should apply meta.defaultOptions and ignore schema defaults", () => { linter.defineRule("my-rule", { meta: { defaultOptions: [{ @@ -6803,7 +6803,7 @@ var a = "test2"; assert.deepStrictEqual( JSON.parse(messages[0].message), - { inBoth: "from-default-options", inDefaultOptions: "from-default-options", inSchema: "from-schema" } + { inBoth: "from-default-options", inDefaultOptions: "from-default-options" } ); }); }); From a64b798efb9d9010744a531ed4d1592b6779c46e Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 8 Jun 2024 16:05:20 -0400 Subject: [PATCH 26/26] chore: node:assert --- tests/lib/shared/deep-merge-arrays.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/shared/deep-merge-arrays.js b/tests/lib/shared/deep-merge-arrays.js index c374ae7bf03..bbdaef2dff0 100644 --- a/tests/lib/shared/deep-merge-arrays.js +++ b/tests/lib/shared/deep-merge-arrays.js @@ -6,7 +6,7 @@ // Requirements //------------------------------------------------------------------------------ -const assert = require("assert"); +const assert = require("node:assert"); const { deepMergeArrays } = require("../../../lib/shared/deep-merge-arrays"); //------------------------------------------------------------------------------