Skip to content

Commit cbb108a

Browse files
committed
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.
1 parent 9c10d86 commit cbb108a

File tree

7 files changed

+98
-4
lines changed

7 files changed

+98
-4
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ function lintFileImpl(
156156
// Set `options` for rule
157157
const optionsId = optionsIds[i];
158158
debugAssert(optionsId < allOptions.length, "Options ID out of bounds");
159-
ruleDetails.options = allOptions[optionsId];
159+
ruleDetails.options =
160+
optionsId === DEFAULT_OPTIONS_ID ? ruleDetails.defaultOptions : allOptions[optionsId];
160161

161162
let { visitor } = ruleDetails;
162163
if (visitor === null) {

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createContext } from "./context.js";
2+
import { DEFAULT_OPTIONS } from "./options.js";
23
import { getErrorMessage } from "../utils/utils.js";
34

45
import type { Writable } from "type-fest";
@@ -8,7 +9,8 @@ import type { RuleMeta } from "./rule_meta.ts";
89
import type { AfterHook, BeforeHook, Visitor, VisitorWithHooks } from "./types.ts";
910
import type { SetNullable } from "../utils/types.ts";
1011

11-
const ObjectKeys = Object.keys;
12+
const ObjectKeys = Object.keys,
13+
{ isArray } = Array;
1214

1315
/**
1416
* Linter plugin, comprising multiple rules
@@ -55,6 +57,7 @@ interface RuleDetailsBase {
5557
readonly context: Readonly<Context>;
5658
readonly isFixable: boolean;
5759
readonly messages: Readonly<Record<string, string>> | null;
60+
readonly defaultOptions: Readonly<Options>;
5861
// Updated for each file
5962
ruleIndex: number;
6063
options: Readonly<Options> | null; // Initially `null`, set to options object before linting a file
@@ -147,7 +150,8 @@ async function loadPluginImpl(url: string, packageName: string | null): Promise<
147150

148151
// Validate `rule.meta` and convert to vars with standardized shape
149152
let isFixable = false,
150-
messages: Record<string, string> | null = null;
153+
messages: Record<string, string> | null = null,
154+
defaultOptions: Readonly<Options> = DEFAULT_OPTIONS;
151155
const ruleMeta = rule.meta;
152156
if (ruleMeta != null) {
153157
if (typeof ruleMeta !== "object") throw new TypeError("Invalid `rule.meta`");
@@ -159,6 +163,15 @@ async function loadPluginImpl(url: string, packageName: string | null): Promise<
159163
isFixable = true;
160164
}
161165

166+
const inputDefaultOptions = ruleMeta.defaultOptions;
167+
if (inputDefaultOptions != null) {
168+
// TODO: Validate is JSON-serializable, and validate against provided options schema
169+
if (!isArray(inputDefaultOptions)) {
170+
throw new TypeError("`rule.meta.defaultOptions` must be an array if provided");
171+
}
172+
defaultOptions = inputDefaultOptions;
173+
}
174+
162175
// Extract messages for messageId support
163176
const inputMessages = ruleMeta.messages;
164177
if (inputMessages != null) {
@@ -175,6 +188,7 @@ async function loadPluginImpl(url: string, packageName: string | null): Promise<
175188
context: null!, // Filled in below
176189
isFixable,
177190
messages,
191+
defaultOptions,
178192
ruleIndex: 0,
179193
options: null,
180194
visitor: null,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { JsonValue } from "./json.ts";
1010
export type Options = JsonValue[];
1111

1212
// Default rule options
13-
const DEFAULT_OPTIONS: Readonly<Options> = Object.freeze([]);
13+
export const DEFAULT_OPTIONS: Readonly<Options> = Object.freeze([]);
1414

1515
// All rule options
1616
export const allOptions: Readonly<Options>[] = [DEFAULT_OPTIONS];
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"jsPlugins": ["./plugin.ts"],
3+
"categories": {
4+
"correctness": "off"
5+
},
6+
"rules": {
7+
"options-plugin/options": "error",
8+
"options-plugin/default-options": "error"
9+
}
10+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
debugger;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Exit code
2+
1
3+
4+
# stdout
5+
```
6+
x options-plugin(default-options): options: ["string",123,true,{"toBe":false,"notToBe":true}]
7+
,-[files/index.js:1:1]
8+
1 | debugger;
9+
: ^
10+
`----
11+
12+
x options-plugin(options): options: []
13+
,-[files/index.js:1:1]
14+
1 | debugger;
15+
: ^
16+
`----
17+
18+
Found 0 warnings and 2 errors.
19+
Finished in Xms on 1 file using X threads.
20+
```
21+
22+
# stderr
23+
```
24+
WARNING: JS plugins are experimental and not subject to semver.
25+
Breaking changes are possible while JS plugins support is under development.
26+
```
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Node, Plugin } from "#oxlint";
2+
3+
const SPAN: Node = {
4+
start: 0,
5+
end: 0,
6+
range: [0, 0],
7+
loc: {
8+
start: { line: 0, column: 0 },
9+
end: { line: 0, column: 0 },
10+
},
11+
};
12+
13+
const plugin: Plugin = {
14+
meta: {
15+
name: "options-plugin",
16+
},
17+
rules: {
18+
options: {
19+
create(context) {
20+
context.report({
21+
message: `options: ${JSON.stringify(context.options)}`,
22+
node: SPAN,
23+
});
24+
return {};
25+
},
26+
},
27+
"default-options": {
28+
meta: {
29+
defaultOptions: ["string", 123, true, { toBe: false, notToBe: true }],
30+
},
31+
create(context) {
32+
context.report({
33+
message: `options: ${JSON.stringify(context.options)}`,
34+
node: SPAN,
35+
});
36+
return {};
37+
},
38+
},
39+
},
40+
};
41+
42+
export default plugin;

0 commit comments

Comments
 (0)