@oxlint/plugins 1.57.0 → 1.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.cjs +221 -23
  2. package/index.js +221 -23
  3. package/package.json +1 -1
package/index.cjs CHANGED
@@ -42,26 +42,220 @@ function eslintCompatPlugin(plugin) {
42
42
  if (typeof plugin != "object" || !plugin) throw Error("Plugin must be an object");
43
43
  let { rules } = plugin;
44
44
  if (typeof rules != "object" || !rules) throw Error("Plugin must have an object as `rules` property");
45
- for (let ruleName in rules) Object.hasOwn(rules, ruleName) && convertRule(rules[ruleName]);
45
+ let afterHooksState = new AfterHooksState();
46
+ for (let ruleName in rules) Object.hasOwn(rules, ruleName) && convertRule(rules[ruleName], afterHooksState);
46
47
  return plugin;
47
48
  }
48
49
  /**
50
+ * Class containing state for tracking if any `after` hooks for a plugin's rules need to be called.
51
+ *
52
+ * # Aims
53
+ *
54
+ * Aims are:
55
+ * 1. `after` hook of each rule runs after all other AST visit functions, and CFG event handlers.
56
+ * 2. `after` hooks for *all* a plugin's rules run after *all* that plugin's rules have completed visiting AST.
57
+ * 3. `after` hooks for *all* a plugin's rules run before *any* of plugin's rules begin linting another file.
58
+ * 4. In the case of an error during AST traversal, `after` hooks are always still run.
59
+ *
60
+ * The above exactly matches the behavior when running a `createOnce` rule in Oxlint.
61
+ *
62
+ * # Why this is important
63
+ *
64
+ * All the complication comes from ensuring `after` hooks run even after an error during AST traversal.
65
+ *
66
+ * In ESLint CLI, an error will crash the process, so it doesn't particularly matter if `after` hooks run or not,
67
+ * but language servers will typically swallow errors, and keep the process running.
68
+ *
69
+ * Rules using `before` and `after` hooks will often rely on both hooks running in a predictable order,
70
+ * to maintain some internal state. For example, they may use `before` and `after` hooks to maintain a per-file
71
+ * cache of data which is shared between rules. The cache use case is why rule (2) above is important.
72
+ *
73
+ * Below is an example of using `before` and `after` hooks to maintain a per-file cache, shared between rules.
74
+ * It relies on all `before` hooks running before any rule starts visiting the AST,
75
+ * and all `after` hooks running after all rules have finished visiting the AST.
76
+ *
77
+ * ```ts
78
+ * let cache: Data | null = null;
79
+ *
80
+ * let numRunningRules = 0;
81
+ *
82
+ * const setupCache = (context) => {
83
+ * if (cache === null) cache = new Data(context);
84
+ * numRunningRules++;
85
+ * };
86
+ *
87
+ * const teardownCache = () => {
88
+ * numRunningRules--;
89
+ * if (numRunningRules === 0) cache = null;
90
+ * };
91
+ *
92
+ * const rule1 = {
93
+ * createOnce(context) {
94
+ * return {
95
+ * before() {
96
+ * setupCache(context);
97
+ * },
98
+ * Identifier(node) {
99
+ * // Use `cache`
100
+ * },
101
+ * after: teardownCache,
102
+ * };
103
+ * },
104
+ * };
105
+ *
106
+ * const rule2 = {
107
+ * // Same as above
108
+ * };
109
+ *
110
+ * const rule3 = {
111
+ * // Same as above
112
+ * };
113
+ * ```
114
+ *
115
+ * If `after` hooks did not always run, the next lint run could get stale state, and malfunction.
116
+ * If `after` hooks ran in the wrong order (e.g. after some `before` hooks for next file),
117
+ * `numRunningRules` would never get to 0, and cache would never be cleared.
118
+ *
119
+ * Note that because all rules run together in a single AST traversal, if a rule from plugin X throws an error,
120
+ * it can disrupt rules from plugin Y. This would make it hard to debug.
121
+ *
122
+ * # Mechanism
123
+ *
124
+ * ## Initialization
125
+ *
126
+ * Rules with an `after` hook register themselves by:
127
+ *
128
+ * 1. Calling `registerResetFunction` to register a function to run `after` hook and clean up internal state.
129
+ * This call adds the reset fn to `resetFunctions`, and adds `AFTER_HOOK_INACTIVE` to `pendingStates`.
130
+ * 2. Adding an `onCodePathEnd` CFG event handler to the visitor which calls `ruleFinished` at end of AST traversal.
131
+ *
132
+ * ## Per-file setup
133
+ *
134
+ * Before linting a file, `create` will call `setupAfterHook` which is created by `createContextAndVisitor`.
135
+ * This registers that the `after` hook for the rule needs to run, by setting `pendingStates[ruleIndex]`
136
+ * to `AFTER_HOOK_PENDING`, and incrementing `pendingCount`.
137
+ *
138
+ * If a cleanup microtask has not been scheduled yet, one is scheduled now (see reason below).
139
+ *
140
+ * ## Normal operation
141
+ *
142
+ * AST traversal for each rule ends with `ruleFinished` hook being called from `onCodePathEnd` CFG event handler.
143
+ * It increments `lintFinishedCount`. If `lintFinishedCount` equals `pendingCount`, all rules have finished linting
144
+ * the file, and `reset` is called, which calls all the pending `after` hooks.
145
+ *
146
+ * ## Error handling
147
+ *
148
+ * If an error is thrown during AST traversal, we ensure that `after` hooks are still run by 2 mechanisms:
149
+ *
150
+ * ### 1. Next microtick
151
+ *
152
+ * Before any rules began linting files, a microtask was scheduled, which runs on next micro-tick.
153
+ * All language servers we're aware of run each lint task in a separate tick, so this microtask will run in next tick
154
+ * after a linting run, before the next lint task starts.
155
+ *
156
+ * If the linting run completed successfully, the microtask does nothing.
157
+ *
158
+ * But if an error was thrown during AST traversal, this will be visible from the state of `pendingCount`.
159
+ * The microtask will run any `after` hooks which need to be run, and reset state to reflect that there are
160
+ * no more pending `after` hooks.
161
+ *
162
+ * ### 2. Fallback: Next lint run
163
+ *
164
+ * Before linting any file, the state of `pendingCount` is checked.
165
+ * If any `after` hooks are still pending, they are run immediately.
166
+ * They're run before the `context` objects in `createOnce` closures are updated to the next file,
167
+ * so they run with access to the old `context` object from the last file.
168
+ *
169
+ * This fallback should not be required, but it's included as "belt and braces", to handle if any language server
170
+ * or other environment running ESLint programmatically, does not pause a tick between linting runs.
171
+ */
172
+ var AfterHooksState = class {
173
+ resetFunctions = [];
174
+ pendingStates = [];
175
+ pendingCount = 0;
176
+ lintFinishedCount = 0;
177
+ resetIsScheduled = !1;
178
+ sourceCode = null;
179
+ resetMicrotask = this.resetMicrotaskImpl.bind(this);
180
+ /**
181
+ * Register a function to run `after` hook for a rule, and reset state.
182
+ * @param reset - Function to run `after` hook and reset state
183
+ * @returns Index of rule
184
+ */
185
+ registerResetFunction(reset) {
186
+ let { pendingStates } = this, index = pendingStates.length;
187
+ return pendingStates.push(0), this.resetFunctions.push(reset), index;
188
+ }
189
+ /**
190
+ * Register that a rule with `after` hook has completed linting a file.
191
+ * Called by `onCodePathEnd` CFG event handler which is added to visitor for rules with `after` hooks.
192
+ *
193
+ * If all rules with an `after` hook which needs to be run have completed linting the file, run all `after` hooks.
194
+ */
195
+ ruleFinished() {
196
+ this.lintFinishedCount++, this.lintFinishedCount === this.pendingCount && this.reset(!1);
197
+ }
198
+ /**
199
+ * Call all reset functions where corresponding entry in `pendingStates` is `AFTER_HOOK_PENDING`.
200
+ * Should only be called when some `after` hooks are pending.
201
+ *
202
+ * @param ignoreErrors - `true` to catch and silently ignore any errors which occur in `after` hooks.
203
+ * `false` to throw them,
204
+ * @throws {unknown} If `ignoreErrors` is `false` and an error occurs in any `after` hooks.
205
+ */
206
+ reset(ignoreErrors) {
207
+ this.pendingCount;
208
+ let { resetFunctions, pendingStates } = this, hooksLen = pendingStates.length, hasError = !1, error;
209
+ for (let i = 0; i < hooksLen; i++) if (pendingStates[i] !== 0) {
210
+ pendingStates[i] = 0;
211
+ try {
212
+ resetFunctions[i]();
213
+ } catch (e) {
214
+ hasError === !1 && (hasError = !0, error = e);
215
+ }
216
+ }
217
+ if (this.pendingCount = 0, this.lintFinishedCount = 0, this.sourceCode = null, hasError === !0 && ignoreErrors === !1) throw error;
218
+ }
219
+ /**
220
+ * Schedule a microtask to run `reset` functions.
221
+ */
222
+ scheduleReset() {
223
+ queueMicrotask(this.resetMicrotask), this.resetIsScheduled = !0;
224
+ }
225
+ /**
226
+ * Function which is scheduled as the cleanup microtask.
227
+ * `scheduleReset` uses `resetMicrotask` which is this method bound to `this`.
228
+ */
229
+ resetMicrotaskImpl() {
230
+ this.resetIsScheduled = !1, this.pendingCount !== 0 && this.reset(!0);
231
+ }
232
+ };
233
+ /**
49
234
  * Convert a rule.
50
235
  *
51
236
  * The `rule` object passed in is mutated in-place.
52
237
  *
53
238
  * @param rule - Rule to convert
239
+ * @param afterHooksState - State of `after` hooks
54
240
  * @throws {Error} If `rule` is not an object
55
241
  */
