Skip to content
Merged
Show file tree
Hide file tree
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
67 changes: 56 additions & 11 deletions apps/oxlint/src-js/plugins/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,17 +189,7 @@ export function registerPlugin(
// ESLint treats empty `defaultOptions` the same as no `defaultOptions`,
// and does not validate against schema
if (inputDefaultOptions.length !== 0) {
// Serialize to JSON and deserialize again.
// This is the simplest way to make sure that `defaultOptions` does not contain any `undefined` values,
// or circular references. It may also be the fastest, as `JSON.parse` and `JSON.serialize` are native code.
// If we move to doing options merging on Rust side, we'll need to convert to JSON anyway.
try {
defaultOptions = JSON.parse(JSON.stringify(inputDefaultOptions)) as Options;
} catch (err) {
throw new Error(
`\`rule.meta.defaultOptions\` must be JSON-serializable: ${getErrorMessage(err)}`,
);
}
defaultOptions = conformDefaultOptions(inputDefaultOptions);

// Validate default options against schema, if schema was provided.
// This also applies any defaults from schema.
Expand Down Expand Up @@ -368,6 +358,61 @@ function normalizePluginName(name: string): string {
return name;
}

/**
* Serialize default options to JSON and deserialize again.
*
* This is the simplest way to make sure that `defaultOptions` does not contain any `undefined` values,
* or circular references. It may also be the fastest, as `JSON.parse` and `JSON.stringify` are native code.
* If we move to doing options merging on Rust side, we'll need to convert to JSON anyway.
*
* Special handling for `Infinity` / `-Infinity` values, to ensure they survive the round trip.
* Without this, they would be converted to `null`.
*
* @param defaultOptions - Default options array
* @returns Conformed default options array
*/
function conformDefaultOptions(defaultOptions: Options): Options {
let json,
containsInfinity = false;
try {
json = JSON.stringify(defaultOptions, (key, value) => {
if (value === Infinity || value === -Infinity) {
containsInfinity = true;
return value === Infinity ? POS_INFINITY_PLACEHOLDER : NEG_INFINITY_PLACEHOLDER;
}
return value;
});
} catch (err) {
throw new Error(
`\`rule.meta.defaultOptions\` must be JSON-serializable: ${getErrorMessage(err)}`,
);
}

if (containsInfinity) {
const plainJson = JSON.stringify(defaultOptions);
if (
plainJson.includes(POS_INFINITY_PLACEHOLDER) ||
plainJson.includes(NEG_INFINITY_PLACEHOLDER)
) {
throw new Error(
`\`rule.meta.defaultOptions\` cannot contain the strings "${POS_INFINITY_PLACEHOLDER}" or "${NEG_INFINITY_PLACEHOLDER}"`,
);
}

// `JSON.parse` will convert these back to `Infinity` / `-Infinity`
json = json
.replaceAll(POS_INFINITY_PLACEHOLDER_STR, "1e+400")
.replaceAll(NEG_INFINITY_PLACEHOLDER_STR, "-1e+400");
}

return JSON.parse(json);
}

const POS_INFINITY_PLACEHOLDER = "$_$_$_POS_INFINITY_$_$_$";
const NEG_INFINITY_PLACEHOLDER = "$_$_$_NEG_INFINITY_$_$_$";
const POS_INFINITY_PLACEHOLDER_STR = JSON.stringify(POS_INFINITY_PLACEHOLDER);
const NEG_INFINITY_PLACEHOLDER_STR = JSON.stringify(NEG_INFINITY_PLACEHOLDER);

/**
* Validate and conform `before` / `after` hook function.
* @param hookFn - Hook function, or `null` / `undefined`
Expand Down
16 changes: 12 additions & 4 deletions apps/oxlint/test/fixtures/options/output.snap.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
| true,
| {
| "toBe": false,
| "notToBe": true
| "notToBe": true,
| "inf": "<Infinity>",
| "negInf": "<-Infinity>"
| },
| {
| "deep": [
Expand Down Expand Up @@ -58,7 +60,9 @@
| },
| "fromDefault": 3
| },
| "fromDefault": 1
| "fromDefault": 1,
| "inf": "<Infinity>",
| "negInf": "<-Infinity>"
| },
| 15,
| true,
Expand Down Expand Up @@ -149,7 +153,9 @@
| true,
| {
| "toBe": false,
| "notToBe": true
| "notToBe": true,
| "inf": "<Infinity>",
| "negInf": "<-Infinity>"
| },
| {
| "deep": [
Expand Down Expand Up @@ -190,7 +196,9 @@
| "nested": {
| "fromDefault": 3,
| "overrideDefault": 4
| }
| },
| "inf": "<Infinity>",
| "negInf": "<-Infinity>"
| },
| {
| "fromConfig": 22,
Expand Down
38 changes: 28 additions & 10 deletions apps/oxlint/test/fixtures/options/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const plugin: Plugin = {
create(context) {
context.report({
message:
`\noptions: ${JSON.stringify(context.options, null, 2)}\n` +
`\noptions: ${stringifyOptions(context.options)}\n` +
`isDeepFrozen: ${isDeepFrozen(context.options)}`,
node: SPAN,
});
Expand All @@ -34,7 +34,7 @@ const plugin: Plugin = {
create(context) {
context.report({
message:
`\noptions: ${JSON.stringify(context.options, null, 2)}\n` +
`\noptions: ${stringifyOptions(context.options)}\n` +
`isDeepFrozen: ${isDeepFrozen(context.options)}`,
node: SPAN,
});
Expand All @@ -48,15 +48,15 @@ const plugin: Plugin = {
"string",
123,
true,
{ toBe: false, notToBe: true },
{ toBe: false, notToBe: true, inf: Infinity, negInf: -Infinity },
{ deep: [{ deeper: { evenDeeper: [{ soDeep: { soSoDeep: true } }] } }] },
],
schema: false,
},
create(context) {
context.report({
message:
`\noptions: ${JSON.stringify(context.options, null, 2)}\n` +
`\noptions: ${stringifyOptions(context.options)}\n` +
`isDeepFrozen: ${isDeepFrozen(context.options)}`,
node: SPAN,
});
Expand All @@ -67,7 +67,13 @@ const plugin: Plugin = {
"merge-options": {
meta: {
defaultOptions: [
{ fromDefault: 1, overrideDefault: 2, nested: { fromDefault: 3, overrideDefault: 4 } },
{
fromDefault: 1,
overrideDefault: 2,
nested: { fromDefault: 3, overrideDefault: 4 },
inf: Infinity,
negInf: -Infinity,
},
{ fromDefault: 5 },
{ fromDefault: 6 },
7,
Expand All @@ -77,7 +83,7 @@ const plugin: Plugin = {
create(context) {
context.report({
message:
`\noptions: ${JSON.stringify(context.options, null, 2)}\n` +
`\noptions: ${stringifyOptions(context.options)}\n` +
`isDeepFrozen: ${isDeepFrozen(context.options)}`,
node: SPAN,
});
Expand All @@ -93,7 +99,7 @@ const plugin: Plugin = {
create(context) {
context.report({
message:
`\noptions: ${JSON.stringify(context.options, null, 2)}\n` +
`\noptions: ${stringifyOptions(context.options)}\n` +
`isDeepFrozen: ${isDeepFrozen(context.options)}`,
node: SPAN,
});
Expand Down Expand Up @@ -123,7 +129,7 @@ const plugin: Plugin = {
create(context) {
context.report({
message:
`\noptions: ${JSON.stringify(context.options, null, 2)}\n` +
`\noptions: ${stringifyOptions(context.options)}\n` +
`isDeepFrozen: ${isDeepFrozen(context.options)}`,
node: SPAN,
});
Expand Down Expand Up @@ -160,7 +166,7 @@ const plugin: Plugin = {
create(context) {
context.report({
message:
`\noptions: ${JSON.stringify(context.options, null, 2)}\n` +
`\noptions: ${stringifyOptions(context.options)}\n` +
`isDeepFrozen: ${isDeepFrozen(context.options)}`,
node: SPAN,
});
Expand Down Expand Up @@ -189,7 +195,7 @@ const plugin: Plugin = {
create(context) {
context.report({
message:
`\noptions: ${JSON.stringify(context.options, null, 2)}\n` +
`\noptions: ${stringifyOptions(context.options)}\n` +
`isDeepFrozen: ${isDeepFrozen(context.options)}`,
node: SPAN,
});
Expand All @@ -201,6 +207,18 @@ const plugin: Plugin = {

export default plugin;

function stringifyOptions(options: unknown): string {
return JSON.stringify(
options,
(key, value) => {
if (value === Infinity) return "<Infinity>";
if (value === -Infinity) return "<-Infinity>";
return value;
},
2,
);
}

function isDeepFrozen(value: unknown): boolean {
if (value === null || typeof value !== "object") return true;
if (!Object.isFrozen(value)) return false;
Expand Down
Loading