From cbb108aa1c20167921867a70c5b48f035ad9927e Mon Sep 17 00:00:00 2001 From: overlookmotel <557937+overlookmotel@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:27:26 +0000 Subject: [PATCH] feat(linter/plugins): support default options (#16170) A step towards #15630. Where a rule provides default options, pass them to `create` function as `context.options`. We don't yet have full options support (#14825), so this is incomplete. Once user can provide options for rules in config, those options need to be merged with default options. Initially, we should probably do that merging on JS side (borrowing ESLint's code), and then later on port it to Rust. --- apps/oxlint/src-js/plugins/lint.ts | 3 +- apps/oxlint/src-js/plugins/load.ts | 18 +++++++- apps/oxlint/src-js/plugins/options.ts | 2 +- .../test/fixtures/options/.oxlintrc.json | 10 +++++ .../test/fixtures/options/files/index.js | 1 + .../test/fixtures/options/output.snap.md | 26 ++++++++++++ apps/oxlint/test/fixtures/options/plugin.ts | 42 +++++++++++++++++++ 7 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 apps/oxlint/test/fixtures/options/.oxlintrc.json create mode 100644 apps/oxlint/test/fixtures/options/files/index.js create mode 100644 apps/oxlint/test/fixtures/options/output.snap.md create mode 100644 apps/oxlint/test/fixtures/options/plugin.ts diff --git a/apps/oxlint/src-js/plugins/lint.ts b/apps/oxlint/src-js/plugins/lint.ts index 1e87a5ea2d537..42666c1cd051b 100644 --- a/apps/oxlint/src-js/plugins/lint.ts +++ b/apps/oxlint/src-js/plugins/lint.ts @@ -156,7 +156,8 @@ function lintFileImpl( // Set `options` for rule const optionsId = optionsIds[i]; debugAssert(optionsId < allOptions.length, "Options ID out of bounds"); - ruleDetails.options = allOptions[optionsId]; + ruleDetails.options = + optionsId === DEFAULT_OPTIONS_ID ? ruleDetails.defaultOptions : allOptions[optionsId]; let { visitor } = ruleDetails; if (visitor === null) { diff --git a/apps/oxlint/src-js/plugins/load.ts b/apps/oxlint/src-js/plugins/load.ts index 3bd0c602d23b8..58f5ff4d363d0 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 { DEFAULT_OPTIONS } from "./options.js"; import { getErrorMessage } from "../utils/utils.js"; import type { Writable } from "type-fest"; @@ -8,7 +9,8 @@ import type { RuleMeta } from "./rule_meta.ts"; import type { AfterHook, BeforeHook, Visitor, VisitorWithHooks } from "./types.ts"; import type { SetNullable } from "../utils/types.ts"; -const ObjectKeys = Object.keys; +const ObjectKeys = Object.keys, + { isArray } = Array; /** * Linter plugin, comprising multiple rules @@ -55,6 +57,7 @@ interface RuleDetailsBase { readonly context: Readonly; readonly isFixable: boolean; readonly messages: Readonly> | null; + readonly defaultOptions: Readonly; // Updated for each file ruleIndex: number; options: Readonly | null; // Initially `null`, set to options object before linting a file @@ -147,7 +150,8 @@ async function loadPluginImpl(url: string, packageName: string | null): Promise< // Validate `rule.meta` and convert to vars with standardized shape let isFixable = false, - messages: Record | null = null; + messages: Record | null = null, + defaultOptions: Readonly = DEFAULT_OPTIONS; const ruleMeta = rule.meta; if (ruleMeta != null) { if (typeof ruleMeta !== "object") throw new TypeError("Invalid `rule.meta`"); @@ -159,6 +163,15 @@ async function loadPluginImpl(url: string, packageName: string | null): Promise< isFixable = true; } + const inputDefaultOptions = ruleMeta.defaultOptions; + if (inputDefaultOptions != null) { + // TODO: Validate is JSON-serializable, and validate against provided options schema + if (!isArray(inputDefaultOptions)) { + throw new TypeError("`rule.meta.defaultOptions` must be an array if provided"); + } + defaultOptions = inputDefaultOptions; + } + // Extract messages for messageId support const inputMessages = ruleMeta.messages; if (inputMessages != null) { @@ -175,6 +188,7 @@ async function loadPluginImpl(url: string, packageName: string | null): Promise< context: null!, // Filled in below isFixable, messages, + defaultOptions, ruleIndex: 0, options: null, visitor: null, diff --git a/apps/oxlint/src-js/plugins/options.ts b/apps/oxlint/src-js/plugins/options.ts index f396662b81f2d..33df678e50927 100644 --- a/apps/oxlint/src-js/plugins/options.ts +++ b/apps/oxlint/src-js/plugins/options.ts @@ -10,7 +10,7 @@ import type { JsonValue } from "./json.ts"; export type Options = JsonValue[]; // Default rule options -const DEFAULT_OPTIONS: Readonly = Object.freeze([]); +export const DEFAULT_OPTIONS: Readonly = Object.freeze([]); // All rule options export const allOptions: Readonly[] = [DEFAULT_OPTIONS]; diff --git a/apps/oxlint/test/fixtures/options/.oxlintrc.json b/apps/oxlint/test/fixtures/options/.oxlintrc.json new file mode 100644 index 0000000000000..dfbce6c0cf2b1 --- /dev/null +++ b/apps/oxlint/test/fixtures/options/.oxlintrc.json @@ -0,0 +1,10 @@ +{ + "jsPlugins": ["./plugin.ts"], + "categories": { + "correctness": "off" + }, + "rules": { + "options-plugin/options": "error", + "options-plugin/default-options": "error" + } +} diff --git a/apps/oxlint/test/fixtures/options/files/index.js b/apps/oxlint/test/fixtures/options/files/index.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/apps/oxlint/test/fixtures/options/files/index.js @@ -0,0 +1 @@ +debugger; diff --git a/apps/oxlint/test/fixtures/options/output.snap.md b/apps/oxlint/test/fixtures/options/output.snap.md new file mode 100644 index 0000000000000..d31f8142310b5 --- /dev/null +++ b/apps/oxlint/test/fixtures/options/output.snap.md @@ -0,0 +1,26 @@ +# Exit code +1 + +# stdout +``` + x options-plugin(default-options): options: ["string",123,true,{"toBe":false,"notToBe":true}] + ,-[files/index.js:1:1] + 1 | debugger; + : ^ + `---- + + x options-plugin(options): options: [] + ,-[files/index.js:1:1] + 1 | debugger; + : ^ + `---- + +Found 0 warnings and 2 errors. +Finished in Xms on 1 file using X threads. +``` + +# stderr +``` +WARNING: JS plugins are experimental and not subject to semver. +Breaking changes are possible while JS plugins support is under development. +``` diff --git a/apps/oxlint/test/fixtures/options/plugin.ts b/apps/oxlint/test/fixtures/options/plugin.ts new file mode 100644 index 0000000000000..c70aa3b72b7a9 --- /dev/null +++ b/apps/oxlint/test/fixtures/options/plugin.ts @@ -0,0 +1,42 @@ +import type { Node, Plugin } from "#oxlint"; + +const SPAN: Node = { + start: 0, + end: 0, + range: [0, 0], + loc: { + start: { line: 0, column: 0 }, + end: { line: 0, column: 0 }, + }, +}; + +const plugin: Plugin = { + meta: { + name: "options-plugin", + }, + rules: { + options: { + create(context) { + context.report({ + message: `options: ${JSON.stringify(context.options)}`, + node: SPAN, + }); + return {}; + }, + }, + "default-options": { + meta: { + defaultOptions: ["string", 123, true, { toBe: false, notToBe: true }], + }, + create(context) { + context.report({ + message: `options: ${JSON.stringify(context.options)}`, + node: SPAN, + }); + return {}; + }, + }, + }, +}; + +export default plugin;