Skip to content

Commit a0a8490

Browse files
committed
feat(linter/plugins): add createOnce API
1 parent a089a79 commit a0a8490

File tree

10 files changed

+377
-21
lines changed

10 files changed

+377
-21
lines changed

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,34 +73,44 @@ export class Context {
7373

7474
// Getter for full rule name, in form `<plugin>/<rule>`
7575
get id() {
76-
return this.#internal.id;
76+
const internal = this.#internal;
77+
if (internal.filePath === '') throw new Error('Cannot access `context.id` in `createOnce`');
78+
return internal.id;
7779
}
7880

7981
// Getter for absolute path of file being linted.
8082
get filename() {
81-
return this.#internal.filePath;
83+
const { filePath } = this.#internal;
84+
if (filePath === '') throw new Error('Cannot access `context.filename` in `createOnce`');
85+
return filePath;
8286
}
8387

8488
// Getter for absolute path of file being linted.
8589
// TODO: Unclear how this differs from `filename`.
8690
get physicalFilename() {
87-
return this.#internal.filePath;
91+
const { filePath } = this.#internal;
92+
if (filePath === '') throw new Error('Cannot access `context.physicalFilename` in `createOnce`');
93+
return filePath;
8894
}
8995

9096
// Getter for options for file being linted.
9197
get options() {
92-
return this.#internal.options;
98+
const internal = this.#internal;
99+
if (internal.filePath === '') throw new Error('Cannot access `context.options` in `createOnce`');
100+
return internal.options;
93101
}
94102

95103
/**
96104
* Report error.
97105
* @param diagnostic - Diagnostic object
98106
*/
99107
report(diagnostic: Diagnostic): void {
108+
const internal = this.#internal;
109+
if (internal.filePath === '') throw new Error('Cannot report errors in `createOnce`');
100110
diagnostics.push({
101111
message: diagnostic.message,
102112
loc: { start: diagnostic.node.start, end: diagnostic.node.end },
103-
ruleIndex: this.#internal.ruleIndex,
113+
ruleIndex: internal.ruleIndex,
104114
});
105115
}
106116

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

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { deserializeProgramOnly } from '../../dist/generated/deserialize/ts.mjs'
2222
// @ts-expect-error we need to generate `.d.ts` file for this module
2323
import { walkProgram } from '../../dist/generated/visit/walk.mjs';
2424

25+
import type { AfterHook } from './types.ts';
26+
2527
// Buffer with typed array views of itself stored as properties
2628
interface BufferWithArrays extends Uint8Array {
2729
uint32: Uint32Array;
@@ -38,6 +40,9 @@ const buffers: (BufferWithArrays | null)[] = [];
3840
// Text decoder, for decoding source text from buffer
3941
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true });
4042

