@oxlint/plugins 1.56.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.
- package/index.cjs +221 -23
- package/index.d.ts +55 -28
- package/index.js +221 -23
- 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
|
-
|
|
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) =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
* @
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
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.d.ts
CHANGED
|
@@ -241,7 +241,7 @@ type Options = JsonValue[];
|
|
|
241
241
|
*/
|
|
242
242
|
type RuleOptionsSchema = JSONSchema4 | JSONSchema4[] | false;
|
|
243
243
|
//#endregion
|
|
244
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
244
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/is-any.d.ts
|
|
245
245
|
/**
|
|
246
246
|
Returns a boolean for whether the given type is `any`.
|
|
247
247
|
|
|
@@ -272,7 +272,7 @@ const anyA = get(anyObject, 'a');
|
|
|
272
272
|
*/
|
|
273
273
|
type IsAny<T> = 0 extends 1 & NoInfer<T> ? true : false;
|
|
274
274
|
//#endregion
|
|
275
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
275
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/is-optional-key-of.d.ts
|
|
276
276
|
/**
|
|
277
277
|
Returns a boolean for whether the given key is an optional key of type.
|
|
278
278
|
|
|
@@ -315,7 +315,7 @@ type T5 = IsOptionalKeyOf<User | Admin, 'surname'>;
|
|
|
315
315
|
*/
|
|
316
316
|
type IsOptionalKeyOf<Type extends object, Key extends keyof Type> = IsAny<Type | Key> extends true ? never : Key extends keyof Type ? Type extends Record<Key, Type[Key]> ? false : true : false;
|
|
317
317
|
//#endregion
|
|
318
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
318
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/optional-keys-of.d.ts
|
|
319
319
|
/**
|
|
320
320
|
Extract all optional keys from the given type.
|
|
321
321
|
|
|
@@ -353,7 +353,7 @@ type OptionalKeysOf<Type extends object> = Type extends unknown // For distribut
|
|
|
353
353
|
? (keyof { [Key in keyof Type as IsOptionalKeyOf<Type, Key> extends false ? never : Key]: never }) & keyof Type // Intersect with `keyof Type` to ensure result of `OptionalKeysOf<Type>` is always assignable to `keyof Type`
|
|
354
354
|
: never;
|
|
355
355
|
//#endregion
|
|
356
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
356
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/required-keys-of.d.ts
|
|
357
357
|
/**
|
|
358
358
|
Extract all required keys from the given type.
|
|
359
359
|
|
|
@@ -387,7 +387,7 @@ const validator3 = createValidation<User>('luckyNumber', value => value > 0);
|
|
|
387
387
|
type RequiredKeysOf<Type extends object> = Type extends unknown // For distributing `Type`
|
|
388
388
|
? Exclude<keyof Type, OptionalKeysOf<Type>> : never;
|
|
389
389
|
//#endregion
|
|
390
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
390
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/is-never.d.ts
|
|
391
391
|
/**
|
|
392
392
|
Returns a boolean for whether the given type is `never`.
|
|
393
393
|
|
|
@@ -443,7 +443,7 @@ type B = IsTrueFixed<never>;
|
|
|
443
443
|
*/
|
|
444
444
|
type IsNever<T> = [T] extends [never] ? true : false;
|
|
445
445
|
//#endregion
|
|
446
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
446
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/if.d.ts
|
|
447
447
|
/**
|
|
448
448
|
An if-else-like type that resolves depending on whether the given `boolean` type is `true` or `false`.
|
|
449
449
|
|
|
@@ -538,7 +538,7 @@ type Works = IncludesWithoutIf<HundredZeroes, '1'>;
|
|
|
538
538
|
*/
|
|
539
539
|
type If<Type extends boolean, IfBranch, ElseBranch> = IsNever<Type> extends true ? ElseBranch : Type extends true ? IfBranch : ElseBranch;
|
|
540
540
|
//#endregion
|
|
541
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
541
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/internal/type.d.ts
|
|
542
542
|
/**
|
|
543
543
|
An if-else-like type that resolves depending on whether the given type is `any` or `never`.
|
|
544
544
|
|
|
@@ -585,7 +585,7 @@ type T2 = TrimLeftOptimised<NineHundredNinetyNineSpaces>;
|
|
|
585
585
|
*/
|
|
586
586
|
type IfNotAnyOrNever<T, IfNotAnyOrNever, IfAny = any, IfNever = never> = If<IsAny<T>, IfAny, If<IsNever<T>, IfNever, IfNotAnyOrNever>>;
|
|
587
587
|
//#endregion
|
|
588
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
588
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/simplify.d.ts
|
|
589
589
|
/**
|
|
590
590
|
Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aide with assignability.
|
|
591
591
|
|
|
@@ -646,7 +646,7 @@ fn(someInterface as Simplify<SomeInterface>); // Good: transform an `interface`
|
|
|
646
646
|
*/
|
|
647
647
|
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
|
|
648
648
|
//#endregion
|
|
649
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
649
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/is-equal.d.ts
|
|
650
650
|
/**
|
|
651
651
|
Returns a boolean for whether the two given types are equal.
|
|
652
652
|
|
|
@@ -677,7 +677,7 @@ type IsEqual<A, B> = [A] extends [B] ? [B] extends [A] ? _IsEqual<A, B> : false
|
|
|
677
677
|
// This version fails the `equalWrappedTupleIntersectionToBeNeverAndNeverExpanded` test in `test-d/is-equal.ts`.
|
|
678
678
|
type _IsEqual<A, B> = (<G>() => G extends A & G | G ? 1 : 2) extends (<G>() => G extends B & G | G ? 1 : 2) ? true : false;
|
|
679
679
|
//#endregion
|
|
680
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
680
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/omit-index-signature.d.ts
|
|
681
681
|
/**
|
|
682
682
|
Omit any index signatures from the given object type, leaving only explicitly defined properties.
|
|
683
683
|
|
|
@@ -771,7 +771,7 @@ type ExampleWithoutIndexSignatures = OmitIndexSignature<Example>;
|
|
|
771
771
|
*/
|
|
772
772
|
type OmitIndexSignature<ObjectType> = { [KeyType in keyof ObjectType as {} extends Record<KeyType, unknown> ? never : KeyType]: ObjectType[KeyType] };
|
|
773
773
|
//#endregion
|
|
774
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
774
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/pick-index-signature.d.ts
|
|
775
775
|
/**
|
|
776
776
|
Pick only index signatures from the given object type, leaving out all explicitly defined properties.
|
|
777
777
|
|
|
@@ -819,12 +819,37 @@ type ExampleIndexSignature = PickIndexSignature<Example>;
|
|
|
819
819
|
*/
|
|
820
820
|
type PickIndexSignature<ObjectType> = { [KeyType in keyof ObjectType as {} extends Record<KeyType, unknown> ? KeyType : never]: ObjectType[KeyType] };
|
|
821
821
|
//#endregion
|
|
822
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
822
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/merge.d.ts
|
|
823
823
|
// Merges two objects without worrying about index signatures.
|
|
824
824
|
type SimpleMerge<Destination, Source> = Simplify<{ [Key in keyof Destination as Key extends keyof Source ? never : Key]: Destination[Key] } & Source>;
|
|
825
825
|
/**
|
|
826
826
|
Merge two types into a new type. Keys of the second type overrides keys of the first type.
|
|
827
827
|
|
|
828
|
+
This is different from the TypeScript `&` (intersection) operator. With `&`, conflicting property types are intersected, which often results in `never`. For example, `{a: string} & {a: number}` makes `a` become `string & number`, which resolves to `never`. With `Merge`, the second type's keys cleanly override the first, so `Merge<{a: string}, {a: number}>` gives `{a: number}` as expected. `Merge` also produces a flattened type (via `Simplify`), making it more readable in IDE tooltips compared to `A & B`.
|
|
829
|
+
|
|
830
|
+
@example
|
|
831
|
+
```
|
|
832
|
+
import type {Merge} from 'type-fest';
|
|
833
|
+
|
|
834
|
+
type Foo = {
|
|
835
|
+
a: string;
|
|
836
|
+
b: number;
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
type Bar = {
|
|
840
|
+
a: number; // Conflicts with Foo['a']
|
|
841
|
+
c: boolean;
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
// With `&`, `a` becomes `string & number` which is `never`. Not what you want.
|
|
845
|
+
type WithIntersection = (Foo & Bar)['a'];
|
|
846
|
+
//=> never
|
|
847
|
+
|
|
848
|
+
// With `Merge`, `a` is cleanly overridden to `number`.
|
|
849
|
+
type WithMerge = Merge<Foo, Bar>['a'];
|
|
850
|
+
//=> number
|
|
851
|
+
```
|
|
852
|
+
|
|
828
853
|
@example
|
|
829
854
|
```
|
|
830
855
|
import type {Merge} from 'type-fest';
|
|
@@ -866,7 +891,7 @@ type Merge<Destination, Source> = Destination extends unknown // For distributin
|
|
|
866
891
|
// Should never happen
|
|
867
892
|
type _Merge<Destination, Source> = Simplify<SimpleMerge<PickIndexSignature<Destination>, PickIndexSignature<Source>> & SimpleMerge<OmitIndexSignature<Destination>, OmitIndexSignature<Source>>>;
|
|
868
893
|
//#endregion
|
|
869
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
894
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/internal/object.d.ts
|
|
870
895
|
/**
|
|
871
896
|
Merges user specified options with default options.
|
|
872
897
|
|
|
@@ -921,7 +946,7 @@ type Result = ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, SpecifiedOp
|
|
|
921
946
|
*/
|
|
922
947
|
type ApplyDefaultOptions<Options extends object, Defaults extends Simplify<Omit<Required<Options>, RequiredKeysOf<Options>> & Partial<Record<RequiredKeysOf<Options>, never>>>, SpecifiedOptions extends Options> = If<IsAny<SpecifiedOptions>, Defaults, If<IsNever<SpecifiedOptions>, Defaults, Simplify<Merge<Defaults, { [Key in keyof SpecifiedOptions as Key extends OptionalKeysOf<Options> ? undefined extends SpecifiedOptions[Key] ? never : Key : Key]: SpecifiedOptions[Key] }> & Required<Options>>>>;
|
|
923
948
|
//#endregion
|
|
924
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
949
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/except.d.ts
|
|
925
950
|
/**
|
|
926
951
|
Filter out keys from an object.
|
|
927
952
|
|
|
@@ -1019,7 +1044,7 @@ type PostPayloadFixed = Except<UserData, 'email'>;
|
|
|
1019
1044
|
type Except<ObjectType, KeysType extends keyof ObjectType, Options extends ExceptOptions = {}> = _Except<ObjectType, KeysType, ApplyDefaultOptions<ExceptOptions, DefaultExceptOptions, Options>>;
|
|
1020
1045
|
type _Except<ObjectType, KeysType extends keyof ObjectType, Options extends Required<ExceptOptions>> = { [KeyType in keyof ObjectType as Filter<KeyType, KeysType>]: ObjectType[KeyType] } & (Options['requireExactProps'] extends true ? Partial<Record<KeysType, never>> : {});
|
|
1021
1046
|
//#endregion
|
|
1022
|
-
//#region ../../node_modules/.pnpm/type-fest@5.
|
|
1047
|
+
//#region ../../node_modules/.pnpm/type-fest@5.5.0/node_modules/type-fest/source/require-at-least-one.d.ts
|
|
1023
1048
|
/**
|
|
1024
1049
|
Create a type that requires at least one of the given keys. The remaining keys are kept as is.
|
|
1025
1050
|
|
|
@@ -1048,15 +1073,6 @@ type _RequireAtLeastOne<ObjectType, KeysType extends keyof ObjectType> = { // Fo
|
|
|
1048
1073
|
Partial<Pick<ObjectType, Exclude<KeysType, Key>>> }[KeysType] & // 3. Add the remaining keys not in `KeysType`
|
|
1049
1074
|
Except<ObjectType, KeysType>;
|
|
1050
1075
|
//#endregion
|
|
1051
|
-
//#region src-js/plugins/comments.d.ts
|
|
1052
|
-
/**
|
|
1053
|
-
* Comment.
|
|
1054
|
-
*/
|
|
1055
|
-
interface CommentType extends Span {
|
|
1056
|
-
type: "Line" | "Block" | "Shebang";
|
|
1057
|
-
value: string;
|
|
1058
|
-
}
|
|
1059
|
-
//#endregion
|
|
1060
1076
|
//#region src-js/plugins/tokens.d.ts
|
|
1061
1077
|
/**
|
|
1062
1078
|
* AST token type.
|
|
@@ -1107,7 +1123,15 @@ interface StringToken extends BaseToken {
|
|
|
1107
1123
|
interface TemplateToken extends BaseToken {
|
|
1108
1124
|
type: "Template";
|
|
1109
1125
|
}
|
|
1110
|
-
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src-js/plugins/comments.d.ts
|
|
1128
|
+
/**
|
|
1129
|
+
* Comment.
|
|
1130
|
+
*/
|
|
1131
|
+
interface CommentType extends Span {
|
|
1132
|
+
type: "Line" | "Block" | "Shebang";
|
|
1133
|
+
value: string;
|
|
1134
|
+
}
|
|
1111
1135
|
declare namespace types_d_exports {
|
|
1112
1136
|
export { AccessorProperty, AccessorPropertyType, Argument, ArrayAssignmentTarget, ArrayExpression, ArrayExpressionElement, ArrayPattern, ArrowFunctionExpression, AssignmentExpression, AssignmentOperator, AssignmentPattern, AssignmentTarget, AssignmentTargetMaybeDefault, AssignmentTargetPattern, AssignmentTargetProperty, AssignmentTargetPropertyIdentifier, AssignmentTargetPropertyProperty, AssignmentTargetRest, AssignmentTargetWithDefault, AwaitExpression, BigIntLiteral, BinaryExpression, BinaryOperator, BindingIdentifier, BindingPattern, BindingProperty, BindingRestElement, BlockStatement, BooleanLiteral, BreakStatement, CallExpression, CatchClause, ChainElement, ChainExpression, Class, ClassBody, ClassElement, ClassType, CommentType as Comment, ComputedMemberExpression, ConditionalExpression, ContinueStatement, DebuggerStatement, Declaration, Decorator, Directive, DoWhileStatement, EmptyStatement, ExportAllDeclaration, ExportDefaultDeclaration, ExportDefaultDeclarationKind, ExportNamedDeclaration, ExportSpecifier, Expression, ExpressionStatement, ForInStatement, ForOfStatement, ForStatement, ForStatementInit, ForStatementLeft, FormalParameter, FormalParameterRest, Function$1 as Function, FunctionBody, FunctionType, Hashbang, IdentifierName, IdentifierReference, IfStatement, ImportAttribute, ImportAttributeKey, ImportDeclaration, ImportDeclarationSpecifier, ImportDefaultSpecifier, ImportExpression, ImportNamespaceSpecifier, ImportOrExportKind, ImportPhase, ImportSpecifier, JSDocNonNullableType, JSDocNullableType, JSDocUnknownType, JSXAttribute, JSXAttributeItem, JSXAttributeName, JSXAttributeValue, JSXChild, JSXClosingElement, JSXClosingFragment, JSXElement, JSXElementName, JSXEmptyExpression, JSXExpression, JSXExpressionContainer, JSXFragment, JSXIdentifier, JSXMemberExpression, JSXMemberExpressionObject, JSXNamespacedName, JSXOpeningElement, JSXOpeningFragment, JSXSpreadAttribute, JSXSpreadChild, JSXText, LabelIdentifier, LabeledStatement, LogicalExpression, LogicalOperator, MemberExpression, MetaProperty, MethodDefinition, MethodDefinitionKind, MethodDefinitionType, ModuleDeclaration, ModuleExportName, ModuleKind, NewExpression, Node$1 as Node, NullLiteral, NumericLiteral, ObjectAssignmentTarget, ObjectExpression, ObjectPattern, ObjectProperty, ObjectPropertyKind, ParamPattern, ParenthesizedExpression, PrivateFieldExpression, PrivateIdentifier, PrivateInExpression, Program, PropertyDefinition, PropertyDefinitionType, PropertyKey$1 as PropertyKey, PropertyKind, RegExpLiteral, ReturnStatement, SequenceExpression, SimpleAssignmentTarget, Span, SpreadElement, Statement, StaticBlock, StaticMemberExpression, StringLiteral, Super, SwitchCase, SwitchStatement, TSAccessibility, TSAnyKeyword, TSArrayType, TSAsExpression, TSBigIntKeyword, TSBooleanKeyword, TSCallSignatureDeclaration, TSClassImplements, TSConditionalType, TSConstructSignatureDeclaration, TSConstructorType, TSEnumBody, TSEnumDeclaration, TSEnumMember, TSEnumMemberName, TSExportAssignment, TSExternalModuleReference, TSFunctionType, TSGlobalDeclaration, TSImportEqualsDeclaration, TSImportType, TSImportTypeQualifiedName, TSImportTypeQualifier, TSIndexSignature, TSIndexSignatureName, TSIndexedAccessType, TSInferType, TSInstantiationExpression, TSInterfaceBody, TSInterfaceDeclaration, TSInterfaceHeritage, TSIntersectionType, TSIntrinsicKeyword, TSLiteral, TSLiteralType, TSMappedType, TSMappedTypeModifierOperator, TSMethodSignature, TSMethodSignatureKind, TSModuleBlock, TSModuleDeclaration, TSModuleDeclarationKind, TSModuleReference, TSNamedTupleMember, TSNamespaceExportDeclaration, TSNeverKeyword, TSNonNullExpression, TSNullKeyword, TSNumberKeyword, TSObjectKeyword, TSOptionalType, TSParameterProperty, TSParenthesizedType, TSPropertySignature, TSQualifiedName, TSRestType, TSSatisfiesExpression, TSSignature, TSStringKeyword, TSSymbolKeyword, TSTemplateLiteralType, TSThisParameter, TSThisType, TSTupleElement, TSTupleType, TSType, TSTypeAliasDeclaration, TSTypeAnnotation, TSTypeAssertion, TSTypeLiteral, TSTypeName, TSTypeOperator, TSTypeOperatorOperator, TSTypeParameter, TSTypeParameterDeclaration, TSTypeParameterInstantiation, TSTypePredicate, TSTypePredicateName, TSTypeQuery, TSTypeQueryExprName, TSTypeReference, TSUndefinedKeyword, TSUnionType, TSUnknownKeyword, TSVoidKeyword, TaggedTemplateExpression, TemplateElement, TemplateElementValue, TemplateLiteral, ThisExpression, ThrowStatement, TokenType as Token, TryStatement, UnaryExpression, UnaryOperator, UpdateExpression, UpdateOperator, V8IntrinsicExpression, VariableDeclaration, VariableDeclarationKind, VariableDeclarator, WhileStatement, WithStatement, YieldExpression };
|
|
1113
1137
|
}
|
|
@@ -2395,12 +2419,12 @@ interface JSDocUnknownType extends Span {
|
|
|
2395
2419
|
type: "TSJSDocUnknownType";
|
|
2396
2420
|
parent: Node$1;
|
|
2397
2421
|
}
|
|
2422
|
+
type ModuleKind = "script" | "module" | "commonjs";
|
|
2398
2423
|
type AssignmentOperator = "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "**=" | "<<=" | ">>=" | ">>>=" | "|=" | "^=" | "&=" | "||=" | "&&=" | "??=";
|
|
2399
2424
|
type BinaryOperator = "==" | "!=" | "===" | "!==" | "<" | "<=" | ">" | ">=" | "+" | "-" | "*" | "/" | "%" | "**" | "<<" | ">>" | ">>>" | "|" | "^" | "&" | "in" | "instanceof";
|
|
2400
2425
|
type LogicalOperator = "||" | "&&" | "??";
|
|
2401
2426
|
type UnaryOperator = "+" | "-" | "!" | "~" | "typeof" | "void" | "delete";
|
|
2402
2427
|
type UpdateOperator = "++" | "--";
|
|
2403
|
-
type ModuleKind = "script" | "module" | "commonjs";
|
|
2404
2428
|
type Node$1 = Program | IdentifierName | IdentifierReference | BindingIdentifier | LabelIdentifier | ThisExpression | ArrayExpression | ObjectExpression | ObjectProperty | TemplateLiteral | TaggedTemplateExpression | TemplateElement | ComputedMemberExpression | StaticMemberExpression | PrivateFieldExpression | CallExpression | NewExpression | MetaProperty | SpreadElement | UpdateExpression | UnaryExpression | BinaryExpression | PrivateInExpression | LogicalExpression | ConditionalExpression | AssignmentExpression | ArrayAssignmentTarget | ObjectAssignmentTarget | AssignmentTargetRest | AssignmentTargetWithDefault | AssignmentTargetPropertyIdentifier | AssignmentTargetPropertyProperty | SequenceExpression | Super | AwaitExpression | ChainExpression | ParenthesizedExpression | Directive | Hashbang | BlockStatement | VariableDeclaration | VariableDeclarator | EmptyStatement | ExpressionStatement | IfStatement | DoWhileStatement | WhileStatement | ForStatement | ForInStatement | ForOfStatement | ContinueStatement | BreakStatement | ReturnStatement | WithStatement | SwitchStatement | SwitchCase | LabeledStatement | ThrowStatement | TryStatement | CatchClause | DebuggerStatement | AssignmentPattern | ObjectPattern | BindingProperty | ArrayPattern | BindingRestElement | Function$1 | FunctionBody | ArrowFunctionExpression | YieldExpression | Class | ClassBody | MethodDefinition | PropertyDefinition | PrivateIdentifier | StaticBlock | AccessorProperty | ImportExpression | ImportDeclaration | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ImportAttribute | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration | ExportSpecifier | V8IntrinsicExpression | BooleanLiteral | NullLiteral | NumericLiteral | StringLiteral | BigIntLiteral | RegExpLiteral | JSXElement | JSXOpeningElement | JSXClosingElement | JSXFragment | JSXOpeningFragment | JSXClosingFragment | JSXNamespacedName | JSXMemberExpression | JSXExpressionContainer | JSXEmptyExpression | JSXAttribute | JSXSpreadAttribute | JSXIdentifier | JSXSpreadChild | JSXText | TSThisParameter | TSEnumDeclaration | TSEnumBody | TSEnumMember | TSTypeAnnotation | TSLiteralType | TSConditionalType | TSUnionType | TSIntersectionType | TSParenthesizedType | TSTypeOperator | TSArrayType | TSIndexedAccessType | TSTupleType | TSNamedTupleMember | TSOptionalType | TSRestType | TSAnyKeyword | TSStringKeyword | TSBooleanKeyword | TSNumberKeyword | TSNeverKeyword | TSIntrinsicKeyword | TSUnknownKeyword | TSNullKeyword | TSUndefinedKeyword | TSVoidKeyword | TSSymbolKeyword | TSThisType | TSObjectKeyword | TSBigIntKeyword | TSTypeReference | TSQualifiedName | TSTypeParameterInstantiation | TSTypeParameter | TSTypeParameterDeclaration | TSTypeAliasDeclaration | TSClassImplements | TSInterfaceDeclaration | TSInterfaceBody | TSPropertySignature | TSIndexSignature | TSCallSignatureDeclaration | TSMethodSignature | TSConstructSignatureDeclaration | TSIndexSignatureName | TSInterfaceHeritage | TSTypePredicate | TSModuleDeclaration | TSGlobalDeclaration | TSModuleBlock | TSTypeLiteral | TSInferType | TSTypeQuery | TSImportType | TSImportTypeQualifiedName | TSFunctionType | TSConstructorType | TSMappedType | TSTemplateLiteralType | TSAsExpression | TSSatisfiesExpression | TSTypeAssertion | TSImportEqualsDeclaration | TSExternalModuleReference | TSNonNullExpression | Decorator | TSExportAssignment | TSNamespaceExportDeclaration | TSInstantiationExpression | JSDocNullableType | JSDocNonNullableType | JSDocUnknownType | ParamPattern;
|
|
2405
2429
|
//#endregion
|
|
2406
2430
|
//#region src-js/generated/visitor.d.ts
|
|
@@ -3075,6 +3099,9 @@ declare function getScope(node: Node$1): Scope;
|
|
|
3075
3099
|
*/
|
|
3076
3100
|
declare function markVariableAsUsed(name: string, refNode?: Node$1): boolean;
|
|
3077
3101
|
//#endregion
|
|
3102
|
+
//#region src-js/plugins/tokens_and_comments.d.ts
|
|
3103
|
+
type TokenOrComment = TokenType | CommentType;
|
|
3104
|
+
//#endregion
|
|
3078
3105
|
//#region src-js/plugins/tokens_methods.d.ts
|
|
3079
3106
|
/**
|
|
3080
3107
|
* Options for various `SourceCode` methods e.g. `getFirstToken`.
|
|
@@ -3284,11 +3311,11 @@ declare function getLastTokenBetween<Options extends SkipOptions | number | Filt
|
|
|
3284
3311
|
declare function getLastTokensBetween<Options extends CountOptions | number | FilterFn | null | undefined>(left: NodeOrToken, right: NodeOrToken, countOptions?: Options): TokenResult<Options>[];
|
|
3285
3312
|
/**
|
|
3286
3313
|
* Get the token starting at the specified index.
|
|
3287
|
-
* @param
|
|
3314
|
+
* @param offset - Start offset of the token.
|
|
3288
3315
|
* @param rangeOptions - Options object.
|
|
3289
3316
|
* @returns `Token` (or `Token | Comment` if `includeComments` is `true`), or `null` if none found.
|
|
3290
3317
|
*/
|
|
3291
|
-
declare function getTokenByRangeStart<Options extends RangeOptions | null | undefined>(
|
|
3318
|
+
declare function getTokenByRangeStart<Options extends RangeOptions | null | undefined>(offset: number, rangeOptions?: Options): TokenResult<Options> | null;
|
|
3292
3319
|
/**
|
|
3293
3320
|
* Determine if two nodes or tokens have at least one whitespace character between them.
|
|
3294
3321
|
* Order does not matter.
|
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
|
-
|
|
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) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
* @
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
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
|