Skip to content

Commit c4b65f8

Browse files
committed
refactor(linter/plugins): move definePlugin and defineRule into own file (#16169)
Pure refactor. Move `definePlugin` and `defineRule` into their own module. Rule tester will live in this directory too.
1 parent cbb108a commit c4b65f8

File tree

2 files changed

+236
-231
lines changed

2 files changed

+236
-231
lines changed

apps/oxlint/src-js/index.ts

Lines changed: 1 addition & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
import { debugAssertIsNonNull } from "./utils/asserts.js";
2-
3-
import type { Context, FileContext, LanguageOptions } from "./plugins/context.ts";
4-
import type { CreateOnceRule, Plugin, Rule } from "./plugins/load.ts";
5-
import type { Settings } from "./plugins/settings.ts";
6-
import type { SourceCode } from "./plugins/source_code.ts";
7-
import type { BeforeHook, Visitor, VisitorWithHooks } from "./plugins/types.ts";
8-
import type { SetNullable } from "./utils/types.ts";
1+
export { definePlugin, defineRule } from "./package/define.js";
92

103
export type * as ESTree from "./generated/types.d.ts";
114
export type { Context, LanguageOptions } from "./plugins/context.ts";
@@ -64,226 +57,3 @@ export type {
6457
Visitor,
6558
VisitorWithHooks,
6659
} from "./plugins/types.ts";
67-
68-
const {
69-
defineProperty,
70-
getPrototypeOf,
71-
hasOwn,
72-
setPrototypeOf,
73-
create: ObjectCreate,
74-
freeze,
75-
assign: ObjectAssign,
76-
} = Object;
77-
78-
// Empty visitor object, returned by `create` when `before` hook returns `false`.
79-
const EMPTY_VISITOR: Visitor = {};
80-
81-
/**
82-
* Define a plugin.
83-
*
84-
* If any of the plugin's rules use the Oxlint alternative `createOnce` API,
85-
* add ESLint-compatible `create` methods to those rules, which delegate to `createOnce`.
86-
* This makes the plugin compatible with ESLint.
87-
*
88-
* The `plugin` object passed in is mutated in-place.
89-
*
90-
* @param plugin - Plugin to define
91-
* @returns Plugin with all rules having `create` method
92-
* @throws {Error} If `plugin` is not an object, or `plugin.rules` is not an object
93-
*/
94-
export function definePlugin(plugin: Plugin): Plugin {
95-
// Validate type of `plugin`
96-
if (plugin === null || typeof plugin !== "object") throw new Error("Plugin must be an object");
97-
98-
const { rules } = plugin;
99-
if (rules === null || typeof rules !== "object")
100-
throw new Error("Plugin must have an object as `rules` property");
101-
102-
// Make each rule in the plugin ESLint-compatible by calling `defineRule` on it
103-
for (const ruleName in rules) {
104-
if (hasOwn(rules, ruleName)) {
105-
rules[ruleName] = defineRule(rules[ruleName]);
106-
}
107-
}
108-
109-
return plugin;
110-
}
111-
112-
/**
113-
* Define a rule.
114-
*
115-
* If `rule` uses the Oxlint alternative `createOnce` API, add an ESLint-compatible
116-
* `create` method to the rule, which delegates to `createOnce`.
117-
* This makes the rule compatible with ESLint.
118-
*
119-
* The `rule` object passed in is mutated in-place.
120-
*
121-
* @param rule - Rule to define
122-
* @returns Rule with `create` method
123-
* @throws {Error} If `rule` is not an object
124-
*/
125-
export function defineRule(rule: Rule): Rule {
126-
// Validate type of `rule`
127-
if (rule === null || typeof rule !== "object") throw new Error("Rule must be an object");
128-
129-
// If rule already has `create` method, return it as is
130-
if ("create" in rule) return rule;
131-
132-
// Add `create` function to `rule`
133-
let context: Context | null = null,
134-
visitor: Visitor | undefined,
135-
beforeHook: BeforeHook | null;
136-
137-
rule.create = (eslintContext) => {
138-
// Lazily call `createOnce` on first invocation of `create`
139-
if (context === null) {
140-
({ context, visitor, beforeHook } = createContextAndVisitor(rule));
141-
}
142-
debugAssertIsNonNull(visitor);
143-
144-
// Copy properties from ESLint's context object to `context`.
145-
// ESLint's context object is an object of form `{ id, options, report }`, with all other properties
146-
// and methods on another object which is its prototype.
147-
defineProperty(context, "id", { value: eslintContext.id });
148-
defineProperty(context, "options", { value: eslintContext.options });
149-
defineProperty(context, "report", { value: eslintContext.report });
150-
setPrototypeOf(context, getPrototypeOf(eslintContext));
151-
152-
// If `before` hook returns `false`, skip traversal by returning an empty object as visitor
153-
if (beforeHook !== null) {
154-
const shouldRun = beforeHook();
155-
if (shouldRun === false) return EMPTY_VISITOR;
156-
}
157-
158-
// Return same visitor each time
159-
return visitor;
160-
};
161-
162-
return rule;
163-
}
164-
165-
// Cached current working directory
166-
let cwd: string | null = null;
167-
168-
// File context object. Used as prototype for `Context` objects for each rule during `createOnce` call.
169-
// When running the rules, ESLint's `context` object is switching in as prototype for `Context` objects.
170-
//
171-
// Only `cwd` property and `extends` method are available in `createOnce`, so only those are implemented here.
172-
// All other getters/methods throw, same as they do in main implementation.
173-
//
174-
// See `FILE_CONTEXT` in `plugins/context.ts` for details of all the getters/methods.
175-
const FILE_CONTEXT: FileContext = freeze({
176-
get filename(): string {
177-
throw new Error("Cannot access `context.filename` in `createOnce`");
178-
},
179-
180-
getFilename(): string {
181-
throw new Error("Cannot call `context.getFilename` in `createOnce`");
182-
},
183-
184-
get physicalFilename(): string {
185-
throw new Error("Cannot access `context.physicalFilename` in `createOnce`");
186-
},
187-
188-
getPhysicalFilename(): string {
189-
throw new Error("Cannot call `context.getPhysicalFilename` in `createOnce`");
190-
},
191-
192-
get cwd(): string {
193-
// Note: We can allow accessing `cwd` in `createOnce`, as it's global
194-
if (cwd === null) cwd = process.cwd();
195-
return cwd;
196-
},
197-
198-
getCwd(): string {
199-
if (cwd === null) cwd = process.cwd();
200-
return cwd;
201-
},
202-
203-
get sourceCode(): SourceCode {
204-
throw new Error("Cannot access `context.sourceCode` in `createOnce`");
205-
},
206-
207-
getSourceCode(): SourceCode {
208-
throw new Error("Cannot call `context.getSourceCode` in `createOnce`");
209-
},
210-
211-
get languageOptions(): LanguageOptions {
212-
throw new Error("Cannot access `context.languageOptions` in `createOnce`");
213-
},
214-
215-
get settings(): Readonly<Settings> {
216-
throw new Error("Cannot access `context.settings` in `createOnce`");
217-
},
218-
219-
extend(this: FileContext, extension: Record<string | number | symbol, unknown>): FileContext {
220-
// Note: We can allow calling `extend` in `createOnce`, as it involves no file-specific state
221-
return freeze(ObjectAssign(ObjectCreate(this), extension));
222-
},
223-
224-
get parserOptions(): Record<string, unknown> {
225-
throw new Error("Cannot access `context.parserOptions` in `createOnce`");
226-
},
227-
228-
get parserPath(): string {
229-
throw new Error("Cannot access `context.parserPath` in `createOnce`");
230-
},
231-
});
232-
233-
/**
234-
* Call `createOnce` method of rule, and return `Context`, `Visitor`, and `beforeHook` (if any).
235-
*
236-
* @param rule - Rule with `createOnce` method
237-
* @returns Object with `context`, `visitor`, and `beforeHook` properties
238-
*/
239-
function createContextAndVisitor(rule: CreateOnceRule): {
240-
context: Context;
241-
visitor: Visitor;
242-
beforeHook: BeforeHook | null;
243-
} {
244-
// Validate type of `createOnce`
245-
const { createOnce } = rule;
246-
if (createOnce == null)
247-
throw new Error("Rules must define either a `create` or `createOnce` method");
248-
if (typeof createOnce !== "function")
249-
throw new Error("Rule `createOnce` property must be a function");
250-
251-
// Call `createOnce` with empty context object.
252-
// Really, accessing `options` or calling `report` should throw, because they're illegal in `createOnce`.
253-
// But any such bugs should have been caught when testing the rule in Oxlint, so should be OK to take this shortcut.
254-
// `FILE_CONTEXT` prototype provides `cwd` property and `extends` method, which are available in `createOnce`.
255-
const context: Context = ObjectCreate(FILE_CONTEXT, {
256-
id: { value: "", enumerable: true, configurable: true },
257-
options: { value: null, enumerable: true, configurable: true },
258-
report: { value: null, enumerable: true, configurable: true },
259-
});
260-
261-
let {
262-
before: beforeHook,
263-
after: afterHook,
264-
...visitor
265-
} = createOnce.call(rule, context) as SetNullable<VisitorWithHooks, "before" | "after">;
266-
267-
if (beforeHook === undefined) {
268-
beforeHook = null;
269-
} else if (beforeHook !== null && typeof beforeHook !== "function") {
270-
throw new Error("`before` property of visitor must be a function if defined");
271-
}
272-
273-
// Add `after` hook to `Program:exit` visit fn
274-
if (afterHook != null) {
275-
if (typeof afterHook !== "function")
276-
throw new Error("`after` property of visitor must be a function if defined");
277-
278-
const programExit = visitor["Program:exit"];
279-
visitor["Program:exit"] =
280-
programExit == null
281-
? (_node) => afterHook()
282-
: (node) => {
283-
programExit(node);
284-
afterHook();
285-
};
286-
}
287-
288-
return { context, visitor, beforeHook };
289-
}

0 commit comments

Comments
 (0)