diff --git a/apps/oxlint/src-js/plugins/options.ts b/apps/oxlint/src-js/plugins/options.ts index 33df678e50927..fdb70c038a14b 100644 --- a/apps/oxlint/src-js/plugins/options.ts +++ b/apps/oxlint/src-js/plugins/options.ts @@ -2,8 +2,18 @@ * Options for rules. */ +import { + deepFreezeJsonValue as deepFreezeValue, + deepFreezeJsonArray as deepFreezeArray, + deepFreezeJsonObject as deepFreezeObject, +} from "./json.js"; + import type { JsonValue } from "./json.ts"; +const { freeze } = Object, + { isArray } = Array, + { min } = Math; + /** * Options for a rule on a file. */ @@ -17,3 +27,104 @@ export const allOptions: Readonly[] = [DEFAULT_OPTIONS]; // Index into `allOptions` for default options export const DEFAULT_OPTIONS_ID = 0; + +/** + * Merge user-provided options from config with rule's default options. + * + * Config options take precedence over default options. + * + * Returned options are deep frozen. + * `ruleOptions` may be frozen in place (or partially frozen) too. + * `defaultOptions` must already be deep frozen before calling this function. + * + * Follows the same merging logic as ESLint's `getRuleOptions`. + * https://github.com/eslint/eslint/blob/0f5a94a84beee19f376025c74f703f275d52c94b/lib/linter/linter.js#L443-L454 + * https://github.com/eslint/eslint/blob/0f5a94a84beee19f376025c74f703f275d52c94b/lib/shared/deep-merge-arrays.js + * + * Notably, nested arrays are not merged - config options wins. e.g.: + * - Config options: [ [1] ] + * - Default options: [ [2, 3], 4 ] + * - Merged options: [ [1], 4 ] + * + * @param configOptions - Options from config + * @param defaultOptions - Default options from `rule.meta.defaultOptions` + * @returns Merged options + */ +export function mergeOptions( + configOptions: Options | null, + defaultOptions: Readonly | null, +): Readonly { + if (configOptions === null) { + return defaultOptions === null ? DEFAULT_OPTIONS : defaultOptions; + } + + if (defaultOptions === null) { + deepFreezeArray(configOptions); + return configOptions; + } + + // Both are defined - merge them + const merged = []; + + const defaultOptionsLength = defaultOptions.length, + ruleOptionsLength = configOptions.length, + bothLength = min(defaultOptionsLength, ruleOptionsLength); + + let i = 0; + for (; i < bothLength; i++) { + merged.push(mergeValues(configOptions[i], defaultOptions[i])); + } + + if (defaultOptionsLength > ruleOptionsLength) { + for (; i < defaultOptionsLength; i++) { + merged.push(defaultOptions[i]); + } + } else { + for (; i < ruleOptionsLength; i++) { + const prop = configOptions[i]; + deepFreezeValue(prop); + merged.push(prop); + } + } + + return freeze(merged); +} + +/** + * Merge value from user-provided options with value from default options. + * + * @param configValue - Value from config + * @param defaultValue - Value from default options + * @returns Merged value + */ +function mergeValues(configValue: JsonValue, defaultValue: JsonValue): JsonValue { + // If config value is a primitive, it wins + if (configValue === null || typeof configValue !== "object") return configValue; + + // If config value is an array, it wins + if (isArray(configValue)) { + deepFreezeArray(configValue); + return configValue; + } + + // If default value is a primitive or an array, config value wins (it's an object) + if (defaultValue === null || typeof defaultValue !== "object" || isArray(defaultValue)) { + deepFreezeObject(configValue); + return configValue; + } + + // Both are objects (not arrays) + const merged = { ...defaultValue, ...configValue }; + + // Symbol properties are not possible in JSON, so no need to handle them here. + // All properties are enumerable own properties, so can use simple `for..in` loop. + for (const key in configValue) { + if (key in defaultValue) { + merged[key] = mergeValues(configValue[key], defaultValue[key]); + } else { + deepFreezeValue(configValue[key]); + } + } + + return freeze(merged); +}