Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions apps/oxlint/src-js/plugins/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -17,3 +27,104 @@ export const allOptions: Readonly<Options>[] = [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<Options> | null,
): Readonly<Options> {
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);
}
Loading