56
- function convertRule(rule) {
242
+ function convertRule(rule, afterHooksState) {
57
243
  if (typeof rule != "object" || !rule) throw Error("Rule must be an object");
58
244
  if ("create" in rule) return;
59
- let context = null, visitor, beforeHook;
60
- rule.create = (eslintContext) => (context === null && ({context, visitor, beforeHook} = createContextAndVisitor(rule)), Object.defineProperties(context, {
61
- id: { value: eslintContext.id },
62
- options: { value: eslintContext.options },
63
- report: { value: eslintContext.report }
64
- }), Object.setPrototypeOf(context, Object.getPrototypeOf(eslintContext)), beforeHook !== null && beforeHook() === !1 ? EMPTY_VISITOR : visitor);
245
+ let context = null, visitor, beforeHook, setupAfterHook;
246
+ rule.create = (eslintContext) => {
247
+ context === null && ({context, visitor, beforeHook, setupAfterHook} = createContextAndVisitor(rule, afterHooksState));
248
+ let eslintFileContext = Object.getPrototypeOf(eslintContext);
249
+ if (setupAfterHook !== null) {
250
+ let { sourceCode } = eslintFileContext;
251
+ afterHooksState.sourceCode !== sourceCode && (afterHooksState.sourceCode = sourceCode, afterHooksState.pendingCount !== 0 && afterHooksState.reset(!0));
252
+ }
253
+ return Object.defineProperties(context, {
254
+ id: { value: eslintContext.id },
255
+ options: { value: eslintContext.options },
256
+ report: { value: eslintContext.report }
257
+ }), Object.setPrototypeOf(context, eslintFileContext), beforeHook !== null && beforeHook() === !1 ? EMPTY_VISITOR : (setupAfterHook !== null && (setupAfterHook(eslintFileContext.sourceCode.ast), afterHooksState.resetIsScheduled === !1 && afterHooksState.scheduleReset()), visitor);
258
+ };
65
259
  }
66
260
  const FILE_CONTEXT = Object.freeze({
67
261
  get filename() {
@@ -108,9 +302,11 @@ const FILE_CONTEXT = Object.freeze({
108
302
  * Call `createOnce` method of rule, and return `Context`, `Visitor`, and `beforeHook` (if any).
109
303
  *
110
304
  * @param rule - Rule with `createOnce` method
111
- * @returns Object with `context`, `visitor`, and `beforeHook` properties
305
+ * @param afterHooksState - State of `after` hooks
306
+ * @returns Object with `context`, `visitor`, and `beforeHook` properties,
307
+ * and `setupAfterHook` function if visitor has an `after` hook
112
308
  */
113
- function createContextAndVisitor(rule) {
309
+ function createContextAndVisitor(rule, afterHooksState) {
114
310
  let { createOnce } = rule;
115
311
  if (createOnce == null) throw Error("Rules must define either a `create` or `createOnce` method");
116
312
  if (typeof createOnce != "function") throw Error("Rule `createOnce` property must be a function");
@@ -135,25 +331,27 @@ function createContextAndVisitor(rule) {
135
331
  }), { before: beforeHook, after: afterHook, ...visitor } = createOnce.call(rule, context);
136
332
  if (beforeHook === void 0) beforeHook = null;
137
333
  else if (beforeHook !== null && typeof beforeHook != "function") throw Error("`before` property of visitor must be a function if defined");
334
+ let setupAfterHook = null;
138
335
  if (afterHook != null) {
139
336
  if (typeof afterHook != "function") throw Error("`after` property of visitor must be a function if defined");
140
- let maxAttrs = -1;
141
- for (let key in visitor) {
142
- if (!Object.hasOwn(visitor, key) || !key.endsWith(":exit")) continue;
143
- let end = key.length - 5, count = 0;
144
- for (let i = 0; i < end; i++) {
145
- let c = key.charCodeAt(i);
146
- (c === 91 || c === 46 || c === 58) && count++;
147
- }
148
- count > maxAttrs && (maxAttrs = count);
149
- }
150
- let key = `Program${"[type]".repeat(maxAttrs + 1)}:exit`;
151
- visitor[key] = (_node) => afterHook();
337
+ let program = null, ruleIndex = afterHooksState.registerResetFunction(() => {
338
+ program = null, afterHook();
339
+ });
340
+ setupAfterHook = (ast) => {
341
+ program = ast, afterHooksState.pendingStates[ruleIndex] = 1, afterHooksState.pendingCount++;
342
+ };
343
+ let onCodePathEnd = visitor.onCodePathEnd;
344
+ visitor.onCodePathEnd = onCodePathEnd == null ? function(_codePath, node) {
345
+ node === program && afterHooksState.ruleFinished();
346
+ } : function(codePath, node) {
347
+ onCodePathEnd.call(this, codePath, node), node === program && afterHooksState.ruleFinished();
348
+ };
152
349
  }
153
350
  return {
154
351
  context,
155
352
  visitor,
156
- beforeHook
353
+ beforeHook,
354
+ setupAfterHook
157
355
  };
158
356
  }
159
357
  exports.definePlugin = definePlugin, exports.defineRule = defineRule, exports.eslintCompatPlugin = eslintCompatPlugin;
package/index.js CHANGED
@@ -41,26 +41,220 @@ function eslintCompatPlugin(plugin) {
41
41
  if (typeof plugin != "object" || !plugin) throw Error("Plugin must be an object");
42
42
  let { rules } = plugin;
43
43
  if (typeof rules != "object" || !rules) throw Error("Plugin must have an object as `rules` property");
44
- for (let ruleName in rules) Object.hasOwn(rules, ruleName) && convertRule(rules[ruleName]);
44
+ let afterHooksState = new AfterHooksState();
45
+ for (let ruleName in rules) Object.hasOwn(rules, ruleName) && convertRule(rules[ruleName], afterHooksState);
45
46
  return plugin;
46
47
  }
47
48
  /**
49
+ * Class containing state for tracking if any `after` hooks for a plugin's rules need to be called.
50
+ *
51
+ * # Aims
52
+ *
53
+ * Aims are:
54
+ * 1. `after` hook of each rule runs after all other AST visit functions, and CFG event handlers.
55
+ * 2. `after` hooks for *all* a plugin's rules run after *all* that plugin's rules have completed visiting AST.
56
+ * 3. `after` hooks for *all* a plugin's rules run before *any* of plugin's rules begin linting another file.
57
+ * 4. In the case of an error during AST traversal, `after` hooks are always still run.
58
+ *
59
+ * The above exactly matches the behavior when running a `createOnce` rule in Oxlint.
60
+ *
61
+ * # Why this is important
62
+ *
63
+ * All the complication comes from ensuring `after` hooks run even after an error during AST traversal.
64
+ *
65
+ * In ESLint CLI, an error will crash the process, so it doesn't particularly matter if `after` hooks run or not,
66
+ * but language servers will typically swallow errors, and keep the process running.
67
+ *
68
+ * Rules using `before` and `after` hooks will often rely on both hooks running in a predictable order,
69
+ * to maintain some internal state. For example, they may use `before` and `after` hooks to maintain a per-file
70
+ * cache of data which is shared between rules. The cache use case is why rule (2) above is important.
71
+ *
72
+ * Below is an example of using `before` and `after` hooks to maintain a per-file cache, shared between rules.
73
+ * It relies on all `before` hooks running before any rule starts visiting the AST,
74
+ * and all `after` hooks running after all rules have finished visiting the AST.
75
+ *
76
+ * ```ts
77
+ * let cache: Data | null = null;
78
+ *
79
+ * let numRunningRules = 0;
80
+ *
81
+ * const setupCache = (context) => {
82
+ * if (cache === null) cache = new Data(context);
83
+ * numRunningRules++;
84
+ * };
85
+ *
86
+ * const teardownCache = () => {
87
+ * numRunningRules--;
88
+ * if (numRunningRules === 0) cache = null;
89
+ * };
90
+ *
91
+ * const rule1 = {
92
+ * createOnce(context) {
93
+ * return {
94
+ * before() {
95
+ * setupCache(context);
96
+ * },
97
+ * Identifier(node) {
98
+ * // Use `cache`
99
+ * },
100
+ * after: teardownCache,
101
+ * };
102
+ * },
103
+ * };
104
+ *
105
+ * const rule2 = {
106
+ * // Same as above
107
+ * };
108
+ *
109
+ * const rule3 = {
110
+ * // Same as above
111
+ * };
112
+ * ```
113
+ *
114
+ * If `after` hooks did not always run, the next lint run could get stale state, and malfunction.
115
+ * If `after` hooks ran in the wrong order (e.g. after some `before` hooks for next file),
116
+ * `numRunningRules` would never get to 0, and cache would never be cleared.
117
+ *
118
+ * Note that because all rules run together in a single AST traversal, if a rule from plugin X throws an error,
119
+ * it can disrupt rules from plugin Y. This would make it hard to debug.
120
+ *
121
+ * # Mechanism
122
+ *
123
+ * ## Initialization
124
+ *
125
+ * Rules with an `after` hook register themselves by:
126
+ *
127
+ * 1. Calling `registerResetFunction` to register a function to run `after` hook and clean up internal state.
128
+ * This call adds the reset fn to `resetFunctions`, and adds `AFTER_HOOK_INACTIVE` to `pendingStates`.
129
+ * 2. Adding an `onCodePathEnd` CFG event handler to the visitor which calls `ruleFinished` at end of AST traversal.
130
+ *
131
+ * ## Per-file setup
132
+ *
133
+ * Before linting a file, `create` will call `setupAfterHook` which is created by `createContextAndVisitor`.
134
+ * This registers that the `after` hook for the rule needs to run, by setting `pendingStates[ruleIndex]`
135
+ * to `AFTER_HOOK_PENDING`, and incrementing `pendingCount`.
136
+ *
137
+ * If a cleanup microtask has not been scheduled yet, one is scheduled now (see reason below).
138
+ *
139
+ * ## Normal operation
140
+ *
141
+ * AST traversal for each rule ends with `ruleFinished` hook being called from `onCodePathEnd` CFG event handler.
142
+ * It increments `lintFinishedCount`. If `lintFinishedCount` equals `pendingCount`, all rules have finished linting
143
+ * the file, and `reset` is called, which calls all the pending `after` hooks.
144
+ *
145
+ * ## Error handling
146
+ *
147
+ * If an error is thrown during AST traversal, we ensure that `after` hooks are still run by 2 mechanisms:
148
+ *
149
+ * ### 1. Next microtick
150
+ *
151
+ * Before any rules began linting files, a microtask was scheduled, which runs on next micro-tick.
152
+ * All language servers we're aware of run each lint task in a separate tick, so this microtask will run in next tick
153
+ * after a linting run, before the next lint task starts.
154
+ *
155
+ * If the linting run completed successfully, the microtask does nothing.
156
+ *
157
+ * But if an error was thrown during AST traversal, this will be visible from the state of `pendingCount`.
158
+ * The microtask will run any `after` hooks which need to be run, and reset state to reflect that there are
159
+ * no more pending `after` hooks.
160
+ *
161
+ * ### 2. Fallback: Next lint run
162
+ *
163
+ * Before linting any file, the state of `pendingCount` is checked.
164
+ * If any `after` hooks are still pending, they are run immediately.
165
+ * They're run before the `context` objects in `createOnce` closures are updated to the next file,
166
+ * so they run with access to the old `context` object from the last file.
167
+ *
168
+ * This fallback should not be required, but it's included as "belt and braces", to handle if any language server
169
+ * or other environment running ESLint programmatically, does not pause a tick between linting runs.
170
+ */
171
+ var AfterHooksState = class {
172
+ resetFunctions = [];
173
+ pendingStates = [];
174
+ pendingCount = 0;
175
+ lintFinishedCount = 0;
176
+ resetIsScheduled = !1;
177
+ sourceCode = null;
178
+ resetMicrotask = this.resetMicrotaskImpl.bind(this);
179
+ /**
180
+ * Register a function to run `after` hook for a rule, and reset state.
181
+ * @param reset - Function to run `after` hook and reset state
182
+ * @returns Index of rule
183
+ */
184
+ registerResetFunction(reset) {
185
+ let { pendingStates } = this, index = pendingStates.length;
186
+ return pendingStates.push(0), this.resetFunctions.push(reset), index;
187
+ }
188
+ /**
189
+ * Register that a rule with `after` hook has completed linting a file.
190
+ * Called by `onCodePathEnd` CFG event handler which is added to visitor for rules with `after` hooks.
191
+ *
192
+ * If all rules with an `after` hook which needs to be run have completed linting the file, run all `after` hooks.
193
+ */
194
+ ruleFinished() {
195
+ this.lintFinishedCount++, this.lintFinishedCount === this.pendingCount && this.reset(!1);
196
+ }
197
+ /**
198
+ * Call all reset functions where corresponding entry in `pendingStates` is `AFTER_HOOK_PENDING`.
199
+ * Should only be called when some `after` hooks are pending.
200
+ *
201
+ * @param ignoreErrors - `true` to catch and silently ignore any errors which occur in `after` hooks.
202
+ * `false` to throw them,
203
+ * @throws {unknown} If `ignoreErrors` is `false` and an error occurs in any `after` hooks.
204
+ */
205
+ reset(ignoreErrors) {
206
+ this.pendingCount;
207
+ let { resetFunctions, pendingStates } = this, hooksLen = pendingStates.length, hasError = !1, error;
208
+ for (let i = 0; i < hooksLen; i++) if (pendingStates[i] !== 0) {
209
+ pendingStates[i] = 0;
210
+ try {
211
+ resetFunctions[i]();
212
+ } catch (e) {
213
+ hasError === !1 && (hasError = !0, error = e);
214
+ }
215
+ }
216
+ if (this.pendingCount = 0, this.lintFinishedCount = 0, this.sourceCode = null, hasError === !0 && ignoreErrors === !1) throw error;
217
+ }
218
+ /**
219
+ * Schedule a microtask to run `reset` functions.
220
+ */
221
+ scheduleReset() {
222
+ queueMicrotask(this.resetMicrotask), this.resetIsScheduled = !0;
223
+ }
224
+ /**
225
+ * Function which is scheduled as the cleanup microtask.
226
+ * `scheduleReset` uses `resetMicrotask` which is this method bound to `this`.
227
+ */
228
+ resetMicrotaskImpl() {
229
+ this.resetIsScheduled = !1, this.pendingCount !== 0 && this.reset(!0);
230
+ }
231
+ };
232
+ /**
48
233
  * Convert a rule.
49
234
  *
50
235
  * The `rule` object passed in is mutated in-place.
51
236
  *
52
237
  * @param rule - Rule to convert
238
+ * @param afterHooksState - State of `after` hooks
53
239
  * @throws {Error} If `rule` is not an object
54
240
  */
55
- function convertRule(rule) {
241
+ function convertRule(rule, afterHooksState) {
56
242
  if (typeof rule != "object" || !rule) throw Error("Rule must be an object");
57
243
  if ("create" in rule) return;
58
- let context = null, visitor, beforeHook;
59
- rule.create = (eslintContext) => (context === null && ({context, visitor, beforeHook} = createContextAndVisitor(rule)), Object.defineProperties(context, {
60
- id: { value: eslintContext.id },
61
- options: { value: eslintContext.options },
62
- report: { value: eslintContext.report }
63
- }), Object.setPrototypeOf(context, Object.getPrototypeOf(eslintContext)), beforeHook !== null && beforeHook() === !1 ? EMPTY_VISITOR : visitor);
244
+ let context = null, visitor, beforeHook, setupAfterHook;
245
+ rule.create = (eslintContext) => {
246
+ context === null && ({context, visitor, beforeHook, setupAfterHook} = createContextAndVisitor(rule, afterHooksState));
247
+ let eslintFileContext = Object.getPrototypeOf(eslintContext);
248
+ if (setupAfterHook !== null) {
249
+ let { sourceCode } = eslintFileContext;
250
+ afterHooksState.sourceCode !== sourceCode && (afterHooksState.sourceCode = sourceCode, afterHooksState.pendingCount !== 0 && afterHooksState.reset(!0));
251
+ }
252
+ return Object.defineProperties(context, {
253
+ id: { value: eslintContext.id },
254
+ options: { value: eslintContext.options },
255
+ report: { value: eslintContext.report }
256
+ }), Object.setPrototypeOf(context, eslintFileContext), beforeHook !== null && beforeHook() === !1 ? EMPTY_VISITOR : (setupAfterHook !== null && (setupAfterHook(eslintFileContext.sourceCode.ast), afterHooksState.resetIsScheduled === !1 && afterHooksState.scheduleReset()), visitor);
257
+ };
64
258
  }
65
259
  const FILE_CONTEXT = Object.freeze({
66
260
  get filename() {
@@ -107,9 +301,11 @@ const FILE_CONTEXT = Object.freeze({
107
301
  * Call `createOnce` method of rule, and return `Context`, `Visitor`, and `beforeHook` (if any).
108
302
  *
109
303
  * @param rule - Rule with `createOnce` method
110
- * @returns Object with `context`, `visitor`, and `beforeHook` properties
304
+ * @param afterHooksState - State of `after` hooks
305
+ * @returns Object with `context`, `visitor`, and `beforeHook` properties,
306
+ * and `setupAfterHook` function if visitor has an `after` hook
111
307
  */
112
- function createContextAndVisitor(rule) {
308
+ function createContextAndVisitor(rule, afterHooksState) {
113
309
  let { createOnce } = rule;
114
310
  if (createOnce == null) throw Error("Rules must define either a `create` or `createOnce` method");
115
311
  if (typeof createOnce != "function") throw Error("Rule `createOnce` property must be a function");
@@ -134,25 +330,27 @@ function createContextAndVisitor(rule) {
134
330
  }), { before: beforeHook, after: afterHook, ...visitor } = createOnce.call(rule, context);
135
331
  if (beforeHook === void 0) beforeHook = null;
136
332
  else if (beforeHook !== null && typeof beforeHook != "function") throw Error("`before` property of visitor must be a function if defined");
333
+ let setupAfterHook = null;
137
334
  if (afterHook != null) {
138
335
  if (typeof afterHook != "function") throw Error("`after` property of visitor must be a function if defined");
139
- let maxAttrs = -1;
140
- for (let key in visitor) {
141
- if (!Object.hasOwn(visitor, key) || !key.endsWith(":exit")) continue;
142
- let end = key.length - 5, count = 0;
143
- for (let i = 0; i < end; i++) {
144
- let c = key.charCodeAt(i);
145
- (c === 91 || c === 46 || c === 58) && count++;
146
- }
147
- count > maxAttrs && (maxAttrs = count);
148
- }
149
- let key = `Program${"[type]".repeat(maxAttrs + 1)}:exit`;
150
- visitor[key] = (_node) => afterHook();
336
+ let program = null, ruleIndex = afterHooksState.registerResetFunction(() => {
337
+ program = null, afterHook();
338
+ });
339
+ setupAfterHook = (ast) => {
340
+ program = ast, afterHooksState.pendingStates[ruleIndex] = 1, afterHooksState.pendingCount++;
341
+ };
342
+ let onCodePathEnd = visitor.onCodePathEnd;
343
+ visitor.onCodePathEnd = onCodePathEnd == null ? function(_codePath, node) {
344
+ node === program && afterHooksState.ruleFinished();
345
+ } : function(codePath, node) {
346
+ onCodePathEnd.call(this, codePath, node), node === program && afterHooksState.ruleFinished();
347
+ };
151
348
  }
152
349
  return {
153
350
  context,
154
351
  visitor,
155
- beforeHook
352
+ beforeHook,
353
+ setupAfterHook
156
354
  };
157
355
  }
158
356
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxlint/plugins",
3
- "version": "1.57.0",
3
+ "version": "1.58.0",
4
4
  "description": "Plugin utilities for Oxlint",
5
5
  "keywords": [
6
6
  "eslint",