43+
// Array of `after` hooks to run after traversal. This array reused for every file.
44+
const afterHooks: AfterHook[] = [];
45+
4146
// Run rules on a file.
4247
export function lintFile(filePath: string, bufferId: number, buffer: Uint8Array | null, ruleIds: number[]): string {
4348
// If new buffer, add it to `buffers` array. Otherwise, get existing buffer from array.
@@ -69,13 +74,32 @@ export function lintFile(filePath: string, bufferId: number, buffer: Uint8Array
6974

7075
// Get visitors for this file from all rules
7176
initCompiledVisitor();
77+
7278
for (let i = 0; i < ruleIds.length; i++) {
73-
const ruleId = ruleIds[i];
74-
const { rule, context } = registeredRules[ruleId];
79+
const ruleId = ruleIds[i],
80+
ruleAndContext = registeredRules[ruleId];
81+
const { rule, context } = ruleAndContext;
7582
setupContextForFile(context, i, filePath);
76-
const visitor = rule.create(context);
83+
84+
let { visitor } = ruleAndContext;
85+
if (visitor === null) {
86+
// Rule defined with `create` method
87+
visitor = rule.create(context);
88+
} else {
89+
// Rule defined with `createOnce` method
90+
const { beforeHook, afterHook } = ruleAndContext;
91+
if (beforeHook !== null) {
92+
// If `before` hook returns `false`, skip this rule
93+
const shouldRun = beforeHook();
94+
if (shouldRun === false) continue;
95+
}
96+
// Note: If `before` hook returned `false`, `after` hook is not called
97+
if (afterHook !== null) afterHooks.push(afterHook);
98+
}
99+
77100
addVisitorToCompiled(visitor);
78101
}
102+
79103
const needsVisit = finalizeCompiledVisitor();
80104

81105
// Visit AST.
@@ -110,6 +134,15 @@ export function lintFile(filePath: string, bufferId: number, buffer: Uint8Array
110134
*/
111135
}
112136

137+
// Run `after` hooks
138+
if (afterHooks.length !== 0) {
139+
for (const afterHook of afterHooks) {
140+
afterHook();
141+
}
142+
// Reset array, ready for next file
143+
afterHooks.length = 0;
144+
}
145+
113146
// Send diagnostics back to Rust
114147
const ret = JSON.stringify(diagnostics);
115148
diagnostics.length = 0;

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

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Context } from './context.js';
22
import { getErrorMessage } from './utils.js';
33

4-
import type { Visitor } from './types.ts';
4+
import type { AfterHook, BeforeHook, Visitor, VisitorWithHooks } from './types.ts';
55

66
// Linter plugin, comprising multiple rules
77
interface Plugin {
@@ -13,20 +13,46 @@ interface Plugin {
1313
};
1414
}
1515

16-
// Linter rule
17-
interface Rule {
16+
// Linter rule.
17+
// `Rule` can have either `create` method, or `createOnce` method.
18+
// If `createOnce` method is present, `create` is ignored.
19+
type Rule = CreateRule | CreateOnceRule;
20+
21+
interface CreateRule {
1822
create: (context: Context) => Visitor;
1923
}
2024

25+
interface CreateOnceRule {
26+
create?: (context: Context) => Visitor;
27+
createOnce: (context: Context) => VisitorWithHooks;
28+
}
29+
30+
// Linter rule and context object.
31+
// If `rule` has a `createOnce` method, the visitor it returns is stored in `visitor`.
32+
type RuleAndContext = CreateRuleAndContext | CreateOnceRuleAndContext;
33+
34+
interface CreateRuleAndContext {
35+
rule: CreateRule;
36+
context: Context;
37+
visitor: null;
38+
beforeHook: null;
39+
afterHook: null;
40+
}
41+
42+
interface CreateOnceRuleAndContext {
43+
rule: CreateOnceRule;
44+
context: Context;
45+
visitor: Visitor;
46+
beforeHook: BeforeHook | null;
47+
afterHook: AfterHook | null;
48+
}
49+
2150
// Absolute paths of plugins which have been loaded
2251
const registeredPluginPaths = new Set<string>();
2352

2453
// Rule objects for loaded rules.
2554
// Indexed by `ruleId`, which is passed to `lintFile`.
26-
export const registeredRules: {
27-
rule: Rule;
28-
context: Context;
29-
}[] = [];
55+
export const registeredRules: RuleAndContext[] = [];
3056

3157
/**
3258
* Load a plugin.
@@ -64,11 +90,21 @@ async function loadPluginImpl(path: string): Promise<string> {
6490
const ruleNamesLen = ruleNames.length;
6591

6692
for (let i = 0; i < ruleNamesLen; i++) {
67-
const ruleName = ruleNames[i];
68-
registeredRules.push({
69-
rule: rules[ruleName],
70-
context: new Context(`${pluginName}/${ruleName}`),
71-
});
93+
const ruleName = ruleNames[i],
94+
rule = rules[ruleName];
95+
96+
const context = new Context(`${pluginName}/${ruleName}`);
97+
98+
let ruleAndContext;
99+
if ('createOnce' in rule) {
100+
// TODO: Compile visitor object to array here, instead of repeating compilation on each file
101+
const { before: beforeHook, after: afterHook, ...visitor } = rule.createOnce(context);
102+
ruleAndContext = { rule, context, visitor, beforeHook: beforeHook || null, afterHook: afterHook || null };
103+
} else {
104+
ruleAndContext = { rule, context, visitor: null, beforeHook: null, afterHook: null };
105+
}
106+
107+
registeredRules.push(ruleAndContext);
72108
}
73109

74110
return JSON.stringify({ Success: { name: pluginName, offset, ruleNames } });

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,21 @@ export interface Visitor {
66
}
77
*/
88

9-
export type { VisitorObject as Visitor } from '../../dist/generated/visit/visitor.d.ts';
9+
import type { VisitorObject as Visitor } from '../../dist/generated/visit/visitor.d.ts';
10+
export type { Visitor };
11+
12+
// Hook function that runs before traversal.
13+
// If returns `false`, traversal is skipped for the rule.
14+
export type BeforeHook = () => boolean | undefined;
15+
16+
// Hook function that runs after traversal.
17+
export type AfterHook = () => void;
18+
19+
// Visitor object returned by a `Rule`'s `createOnce` function.
20+
export interface VisitorWithHooks extends Visitor {
21+
before?: BeforeHook;
22+
after?: AfterHook;
23+
}
1024

1125
// Visit function for a specific AST node type.
1226
export type VisitFn = (node: Node) => void;

0 commit comments

Comments
 (0)