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;