diff --git a/apps/oxlint/src-js/plugins/load.ts b/apps/oxlint/src-js/plugins/load.ts index 73f9a429c25a3..8351af794696c 100644 --- a/apps/oxlint/src-js/plugins/load.ts +++ b/apps/oxlint/src-js/plugins/load.ts @@ -1,4 +1,5 @@ import { createContext } from "./context.js"; +import { deepFreezeJsonArray } from "./json.js"; import { DEFAULT_OPTIONS } from "./options.js"; import { getErrorMessage } from "../utils/utils.js"; @@ -166,6 +167,7 @@ function registerPlugin(plugin: Plugin, packageName: string | null): PluginDetai if (!isArray(inputDefaultOptions)) { throw new TypeError("`rule.meta.defaultOptions` must be an array if provided"); } + deepFreezeJsonArray(inputDefaultOptions); defaultOptions = inputDefaultOptions; } diff --git a/apps/oxlint/test/fixtures/options/output.snap.md b/apps/oxlint/test/fixtures/options/output.snap.md index d31f8142310b5..1986f2f332bac 100644 --- a/apps/oxlint/test/fixtures/options/output.snap.md +++ b/apps/oxlint/test/fixtures/options/output.snap.md @@ -3,13 +3,40 @@ # stdout ``` - x options-plugin(default-options): options: ["string",123,true,{"toBe":false,"notToBe":true}] + x options-plugin(default-options): + | options: [ + | "string", + | 123, + | true, + | { + | "toBe": false, + | "notToBe": true + | }, + | { + | "deep": [ + | { + | "deeper": { + | "evenDeeper": [ + | { + | "soDeep": { + | "soSoDeep": true + | } + | } + | ] + | } + | } + | ] + | } + | ] + | isDeepFrozen: true ,-[files/index.js:1:1] 1 | debugger; : ^ `---- - x options-plugin(options): options: [] + x options-plugin(options): + | options: [] + | isDeepFrozen: true ,-[files/index.js:1:1] 1 | debugger; : ^ diff --git a/apps/oxlint/test/fixtures/options/plugin.ts b/apps/oxlint/test/fixtures/options/plugin.ts index c70aa3b72b7a9..f7688f0ac88f6 100644 --- a/apps/oxlint/test/fixtures/options/plugin.ts +++ b/apps/oxlint/test/fixtures/options/plugin.ts @@ -18,7 +18,9 @@ const plugin: Plugin = { options: { create(context) { context.report({ - message: `options: ${JSON.stringify(context.options)}`, + message: + `\noptions: ${JSON.stringify(context.options, null, 2)}\n` + + `isDeepFrozen: ${isDeepFrozen(context.options)}`, node: SPAN, }); return {}; @@ -26,11 +28,19 @@ const plugin: Plugin = { }, "default-options": { meta: { - defaultOptions: ["string", 123, true, { toBe: false, notToBe: true }], + defaultOptions: [ + "string", + 123, + true, + { toBe: false, notToBe: true }, + { deep: [{ deeper: { evenDeeper: [{ soDeep: { soSoDeep: true } }] } }] }, + ], }, create(context) { context.report({ - message: `options: ${JSON.stringify(context.options)}`, + message: + `\noptions: ${JSON.stringify(context.options, null, 2)}\n` + + `isDeepFrozen: ${isDeepFrozen(context.options)}`, node: SPAN, }); return {}; @@ -40,3 +50,10 @@ const plugin: Plugin = { }; export default plugin; + +function isDeepFrozen(value: unknown): boolean { + if (value === null || typeof value !== "object") return true; + if (!Object.isFrozen(value)) return false; + if (Array.isArray(value)) return value.every(isDeepFrozen); + return Object.values(value).every(isDeepFrozen); +}