Skip to content

Commit 1b51aa1

Browse files
committed
feat(linter/plugins): implement options merging
1 parent 8881d7f commit 1b51aa1

File tree

1 file changed

+111
-0
lines changed

1 file changed

+111
-0
lines changed

apps/oxlint/src-js/plugins/options.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@
22
* Options for rules.
33
*/
44

5+
import {
6+
deepFreezeJsonValue as deepFreezeValue,
7+
deepFreezeJsonArray as deepFreezeArray,
8+
deepFreezeJsonObject as deepFreezeObject,
9+
} from "./json.js";
10+
511
import type { JsonValue } from "./json.ts";
612

13+
const { freeze } = Object,
14+
{ isArray } = Array,
15+
{ min } = Math;
16+
717
/**
818
* Options for a rule on a file.
919
*/
@@ -17,3 +27,104 @@ export const allOptions: Readonly<Options>[] = [DEFAULT_OPTIONS];
1727

1828
// Index into `allOptions` for default options
1929
export const DEFAULT_OPTIONS_ID = 0;
30+
31+
/**
32+
* Merge user-provided options from config with rule's default options.
33+
*
34+
* Config options take precedence over default options.
35+
*
36+
* Returned options are deep frozen.
37+
* `ruleOptions` may be frozen in place (or partially frozen) too.
38+
* `defaultOptions` must already be deep frozen before calling this function.
39+
*
40+
* Follows the same merging logic as ESLint's `getRuleOptions`.
41+
* https://github.com/eslint/eslint/blob/0f5a94a84beee19f376025c74f703f275d52c94b/lib/linter/linter.js#L443-L454
42+
* https://github.com/eslint/eslint/blob/0f5a94a84beee19f376025c74f703f275d52c94b/lib/shared/deep-merge-arrays.js
43+
*
44+
* Notably, nested arrays are not merged - config options wins. e.g.:
45+
* - Config options: [ [1] ]
46+
* - Default options: [ [2, 3], 4 ]
47+
* - Merged options: [ [1], 4 ]
48+
*
49+
* @param configOptions - Options from config
50+
* @param defaultOptions - Default options from `rule.meta.defaultOptions`
51+
* @returns Merged options
52+
*/
53+
export function mergeOptions(
54+
configOptions: Options | null,
55+
defaultOptions: Readonly<Options> | null,
56+
): Readonly<Options> {
57+
if (configOptions === null) {
58+
return defaultOptions === null ? DEFAULT_OPTIONS : defaultOptions;
59+
}
60+
61+
if (defaultOptions === null) {
62+
deepFreezeArray(configOptions);
63+
return configOptions;
64+
}
65+
66+
// Both are defined - merge them
67+
const merged = [];
68+
69+
const defaultOptionsLength = defaultOptions.length,
70+
ruleOptionsLength = configOptions.length,
71+
bothLength = min(defaultOptionsLength, ruleOptionsLength);
72+
73+
let i = 0;
74+
for (; i < bothLength; i++) {
75+
merged.push(mergeValues(configOptions[i], defaultOptions[i]));
76+
}
77+
78+
if (defaultOptionsLength > ruleOptionsLength) {
79+
for (; i < defaultOptionsLength; i++) {
80+
merged.push(defaultOptions[i]);
81+
}
82+
} else {
83+
for (; i < ruleOptionsLength; i++) {
84+
const prop = configOptions[i];
85+
deepFreezeValue(prop);
86+
merged.push(prop);
87+
}
88+
}
89+
90+
return freeze(merged);
91+
}
92+
93+
/**
94+
* Merge value from user-provided options with value from default options.
95+
*
96+
* @param configValue - Value from config
97+
* @param defaultValue - Value from default options
98+
* @returns Merged value
99+
*/
100+
function mergeValues(configValue: JsonValue, defaultValue: JsonValue): JsonValue {
101+
// If config value is a primitive, it wins
102+
if (configValue === null || typeof configValue !== "object") return configValue;
103+
104+
// If config value is an array, it wins
105+
if (isArray(configValue)) {
106+
deepFreezeArray(configValue);
107+
return configValue;
108+
}
109+
110+
// If default value is a primitive or an array, config value wins (it's an object)
111+
if (defaultValue === null || typeof defaultValue !== "object" || isArray(defaultValue)) {
112+
deepFreezeObject(configValue);
113+
return configValue;
114+
}
115+
116+
// Both are objects (not arrays)
117+
const merged = { ...defaultValue, ...configValue };
118+
119+
// Symbol properties are not possible in JSON, so no need to handle them here.
120+
// All properties are enumerable own properties, so can use simple `for..in` loop.
121+
for (const key in configValue) {
122+
if (key in defaultValue) {
123+
merged[key] = mergeValues(configValue[key], defaultValue[key]);
124+
} else {
125+
deepFreezeValue(configValue[key]);
126+
}
127+
}
128+
129+
return freeze(merged);
130+
}

0 commit comments

Comments
 (0)