|
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"; |
9 | 2 |
|
10 | 3 | export type * as ESTree from "./generated/types.d.ts"; |
11 | 4 | export type { Context, LanguageOptions } from "./plugins/context.ts"; |
@@ -64,226 +57,3 @@ export type { |
64 | 57 | Visitor, |
65 | 58 | VisitorWithHooks, |
66 | 59 | } 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