@utilfirst/eslint-plugin 0.3.0 → 0.4.1

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.
@@ -0,0 +1,3869 @@
1
+ import { defineRule, eslintCompatPlugin } from "@oxlint/plugins";
2
+ import { AST_NODE_TYPES } from "@typescript-eslint/utils";
3
+ import { z } from "zod";
4
+
5
+ //#region package.json
6
+ var version = "0.4.1";
7
+
8
+ //#endregion
9
+ //#region src/rules/consistent-blank-lines.ts
10
+ const SKIP_KEYS = new Set([
11
+ "parent",
12
+ "loc",
13
+ "range",
14
+ "start",
15
+ "end"
16
+ ]);
17
+ const FN_DECL_TYPES = new Set([
18
+ AST_NODE_TYPES.FunctionDeclaration,
19
+ AST_NODE_TYPES.FunctionExpression,
20
+ AST_NODE_TYPES.ArrowFunctionExpression
21
+ ]);
22
+ const IMPORT_SPECIFIER_TYPES = new Set([
23
+ AST_NODE_TYPES.ImportSpecifier,
24
+ AST_NODE_TYPES.ImportDefaultSpecifier,
25
+ AST_NODE_TYPES.ImportNamespaceSpecifier
26
+ ]);
27
+ const OPAQUE_BODY_TYPES = new Set([AST_NODE_TYPES.FunctionDeclaration, AST_NODE_TYPES.ClassDeclaration]);
28
+ const EXPRESSION_CONTINUATION_TYPES = new Set([
29
+ AST_NODE_TYPES.IfStatement,
30
+ AST_NODE_TYPES.ReturnStatement,
31
+ AST_NODE_TYPES.ThrowStatement,
32
+ AST_NODE_TYPES.BreakStatement,
33
+ AST_NODE_TYPES.ContinueStatement
34
+ ]);
35
+ const HEAVY_NEXT_TYPES = new Set([
36
+ AST_NODE_TYPES.VariableDeclaration,
37
+ AST_NODE_TYPES.FunctionDeclaration,
38
+ AST_NODE_TYPES.ClassDeclaration,
39
+ AST_NODE_TYPES.TSInterfaceDeclaration,
40
+ AST_NODE_TYPES.TSDeclareFunction,
41
+ AST_NODE_TYPES.ReturnStatement,
42
+ AST_NODE_TYPES.ThrowStatement
43
+ ]);
44
+ const TERMINATING_STATEMENT_TYPES = new Set([
45
+ AST_NODE_TYPES.ReturnStatement,
46
+ AST_NODE_TYPES.ThrowStatement,
47
+ AST_NODE_TYPES.BreakStatement,
48
+ AST_NODE_TYPES.ContinueStatement
49
+ ]);
50
+ function parentOf(node) {
51
+ return node.parent;
52
+ }
53
+ const consistentBlankLines = {
54
+ meta: {
55
+ type: "layout",
56
+ docs: {
57
+ description: "Apply explicit gap policies between adjacent statement-list and JSXChild items.",
58
+ url: "https://github.com/utilfirst/utilfirst-eslint-plugin/blob/main/docs/rules/consistent-blank-lines.md"
59
+ },
60
+ fixable: "whitespace",
61
+ defaultOptions: [],
62
+ schema: [],
63
+ messages: {
64
+ extra: "Unexpected blank line between items that require a tight gap.",
65
+ missing: "Expected one blank line between items that require separation."
66
+ }
67
+ },
68
+ create(context) {
69
+ const sourceCode = context.sourceCode;
70
+ const lineBreak = sourceCode.text.includes("\r\n") ? "\r\n" : "\n";
71
+ function checkBlock(statements) {
72
+ for (let i = 0; i < statements.length - 1; i++) {
73
+ const prev = statements[i];
74
+ const next = statements[i + 1];
75
+ if (prev && next) {
76
+ const { effectiveEnd, leadingComments } = partitionStatementComments({
77
+ comments: sourceCode.getCommentsBefore(next),
78
+ prev
79
+ });
80
+ checkPair({
81
+ effectiveEnd,
82
+ effectiveStart: leadingComments[0] ?? next,
83
+ next,
84
+ policy: statementGapPolicy({
85
+ leadingComments,
86
+ next,
87
+ prev,
88
+ sourceCode
89
+ }),
90
+ prev
91
+ });
92
+ }
93
+ }
94
+ }
95
+ function checkJsxChildren(children) {
96
+ const siblings = [];
97
+ let pendingLeading = [];
98
+ for (const child of children) {
99
+ if (child.type === AST_NODE_TYPES.JSXText && child.value.trim() === "") continue;
100
+ if (isCommentOnlyContainer(child)) {
101
+ pendingLeading.push(child);
102
+ continue;
103
+ }
104
+ siblings.push({
105
+ leading: pendingLeading,
106
+ node: child
107
+ });
108
+ pendingLeading = [];
109
+ }
110
+ for (let i = 0; i < siblings.length - 1; i++) {
111
+ const prevSibling = siblings[i];
112
+ const nextSibling = siblings[i + 1];
113
+ if (prevSibling && nextSibling) {
114
+ const { node: prev } = prevSibling;
115
+ const { leading, node: next } = nextSibling;
116
+ checkPair({
117
+ effectiveEnd: prev,
118
+ effectiveStart: leading[0] ?? next,
119
+ next,
120
+ policy: jsxGapPolicy({
121
+ leading,
122
+ next,
123
+ prev
124
+ }),
125
+ prev
126
+ });
127
+ }
128
+ }
129
+ }
130
+ function checkPair({ effectiveEnd, effectiveStart, next, policy, prev }) {
131
+ if (policy === "preserve") return;
132
+ const prevEndLine = effectiveEnd.loc.end.line;
133
+ const nextStartLine = effectiveStart.loc.start.line;
134
+ if (nextStartLine === prevEndLine) {
135
+ if (policy === "tight") return;
136
+ const indentation = indentationOf(prev, sourceCode);
137
+ context.report({
138
+ node: next,
139
+ messageId: "missing",
140
+ fix: (fixer) => fixer.replaceTextRange([effectiveEnd.range[1], effectiveStart.range[0]], `${lineBreak}${lineBreak}${indentation}`)
141
+ });
142
+ return;
143
+ }
144
+ const paddingCount = nextStartLine - prevEndLine - 1;
145
+ const targetPadding = policy === "separate" ? 1 : 0;
146
+ if (paddingCount === targetPadding) return;
147
+ context.report({
148
+ node: next,
149
+ messageId: paddingCount < targetPadding ? "missing" : "extra",
150
+ fix(fixer) {
151
+ const start = sourceCode.getIndexFromLoc({
152
+ line: prevEndLine + 1,
153
+ column: 0
154
+ });
155
+ const end = sourceCode.getIndexFromLoc({
156
+ line: nextStartLine,
157
+ column: 0
158
+ });
159
+ return fixer.replaceTextRange([start, end], lineBreak.repeat(targetPadding));
160
+ }
161
+ });
162
+ }
163
+ return {
164
+ Program(node) {
165
+ checkBlock(node.body);
166
+ },
167
+ BlockStatement(node) {
168
+ checkBlock(node.body);
169
+ },
170
+ SwitchCase(node) {
171
+ checkBlock(node.consequent);
172
+ },
173
+ StaticBlock(node) {
174
+ checkBlock(node.body);
175
+ },
176
+ JSXElement(node) {
177
+ checkJsxChildren(node.children);
178
+ },
179
+ JSXFragment(node) {
180
+ checkJsxChildren(node.children);
181
+ }
182
+ };
183
+ }
184
+ };
185
+ function partitionStatementComments({ comments, prev }) {
186
+ let effectiveEnd = prev;
187
+ let leadingIndex = 0;
188
+ while (true) {
189
+ const comment = comments[leadingIndex];
190
+ if (!comment || comment.loc.start.line !== effectiveEnd.loc.end.line) break;
191
+ effectiveEnd = comment;
192
+ leadingIndex++;
193
+ }
194
+ return {
195
+ effectiveEnd,
196
+ leadingComments: comments.slice(leadingIndex)
197
+ };
198
+ }
199
+ function indentationOf(node, sourceCode) {
200
+ const lineStart = sourceCode.getIndexFromLoc({
201
+ line: node.loc.start.line,
202
+ column: 0
203
+ });
204
+ const linePrefix = sourceCode.text.slice(lineStart, node.range[0]);
205
+ return /^[\t ]*/u.exec(linePrefix)?.[0] ?? "";
206
+ }
207
+ function statementGapPolicy({ leadingComments, next, prev, sourceCode }) {
208
+ if (itemsSpanMultipleLines(leadingComments)) return "separate";
209
+ const prevHook = hookStatementOf(prev);
210
+ const nextHook = hookStatementOf(next);
211
+ if (prevHook || nextHook) return prevHook?.kind === "variable" && nextHook?.kind === "variable" && declarationsHaveSameShell({
212
+ next,
213
+ prev
214
+ }) ? "tight" : "separate";
215
+ if (isImportPair(prev, next) || isReExport(prev) && isReExport(next) || isExpressionStatementPair(prev, next)) return "preserve";
216
+ if (sharesNameFlow({
217
+ next,
218
+ prev,
219
+ sourceCode
220
+ }) || statementsRequireTightGap({
221
+ next,
222
+ prev,
223
+ sourceCode
224
+ })) return "tight";
225
+ return "separate";
226
+ }
227
+ function hookStatementOf(stmt) {
228
+ const declaration = unwrapExport(stmt);
229
+ if (declaration.type === AST_NODE_TYPES.VariableDeclaration && declaration.declarations.length === 1 && isHookCall(declaration.declarations[0].init)) return { kind: "variable" };
230
+ if (declaration.type === AST_NODE_TYPES.ExpressionStatement && isHookCall(declaration.expression)) return { kind: "expression" };
231
+ return null;
232
+ }
233
+ function declarationsHaveSameShell({ next, prev }) {
234
+ const previousDeclaration = unwrapExport(prev);
235
+ const nextDeclaration = unwrapExport(next);
236
+ return previousDeclaration.type === AST_NODE_TYPES.VariableDeclaration && nextDeclaration.type === AST_NODE_TYPES.VariableDeclaration && isExported(prev) === isExported(next) && !isMultiLine(prev) && !isMultiLine(next) && previousDeclaration.declarations.length === 1 && nextDeclaration.declarations.length === 1;
237
+ }
238
+ function isExported(stmt) {
239
+ return stmt.type === AST_NODE_TYPES.ExportNamedDeclaration || stmt.type === AST_NODE_TYPES.ExportDefaultDeclaration;
240
+ }
241
+ function sharesNameFlow({ next, prev, sourceCode }) {
242
+ if (isMultiLine(prev)) return false;
243
+ const nextType = unwrapExport(next).type;
244
+ if (isMultiLine(next) && HEAVY_NEXT_TYPES.has(nextType)) return false;
245
+ if (nextType === AST_NODE_TYPES.TSTypeAliasDeclaration) return false;
246
+ const introduced = collectIntroducedOrAssignedNames(prev);
247
+ if (introduced.size === 0) return false;
248
+ const referenced = collectReferencedNames(next, sourceCode);
249
+ for (const name of introduced) if (referenced.has(name)) return true;
250
+ return false;
251
+ }
252
+ function isHookCall(node) {
253
+ return node?.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.Identifier && /^use[A-Z]/u.test(node.callee.name);
254
+ }
255
+ function statementsRequireTightGap({ next, prev, sourceCode }) {
256
+ if (isMatchingVarDeclPair({
257
+ next,
258
+ prev,
259
+ sourceCode
260
+ })) return true;
261
+ if (isMatchingTypeAliasPair(prev, next)) return true;
262
+ if (prev.type === AST_NODE_TYPES.IfStatement && next.type === AST_NODE_TYPES.IfStatement) return !(isGuardIf(prev) && !isGuardIf(next));
263
+ if (prev.type === AST_NODE_TYPES.ExpressionStatement && EXPRESSION_CONTINUATION_TYPES.has(next.type)) {
264
+ if (HEAVY_NEXT_TYPES.has(next.type) && isMultiLine(next)) return false;
265
+ return !(next.type === AST_NODE_TYPES.IfStatement && isGuardIf(next));
266
+ }
267
+ return false;
268
+ }
269
+ function isImportPair(prev, next) {
270
+ return prev.type === AST_NODE_TYPES.ImportDeclaration && next.type === AST_NODE_TYPES.ImportDeclaration;
271
+ }
272
+ function isExpressionStatementPair(prev, next) {
273
+ return prev.type === AST_NODE_TYPES.ExpressionStatement && next.type === AST_NODE_TYPES.ExpressionStatement;
274
+ }
275
+ function isReExport(stmt) {
276
+ return stmt.type === AST_NODE_TYPES.ExportNamedDeclaration && stmt.source !== null || stmt.type === AST_NODE_TYPES.ExportAllDeclaration;
277
+ }
278
+ function isMatchingVarDeclPair({ next, prev, sourceCode }) {
279
+ const previousDeclaration = unwrapExport(prev);
280
+ const nextDeclaration = unwrapExport(next);
281
+ if (previousDeclaration.type !== AST_NODE_TYPES.VariableDeclaration || nextDeclaration.type !== AST_NODE_TYPES.VariableDeclaration) return false;
282
+ if (!isConstOrLet(previousDeclaration) || !isConstOrLet(nextDeclaration)) return false;
283
+ if (isMultiLine(prev) || isMultiLine(next)) return false;
284
+ if (previousDeclaration.declarations.length !== 1 || nextDeclaration.declarations.length !== 1) return false;
285
+ if (!initializersBelongTogether({
286
+ nextInit: nextDeclaration.declarations[0].init,
287
+ prevInit: previousDeclaration.declarations[0].init,
288
+ sourceCode
289
+ })) return false;
290
+ return isExported(prev) === isExported(next);
291
+ }
292
+ function isMatchingTypeAliasPair(prev, next) {
293
+ const previousDeclaration = unwrapExport(prev);
294
+ const nextDeclaration = unwrapExport(next);
295
+ if (previousDeclaration.type !== AST_NODE_TYPES.TSTypeAliasDeclaration || nextDeclaration.type !== AST_NODE_TYPES.TSTypeAliasDeclaration) return false;
296
+ if (isMultiLine(prev) || isMultiLine(next)) return false;
297
+ return isExported(prev) === isExported(next);
298
+ }
299
+ function unwrapExport(stmt) {
300
+ if (stmt.type === AST_NODE_TYPES.ExportNamedDeclaration && stmt.declaration) return stmt.declaration;
301
+ if (stmt.type === AST_NODE_TYPES.ExportDefaultDeclaration) return stmt.declaration;
302
+ return stmt;
303
+ }
304
+ function isConstOrLet(decl) {
305
+ return decl.kind === "const" || decl.kind === "let";
306
+ }
307
+ function initializersBelongTogether({ nextInit, prevInit, sourceCode }) {
308
+ const previousCallIdentity = callIdentityOf(prevInit, sourceCode);
309
+ const nextCallIdentity = callIdentityOf(nextInit, sourceCode);
310
+ if (previousCallIdentity !== null && nextCallIdentity !== null) return previousCallIdentity === nextCallIdentity;
311
+ if (previousCallIdentity !== null || nextCallIdentity !== null) return false;
312
+ return true;
313
+ }
314
+ function callIdentityOf(expr, sourceCode) {
315
+ if (!expr) return null;
316
+ const call = expr.type === AST_NODE_TYPES.ChainExpression ? expr.expression : expr;
317
+ if (call.type !== AST_NODE_TYPES.CallExpression) return null;
318
+ return `${call.optional ? "optional" : "direct"}:${sourceCode.getText(call.callee)}`;
319
+ }
320
+ function isGuardIf(stmt) {
321
+ return stmt.type === AST_NODE_TYPES.IfStatement && blockAlwaysTerminates(stmt.consequent);
322
+ }
323
+ function blockAlwaysTerminates(node) {
324
+ if (TERMINATING_STATEMENT_TYPES.has(node.type)) return true;
325
+ if (node.type === AST_NODE_TYPES.BlockStatement) {
326
+ const last = node.body.at(-1);
327
+ return last ? blockAlwaysTerminates(last) : false;
328
+ }
329
+ if (node.type === AST_NODE_TYPES.IfStatement) {
330
+ if (!node.alternate) return false;
331
+ return blockAlwaysTerminates(node.consequent) && blockAlwaysTerminates(node.alternate);
332
+ }
333
+ if (node.type === AST_NODE_TYPES.TryStatement) {
334
+ if (node.finalizer && blockAlwaysTerminates(node.finalizer)) return true;
335
+ if (!blockAlwaysTerminates(node.block)) return false;
336
+ if (!node.handler) return true;
337
+ return blockAlwaysTerminates(node.handler.body);
338
+ }
339
+ return false;
340
+ }
341
+ function collectIntroducedOrAssignedNames(stmt) {
342
+ const set = /* @__PURE__ */ new Set();
343
+ const s = unwrapExport(stmt);
344
+ if (s.type === AST_NODE_TYPES.VariableDeclaration) for (const decl of s.declarations) collectBindingNames(decl.id, set);
345
+ else if (s.type === AST_NODE_TYPES.FunctionDeclaration && s.id) set.add(s.id.name);
346
+ else if (s.type === AST_NODE_TYPES.ClassDeclaration && s.id) set.add(s.id.name);
347
+ else if (s.type === AST_NODE_TYPES.TSInterfaceDeclaration) set.add(s.id.name);
348
+ else if (s.type === AST_NODE_TYPES.TSDeclareFunction && s.id) set.add(s.id.name);
349
+ else if (s.type === AST_NODE_TYPES.ExpressionStatement) {
350
+ const expr = s.expression;
351
+ if (expr.type === AST_NODE_TYPES.AssignmentExpression) collectAssignmentRoots(expr.left, set);
352
+ else if (expr.type === AST_NODE_TYPES.UpdateExpression && expr.argument.type === AST_NODE_TYPES.Identifier) set.add(expr.argument.name);
353
+ }
354
+ return set;
355
+ }
356
+ function collectBindingNames(node, set) {
357
+ switch (node.type) {
358
+ case AST_NODE_TYPES.Identifier:
359
+ set.add(node.name);
360
+ break;
361
+ case AST_NODE_TYPES.ObjectPattern:
362
+ for (const prop of node.properties) if (prop.type === AST_NODE_TYPES.Property) collectBindingNames(prop.value, set);
363
+ else collectBindingNames(prop.argument, set);
364
+ break;
365
+ case AST_NODE_TYPES.ArrayPattern:
366
+ for (const el of node.elements) if (el) collectBindingNames(el, set);
367
+ break;
368
+ case AST_NODE_TYPES.RestElement:
369
+ collectBindingNames(node.argument, set);
370
+ break;
371
+ case AST_NODE_TYPES.AssignmentPattern:
372
+ collectBindingNames(node.left, set);
373
+ break;
374
+ default: break;
375
+ }
376
+ }
377
+ function collectAssignmentRoots(target, set) {
378
+ if (target.type === AST_NODE_TYPES.Identifier) set.add(target.name);
379
+ else if (target.type === AST_NODE_TYPES.MemberExpression) {
380
+ let root = target;
381
+ while (root.type === AST_NODE_TYPES.MemberExpression) root = root.object;
382
+ if (root.type === AST_NODE_TYPES.Identifier) set.add(root.name);
383
+ else if (root.type === AST_NODE_TYPES.ThisExpression) set.add("this");
384
+ } else if (target.type === AST_NODE_TYPES.ObjectPattern || target.type === AST_NODE_TYPES.ArrayPattern) collectBindingNames(target, set);
385
+ }
386
+ function collectReferencedNames(root, sourceCode) {
387
+ const set = /* @__PURE__ */ new Set();
388
+ walk(root, (node) => {
389
+ if (node.type === AST_NODE_TYPES.Identifier && !isDeclarationOrPropertyKey(node)) {
390
+ if (resolvesOutsideRoot({
391
+ idNode: node,
392
+ root,
393
+ sourceCode
394
+ })) set.add(node.name);
395
+ } else if (node.type === AST_NODE_TYPES.ThisExpression) {
396
+ if (thisResolvesOutsideRoot(node, root)) set.add("this");
397
+ } else if (node.type === AST_NODE_TYPES.JSXIdentifier && isJsxComponentIdentifier(node) && resolvesOutsideRoot({
398
+ idNode: node,
399
+ root,
400
+ sourceCode
401
+ })) set.add(node.name);
402
+ });
403
+ return set;
404
+ }
405
+ function resolvesOutsideRoot({ idNode, root, sourceCode }) {
406
+ let scope = sourceCode.getScope(idNode);
407
+ while (scope) {
408
+ const variable = scope.variables.find((v) => v.name === idNode.name);
409
+ if (variable) {
410
+ for (const def of variable.defs) if (isInSubtree(def.node, root)) return false;
411
+ return true;
412
+ }
413
+ scope = scope.upper;
414
+ }
415
+ return true;
416
+ }
417
+ function thisResolvesOutsideRoot(thisNode, root) {
418
+ let parent = parentOf(thisNode);
419
+ while (parent) {
420
+ if (parent.type === AST_NODE_TYPES.FunctionDeclaration || parent.type === AST_NODE_TYPES.FunctionExpression) return !isInSubtree(parent, root);
421
+ parent = parentOf(parent);
422
+ }
423
+ return true;
424
+ }
425
+ function isJsxComponentIdentifier(node) {
426
+ if (!/^[A-Z]/u.test(node.name)) return false;
427
+ const parent = node.parent;
428
+ if (parent.type === AST_NODE_TYPES.JSXOpeningElement && parent.name === node) return true;
429
+ if (parent.type === AST_NODE_TYPES.JSXClosingElement && parent.name === node) return true;
430
+ if (parent.type === AST_NODE_TYPES.JSXMemberExpression && parent.object === node) return true;
431
+ return false;
432
+ }
433
+ function isInSubtree(node, root) {
434
+ let cur = node;
435
+ while (cur) {
436
+ if (cur === root) return true;
437
+ cur = parentOf(cur);
438
+ }
439
+ return false;
440
+ }
441
+ function isDeclarationOrPropertyKey(idNode) {
442
+ const parent = idNode.parent;
443
+ if (parent.type === AST_NODE_TYPES.LabeledStatement && parent.label === idNode || (parent.type === AST_NODE_TYPES.BreakStatement || parent.type === AST_NODE_TYPES.ContinueStatement) && parent.label === idNode) return true;
444
+ if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.id === idNode) return true;
445
+ if (FN_DECL_TYPES.has(parent.type) && "id" in parent && parent.id === idNode) return true;
446
+ if ((parent.type === AST_NODE_TYPES.ClassDeclaration || parent.type === AST_NODE_TYPES.ClassExpression) && parent.id === idNode) return true;
447
+ if (parent.type === AST_NODE_TYPES.MemberExpression && parent.property === idNode && !parent.computed) return true;
448
+ if (IMPORT_SPECIFIER_TYPES.has(parent.type) && "local" in parent && parent.local === idNode) return true;
449
+ if (parent.type === AST_NODE_TYPES.ImportSpecifier) return true;
450
+ if (parent.type === AST_NODE_TYPES.ExportSpecifier && parent.exported === idNode && parent.local !== idNode) return true;
451
+ if (parent.type === AST_NODE_TYPES.MetaProperty) return true;
452
+ if (parent.type === AST_NODE_TYPES.TSQualifiedName && parent.right === idNode) return true;
453
+ if ((parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.PropertyDefinition || parent.type === AST_NODE_TYPES.AccessorProperty || parent.type === AST_NODE_TYPES.TSMethodSignature || parent.type === AST_NODE_TYPES.TSPropertySignature) && parent.key === idNode && !parent.computed) return true;
454
+ if (parent.type === AST_NODE_TYPES.Property && parent.key === idNode && !parent.computed && !parent.shorthand) return true;
455
+ if (isInBindingPosition(idNode)) return true;
456
+ return false;
457
+ }
458
+ function isInBindingPosition(idNode) {
459
+ let cur = idNode;
460
+ let parent = parentOf(cur);
461
+ while (parent) {
462
+ if (FN_DECL_TYPES.has(parent.type) && "params" in parent && Array.isArray(parent.params)) {
463
+ for (const parameter of parent.params) if (parameter === cur) return true;
464
+ }
465
+ if (parent.type === AST_NODE_TYPES.ObjectPattern || parent.type === AST_NODE_TYPES.ArrayPattern) return true;
466
+ if (parent.type === AST_NODE_TYPES.Property) {
467
+ if (parent.computed && parent.key === cur) return false;
468
+ cur = parent;
469
+ parent = parentOf(parent);
470
+ continue;
471
+ }
472
+ if (parent.type === AST_NODE_TYPES.AssignmentPattern) {
473
+ if (parent.right === cur) return false;
474
+ cur = parent;
475
+ parent = parentOf(parent);
476
+ continue;
477
+ }
478
+ if (parent.type === AST_NODE_TYPES.RestElement) {
479
+ cur = parent;
480
+ parent = parentOf(parent);
481
+ continue;
482
+ }
483
+ return false;
484
+ }
485
+ return false;
486
+ }
487
+ function jsxGapPolicy({ leading, next, prev }) {
488
+ if (itemsSpanMultipleLines(leading)) return "separate";
489
+ if (isLiteralTextChild(prev) || isLiteralTextChild(next)) return "tight";
490
+ return isMultiLine(prev) || isMultiLine(next) ? "separate" : "tight";
491
+ }
492
+ function isCommentOnlyContainer(child) {
493
+ return child.type === AST_NODE_TYPES.JSXExpressionContainer && child.expression.type === AST_NODE_TYPES.JSXEmptyExpression;
494
+ }
495
+ function isLiteralTextChild(child) {
496
+ if (child.type === AST_NODE_TYPES.JSXText) return child.value.trim() !== "";
497
+ return child.type === AST_NODE_TYPES.JSXExpressionContainer && expressionProducesTextOrNothing(child.expression);
498
+ }
499
+ function expressionProducesTextOrNothing(expr) {
500
+ if (expr.type === AST_NODE_TYPES.Literal) return typeof expr.value === "string";
501
+ if (expr.type === AST_NODE_TYPES.TemplateLiteral) return true;
502
+ if (expr.type === AST_NODE_TYPES.TSAsExpression || expr.type === AST_NODE_TYPES.TSTypeAssertion || expr.type === AST_NODE_TYPES.TSNonNullExpression || expr.type === AST_NODE_TYPES.TSSatisfiesExpression) return expressionProducesTextOrNothing(expr.expression);
503
+ if (expr.type === AST_NODE_TYPES.LogicalExpression && expr.operator === "&&") return expressionProducesTextOrNothing(expr.right);
504
+ if (expr.type === AST_NODE_TYPES.ConditionalExpression) return expressionProducesTextOrNothing(expr.consequent) && expressionProducesTextOrNothing(expr.alternate);
505
+ return false;
506
+ }
507
+ function isNode$1(value) {
508
+ return value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
509
+ }
510
+ function isArray$1(value) {
511
+ return Array.isArray(value);
512
+ }
513
+ function walk(node, fn) {
514
+ fn(node);
515
+ const nodeEntries = Object.entries(node);
516
+ for (const [key, child] of nodeEntries) {
517
+ if (SKIP_KEYS.has(key)) continue;
518
+ if (key === "body" && OPAQUE_BODY_TYPES.has(node.type)) continue;
519
+ if (isArray$1(child)) {
520
+ for (const childNode of child) if (isNode$1(childNode)) walk(childNode, fn);
521
+ } else if (isNode$1(child)) walk(child, fn);
522
+ }
523
+ }
524
+ function isMultiLine(node) {
525
+ return node.loc.start.line !== node.loc.end.line;
526
+ }
527
+ function itemsSpanMultipleLines(items) {
528
+ const first = items[0];
529
+ const last = items.at(-1);
530
+ if (!first || !last) return false;
531
+ return last.loc.end.line > first.loc.start.line;
532
+ }
533
+
534
+ //#endregion
535
+ //#region src/shared/estree.ts
536
+ function staticMemberName(expression) {
537
+ if (!expression.computed && expression.property.type === "Identifier") return expression.property.name;
538
+ if (expression.computed && expression.property.type === "Literal" && typeof expression.property.value === "string") return expression.property.value;
539
+ return null;
540
+ }
541
+
542
+ //#endregion
543
+ //#region src/shared/scope.ts
544
+ function resolveVariable(sourceCode, identifier) {
545
+ let scope = sourceCode.getScope(identifier);
546
+ while (scope !== null) {
547
+ const variable = scope.set.get(identifier.name);
548
+ if (variable !== void 0) return variable;
549
+ scope = scope.upper;
550
+ }
551
+ return null;
552
+ }
553
+
554
+ //#endregion
555
+ //#region src/shared/node-assert.ts
556
+ const nodeAssertSources = new Set(["node:assert", "node:assert/strict"]);
557
+ function importedName$1(node) {
558
+ if (node.type !== "ImportSpecifier") return null;
559
+ return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
560
+ }
561
+ function nodeAssertDefinitions(sourceCode, identifier) {
562
+ const variable = resolveVariable(sourceCode, identifier);
563
+ if (variable === null) return [];
564
+ return variable.defs.filter((definition) => definition.type === "ImportBinding" && definition.parent?.type === "ImportDeclaration" && nodeAssertSources.has(definition.parent.source.value));
565
+ }
566
+ function isNodeAssertModule(sourceCode, identifier) {
567
+ return nodeAssertDefinitions(sourceCode, identifier).some((definition) => definition.node.type === "ImportDefaultSpecifier" || definition.node.type === "ImportNamespaceSpecifier");
568
+ }
569
+ function isNodeAssertObject(sourceCode, identifier) {
570
+ return isNodeAssertModule(sourceCode, identifier) || nodeAssertDefinitions(sourceCode, identifier).some((definition) => importedName$1(definition.node) === "strict");
571
+ }
572
+ function isNodeAssertMemberObject(sourceCode, expression) {
573
+ if (expression.type === "Identifier") return isNodeAssertObject(sourceCode, expression);
574
+ return expression.type === "MemberExpression" && staticMemberName(expression) === "strict" && expression.object.type === "Identifier" && isNodeAssertModule(sourceCode, expression.object);
575
+ }
576
+ function importedAssertFunctionName(sourceCode, identifier) {
577
+ for (const definition of nodeAssertDefinitions(sourceCode, identifier)) {
578
+ if (definition.node.type === "ImportDefaultSpecifier") return "ok";
579
+ const name = importedName$1(definition.node);
580
+ if (name !== null) return name === "strict" ? "ok" : name;
581
+ }
582
+ return null;
583
+ }
584
+ /** Resolve a Node assert call through supported import and strict-object forms. */
585
+ function nodeAssertCall(sourceCode, node) {
586
+ if (node.callee.type === "Identifier") {
587
+ const methodName$1 = importedAssertFunctionName(sourceCode, node.callee);
588
+ return methodName$1 === null ? null : {
589
+ arguments: node.arguments,
590
+ methodName: methodName$1
591
+ };
592
+ }
593
+ if (node.callee.type !== "MemberExpression" || !isNodeAssertMemberObject(sourceCode, node.callee.object)) return null;
594
+ const methodName = staticMemberName(node.callee);
595
+ if (methodName === "strict" && node.callee.object.type === "Identifier" && isNodeAssertModule(sourceCode, node.callee.object)) return {
596
+ arguments: node.arguments,
597
+ methodName: "ok"
598
+ };
599
+ return methodName === null ? null : {
600
+ arguments: node.arguments,
601
+ methodName
602
+ };
603
+ }
604
+
605
+ //#endregion
606
+ //#region src/shared/test-framework.ts
607
+ const testFrameworkSources = new Set([
608
+ "@jest/globals",
609
+ "node:test",
610
+ "vitest"
611
+ ]);
612
+ const testCaseNames = new Set(["it", "test"]);
613
+ const testControllerNames = new Set(["jest", "vi"]);
614
+ const expectationNames = new Set(["expect"]);
615
+ const testHookNames = new Set([
616
+ "afterAll",
617
+ "afterEach",
618
+ "beforeAll",
619
+ "beforeEach"
620
+ ]);
621
+ const testSetupHookNames = new Set(["beforeAll", "beforeEach"]);
622
+ const testSuiteNames = new Set(["describe", "suite"]);
623
+ function hasExpectationModifier(expression, modifierName) {
624
+ let currentExpression = expression;
625
+ while (currentExpression.type === "MemberExpression") {
626
+ if (staticMemberName(currentExpression) === modifierName) return true;
627
+ currentExpression = currentExpression.object;
628
+ }
629
+ return false;
630
+ }
631
+ function importedName(node) {
632
+ if (node.type !== "ImportSpecifier") return null;
633
+ return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
634
+ }
635
+ function memberChain(expression) {
636
+ if (expression.type === "Identifier") return {
637
+ names: [],
638
+ root: expression
639
+ };
640
+ if (expression.type === "MemberExpression") {
641
+ const parentChain = memberChain(expression.object);
642
+ const memberName = staticMemberName(expression);
643
+ if (parentChain === null || memberName === null) return null;
644
+ return {
645
+ names: [...parentChain.names, memberName],
646
+ root: parentChain.root
647
+ };
648
+ }
649
+ if (expression.type === "CallExpression" && expression.callee.type !== "Super" && expression.callee.type !== "V8IntrinsicExpression") return memberChain(expression.callee);
650
+ return null;
651
+ }
652
+ function isTestFrameworkReference({ acceptedNames, expression, sourceCode }) {
653
+ return testFrameworkReferenceName({
654
+ acceptedNames,
655
+ expression,
656
+ sourceCode
657
+ }) !== null;
658
+ }
659
+ function testFrameworkReferenceName({ acceptedNames, expression, sourceCode }) {
660
+ const chain = memberChain(expression);
661
+ if (chain === null) return null;
662
+ const variable = resolveVariable(sourceCode, chain.root);
663
+ if (variable === null || variable.defs.length === 0) return acceptedNames.has(chain.root.name) ? chain.root.name : null;
664
+ for (const definition of variable.defs) {
665
+ if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration" || !testFrameworkSources.has(definition.parent.source.value)) continue;
666
+ const name = importedName(definition.node);
667
+ if (name !== null && acceptedNames.has(name)) return name;
668
+ if (definition.parent.source.value === "node:test" && definition.node.type === "ImportDefaultSpecifier" && acceptedNames.has("test")) return "test";
669
+ if (definition.node.type === "ImportNamespaceSpecifier") {
670
+ const [frameworkMember] = chain.names;
671
+ if (frameworkMember !== void 0 && acceptedNames.has(frameworkMember)) return frameworkMember;
672
+ }
673
+ }
674
+ return null;
675
+ }
676
+ function unwrapExpression$3(expression) {
677
+ let current = expression;
678
+ while (current.type === "ChainExpression" || current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSNonNullExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion") current = current.expression;
679
+ return current;
680
+ }
681
+ function isStableVariable(variable) {
682
+ return variable !== null && variable.references.every((reference) => reference.init || !reference.isWrite());
683
+ }
684
+ function functionFromIdentifier(sourceCode, identifier) {
685
+ const variable = resolveVariable(sourceCode, identifier);
686
+ if (!isStableVariable(variable)) return null;
687
+ for (const definition of variable.defs) {
688
+ if (definition.type === "FunctionName" && (definition.node.type === "FunctionDeclaration" || definition.node.type === "FunctionExpression")) return definition.node;
689
+ if (definition.type === "Variable" && definition.node.type === "VariableDeclarator" && definition.node.init !== null) {
690
+ const callback = functionFromExpression(sourceCode, definition.node.init);
691
+ if (callback !== null) return callback;
692
+ }
693
+ }
694
+ return null;
695
+ }
696
+ function functionFromExpression(sourceCode, expression) {
697
+ const unwrapped = unwrapExpression$3(expression);
698
+ if (unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "FunctionExpression") return unwrapped;
699
+ if (unwrapped.type === "Identifier") return functionFromIdentifier(sourceCode, unwrapped);
700
+ return null;
701
+ }
702
+ function callbackFromArguments(sourceCode, arguments_) {
703
+ for (let index = arguments_.length - 1; index >= 0; index -= 1) {
704
+ const argument = arguments_[index];
705
+ if (argument === void 0 || argument.type === "SpreadElement") continue;
706
+ const callback = functionFromExpression(sourceCode, argument);
707
+ if (callback !== null) return {
708
+ callback,
709
+ hasCallback: true
710
+ };
711
+ if (index > 0 && (argument.type === "Identifier" || argument.type === "MemberExpression")) return {
712
+ callback: null,
713
+ hasCallback: true
714
+ };
715
+ }
716
+ return {
717
+ callback: null,
718
+ hasCallback: false
719
+ };
720
+ }
721
+ function getTestFrameworkCall(sourceCode, node) {
722
+ if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return null;
723
+ const testName = testFrameworkReferenceName({
724
+ acceptedNames: testCaseNames,
725
+ expression: node.callee,
726
+ sourceCode
727
+ });
728
+ const hookName = testFrameworkReferenceName({
729
+ acceptedNames: testHookNames,
730
+ expression: node.callee,
731
+ sourceCode
732
+ });
733
+ const suiteName = testFrameworkReferenceName({
734
+ acceptedNames: testSuiteNames,
735
+ expression: node.callee,
736
+ sourceCode
737
+ });
738
+ if (testName === null && hookName === null && suiteName === null) return null;
739
+ const { callback, hasCallback } = callbackFromArguments(sourceCode, node.arguments);
740
+ if (!hasCallback) return null;
741
+ if (testName !== null) return {
742
+ callback,
743
+ kind: "test"
744
+ };
745
+ if (suiteName !== null) return {
746
+ callback,
747
+ kind: "suite"
748
+ };
749
+ return {
750
+ callback,
751
+ kind: hookName !== null && testSetupHookNames.has(hookName) ? "setup-hook" : "teardown-hook"
752
+ };
753
+ }
754
+ function isNode(value) {
755
+ return value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
756
+ }
757
+ function isArray(value) {
758
+ return Array.isArray(value);
759
+ }
760
+ function visitExecutedNodes({ root, sourceCode, visit }) {
761
+ const activeFunctions = /* @__PURE__ */ new Set();
762
+ const walkFunction = (callback) => {
763
+ if (activeFunctions.has(callback)) return;
764
+ activeFunctions.add(callback);
765
+ for (const parameter of callback.params) walkNode(parameter);
766
+ if (callback.body !== null) walkNode(callback.body);
767
+ activeFunctions.delete(callback);
768
+ };
769
+ const walkNode = (node) => {
770
+ if (node !== root && (node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression")) return;
771
+ const frameworkCall = node.type === "CallExpression" ? getTestFrameworkCall(sourceCode, node) : null;
772
+ const visitorKeys = new Set(sourceCode.visitorKeys[node.type] ?? []);
773
+ const entries = Object.entries(node);
774
+ for (const [key, child] of entries) {
775
+ if (!visitorKeys.has(key)) continue;
776
+ if (isArray(child)) {
777
+ for (const childNode of child) if (isNode(childNode) && childNode !== frameworkCall?.callback) walkNode(childNode);
778
+ } else if (isNode(child) && child !== frameworkCall?.callback) walkNode(child);
779
+ }
780
+ visit(node);
781
+ if (node.type !== "CallExpression" || node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
782
+ const calledFunction = functionFromExpression(sourceCode, node.callee);
783
+ if (calledFunction !== null) walkFunction(calledFunction);
784
+ if (frameworkCall === null) for (const argument of node.arguments) {
785
+ if (argument.type === "SpreadElement") continue;
786
+ const callback = functionFromExpression(sourceCode, argument);
787
+ if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") walkFunction(callback);
788
+ }
789
+ };
790
+ if (root.type === "Program") walkNode(root);
791
+ else walkFunction(root);
792
+ }
793
+ function isTestFrameworkControlCall(sourceCode, node) {
794
+ if (node.callee.type !== "MemberExpression") return false;
795
+ return isTestFrameworkReference({
796
+ acceptedNames: testControllerNames,
797
+ expression: node.callee.object,
798
+ sourceCode
799
+ });
800
+ }
801
+ function isExpectationMatcher(sourceCode, node) {
802
+ if (node.callee.type !== "MemberExpression") return false;
803
+ let expression = node.callee.object;
804
+ while (expression.type === "MemberExpression") expression = expression.object;
805
+ if (expression.type !== "CallExpression") return false;
806
+ return isTestFrameworkReference({
807
+ acceptedNames: expectationNames,
808
+ expression: expression.callee,
809
+ sourceCode
810
+ });
811
+ }
812
+ function isExpectationMemberCall(sourceCode, node) {
813
+ if (node.callee.type !== "MemberExpression") return false;
814
+ return isTestFrameworkReference({
815
+ acceptedNames: expectationNames,
816
+ expression: node.callee.object,
817
+ sourceCode
818
+ });
819
+ }
820
+
821
+ //#endregion
822
+ //#region src/rules/no-call-count-only-test.ts
823
+ const interactionMatchers = new Set([
824
+ "toBeCalled",
825
+ "toBeCalledTimes",
826
+ "toHaveBeenCalled",
827
+ "toHaveBeenCalledOnce",
828
+ "toHaveBeenCalledTimes"
829
+ ]);
830
+ /** Require interaction assertions to accompany observable outcome evidence. */
831
+ const noCallCountOnlyTestRule = defineRule({
832
+ meta: {
833
+ type: "problem",
834
+ docs: { description: "Disallow tests whose assertions only inspect mock call counts or omission." },
835
+ messages: { callCountOnly: "Assert an observable outcome, or document why call multiplicity or omission is the boundary contract." }
836
+ },
837
+ createOnce(context) {
838
+ return { CallExpression(node) {
839
+ const frameworkCall = getTestFrameworkCall(context.sourceCode, node);
840
+ if (frameworkCall?.kind !== "test" || frameworkCall.callback === null) return;
841
+ let assertionCount = 0;
842
+ let interactionCount = 0;
843
+ visitExecutedNodes({
844
+ root: frameworkCall.callback,
845
+ sourceCode: context.sourceCode,
846
+ visit(candidate) {
847
+ if (candidate.type !== "CallExpression") return;
848
+ if (nodeAssertCall(context.sourceCode, candidate) !== null) {
849
+ assertionCount += 1;
850
+ return;
851
+ }
852
+ if (!isExpectationMatcher(context.sourceCode, candidate)) return;
853
+ assertionCount += 1;
854
+ if (candidate.callee.type === "MemberExpression" && interactionMatchers.has(staticMemberName(candidate.callee) ?? "")) interactionCount += 1;
855
+ }
856
+ });
857
+ if (assertionCount > 0 && assertionCount === interactionCount) context.report({
858
+ node,
859
+ messageId: "callCountOnly"
860
+ });
861
+ } };
862
+ }
863
+ });
864
+
865
+ //#endregion
866
+ //#region src/shared/type-assertion.ts
867
+ function isTypeAssertionExpression(node) {
868
+ return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
869
+ }
870
+ function unwrapTypeAssertionBoundary(expression) {
871
+ let current = expression;
872
+ while (current.type === "ParenthesizedExpression" || current.type === "TSInstantiationExpression" || current.type === "TSNonNullExpression" || current.type === "TSSatisfiesExpression") current = current.expression;
873
+ return current;
874
+ }
875
+ function isTransparentAssertionParent(parent, expression) {
876
+ return (parent.type === "ParenthesizedExpression" || parent.type === "TSInstantiationExpression" || parent.type === "TSNonNullExpression" || parent.type === "TSSatisfiesExpression") && parent.expression === expression;
877
+ }
878
+ function isOutermostTypeAssertion(node) {
879
+ let current = node;
880
+ let parent = node.parent;
881
+ while (isTransparentAssertionParent(parent, current)) {
882
+ current = parent;
883
+ parent = parent.parent;
884
+ }
885
+ return !isTypeAssertionExpression(parent) || parent.expression !== current;
886
+ }
887
+
888
+ //#endregion
889
+ //#region src/rules/no-chained-type-assertions.ts
890
+ function isConstAssertion$1(node) {
891
+ const { typeAnnotation } = node;
892
+ return typeAnnotation.type === "TSTypeReference" && typeAnnotation.typeName.type === "Identifier" && typeAnnotation.typeName.name === "const";
893
+ }
894
+ function isForbiddenAssertionChain(node) {
895
+ let assertionCount = 0;
896
+ let hasNonConstAssertion = false;
897
+ let current = node;
898
+ while (isTypeAssertionExpression(current)) {
899
+ assertionCount += 1;
900
+ hasNonConstAssertion ||= !isConstAssertion$1(current);
901
+ current = unwrapTypeAssertionBoundary(current.expression);
902
+ }
903
+ return assertionCount > 1 && hasNonConstAssertion;
904
+ }
905
+ /** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
906
+ const noChainedTypeAssertionsRule = defineRule({
907
+ meta: {
908
+ type: "problem",
909
+ docs: { description: "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains." },
910
+ messages: { chained: "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it." }
911
+ },
912
+ createOnce(context) {
913
+ const checkTypeAssertion = (node) => {
914
+ if (!isOutermostTypeAssertion(node) || !isForbiddenAssertionChain(node)) return;
915
+ context.report({
916
+ node,
917
+ messageId: "chained"
918
+ });
919
+ };
920
+ return {
921
+ TSAsExpression: checkTypeAssertion,
922
+ TSTypeAssertion: checkTypeAssertion
923
+ };
924
+ }
925
+ });
926
+
927
+ //#endregion
928
+ //#region src/rules/no-conditional-undefined-properties.ts
929
+ function unwrapTransparentExpression(expression) {
930
+ let current = expression;
931
+ while (current.type === "ChainExpression" || current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSNonNullExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion") current = current.expression;
932
+ return current;
933
+ }
934
+ function isUndefinedExpression(sourceCode, expression) {
935
+ const unwrapped = unwrapTransparentExpression(expression);
936
+ const undefinedVariable = unwrapped.type === "Identifier" && unwrapped.name === "undefined" ? resolveVariable(sourceCode, unwrapped) : void 0;
937
+ return unwrapped.type === "Identifier" && unwrapped.name === "undefined" && (undefinedVariable === null || undefinedVariable?.defs.length === 0) || unwrapped.type === "UnaryExpression" && unwrapped.operator === "void";
938
+ }
939
+ function hasConditionalUndefinedValue(sourceCode, value) {
940
+ const unwrapped = unwrapTransparentExpression(value);
941
+ if (unwrapped.type !== "ConditionalExpression") return false;
942
+ return [unwrapped.consequent, unwrapped.alternate].some((branch) => isUndefinedExpression(sourceCode, branch) || hasConditionalUndefinedValue(sourceCode, branch));
943
+ }
944
+ function isObjectExpressionProperty(node) {
945
+ return node.type === "Property" && node.parent.type === "ObjectExpression";
946
+ }
947
+ /** Disallow conditional undefined values that retain an optional property. */
948
+ const noConditionalUndefinedPropertiesRule = defineRule({
949
+ meta: {
950
+ type: "problem",
951
+ docs: { description: "Disallow object properties whose conditional value is undefined." },
952
+ messages: { conditionalUndefined: "This conditional keeps the property present with an undefined value. Build a typed object and add the property only when present." }
953
+ },
954
+ createOnce(context) {
955
+ return { Property(node) {
956
+ if (isObjectExpressionProperty(node) && node.kind === "init" && !node.method && hasConditionalUndefinedValue(context.sourceCode, node.value)) context.report({
957
+ node,
958
+ messageId: "conditionalUndefined"
959
+ });
960
+ } };
961
+ }
962
+ });
963
+
964
+ //#endregion
965
+ //#region src/rules/no-enum-declarations.ts
966
+ function isInsideAmbientModule(node) {
967
+ let current = node.parent;
968
+ while (current.type !== "Program") {
969
+ if (current.type === "TSModuleDeclaration" && current.declare) return true;
970
+ current = current.parent;
971
+ }
972
+ return false;
973
+ }
974
+ /** Prefer literal unions or constant objects over repository-owned TypeScript enums. */
975
+ const noEnumDeclarationsRule = defineRule({
976
+ meta: {
977
+ type: "suggestion",
978
+ docs: { description: "Disallow repository-owned enum declarations while preserving ambient enums." },
979
+ messages: { enumDeclaration: "Replace this enum with a literal union or an inferred constant object. Keep ambient enums only when their boundary requires them." }
980
+ },
981
+ create(context) {
982
+ const isDeclarationFile = /\.d\.[cm]?ts$/u.test(context.filename);
983
+ return { TSEnumDeclaration(node) {
984
+ if (!node.declare && !isDeclarationFile && !isInsideAmbientModule(node)) context.report({
985
+ node,
986
+ messageId: "enumDeclaration"
987
+ });
988
+ } };
989
+ }
990
+ });
991
+
992
+ //#endregion
993
+ //#region src/rules/no-imported-constant-restatement.ts
994
+ const equalityMatchers = new Set([
995
+ "toBe",
996
+ "toEqual",
997
+ "toStrictEqual"
998
+ ]);
999
+ const nodeAssertEqualityMethods = new Set([
1000
+ "deepEqual",
1001
+ "deepStrictEqual",
1002
+ "equal",
1003
+ "strictEqual"
1004
+ ]);
1005
+ const importedConstantName = /^[A-Z][A-Z0-9_]*$/u;
1006
+ function expectationSubject(node) {
1007
+ if (node.callee.type !== "MemberExpression") return null;
1008
+ let expression = node.callee.object;
1009
+ while (expression.type === "MemberExpression") expression = expression.object;
1010
+ if (expression.type !== "CallExpression") return null;
1011
+ const [subject] = expression.arguments;
1012
+ return subject === void 0 || subject.type === "SpreadElement" ? null : subject;
1013
+ }
1014
+ function importedConstantRoot(expression) {
1015
+ let currentExpression = expression;
1016
+ while (currentExpression.type === "MemberExpression") {
1017
+ if (staticMemberName(currentExpression) === null) return null;
1018
+ currentExpression = currentExpression.object;
1019
+ }
1020
+ return currentExpression.type === "Identifier" ? currentExpression : null;
1021
+ }
1022
+ function nodeAssertSubject(sourceCode, node) {
1023
+ const assertion = nodeAssertCall(sourceCode, node);
1024
+ if (assertion === null || !nodeAssertEqualityMethods.has(assertion.methodName)) return null;
1025
+ const [subject] = assertion.arguments;
1026
+ return subject === void 0 || subject.type === "SpreadElement" ? null : subject;
1027
+ }
1028
+ function isStaticValue(expression) {
1029
+ if (expression.type === "Literal") return true;
1030
+ if (expression.type === "TemplateLiteral") return expression.expressions.length === 0;
1031
+ if (expression.type === "UnaryExpression") return isStaticValue(expression.argument);
1032
+ if (expression.type === "ArrayExpression") return expression.elements.every((element) => element !== null && element.type !== "SpreadElement" && isStaticValue(element));
1033
+ if (expression.type === "ObjectExpression") return expression.properties.every((property) => {
1034
+ if (property.type !== "Property" || property.kind !== "init" || property.method) return false;
1035
+ if (property.computed && (property.key.type === "PrivateIdentifier" || !isStaticValue(property.key))) return false;
1036
+ return isStaticValue(property.value);
1037
+ });
1038
+ return false;
1039
+ }
1040
+ function isImportedConstant(sourceCode, identifier) {
1041
+ if (!importedConstantName.test(identifier.name)) return false;
1042
+ const variable = resolveVariable(sourceCode, identifier);
1043
+ return variable !== null && variable.defs.some((definition) => definition.type === "ImportBinding");
1044
+ }
1045
+ function isImportedConstantExpression(sourceCode, expression) {
1046
+ const constantRoot = importedConstantRoot(expression);
1047
+ return constantRoot !== null && isImportedConstant(sourceCode, constantRoot);
1048
+ }
1049
+ function isImportedConstantRestatement({ left, right, sourceCode }) {
1050
+ return isImportedConstantExpression(sourceCode, left) && isStaticValue(right) || isStaticValue(left) && isImportedConstantExpression(sourceCode, right);
1051
+ }
1052
+ /** Require tests to exercise behavior instead of restating imported constants. */
1053
+ const noImportedConstantRestatementRule = defineRule({
1054
+ meta: {
1055
+ type: "problem",
1056
+ docs: { description: "Disallow equality assertions that restate an imported constant through a static literal." },
1057
+ messages: { importedConstantRestatement: "Exercise behavior that consumes this constant. Use a reasoned suppression only when the exact literal is an external contract." }
1058
+ },
1059
+ createOnce(context) {
1060
+ return { CallExpression(node) {
1061
+ const isExpectEquality = node.callee.type === "MemberExpression" && isExpectationMatcher(context.sourceCode, node) && equalityMatchers.has(staticMemberName(node.callee) ?? "") && !hasExpectationModifier(node.callee.object, "not");
1062
+ const subject = isExpectEquality ? expectationSubject(node) : nodeAssertSubject(context.sourceCode, node);
1063
+ const expected = node.arguments[isExpectEquality ? 0 : 1];
1064
+ if (subject === null || expected === void 0 || expected.type === "SpreadElement" || !isImportedConstantRestatement({
1065
+ left: subject,
1066
+ right: expected,
1067
+ sourceCode: context.sourceCode
1068
+ })) return;
1069
+ context.report({
1070
+ node,
1071
+ messageId: "importedConstantRestatement"
1072
+ });
1073
+ } };
1074
+ }
1075
+ });
1076
+
1077
+ //#endregion
1078
+ //#region src/shared/type-alias.ts
1079
+ function typeAliasDeclarationOf(definition) {
1080
+ const runtimeDefinition = definition;
1081
+ return runtimeDefinition.type === "Type" && runtimeDefinition.node.type === "TSTypeAliasDeclaration" ? runtimeDefinition.node : null;
1082
+ }
1083
+ function ownsTypeName(definition) {
1084
+ return definition.type === "Type" || definition.type === "ClassName" || definition.type === "ImportBinding";
1085
+ }
1086
+ function typeDefinitions(sourceCode, reference) {
1087
+ if (reference.typeName.type !== "Identifier") return [];
1088
+ let scope = sourceCode.getScope(reference);
1089
+ while (scope !== null) {
1090
+ const definitions = (scope.set.get(reference.typeName.name)?.defs ?? []).filter((definition) => ownsTypeName(definition));
1091
+ if (definitions.length > 0) return definitions;
1092
+ scope = scope.upper;
1093
+ }
1094
+ return [];
1095
+ }
1096
+ function resolveTypeAlias(sourceCode, reference) {
1097
+ if (reference.typeName.type !== "Identifier") return null;
1098
+ for (const definition of typeDefinitions(sourceCode, reference)) {
1099
+ const alias = typeAliasDeclarationOf(definition);
1100
+ if (alias !== null) return alias;
1101
+ }
1102
+ return null;
1103
+ }
1104
+ function hasTypeDefinition(sourceCode, reference) {
1105
+ return typeDefinitions(sourceCode, reference).length > 0;
1106
+ }
1107
+ function resolveTypeInterfaces(sourceCode, reference) {
1108
+ if (reference.typeName.type !== "Identifier") return [];
1109
+ return typeDefinitions(sourceCode, reference).flatMap((definition) => {
1110
+ const runtimeDefinition = definition;
1111
+ return runtimeDefinition.type === "Type" && runtimeDefinition.node.type === "TSInterfaceDeclaration" ? [runtimeDefinition.node] : [];
1112
+ });
1113
+ }
1114
+ function aliasSubstitutions({ alias, base, reference }) {
1115
+ const parameters = alias.typeParameters?.params ?? [];
1116
+ const arguments_ = reference.typeArguments?.params ?? [];
1117
+ const substitutions = new Map(base);
1118
+ for (const [index, parameter] of parameters.entries()) {
1119
+ const argument = arguments_[index] ?? parameter.default;
1120
+ if (argument === null) return null;
1121
+ substitutions.set(parameter.name.name, {
1122
+ substitutions: new Map(substitutions),
1123
+ type: argument
1124
+ });
1125
+ }
1126
+ return substitutions;
1127
+ }
1128
+ function resolvedTypeIncludes({ isMatch, resolvingAliases, shadowedTypeNames, sourceCode, substitutions, transparentTypeNames, type }) {
1129
+ if (isMatch(type)) return true;
1130
+ if (type.type === "TSParenthesizedType") return resolvedTypeIncludes({
1131
+ isMatch,
1132
+ resolvingAliases,
1133
+ shadowedTypeNames,
1134
+ sourceCode,
1135
+ substitutions,
1136
+ transparentTypeNames,
1137
+ type: type.typeAnnotation
1138
+ });
1139
+ if (type.type === "TSUnionType") return type.types.some((member) => resolvedTypeIncludes({
1140
+ isMatch,
1141
+ resolvingAliases,
1142
+ shadowedTypeNames,
1143
+ sourceCode,
1144
+ substitutions,
1145
+ transparentTypeNames,
1146
+ type: member
1147
+ }));
1148
+ if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return false;
1149
+ const name = type.typeName.name;
1150
+ const substitution = substitutions.get(name);
1151
+ if (substitution !== void 0) return resolvedTypeIncludes({
1152
+ isMatch,
1153
+ resolvingAliases,
1154
+ shadowedTypeNames,
1155
+ sourceCode,
1156
+ substitutions: substitution.substitutions,
1157
+ transparentTypeNames,
1158
+ type: substitution.type
1159
+ });
1160
+ if (shadowedTypeNames?.has(name) === true) return false;
1161
+ const alias = resolveTypeAlias(sourceCode, type);
1162
+ if (alias !== null) {
1163
+ if (resolvingAliases.has(alias)) return false;
1164
+ const nextSubstitutions = aliasSubstitutions({
1165
+ alias,
1166
+ base: substitutions,
1167
+ reference: type
1168
+ });
1169
+ if (nextSubstitutions !== null) return resolvedTypeIncludes({
1170
+ isMatch,
1171
+ resolvingAliases: new Set([...resolvingAliases, alias]),
1172
+ shadowedTypeNames,
1173
+ sourceCode,
1174
+ substitutions: nextSubstitutions,
1175
+ transparentTypeNames,
1176
+ type: alias.typeAnnotation
1177
+ });
1178
+ return false;
1179
+ }
1180
+ if (transparentTypeNames?.has(name) !== true) return false;
1181
+ if (hasTypeDefinition(sourceCode, type)) return false;
1182
+ const wrappedType = type.typeArguments?.params[0];
1183
+ return wrappedType !== void 0 && resolvedTypeIncludes({
1184
+ isMatch,
1185
+ resolvingAliases,
1186
+ shadowedTypeNames,
1187
+ sourceCode,
1188
+ substitutions,
1189
+ transparentTypeNames,
1190
+ type: wrappedType
1191
+ });
1192
+ }
1193
+ /** Match a type after resolving local aliases and their generic arguments. */
1194
+ function resolvedTypeIncludesMatch({ isMatch, shadowedTypeNames, sourceCode, transparentTypeNames, type }) {
1195
+ return resolvedTypeIncludes({
1196
+ isMatch,
1197
+ resolvingAliases: /* @__PURE__ */ new Set(),
1198
+ shadowedTypeNames,
1199
+ sourceCode,
1200
+ substitutions: /* @__PURE__ */ new Map(),
1201
+ transparentTypeNames,
1202
+ type
1203
+ });
1204
+ }
1205
+
1206
+ //#endregion
1207
+ //#region src/shared/dictionary-types.ts
1208
+ const BUILT_INS = new Set([
1209
+ "Map",
1210
+ "ReadonlyMap",
1211
+ "Record",
1212
+ "Readonly",
1213
+ "Partial",
1214
+ "Required",
1215
+ "Pick",
1216
+ "Omit",
1217
+ "PropertyKey",
1218
+ "NonNullable",
1219
+ "WeakMap"
1220
+ ]);
1221
+ const TRANSPARENT_WRAPPERS = new Set([
1222
+ "Readonly",
1223
+ "Partial",
1224
+ "Required",
1225
+ "NonNullable"
1226
+ ]);
1227
+ function declaredStatement(statement) {
1228
+ return statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration ?? null : statement;
1229
+ }
1230
+ function createTypeEnvironment(program, sourceCode) {
1231
+ const aliases = /* @__PURE__ */ new Map();
1232
+ const interfaces = /* @__PURE__ */ new Map();
1233
+ const shadowedBuiltIns = /* @__PURE__ */ new Set();
1234
+ for (const statement of program.body) {
1235
+ const declaration = declaredStatement(statement);
1236
+ if (declaration?.type === "ImportDeclaration") {
1237
+ for (const specifier of declaration.specifiers) if (BUILT_INS.has(specifier.local.name)) shadowedBuiltIns.add(specifier.local.name);
1238
+ continue;
1239
+ }
1240
+ if (declaration?.type === "TSTypeAliasDeclaration") {
1241
+ if (aliases.get(declaration.id.name) === void 0) aliases.set(declaration.id.name, declaration);
1242
+ else shadowedBuiltIns.add(declaration.id.name);
1243
+ if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
1244
+ continue;
1245
+ }
1246
+ if (declaration?.type === "TSInterfaceDeclaration") {
1247
+ const declarations = interfaces.get(declaration.id.name) ?? [];
1248
+ declarations.push(declaration);
1249
+ interfaces.set(declaration.id.name, declarations);
1250
+ if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
1251
+ continue;
1252
+ }
1253
+ if (declaration?.type === "TSEnumDeclaration") {
1254
+ if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
1255
+ continue;
1256
+ }
1257
+ if ((declaration?.type === "ClassDeclaration" || declaration?.type === "FunctionDeclaration") && declaration.id !== null && BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
1258
+ }
1259
+ return {
1260
+ aliases,
1261
+ interfaces,
1262
+ shadowedBuiltIns,
1263
+ sourceCode
1264
+ };
1265
+ }
1266
+ function typeReferenceName$2(type) {
1267
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
1268
+ }
1269
+ function isBuiltIn({ environment, name, reference }) {
1270
+ return BUILT_INS.has(name) && !environment.shadowedBuiltIns.has(name) && !hasTypeDefinition(environment.sourceCode, reference);
1271
+ }
1272
+ function resolvedAlias(environment, reference) {
1273
+ return resolveTypeAlias(environment.sourceCode, reference) ?? environment.aliases.get(typeReferenceName$2(reference) ?? "");
1274
+ }
1275
+ function resolvedInterfaces(environment, reference) {
1276
+ const lexicalInterfaces = resolveTypeInterfaces(environment.sourceCode, reference);
1277
+ return lexicalInterfaces.length > 0 ? lexicalInterfaces : environment.interfaces.get(typeReferenceName$2(reference) ?? "");
1278
+ }
1279
+ function isUnappliedReferenceTo(type, name) {
1280
+ const unwrapped = unwrapTransparentType(type);
1281
+ return unwrapped.type === "TSTypeReference" && typeReferenceName$2(unwrapped) === name && (unwrapped.typeArguments?.params.length ?? 0) === 0;
1282
+ }
1283
+ function unwrapTransparentType(type) {
1284
+ let current = type;
1285
+ while (current.type === "TSParenthesizedType" || current.type === "TSTypeOperator" && current.operator === "readonly") current = current.typeAnnotation;
1286
+ return current;
1287
+ }
1288
+ function isNeverType(type) {
1289
+ return unwrapTransparentType(type).type === "TSNeverKeyword";
1290
+ }
1291
+ function optionalPropertyTypeAnnotation(member) {
1292
+ return member.typeAnnotation;
1293
+ }
1294
+ function optionalMappedTypeAnnotation(type) {
1295
+ return type.typeAnnotation;
1296
+ }
1297
+ function isEffectivelyEmptyMember(member) {
1298
+ if (member.type !== "TSPropertySignature" || !member.optional) return false;
1299
+ const typeAnnotation = optionalPropertyTypeAnnotation(member);
1300
+ return typeAnnotation !== null && typeAnnotation !== void 0 && isNeverType(typeAnnotation.typeAnnotation);
1301
+ }
1302
+ function isEffectivelyEmptyTypeLiteral(type) {
1303
+ return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember);
1304
+ }
1305
+ function isEffectivelyEmptyInterface(declarations) {
1306
+ if (declarations.length !== 1) return false;
1307
+ const [type] = declarations;
1308
+ return type?.extends.length === 0 && (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember));
1309
+ }
1310
+ function resolvedSubstitutionArgument({ base, resolving = /* @__PURE__ */ new Set(), type }) {
1311
+ const unwrapped = unwrapTransparentType(type);
1312
+ if (unwrapped.type !== "TSTypeReference") return type;
1313
+ const name = typeReferenceName$2(unwrapped);
1314
+ if (name === null || resolving.has(name)) return type;
1315
+ const substitution = base.get(name);
1316
+ if (substitution === void 0) return type;
1317
+ return resolvedSubstitutionArgument({
1318
+ base,
1319
+ resolving: new Set([...resolving, name]),
1320
+ type: substitution
1321
+ });
1322
+ }
1323
+ function aliasSubstitution({ alias, base, type }) {
1324
+ const parameters = alias.typeParameters?.params ?? [];
1325
+ const arguments_ = type.typeArguments?.params ?? [];
1326
+ const next = new Map(base);
1327
+ for (const [index, parameter] of parameters.entries()) {
1328
+ const argument = arguments_[index] ?? parameter.default;
1329
+ if (argument === null) return null;
1330
+ next.set(parameter.name.name, resolvedSubstitutionArgument({
1331
+ base: next,
1332
+ type: argument
1333
+ }));
1334
+ }
1335
+ return next;
1336
+ }
1337
+ function unsafeDirectValue({ environment, resolvingAliases, substitutions, type }) {
1338
+ const unwrapped = unwrapTransparentType(type);
1339
+ if (unwrapped.type === "TSUnknownKeyword") return "unknown";
1340
+ if (unwrapped.type === "TSAnyKeyword") return "any";
1341
+ if (unwrapped.type === "TSObjectKeyword") return "object";
1342
+ if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) return "empty-object";
1343
+ if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => unsafeDirectValue({
1344
+ environment,
1345
+ type: member,
1346
+ substitutions,
1347
+ resolvingAliases
1348
+ }) !== null) ? "union" : null;
1349
+ if (unwrapped.type === "TSIntersectionType") {
1350
+ const unsafeMembers = unwrapped.types.map((member) => unsafeDirectValue({
1351
+ environment,
1352
+ resolvingAliases,
1353
+ substitutions,
1354
+ type: member
1355
+ }));
1356
+ if (unsafeMembers.includes("any")) return "any";
1357
+ return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null) ? unsafeMembers[0] ?? null : null;
1358
+ }
1359
+ if (unwrapped.type !== "TSTypeReference") return null;
1360
+ const name = typeReferenceName$2(unwrapped);
1361
+ if (name === null) return null;
1362
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn({
1363
+ environment,
1364
+ name,
1365
+ reference: unwrapped
1366
+ })) {
1367
+ const wrapped = unwrapped.typeArguments?.params[0];
1368
+ return wrapped === void 0 ? null : unsafeDirectValue({
1369
+ environment,
1370
+ type: wrapped,
1371
+ substitutions,
1372
+ resolvingAliases
1373
+ });
1374
+ }
1375
+ const substitution = substitutions.get(name);
1376
+ if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : unsafeDirectValue({
1377
+ environment,
1378
+ type: substitution,
1379
+ substitutions,
1380
+ resolvingAliases
1381
+ });
1382
+ const interfaceDeclarations = resolvedInterfaces(environment, unwrapped);
1383
+ if (interfaceDeclarations !== void 0) return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null;
1384
+ const alias = resolvedAlias(environment, unwrapped);
1385
+ if (alias === void 0 || resolvingAliases.has(name)) return null;
1386
+ const nextSubstitutions = aliasSubstitution({
1387
+ alias,
1388
+ base: substitutions,
1389
+ type: unwrapped
1390
+ });
1391
+ if (nextSubstitutions === null) return null;
1392
+ return unsafeDirectValue({
1393
+ environment,
1394
+ resolvingAliases: new Set([...resolvingAliases, name]),
1395
+ substitutions: nextSubstitutions,
1396
+ type: alias.typeAnnotation
1397
+ });
1398
+ }
1399
+ function dictionaryValueTypes({ environment, resolvingAliases, substitutions, type }) {
1400
+ const unwrapped = unwrapTransparentType(type);
1401
+ if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.flatMap((member) => member.type === "TSIndexSignature" ? [{
1402
+ type: member.typeAnnotation.typeAnnotation,
1403
+ substitutions
1404
+ }] : []);
1405
+ if (unwrapped.type === "TSMappedType") {
1406
+ const typeAnnotation = optionalMappedTypeAnnotation(unwrapped);
1407
+ return typeAnnotation === null || typeAnnotation === void 0 ? [] : [{
1408
+ type: typeAnnotation,
1409
+ substitutions
1410
+ }];
1411
+ }
1412
+ if (unwrapped.type !== "TSTypeReference") return [];
1413
+ const name = typeReferenceName$2(unwrapped);
1414
+ if (name === null) return [];
1415
+ const substitution = substitutions.get(name);
1416
+ if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? [] : dictionaryValueTypes({
1417
+ environment,
1418
+ type: substitution,
1419
+ substitutions,
1420
+ resolvingAliases
1421
+ });
1422
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn({
1423
+ environment,
1424
+ name,
1425
+ reference: unwrapped
1426
+ })) {
1427
+ const wrapped = unwrapped.typeArguments?.params[0];
1428
+ return wrapped === void 0 ? [] : dictionaryValueTypes({
1429
+ environment,
1430
+ type: wrapped,
1431
+ substitutions,
1432
+ resolvingAliases
1433
+ });
1434
+ }
1435
+ if (name === "Record" && isBuiltIn({
1436
+ environment,
1437
+ name,
1438
+ reference: unwrapped
1439
+ })) {
1440
+ const value = unwrapped.typeArguments?.params[1] ?? null;
1441
+ return value === null ? [] : [{
1442
+ type: value,
1443
+ substitutions
1444
+ }];
1445
+ }
1446
+ if ((name === "Map" || name === "ReadonlyMap" || name === "WeakMap") && isBuiltIn({
1447
+ environment,
1448
+ name,
1449
+ reference: unwrapped
1450
+ })) {
1451
+ const value = unwrapped.typeArguments?.params[1] ?? null;
1452
+ return value === null ? [] : [{
1453
+ type: value,
1454
+ substitutions
1455
+ }];
1456
+ }
1457
+ if ((name === "Pick" || name === "Omit") && isBuiltIn({
1458
+ environment,
1459
+ name,
1460
+ reference: unwrapped
1461
+ })) {
1462
+ const source = unwrapped.typeArguments?.params[0];
1463
+ return source === void 0 ? [] : dictionaryValueTypes({
1464
+ environment,
1465
+ type: source,
1466
+ substitutions,
1467
+ resolvingAliases
1468
+ });
1469
+ }
1470
+ const alias = resolvedAlias(environment, unwrapped);
1471
+ if (alias === void 0 || resolvingAliases.has(name)) return [];
1472
+ const nextSubstitutions = aliasSubstitution({
1473
+ alias,
1474
+ base: substitutions,
1475
+ type: unwrapped
1476
+ });
1477
+ if (nextSubstitutions === null) return [];
1478
+ return dictionaryValueTypes({
1479
+ environment,
1480
+ resolvingAliases: new Set([...resolvingAliases, name]),
1481
+ substitutions: nextSubstitutions,
1482
+ type: alias.typeAnnotation
1483
+ });
1484
+ }
1485
+ function classifyUnsafeDictionaryValue(valueType, environment) {
1486
+ const unsafeValue = unsafeDirectValue({
1487
+ environment,
1488
+ resolvingAliases: /* @__PURE__ */ new Set(),
1489
+ substitutions: /* @__PURE__ */ new Map(),
1490
+ type: valueType
1491
+ });
1492
+ return unsafeValue === null ? null : {
1493
+ kind: "unsafe-dictionary",
1494
+ unsafeValue
1495
+ };
1496
+ }
1497
+ function classifyUnsafeDictionary(type, environment) {
1498
+ for (const valueType of dictionaryValueTypes({
1499
+ environment,
1500
+ resolvingAliases: /* @__PURE__ */ new Set(),
1501
+ substitutions: /* @__PURE__ */ new Map(),
1502
+ type
1503
+ })) {
1504
+ const unsafeValue = unsafeDirectValue({
1505
+ environment,
1506
+ resolvingAliases: /* @__PURE__ */ new Set(),
1507
+ substitutions: valueType.substitutions,
1508
+ type: valueType.type
1509
+ });
1510
+ if (unsafeValue !== null) return {
1511
+ kind: "unsafe-dictionary",
1512
+ unsafeValue
1513
+ };
1514
+ }
1515
+ return null;
1516
+ }
1517
+ function resolvesToDictionary({ environment, resolvingAliases, substitutions, type }) {
1518
+ return dictionaryValueTypes({
1519
+ environment,
1520
+ resolvingAliases,
1521
+ substitutions,
1522
+ type
1523
+ }).length > 0;
1524
+ }
1525
+ function classifyWideningTarget(type, environment) {
1526
+ const unwrapped = unwrapTransparentType(type);
1527
+ if (unwrapped.type === "TSAnyKeyword") return { kind: "any" };
1528
+ if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
1529
+ if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
1530
+ if (unwrapped.type === "TSTypeLiteral") {
1531
+ if (unwrapped.members.some((member) => member.type === "TSIndexSignature")) return { kind: "open dictionary" };
1532
+ return unwrapped.members.length > 0 ? { kind: "anonymous object" } : null;
1533
+ }
1534
+ if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" };
1535
+ if (unwrapped.type !== "TSTypeReference") return null;
1536
+ const name = typeReferenceName$2(unwrapped);
1537
+ if (name === null) return null;
1538
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn({
1539
+ environment,
1540
+ name,
1541
+ reference: unwrapped
1542
+ })) {
1543
+ const wrapped = unwrapped.typeArguments?.params[0];
1544
+ return wrapped === void 0 ? null : classifyWideningTarget(wrapped, environment);
1545
+ }
1546
+ if (name === "Record" && isBuiltIn({
1547
+ environment,
1548
+ name,
1549
+ reference: unwrapped
1550
+ })) return { kind: "open dictionary" };
1551
+ const alias = resolvedAlias(environment, unwrapped);
1552
+ if (alias === void 0) return null;
1553
+ if ((alias.typeParameters?.params.length ?? 0) > 0) {
1554
+ const substitutions$1 = aliasSubstitution({
1555
+ alias,
1556
+ base: /* @__PURE__ */ new Map(),
1557
+ type: unwrapped
1558
+ });
1559
+ if (substitutions$1 === null) return null;
1560
+ const resolvingAliases = new Set([name]);
1561
+ const broadTarget = classifyAliasBroadTarget({
1562
+ environment,
1563
+ resolvingAliases,
1564
+ substitutions: substitutions$1,
1565
+ type: alias.typeAnnotation
1566
+ });
1567
+ if (broadTarget !== null && broadTarget.kind !== "open dictionary") return broadTarget;
1568
+ return resolvesToDictionary({
1569
+ environment,
1570
+ type: alias.typeAnnotation,
1571
+ substitutions: substitutions$1,
1572
+ resolvingAliases
1573
+ }) ? { kind: "generic container" } : broadTarget;
1574
+ }
1575
+ const substitutions = aliasSubstitution({
1576
+ alias,
1577
+ base: /* @__PURE__ */ new Map(),
1578
+ type: unwrapped
1579
+ });
1580
+ if (substitutions === null) return null;
1581
+ return classifyAliasBroadTarget({
1582
+ environment,
1583
+ type: alias.typeAnnotation,
1584
+ substitutions,
1585
+ resolvingAliases: new Set([name])
1586
+ });
1587
+ }
1588
+ function isBroadMappedKey({ environment, substitutions, type }) {
1589
+ const unwrapped = unwrapTransparentType(type);
1590
+ if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
1591
+ if (unwrapped.type === "TSUnionType") return unwrapped.types.every((member) => isBroadMappedKey({
1592
+ environment,
1593
+ substitutions,
1594
+ type: member
1595
+ }));
1596
+ if (unwrapped.type !== "TSTypeReference") return false;
1597
+ const name = typeReferenceName$2(unwrapped);
1598
+ if (name === null) return false;
1599
+ const substitution = substitutions.get(name);
1600
+ if (substitution !== void 0 && !isUnappliedReferenceTo(substitution, name)) return isBroadMappedKey({
1601
+ environment,
1602
+ substitutions,
1603
+ type: substitution
1604
+ });
1605
+ return name === "PropertyKey" && isBuiltIn({
1606
+ environment,
1607
+ name,
1608
+ reference: unwrapped
1609
+ });
1610
+ }
1611
+ function classifyAliasBroadTarget({ environment, resolvingAliases, substitutions, type }) {
1612
+ const unwrapped = unwrapTransparentType(type);
1613
+ if (unwrapped.type === "TSAnyKeyword") return { kind: "any" };
1614
+ if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
1615
+ if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
1616
+ if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : null;
1617
+ if (unwrapped.type === "TSMappedType") return isBroadMappedKey({
1618
+ environment,
1619
+ substitutions,
1620
+ type: unwrapped.constraint
1621
+ }) ? { kind: "open dictionary" } : null;
1622
+ if (unwrapped.type !== "TSTypeReference") return null;
1623
+ const name = typeReferenceName$2(unwrapped);
1624
+ if (name === null) return null;
1625
+ const substitution = substitutions.get(name);
1626
+ if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : classifyAliasBroadTarget({
1627
+ environment,
1628
+ type: substitution,
1629
+ substitutions,
1630
+ resolvingAliases
1631
+ });
1632
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn({
1633
+ environment,
1634
+ name,
1635
+ reference: unwrapped
1636
+ })) {
1637
+ const wrapped = unwrapped.typeArguments?.params[0];
1638
+ return wrapped === void 0 ? null : classifyAliasBroadTarget({
1639
+ environment,
1640
+ type: wrapped,
1641
+ substitutions,
1642
+ resolvingAliases
1643
+ });
1644
+ }
1645
+ if (name === "Record" && isBuiltIn({
1646
+ environment,
1647
+ name,
1648
+ reference: unwrapped
1649
+ })) return { kind: "open dictionary" };
1650
+ const alias = resolvedAlias(environment, unwrapped);
1651
+ if (alias === void 0 || resolvingAliases.has(name)) return null;
1652
+ const nextSubstitutions = aliasSubstitution({
1653
+ alias,
1654
+ base: substitutions,
1655
+ type: unwrapped
1656
+ });
1657
+ if (nextSubstitutions === null) return null;
1658
+ return classifyAliasBroadTarget({
1659
+ environment,
1660
+ resolvingAliases: new Set([...resolvingAliases, name]),
1661
+ substitutions: nextSubstitutions,
1662
+ type: alias.typeAnnotation
1663
+ });
1664
+ }
1665
+ function isKnownEvidenceExpression(expression) {
1666
+ let current = expression;
1667
+ while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression" || current.type === "TSSatisfiesExpression") current = current.expression;
1668
+ if (current.type === "ObjectExpression") return true;
1669
+ return current.type === "ArrayExpression" || current.type === "ArrowFunctionExpression" || current.type === "ClassExpression" || current.type === "FunctionExpression" || current.type === "NewExpression" || current.type === "Literal" || current.type === "TemplateLiteral" || current.type === "UnaryExpression";
1670
+ }
1671
+
1672
+ //#endregion
1673
+ //#region src/rules/no-known-value-widening.ts
1674
+ function unwrapExpression$2(expression) {
1675
+ let current = expression;
1676
+ while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") current = current.expression;
1677
+ return current;
1678
+ }
1679
+ function variableDeclarator$1(variable) {
1680
+ if (variable.defs.length !== 1) return null;
1681
+ const [definition] = variable.defs;
1682
+ return definition?.type === "Variable" && definition.node.type === "VariableDeclarator" ? definition.node : null;
1683
+ }
1684
+ function isStableConstVariable(variable, declarator) {
1685
+ return declarator.parent.type === "VariableDeclaration" && declarator.parent.kind === "const" && variable.references.every((reference) => reference.init || !reference.isWrite());
1686
+ }
1687
+ function hasKnownEvidence({ expression, sourceCode, visitedVariables = /* @__PURE__ */ new Set() }) {
1688
+ if (isKnownEvidenceExpression(expression)) return true;
1689
+ const unwrapped = unwrapExpression$2(expression);
1690
+ if (unwrapped.type !== "Identifier") return false;
1691
+ const variable = resolveVariable(sourceCode, unwrapped);
1692
+ if (variable === null || visitedVariables.has(variable)) return false;
1693
+ const declarator = variableDeclarator$1(variable);
1694
+ if (declarator === null) return false;
1695
+ if (declarator.init === null || !isStableConstVariable(variable, declarator)) return false;
1696
+ visitedVariables.add(variable);
1697
+ return hasKnownEvidence({
1698
+ expression: declarator.init,
1699
+ sourceCode,
1700
+ visitedVariables
1701
+ });
1702
+ }
1703
+ function annotationTarget(annotation, environment) {
1704
+ return annotation === null || annotation === void 0 ? null : classifyWideningTarget(annotation.typeAnnotation, environment);
1705
+ }
1706
+ function enclosingFunction(node) {
1707
+ let current = node.parent;
1708
+ while (current !== null && current.type !== "Program") {
1709
+ if (current.type === "ArrowFunctionExpression" || current.type === "FunctionDeclaration" || current.type === "FunctionExpression") return current;
1710
+ current = current.parent;
1711
+ }
1712
+ return null;
1713
+ }
1714
+ function sourceKeyName(sourceCode, key) {
1715
+ if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name;
1716
+ if (key.type === "Literal") return String(key.value);
1717
+ return sourceCode.getText(key);
1718
+ }
1719
+ function functionName(sourceCode, owner) {
1720
+ if (owner === null) return "anonymous function";
1721
+ if (owner.id !== null) return owner.id.name;
1722
+ const parent = owner.parent;
1723
+ if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") return parent.id.name;
1724
+ if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key);
1725
+ return "anonymous function";
1726
+ }
1727
+ function isDictionaryAccumulatorTarget(destination) {
1728
+ return destination.kind === "open dictionary" || destination.kind === "generic container";
1729
+ }
1730
+ function isObjectExpression(expression) {
1731
+ return unwrapExpression$2(expression).type === "ObjectExpression";
1732
+ }
1733
+ function hasParentAssertion(node) {
1734
+ return (node.type === "TSAsExpression" || node.type === "TSTypeAssertion") && !isOutermostTypeAssertion(node);
1735
+ }
1736
+ /** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
1737
+ const noKnownValueWideningRule = defineRule({
1738
+ meta: {
1739
+ type: "problem",
1740
+ docs: { description: "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence." },
1741
+ messages: { widening: "The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract." }
1742
+ },
1743
+ createOnce(context) {
1744
+ let environment = null;
1745
+ const reportFlow = ({ destination, expression, subject }) => {
1746
+ if (destination === null) return;
1747
+ if (isDictionaryAccumulatorTarget(destination) && isObjectExpression(expression)) return;
1748
+ if (!hasKnownEvidence({
1749
+ expression,
1750
+ sourceCode: context.sourceCode
1751
+ })) return;
1752
+ context.report({
1753
+ node: expression,
1754
+ messageId: "widening",
1755
+ data: {
1756
+ subject,
1757
+ target: destination.kind
1758
+ }
1759
+ });
1760
+ };
1761
+ const targetFromAnnotation = (annotation) => environment === null ? null : annotationTarget(annotation, environment);
1762
+ return {
1763
+ Program(node) {
1764
+ environment = createTypeEnvironment(node, context.sourceCode);
1765
+ },
1766
+ VariableDeclarator(node) {
1767
+ if (node.init === null || node.id.type !== "Identifier") return;
1768
+ reportFlow({
1769
+ destination: targetFromAnnotation(node.id.typeAnnotation),
1770
+ expression: node.init,
1771
+ subject: `binding \`${node.id.name}\``
1772
+ });
1773
+ },
1774
+ PropertyDefinition(node) {
1775
+ if (node.value === null) return;
1776
+ reportFlow({
1777
+ destination: targetFromAnnotation(node.typeAnnotation),
1778
+ expression: node.value,
1779
+ subject: `property \`${sourceKeyName(context.sourceCode, node.key)}\``
1780
+ });
1781
+ },
1782
+ AccessorProperty(node) {
1783
+ if (node.value === null) return;
1784
+ reportFlow({
1785
+ destination: targetFromAnnotation(node.typeAnnotation),
1786
+ expression: node.value,
1787
+ subject: `property \`${sourceKeyName(context.sourceCode, node.key)}\``
1788
+ });
1789
+ },
1790
+ AssignmentExpression(node) {
1791
+ if (node.operator !== "=" || node.left.type !== "Identifier") return;
1792
+ const variable = resolveVariable(context.sourceCode, node.left);
1793
+ if (variable === null) return;
1794
+ const declarator = variableDeclarator$1(variable);
1795
+ if (declarator?.id.type !== "Identifier") return;
1796
+ reportFlow({
1797
+ destination: targetFromAnnotation(declarator.id.typeAnnotation),
1798
+ expression: node.right,
1799
+ subject: `binding \`${declarator.id.name}\``
1800
+ });
1801
+ },
1802
+ ReturnStatement(node) {
1803
+ if (node.argument === null) return;
1804
+ const owner = enclosingFunction(node);
1805
+ reportFlow({
1806
+ destination: targetFromAnnotation(owner?.returnType),
1807
+ expression: node.argument,
1808
+ subject: `return value of \`${functionName(context.sourceCode, owner)}\``
1809
+ });
1810
+ },
1811
+ ArrowFunctionExpression(node) {
1812
+ if (node.body.type === "BlockStatement") return;
1813
+ reportFlow({
1814
+ destination: targetFromAnnotation(node.returnType),
1815
+ expression: node.body,
1816
+ subject: `return value of \`${functionName(context.sourceCode, node)}\``
1817
+ });
1818
+ },
1819
+ TSAsExpression(node) {
1820
+ if (environment === null || hasParentAssertion(node)) return;
1821
+ reportFlow({
1822
+ destination: classifyWideningTarget(node.typeAnnotation, environment),
1823
+ expression: node.expression,
1824
+ subject: "assertion"
1825
+ });
1826
+ },
1827
+ TSTypeAssertion(node) {
1828
+ if (environment === null || hasParentAssertion(node)) return;
1829
+ reportFlow({
1830
+ destination: classifyWideningTarget(node.typeAnnotation, environment),
1831
+ expression: node.expression,
1832
+ subject: "assertion"
1833
+ });
1834
+ }
1835
+ };
1836
+ }
1837
+ });
1838
+
1839
+ //#endregion
1840
+ //#region src/shared/rule-options.ts
1841
+ /** Normalize the option shapes exposed by the ESLint and Oxlint contexts. */
1842
+ function ruleContextOptionsSchema(optionsSchema) {
1843
+ return z.union([optionsSchema, z.array(optionsSchema)]).nullable().transform((options) => {
1844
+ if (options === null) return;
1845
+ if (Array.isArray(options)) return options[0];
1846
+ return options;
1847
+ });
1848
+ }
1849
+
1850
+ //#endregion
1851
+ //#region src/shared/repository-module.ts
1852
+ const repositoryModulePrefixes = [
1853
+ ".",
1854
+ "/",
1855
+ "#",
1856
+ "@/",
1857
+ "~/"
1858
+ ];
1859
+ const RepositoryModuleContextOptionsSchema = ruleContextOptionsSchema(z.object({ internalModulePrefixes: z.array(z.string().min(1)).optional() }));
1860
+ const repositoryModuleRuleSchema = [{
1861
+ type: "object",
1862
+ properties: { internalModulePrefixes: {
1863
+ type: "array",
1864
+ items: {
1865
+ type: "string",
1866
+ minLength: 1
1867
+ },
1868
+ uniqueItems: true
1869
+ } },
1870
+ additionalProperties: false
1871
+ }];
1872
+ function getInternalModulePrefixes(rawOptions) {
1873
+ const parsedOptions = RepositoryModuleContextOptionsSchema.safeParse(rawOptions);
1874
+ return parsedOptions.success ? parsedOptions.data?.internalModulePrefixes ?? [] : [];
1875
+ }
1876
+ function isRepositoryOwnedModuleSpecifier({ additionalPrefixes = [], internalModulePrefixes, specifier }) {
1877
+ return repositoryModulePrefixes.some((prefix) => specifier.startsWith(prefix)) || additionalPrefixes.some((prefix) => specifier.startsWith(prefix)) || internalModulePrefixes.some((prefix) => specifier.startsWith(prefix));
1878
+ }
1879
+
1880
+ //#endregion
1881
+ //#region src/rules/no-module-mocking.ts
1882
+ const moduleMockMethods = new Set([
1883
+ "doMock",
1884
+ "mock",
1885
+ "unstable_mockModule"
1886
+ ]);
1887
+ function moduleMockCall(sourceCode, node) {
1888
+ if (node.callee.type !== "MemberExpression") return false;
1889
+ if (!isTestFrameworkControlCall(sourceCode, node)) return false;
1890
+ return moduleMockMethods.has(staticMemberName(node.callee) ?? "");
1891
+ }
1892
+ function isString(value) {
1893
+ return typeof value === "string";
1894
+ }
1895
+ function moduleSpecifier(argument) {
1896
+ if (argument?.type === "Literal" && isString(argument.value)) return argument.value;
1897
+ if (argument?.type === "ImportExpression" && argument.source.type === "Literal" && isString(argument.source.value)) return argument.source.value;
1898
+ return null;
1899
+ }
1900
+ /** Ban test framework mocking of repository-owned modules. */
1901
+ const noModuleMockingRule = defineRule({
1902
+ meta: {
1903
+ type: "problem",
1904
+ docs: { description: "Disallow Vitest and Jest mocking of repository-owned modules; tests must replace local dependencies through production seams." },
1905
+ messages: { moduleMock: "Replace this local module mock through a production dependency seam and a faithful test implementation." },
1906
+ schema: repositoryModuleRuleSchema,
1907
+ defaultOptions: [{ internalModulePrefixes: [] }]
1908
+ },
1909
+ createOnce(context) {
1910
+ return { CallExpression(node) {
1911
+ if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
1912
+ if (!moduleMockCall(context.sourceCode, node)) return;
1913
+ const internalModulePrefixes = getInternalModulePrefixes(context.options);
1914
+ const specifier = moduleSpecifier(node.arguments[0]);
1915
+ if (specifier === null || !isRepositoryOwnedModuleSpecifier({
1916
+ internalModulePrefixes,
1917
+ specifier
1918
+ })) return;
1919
+ context.report({
1920
+ node,
1921
+ messageId: "moduleMock"
1922
+ });
1923
+ } };
1924
+ }
1925
+ });
1926
+
1927
+ //#endregion
1928
+ //#region src/rules/no-negated-throw-assertion.ts
1929
+ const throwMatchers = new Set(["toThrow", "toThrowError"]);
1930
+ const nodeNegatedThrowMethods = new Set(["doesNotReject", "doesNotThrow"]);
1931
+ /** Require direct execution instead of redundant non-throw assertions. */
1932
+ const noNegatedThrowAssertionRule = defineRule({
1933
+ meta: {
1934
+ type: "problem",
1935
+ docs: { description: "Disallow negated throw assertions because direct execution already fails on an exception." },
1936
+ messages: { negatedThrow: "Invoke this function directly, then assert its observable result or effect." }
1937
+ },
1938
+ createOnce(context) {
1939
+ return { CallExpression(node) {
1940
+ const assertion = nodeAssertCall(context.sourceCode, node);
1941
+ if (assertion !== null && nodeNegatedThrowMethods.has(assertion.methodName)) {
1942
+ context.report({
1943
+ node,
1944
+ messageId: "negatedThrow"
1945
+ });
1946
+ return;
1947
+ }
1948
+ if (node.callee.type !== "MemberExpression" || !isExpectationMatcher(context.sourceCode, node) || !throwMatchers.has(staticMemberName(node.callee) ?? "") || !hasExpectationModifier(node.callee.object, "not")) return;
1949
+ context.report({
1950
+ node,
1951
+ messageId: "negatedThrow"
1952
+ });
1953
+ } };
1954
+ }
1955
+ });
1956
+
1957
+ //#endregion
1958
+ //#region src/shared/lexical-type-parameters.ts
1959
+ const AstNodeSchema = z.custom((value) => z.object({ type: z.string() }).safeParse(value).success);
1960
+ function collectInferTypeParameterNames({ names, node, visitorKeys }) {
1961
+ if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
1962
+ const childKeys = visitorKeys[node.type] ?? [];
1963
+ for (const [key, value] of Object.entries(node)) {
1964
+ if (!childKeys.includes(key)) continue;
1965
+ const parsedNode = AstNodeSchema.safeParse(value);
1966
+ if (parsedNode.success) {
1967
+ collectInferTypeParameterNames({
1968
+ names,
1969
+ node: parsedNode.data,
1970
+ visitorKeys
1971
+ });
1972
+ continue;
1973
+ }
1974
+ if (!Array.isArray(value)) continue;
1975
+ for (const child of value) {
1976
+ const parsedChild = AstNodeSchema.safeParse(child);
1977
+ if (parsedChild.success) collectInferTypeParameterNames({
1978
+ names,
1979
+ node: parsedChild.data,
1980
+ visitorKeys
1981
+ });
1982
+ }
1983
+ }
1984
+ }
1985
+ /** Collect type binders that are in scope at a node and can shadow module aliases. */
1986
+ function lexicalTypeParameterNames(node, visitorKeys) {
1987
+ const names = /* @__PURE__ */ new Set();
1988
+ let descendant = node;
1989
+ let current = node;
1990
+ while (current.type !== "Program") {
1991
+ if ("typeParameters" in current) for (const parameter of current.typeParameters?.params ?? []) names.add(parameter.name.name);
1992
+ if (current.type === "TSMappedType" && (descendant === current.nameType || descendant === current.typeAnnotation)) names.add(current.key.name);
1993
+ if (current.type === "TSConditionalType" && descendant === current.trueType) collectInferTypeParameterNames({
1994
+ names,
1995
+ node: current.extendsType,
1996
+ visitorKeys
1997
+ });
1998
+ descendant = current;
1999
+ current = current.parent;
2000
+ }
2001
+ return names;
2002
+ }
2003
+
2004
+ //#endregion
2005
+ //#region src/rules/no-object-parameters.ts
2006
+ function parameterAnnotation$1(parameter) {
2007
+ if (parameter.type === "TSParameterProperty") return parameterAnnotation$1(parameter.parameter);
2008
+ if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation$1(parameter.argument);
2009
+ if (parameter.type === "AssignmentPattern") return parameter.left.typeAnnotation;
2010
+ return parameter.typeAnnotation;
2011
+ }
2012
+ function parameterType$2(parameter) {
2013
+ const annotation = parameterAnnotation$1(parameter);
2014
+ if (annotation === null || annotation === void 0) return null;
2015
+ const type = annotation.typeAnnotation;
2016
+ return parameter.type === "RestElement" && type.type === "TSArrayType" ? type.elementType : type;
2017
+ }
2018
+ function parameterName$2(parameter, sourceCode) {
2019
+ return parameter.type === "Identifier" ? parameter.name : sourceCode.getText(parameter).replace(/\s*:\s*object\s*$/u, "");
2020
+ }
2021
+ /** Ban the broad object type on function inputs, including local aliases to object. */
2022
+ const noObjectParametersRule = defineRule({
2023
+ meta: {
2024
+ type: "problem",
2025
+ docs: { description: "Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary." },
2026
+ messages: { objectParameter: "Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function." }
2027
+ },
2028
+ createOnce(context) {
2029
+ const checkParameters = (node) => {
2030
+ const shadowedAliases = lexicalTypeParameterNames(node, context.sourceCode.visitorKeys);
2031
+ for (const parameter of node.params) {
2032
+ const type = parameterType$2(parameter);
2033
+ if (type === null) continue;
2034
+ if (!resolvedTypeIncludesMatch({
2035
+ isMatch: (candidate) => candidate.type === "TSObjectKeyword",
2036
+ shadowedTypeNames: shadowedAliases,
2037
+ sourceCode: context.sourceCode,
2038
+ type
2039
+ })) continue;
2040
+ context.report({
2041
+ node: type,
2042
+ messageId: "objectParameter",
2043
+ data: { parameter: parameterName$2(parameter, context.sourceCode) }
2044
+ });
2045
+ }
2046
+ };
2047
+ return {
2048
+ ArrowFunctionExpression: checkParameters,
2049
+ FunctionDeclaration: checkParameters,
2050
+ FunctionExpression: checkParameters,
2051
+ TSCallSignatureDeclaration: checkParameters,
2052
+ TSConstructSignatureDeclaration: checkParameters,
2053
+ TSConstructorType: checkParameters,
2054
+ TSDeclareFunction: checkParameters,
2055
+ TSEmptyBodyFunctionExpression: checkParameters,
2056
+ TSFunctionType: checkParameters,
2057
+ TSMethodSignature: checkParameters
2058
+ };
2059
+ }
2060
+ });
2061
+
2062
+ //#endregion
2063
+ //#region src/shared/owned-function.ts
2064
+ function getOwnedFunctionName(node) {
2065
+ if ((node.type === "FunctionDeclaration" || node.type === "TSDeclareFunction") && node.id !== null) return node.id.name;
2066
+ if (node.type === "TSMethodSignature") return getStaticPropertyName({
2067
+ isComputed: node.computed,
2068
+ key: node.key
2069
+ });
2070
+ if (node.type === "TSCallSignatureDeclaration" || node.type === "TSConstructSignatureDeclaration" || node.type === "TSConstructorType" || node.type === "TSFunctionType") return getDeclarationOwnerName(node);
2071
+ const { parent } = node;
2072
+ if (parent.type === "VariableDeclarator" && parent.init === node && parent.id.type === "Identifier") return parent.id.name;
2073
+ if (parent.type === "Property" && parent.value === node && parent.parent.type === "ObjectExpression") return getStaticPropertyName({
2074
+ isComputed: parent.computed,
2075
+ key: parent.key
2076
+ });
2077
+ if ((parent.type === "MethodDefinition" || parent.type === "TSAbstractMethodDefinition") && parent.value === node) return getStaticPropertyName({
2078
+ isComputed: parent.computed,
2079
+ key: parent.key
2080
+ });
2081
+ if ((parent.type === "PropertyDefinition" || parent.type === "TSAbstractPropertyDefinition") && parent.value === node) return getStaticPropertyName({
2082
+ isComputed: parent.computed,
2083
+ key: parent.key
2084
+ });
2085
+ return null;
2086
+ }
2087
+ function getDeclarationOwnerName(node) {
2088
+ let current = node.parent;
2089
+ while (current !== null && current.type !== "Program") {
2090
+ if (current.type === "TSTypeAliasDeclaration") return current.id.name;
2091
+ if (current.type === "TSInterfaceDeclaration") return current.id.name;
2092
+ if (current.type === "TSPropertySignature") return getStaticPropertyName({
2093
+ isComputed: current.computed,
2094
+ key: current.key
2095
+ });
2096
+ if (current.type === "TSInterfaceBody") return current.parent.type === "TSInterfaceDeclaration" ? current.parent.id.name : null;
2097
+ if (current.type !== "TSIntersectionType" && current.type !== "TSParenthesizedType" && current.type !== "TSTypeAnnotation" && current.type !== "TSTypeLiteral" && current.type !== "TSUnionType") return null;
2098
+ current = current.parent;
2099
+ }
2100
+ return null;
2101
+ }
2102
+ function getStaticPropertyName({ isComputed, key }) {
2103
+ if (key.type === "PrivateIdentifier") return `#${key.name}`;
2104
+ if (!isComputed && key.type === "Identifier") return key.name;
2105
+ if (key.type === "Literal") return parseStaticPropertyValue(key.value);
2106
+ return null;
2107
+ }
2108
+ function parseStaticPropertyValue(value) {
2109
+ if (typeof value === "string" || typeof value === "number") return String(value);
2110
+ return null;
2111
+ }
2112
+
2113
+ //#endregion
2114
+ //#region src/rules/no-positional-boolean-parameters.ts
2115
+ const ContextOptionsSchema$2 = ruleContextOptionsSchema(z.object({ allowFunctionNames: z.array(z.string()).optional() }));
2116
+ function annotationOf(parameter) {
2117
+ if (parameter.type === "TSParameterProperty") return annotationOf(parameter.parameter);
2118
+ if (parameter.type === "AssignmentPattern") return parameter.left.typeAnnotation;
2119
+ return parameter.typeAnnotation;
2120
+ }
2121
+ function parameterName$1(parameter, sourceCode) {
2122
+ if (parameter.type === "TSParameterProperty") return parameterName$1(parameter.parameter, sourceCode);
2123
+ if (parameter.type === "AssignmentPattern") return parameter.left.type === "Identifier" ? parameter.left.name : sourceCode.getText(parameter.left);
2124
+ return parameter.type === "Identifier" ? parameter.name : sourceCode.getText(parameter);
2125
+ }
2126
+ function isBooleanType(type) {
2127
+ return type.type === "TSBooleanKeyword" || type.type === "TSLiteralType" && "value" in type.literal && typeof type.literal.value === "boolean";
2128
+ }
2129
+ /** Disallow positional boolean flags on repository-owned named callables. */
2130
+ const noPositionalBooleanParametersRule = defineRule({
2131
+ meta: {
2132
+ type: "suggestion",
2133
+ docs: { description: "Disallow explicit boolean parameters on repository-owned named callables." },
2134
+ messages: { positionalBoolean: "Parameter `{{parameter}}` is a positional boolean flag on `{{functionName}}`. Replace it with a named options object." },
2135
+ schema: [{
2136
+ type: "object",
2137
+ properties: { allowFunctionNames: {
2138
+ type: "array",
2139
+ items: {
2140
+ type: "string",
2141
+ minLength: 1
2142
+ },
2143
+ uniqueItems: true
2144
+ } },
2145
+ additionalProperties: false
2146
+ }],
2147
+ defaultOptions: [{ allowFunctionNames: [] }]
2148
+ },
2149
+ createOnce(context) {
2150
+ const checkFunction = (node) => {
2151
+ const functionName$1 = getOwnedFunctionName(node);
2152
+ if (functionName$1 === null) return;
2153
+ const rawOptions = context.options;
2154
+ const parsedOptions = ContextOptionsSchema$2.safeParse(rawOptions);
2155
+ if ((parsedOptions.success ? parsedOptions.data : void 0)?.allowFunctionNames?.includes(functionName$1) === true) return;
2156
+ for (const parameter of node.params) {
2157
+ if (parameter.type === "RestElement") continue;
2158
+ const annotation = annotationOf(parameter);
2159
+ if (annotation === null || annotation === void 0 || !resolvedTypeIncludesMatch({
2160
+ isMatch: isBooleanType,
2161
+ shadowedTypeNames: lexicalTypeParameterNames(node, context.sourceCode.visitorKeys),
2162
+ sourceCode: context.sourceCode,
2163
+ type: annotation.typeAnnotation
2164
+ })) continue;
2165
+ context.report({
2166
+ node: annotation.typeAnnotation,
2167
+ messageId: "positionalBoolean",
2168
+ data: {
2169
+ functionName: functionName$1,
2170
+ parameter: parameterName$1(parameter, context.sourceCode)
2171
+ }
2172
+ });
2173
+ }
2174
+ };
2175
+ return {
2176
+ ArrowFunctionExpression: checkFunction,
2177
+ FunctionDeclaration: checkFunction,
2178
+ FunctionExpression: checkFunction,
2179
+ TSCallSignatureDeclaration: checkFunction,
2180
+ TSConstructSignatureDeclaration: checkFunction,
2181
+ TSConstructorType: checkFunction,
2182
+ TSDeclareFunction: checkFunction,
2183
+ TSEmptyBodyFunctionExpression: checkFunction,
2184
+ TSFunctionType: checkFunction,
2185
+ TSMethodSignature: checkFunction
2186
+ };
2187
+ }
2188
+ });
2189
+
2190
+ //#endregion
2191
+ //#region src/rules/no-promise-settlement-only-assertion.ts
2192
+ /** Require promise assertions to describe a result or specific failure. */
2193
+ const noPromiseSettlementOnlyAssertionRule = defineRule({
2194
+ meta: {
2195
+ type: "problem",
2196
+ docs: { description: "Disallow promise assertions that only prove fulfillment or rejection." },
2197
+ messages: {
2198
+ fulfillmentOnly: "Await this promise directly, then assert its observable result or effect.",
2199
+ rejectionOnly: "Assert the rejection's class, code, message, or other behavioral contract."
2200
+ }
2201
+ },
2202
+ createOnce(context) {
2203
+ return { CallExpression(node) {
2204
+ if (node.callee.type !== "MemberExpression" || !isExpectationMatcher(context.sourceCode, node)) return;
2205
+ const matcherName = staticMemberName(node.callee);
2206
+ if (matcherName === "toBeUndefined" && hasExpectationModifier(node.callee.object, "resolves")) {
2207
+ context.report({
2208
+ node,
2209
+ messageId: "fulfillmentOnly"
2210
+ });
2211
+ return;
2212
+ }
2213
+ if (matcherName === "toBeDefined" && hasExpectationModifier(node.callee.object, "rejects")) context.report({
2214
+ node,
2215
+ messageId: "rejectionOnly"
2216
+ });
2217
+ } };
2218
+ }
2219
+ });
2220
+
2221
+ //#endregion
2222
+ //#region src/shared/reflect-method.ts
2223
+ function isGlobalReflect(sourceCode, expression) {
2224
+ if (expression.type === "Identifier" && expression.name === "Reflect") {
2225
+ const variable$1 = resolveVariable(sourceCode, expression);
2226
+ return variable$1 === null || variable$1.defs.length === 0;
2227
+ }
2228
+ if (expression.type !== "MemberExpression" || staticMemberName(expression) !== "Reflect" || expression.object.type !== "Identifier" || expression.object.name !== "globalThis") return false;
2229
+ const variable = resolveVariable(sourceCode, expression.object);
2230
+ return variable === null || variable.defs.length === 0;
2231
+ }
2232
+ /** Reports whether a call target names one method on the global Reflect object. */
2233
+ function isGlobalReflectMethodCall({ callee, methodName, sourceCode }) {
2234
+ if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
2235
+ if (!isGlobalReflect(sourceCode, callee.object)) return false;
2236
+ return staticMemberName(callee) === methodName;
2237
+ }
2238
+
2239
+ //#endregion
2240
+ //#region src/rules/no-reflect-apply.ts
2241
+ /** Ban Reflect.apply, which bypasses ordinary typed function calls. */
2242
+ const noReflectApplyRule = defineRule({
2243
+ meta: {
2244
+ type: "problem",
2245
+ docs: { description: "Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface." },
2246
+ messages: { reflectApply: "Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface." }
2247
+ },
2248
+ createOnce(context) {
2249
+ return { CallExpression(node) {
2250
+ if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
2251
+ if (isGlobalReflectMethodCall({
2252
+ callee: node.callee,
2253
+ methodName: "apply",
2254
+ sourceCode: context.sourceCode
2255
+ })) context.report({
2256
+ node,
2257
+ messageId: "reflectApply"
2258
+ });
2259
+ } };
2260
+ }
2261
+ });
2262
+
2263
+ //#endregion
2264
+ //#region src/rules/no-reflect-get.ts
2265
+ /** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
2266
+ const noReflectGetRule = defineRule({
2267
+ meta: {
2268
+ type: "problem",
2269
+ docs: { description: "Disallow Reflect.get; use typed property access or parse dynamic input into a domain type." },
2270
+ messages: { reflectGet: "Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it." }
2271
+ },
2272
+ createOnce(context) {
2273
+ return { CallExpression(node) {
2274
+ if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
2275
+ if (isGlobalReflectMethodCall({
2276
+ callee: node.callee,
2277
+ methodName: "get",
2278
+ sourceCode: context.sourceCode
2279
+ })) context.report({
2280
+ node,
2281
+ messageId: "reflectGet"
2282
+ });
2283
+ } };
2284
+ }
2285
+ });
2286
+
2287
+ //#endregion
2288
+ //#region src/rules/no-test-snapshots.ts
2289
+ const snapshotMatchers = new Set([
2290
+ "addSnapshotSerializer",
2291
+ "toMatchFileSnapshot",
2292
+ "toMatchInlineSnapshot",
2293
+ "toMatchSnapshot",
2294
+ "toThrowErrorMatchingInlineSnapshot",
2295
+ "toThrowErrorMatchingSnapshot"
2296
+ ]);
2297
+ /** Require explicit assertions instead of stored or inline snapshots. */
2298
+ const noTestSnapshotsRule = defineRule({
2299
+ meta: {
2300
+ type: "problem",
2301
+ docs: { description: "Disallow snapshot assertions because they obscure the behavior a test protects." },
2302
+ messages: { snapshot: "Replace this snapshot with explicit assertions for the observable contract." }
2303
+ },
2304
+ createOnce(context) {
2305
+ return { CallExpression(node) {
2306
+ if (node.callee.type !== "MemberExpression" || !isExpectationMatcher(context.sourceCode, node) && !isExpectationMemberCall(context.sourceCode, node) || !snapshotMatchers.has(staticMemberName(node.callee) ?? "")) return;
2307
+ context.report({
2308
+ node,
2309
+ messageId: "snapshot"
2310
+ });
2311
+ } };
2312
+ }
2313
+ });
2314
+
2315
+ //#endregion
2316
+ //#region src/rules/no-truthy-falsy-assertion.ts
2317
+ const truthinessMatchers = new Set(["toBeFalsy", "toBeTruthy"]);
2318
+ function isBareNodeTruthinessSubject(expression) {
2319
+ let currentExpression = expression;
2320
+ while (currentExpression.type === "ChainExpression" || currentExpression.type === "ParenthesizedExpression" || currentExpression.type === "TSAsExpression" || currentExpression.type === "TSNonNullExpression" || currentExpression.type === "TSSatisfiesExpression" || currentExpression.type === "TSTypeAssertion") currentExpression = currentExpression.expression;
2321
+ return currentExpression.type === "Identifier" || currentExpression.type === "MemberExpression";
2322
+ }
2323
+ /** Require exact expected values instead of truthiness assertions. */
2324
+ const noTruthyFalsyAssertionRule = defineRule({
2325
+ meta: {
2326
+ type: "problem",
2327
+ docs: { description: "Disallow truthiness assertions because they discard value and type information." },
2328
+ messages: { truthiness: "Assert the exact expected value instead of relying on truthiness." }
2329
+ },
2330
+ createOnce(context) {
2331
+ return { CallExpression(node) {
2332
+ const assertion = nodeAssertCall(context.sourceCode, node);
2333
+ const [subject] = assertion?.arguments ?? [];
2334
+ if (assertion?.methodName === "ok" && subject !== void 0 && subject.type !== "SpreadElement" && isBareNodeTruthinessSubject(subject)) {
2335
+ context.report({
2336
+ node,
2337
+ messageId: "truthiness"
2338
+ });
2339
+ return;
2340
+ }
2341
+ if (node.callee.type !== "MemberExpression" || !isExpectationMatcher(context.sourceCode, node) || !truthinessMatchers.has(staticMemberName(node.callee) ?? "")) return;
2342
+ context.report({
2343
+ node,
2344
+ messageId: "truthiness"
2345
+ });
2346
+ } };
2347
+ }
2348
+ });
2349
+
2350
+ //#endregion
2351
+ //#region src/rules/no-uncontrolled-time-in-test.ts
2352
+ function isGlobalDate(sourceCode, identifier) {
2353
+ const variable = resolveVariable(sourceCode, identifier);
2354
+ return variable === null || variable.defs.length === 0;
2355
+ }
2356
+ function hasNowOption(expression) {
2357
+ if (expression.type !== "ObjectExpression") return false;
2358
+ return expression.properties.some((property) => {
2359
+ if (property.type !== "Property") return false;
2360
+ if (!property.computed && property.key.type === "Identifier") return property.key.name === "now";
2361
+ return property.computed && property.key.type === "Literal" && property.key.value === "now";
2362
+ });
2363
+ }
2364
+ function controlsTime(sourceCode, node) {
2365
+ if (node.callee.type !== "MemberExpression" || !isTestFrameworkControlCall(sourceCode, node)) return false;
2366
+ const memberName = staticMemberName(node.callee);
2367
+ if (memberName === "setSystemTime") return true;
2368
+ if (memberName !== "useFakeTimers") return false;
2369
+ const [options] = node.arguments;
2370
+ return options !== void 0 && options.type !== "SpreadElement" ? hasNowOption(options) : false;
2371
+ }
2372
+ function wallClockRead(sourceCode, node) {
2373
+ if (node.type === "CallExpression") {
2374
+ if (node.callee.type === "MemberExpression" && staticMemberName(node.callee) === "now" && node.callee.object.type === "Identifier" && node.callee.object.name === "Date" && isGlobalDate(sourceCode, node.callee.object)) return node;
2375
+ if (node.callee.type === "Identifier" && node.callee.name === "Date" && isGlobalDate(sourceCode, node.callee)) return node;
2376
+ }
2377
+ if (node.type === "NewExpression" && node.arguments.length === 0 && node.callee.type === "Identifier" && node.callee.name === "Date" && isGlobalDate(sourceCode, node.callee)) return node;
2378
+ return null;
2379
+ }
2380
+ function timeEvent(sourceCode, node) {
2381
+ if (node.type === "CallExpression" && controlsTime(sourceCode, node)) return { kind: "control" };
2382
+ const read = wallClockRead(sourceCode, node);
2383
+ return read === null ? null : {
2384
+ kind: "read",
2385
+ node: read
2386
+ };
2387
+ }
2388
+ function getTimeEvents({ root, sourceCode }) {
2389
+ const events = [];
2390
+ visitExecutedNodes({
2391
+ root,
2392
+ sourceCode,
2393
+ visit(node) {
2394
+ const event = timeEvent(sourceCode, node);
2395
+ if (event !== null) events.push(event);
2396
+ }
2397
+ });
2398
+ return events;
2399
+ }
2400
+ function getSuiteExecution({ root, sourceCode }) {
2401
+ const suite = {
2402
+ events: [],
2403
+ hooks: [],
2404
+ suites: [],
2405
+ tests: []
2406
+ };
2407
+ visitExecutedNodes({
2408
+ root,
2409
+ sourceCode,
2410
+ visit(node) {
2411
+ const event = timeEvent(sourceCode, node);
2412
+ if (event !== null) suite.events.push(event);
2413
+ if (node.type !== "CallExpression") return;
2414
+ const frameworkCall = getTestFrameworkCall(sourceCode, node);
2415
+ if (frameworkCall === null) return;
2416
+ if (frameworkCall.kind === "test") {
2417
+ suite.tests.push(frameworkCall);
2418
+ return;
2419
+ }
2420
+ if (frameworkCall.kind === "suite") {
2421
+ if (frameworkCall.callback !== null) suite.suites.push(getSuiteExecution({
2422
+ root: frameworkCall.callback,
2423
+ sourceCode
2424
+ }));
2425
+ return;
2426
+ }
2427
+ suite.hooks.push(frameworkCall);
2428
+ }
2429
+ });
2430
+ return suite;
2431
+ }
2432
+ function testCountOf(suite) {
2433
+ return suite.tests.length + suite.suites.reduce((count, childSuite) => count + testCountOf(childSuite), 0);
2434
+ }
2435
+ /** Require tests to control wall-clock inputs. */
2436
+ const noUncontrolledTimeInTestRule = defineRule({
2437
+ meta: {
2438
+ type: "problem",
2439
+ docs: { description: "Disallow uncontrolled wall-clock reads in files that contain tests." },
2440
+ messages: { uncontrolledTime: "Control the test clock before reading the current time, or inject the time as an input." }
2441
+ },
2442
+ createOnce(context) {
2443
+ const reportedReads = /* @__PURE__ */ new Set();
2444
+ function reportUncontrolledEvents({ events, hasInitialControl }) {
2445
+ let hasControl = hasInitialControl;
2446
+ for (const event of events) {
2447
+ if (event.kind === "control") {
2448
+ hasControl = true;
2449
+ continue;
2450
+ }
2451
+ if (hasControl || reportedReads.has(event.node)) continue;
2452
+ reportedReads.add(event.node);
2453
+ context.report({
2454
+ node: event.node,
2455
+ messageId: "uncontrolledTime"
2456
+ });
2457
+ }
2458
+ return hasControl;
2459
+ }
2460
+ function checkSuite({ suite, hasInheritedControl }) {
2461
+ let hasSharedControl = reportUncontrolledEvents({
2462
+ events: suite.events,
2463
+ hasInitialControl: hasInheritedControl
2464
+ });
2465
+ for (const hook of suite.hooks) {
2466
+ if (hook.callback === null) continue;
2467
+ const hasHookControl = reportUncontrolledEvents({
2468
+ events: getTimeEvents({
2469
+ root: hook.callback,
2470
+ sourceCode: context.sourceCode
2471
+ }),
2472
+ hasInitialControl: false
2473
+ });
2474
+ if (hook.kind === "setup-hook" && hasHookControl) hasSharedControl = true;
2475
+ }
2476
+ for (const test of suite.tests) {
2477
+ if (test.callback === null) continue;
2478
+ reportUncontrolledEvents({
2479
+ events: getTimeEvents({
2480
+ root: test.callback,
2481
+ sourceCode: context.sourceCode
2482
+ }),
2483
+ hasInitialControl: hasSharedControl
2484
+ });
2485
+ }
2486
+ for (const childSuite of suite.suites) checkSuite({
2487
+ suite: childSuite,
2488
+ hasInheritedControl: hasSharedControl
2489
+ });
2490
+ }
2491
+ return { "Program:exit"(node) {
2492
+ reportedReads.clear();
2493
+ const suite = getSuiteExecution({
2494
+ root: node,
2495
+ sourceCode: context.sourceCode
2496
+ });
2497
+ if (testCountOf(suite) > 0) checkSuite({
2498
+ suite,
2499
+ hasInheritedControl: false
2500
+ });
2501
+ } };
2502
+ }
2503
+ });
2504
+
2505
+ //#endregion
2506
+ //#region src/rules/no-unhandled-detached-promises.ts
2507
+ function unwrapExpression$1(expression) {
2508
+ if (expression.type === "ChainExpression" || expression.type === "ParenthesizedExpression" || expression.type === "TSAsExpression" || expression.type === "TSNonNullExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSTypeAssertion") return unwrapExpression$1(expression.expression);
2509
+ return expression;
2510
+ }
2511
+ function isRejectionHandler(argument) {
2512
+ if (argument === void 0 || argument.type === "SpreadElement") return false;
2513
+ const unwrapped = unwrapExpression$1(argument);
2514
+ if (unwrapped.type === "Identifier") return unwrapped.name !== "undefined";
2515
+ return ![
2516
+ "ArrayExpression",
2517
+ "BinaryExpression",
2518
+ "JSXElement",
2519
+ "JSXFragment",
2520
+ "Literal",
2521
+ "ObjectExpression",
2522
+ "TemplateLiteral"
2523
+ ].includes(unwrapped.type);
2524
+ }
2525
+ function hasRejectionHandler(expression) {
2526
+ const unwrapped = unwrapExpression$1(expression);
2527
+ if (unwrapped.type !== "CallExpression") return false;
2528
+ if (unwrapped.callee.type !== "Super" && unwrapped.callee.type !== "V8IntrinsicExpression" && "object" in unwrapped.callee && "property" in unwrapped.callee) {
2529
+ const memberName = staticMemberName(unwrapped.callee);
2530
+ if (memberName === "catch") return isRejectionHandler(unwrapped.arguments[0]);
2531
+ if (memberName === "then") return isRejectionHandler(unwrapped.arguments[1]);
2532
+ if (memberName === "finally") return false;
2533
+ }
2534
+ return false;
2535
+ }
2536
+ /** Treat `void` calls as detached work and require a rejection handler. */
2537
+ const noUnhandledDetachedPromisesRule = defineRule({
2538
+ meta: {
2539
+ type: "problem",
2540
+ docs: { description: "Disallow void-marked detached call chains that do not install a rejection handler." },
2541
+ messages: { unhandledDetachedPromise: "A `void` call marks detached work and must handle rejection. Add `.catch(...)` or a second `.then(...)` callback at this boundary." }
2542
+ },
2543
+ createOnce(context) {
2544
+ return { UnaryExpression(node) {
2545
+ if (node.operator === "void" && unwrapExpression$1(node.argument).type === "CallExpression" && !hasRejectionHandler(node.argument)) context.report({
2546
+ node,
2547
+ messageId: "unhandledDetachedPromise"
2548
+ });
2549
+ } };
2550
+ }
2551
+ });
2552
+
2553
+ //#endregion
2554
+ //#region src/shared/boundary-decoder.ts
2555
+ function parameterType$1(parameter) {
2556
+ if (parameter.type === "TSParameterProperty") return parameterType$1(parameter.parameter);
2557
+ if (parameter.type === "AssignmentPattern") return parameterType$1(parameter.left);
2558
+ if (parameter.type === "RestElement") {
2559
+ const annotation = parameter.typeAnnotation;
2560
+ if (annotation?.typeAnnotation.type === "TSArrayType") return annotation.typeAnnotation.elementType;
2561
+ return parameterType$1(parameter.argument);
2562
+ }
2563
+ return parameter.typeAnnotation?.typeAnnotation ?? null;
2564
+ }
2565
+ function hasDecodedReturnType(owner) {
2566
+ const returnType = owner.returnType?.typeAnnotation;
2567
+ if (returnType === void 0) return false;
2568
+ return ![
2569
+ "TSAnyKeyword",
2570
+ "TSUndefinedKeyword",
2571
+ "TSUnknownKeyword",
2572
+ "TSVoidKeyword"
2573
+ ].includes(returnType.type);
2574
+ }
2575
+ function bindingIdentifiers(parameter) {
2576
+ if (parameter.type === "Identifier") return [parameter];
2577
+ if (parameter.type === "TSParameterProperty") return bindingIdentifiers(parameter.parameter);
2578
+ if (parameter.type === "AssignmentPattern") return bindingIdentifiers(parameter.left);
2579
+ if (parameter.type === "RestElement") return bindingIdentifiers(parameter.argument);
2580
+ if (parameter.type === "ArrayPattern") return parameter.elements.flatMap((element) => element === null ? [] : bindingIdentifiers(element));
2581
+ return parameter.properties.flatMap((property) => property.type === "Property" ? bindingIdentifiers(property.value) : bindingIdentifiers(property.argument));
2582
+ }
2583
+ function executableBody(owner) {
2584
+ return owner.body ?? null;
2585
+ }
2586
+ function readsParameter({ owner, parameter, sourceCode }) {
2587
+ const body = executableBody(owner);
2588
+ if (body === null) return false;
2589
+ return bindingIdentifiers(parameter).some((identifier) => {
2590
+ return resolveVariable(sourceCode, identifier)?.references.some((reference) => reference.isRead() && reference.identifier.range[0] >= body.range[0] && reference.identifier.range[1] <= body.range[1]) === true;
2591
+ });
2592
+ }
2593
+ /** Identify a boundary that converts an explicitly untrusted input into a typed result. */
2594
+ function isBoundaryDecoder({ owner, parameter, sourceCode }) {
2595
+ return parameterType$1(parameter)?.type === "TSUnknownKeyword" && hasDecodedReturnType(owner) && (executableBody(owner) === null || readsParameter({
2596
+ owner,
2597
+ parameter,
2598
+ sourceCode
2599
+ }));
2600
+ }
2601
+
2602
+ //#endregion
2603
+ //#region src/rules/no-unknown-parameters.ts
2604
+ const ContextOptionsSchema$1 = ruleContextOptionsSchema(z.object({ allowParameterNames: z.array(z.string()).optional() }));
2605
+ function parameterAnnotation(parameter) {
2606
+ if (parameter.type === "TSParameterProperty") return parameterAnnotation(parameter.parameter);
2607
+ if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
2608
+ if (parameter.type === "AssignmentPattern") return parameter.left.typeAnnotation;
2609
+ return parameter.typeAnnotation;
2610
+ }
2611
+ function parameterType(parameter) {
2612
+ const annotation = parameterAnnotation(parameter);
2613
+ if (annotation === null || annotation === void 0) return null;
2614
+ const type = annotation.typeAnnotation;
2615
+ return parameter.type === "RestElement" && type.type === "TSArrayType" ? type.elementType : type;
2616
+ }
2617
+ function parameterName(parameter, sourceText) {
2618
+ if (parameter.type === "TSParameterProperty") return parameterName(parameter.parameter, sourceText);
2619
+ if (parameter.type === "AssignmentPattern") return parameterName(parameter.left, sourceText);
2620
+ if (parameter.type === "RestElement") return parameterName(parameter.argument, sourceText);
2621
+ return parameter.type === "Identifier" ? parameter.name : sourceText.replace(/\s*:\s*unknown\s*$/u, "");
2622
+ }
2623
+ /** Keep unknown inputs at explicit decoding and error-enrichment boundaries. */
2624
+ const noUnknownParametersRule = defineRule({
2625
+ meta: {
2626
+ type: "problem",
2627
+ docs: { description: "Disallow explicitly unknown parameters outside decoders and error-cause enrichment boundaries." },
2628
+ messages: { unknownParameter: "Parameter `{{parameter}}` leaves input unparsed. Accept a named domain type; run the expected schema or parser at the I/O boundary before calling this function." },
2629
+ schema: [{
2630
+ type: "object",
2631
+ properties: { allowParameterNames: {
2632
+ type: "array",
2633
+ items: {
2634
+ type: "string",
2635
+ minLength: 1
2636
+ },
2637
+ uniqueItems: true
2638
+ } },
2639
+ additionalProperties: false
2640
+ }],
2641
+ defaultOptions: [{ allowParameterNames: [] }]
2642
+ },
2643
+ createOnce(context) {
2644
+ const checkParameters = (node) => {
2645
+ const parsedOptions = ContextOptionsSchema$1.safeParse(context.options);
2646
+ const options = parsedOptions.success ? parsedOptions.data : void 0;
2647
+ for (const parameter of node.params) {
2648
+ const type = parameterType(parameter);
2649
+ if (type?.type !== "TSUnknownKeyword") continue;
2650
+ const name = parameterName(parameter, context.sourceCode.getText(parameter));
2651
+ if (name === "cause" || options?.allowParameterNames?.includes(name) === true || isBoundaryDecoder({
2652
+ owner: node,
2653
+ parameter,
2654
+ sourceCode: context.sourceCode
2655
+ })) continue;
2656
+ context.report({
2657
+ node: type,
2658
+ messageId: "unknownParameter",
2659
+ data: { parameter: name }
2660
+ });
2661
+ }
2662
+ };
2663
+ return {
2664
+ ArrowFunctionExpression: checkParameters,
2665
+ FunctionDeclaration: checkParameters,
2666
+ FunctionExpression: checkParameters,
2667
+ TSCallSignatureDeclaration: checkParameters,
2668
+ TSConstructSignatureDeclaration: checkParameters,
2669
+ TSConstructorType: checkParameters,
2670
+ TSDeclareFunction: checkParameters,
2671
+ TSEmptyBodyFunctionExpression: checkParameters,
2672
+ TSFunctionType: checkParameters,
2673
+ TSMethodSignature: checkParameters
2674
+ };
2675
+ }
2676
+ });
2677
+
2678
+ //#endregion
2679
+ //#region src/rules/no-unknown-returns.ts
2680
+ /** Ban function contracts that return unknown instead of a parsed domain type. */
2681
+ const noUnknownReturnsRule = defineRule({
2682
+ meta: {
2683
+ type: "problem",
2684
+ docs: { description: "Disallow functions whose explicit return contract is unknown or Promise<unknown>." },
2685
+ messages: { unknownReturn: "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type." }
2686
+ },
2687
+ createOnce(context) {
2688
+ const checkReturnType = (node) => {
2689
+ const annotation = node.returnType;
2690
+ if (annotation === null || annotation === void 0) return;
2691
+ if (!resolvedTypeIncludesMatch({
2692
+ isMatch: (type) => type.type === "TSUnknownKeyword",
2693
+ shadowedTypeNames: lexicalTypeParameterNames(node, context.sourceCode.visitorKeys),
2694
+ sourceCode: context.sourceCode,
2695
+ transparentTypeNames: new Set(["Promise", "PromiseLike"]),
2696
+ type: annotation.typeAnnotation
2697
+ })) return;
2698
+ context.report({
2699
+ node: annotation.typeAnnotation,
2700
+ messageId: "unknownReturn"
2701
+ });
2702
+ };
2703
+ return {
2704
+ ArrowFunctionExpression: checkReturnType,
2705
+ FunctionDeclaration: checkReturnType,
2706
+ FunctionExpression: checkReturnType,
2707
+ TSCallSignatureDeclaration: checkReturnType,
2708
+ TSConstructSignatureDeclaration: checkReturnType,
2709
+ TSConstructorType: checkReturnType,
2710
+ TSDeclareFunction: checkReturnType,
2711
+ TSEmptyBodyFunctionExpression: checkReturnType,
2712
+ TSFunctionType: checkReturnType,
2713
+ TSMethodSignature: checkReturnType
2714
+ };
2715
+ }
2716
+ });
2717
+
2718
+ //#endregion
2719
+ //#region src/rules/no-unknown-type-aliases.ts
2720
+ /** Ban named aliases that merely conceal TypeScript's unknown top type. */
2721
+ const noUnknownTypeAliasesRule = defineRule({
2722
+ meta: {
2723
+ type: "problem",
2724
+ docs: { description: "Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary." },
2725
+ messages: { unknownAlias: "Type alias `{{alias}}` hides `unknown`. Keep `unknown` explicit at the parsing boundary or on an allowed `cause` field; otherwise use the parsed owner type." }
2726
+ },
2727
+ createOnce(context) {
2728
+ return { TSTypeAliasDeclaration(node) {
2729
+ if (!resolvedTypeIncludesMatch({
2730
+ isMatch: (type) => type.type === "TSUnknownKeyword",
2731
+ shadowedTypeNames: lexicalTypeParameterNames(node, context.sourceCode.visitorKeys),
2732
+ sourceCode: context.sourceCode,
2733
+ type: node.typeAnnotation
2734
+ })) return;
2735
+ context.report({
2736
+ node: node.id,
2737
+ messageId: "unknownAlias",
2738
+ data: { alias: node.id.name }
2739
+ });
2740
+ } };
2741
+ }
2742
+ });
2743
+
2744
+ //#endregion
2745
+ //#region src/rules/no-unsafe-dictionary-type.ts
2746
+ const typeNodeKinds = new Set([
2747
+ "JSDocNonNullableType",
2748
+ "JSDocNullableType",
2749
+ "JSDocUnknownType",
2750
+ "TSAnyKeyword",
2751
+ "TSArrayType",
2752
+ "TSBigIntKeyword",
2753
+ "TSBooleanKeyword",
2754
+ "TSConditionalType",
2755
+ "TSConstructorType",
2756
+ "TSFunctionType",
2757
+ "TSImportType",
2758
+ "TSIndexedAccessType",
2759
+ "TSInferType",
2760
+ "TSIntersectionType",
2761
+ "TSIntrinsicKeyword",
2762
+ "TSLiteralType",
2763
+ "TSMappedType",
2764
+ "TSNamedTupleMember",
2765
+ "TSNeverKeyword",
2766
+ "TSNullKeyword",
2767
+ "TSNumberKeyword",
2768
+ "TSObjectKeyword",
2769
+ "TSParenthesizedType",
2770
+ "TSStringKeyword",
2771
+ "TSSymbolKeyword",
2772
+ "TSTemplateLiteralType",
2773
+ "TSThisType",
2774
+ "TSTupleType",
2775
+ "TSTypeLiteral",
2776
+ "TSTypeOperator",
2777
+ "TSTypePredicate",
2778
+ "TSTypeQuery",
2779
+ "TSTypeReference",
2780
+ "TSUndefinedKeyword",
2781
+ "TSUnionType",
2782
+ "TSUnknownKeyword",
2783
+ "TSVoidKeyword"
2784
+ ]);
2785
+ function isTypeNode(node) {
2786
+ return typeNodeKinds.has(node.type);
2787
+ }
2788
+ function typeReferenceName$1(type) {
2789
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
2790
+ }
2791
+ function isInsideTypeAliasDeclaration(node) {
2792
+ let current = node.parent;
2793
+ while (current !== null && current.type !== "Program") {
2794
+ if (current.type === "TSTypeAliasDeclaration") return true;
2795
+ current = current.parent;
2796
+ }
2797
+ return false;
2798
+ }
2799
+ function isPlainAliasConsumerUse(node, environment) {
2800
+ if (node.type !== "TSTypeReference" || (node.typeArguments?.params.length ?? 0) > 0) return false;
2801
+ const name = typeReferenceName$1(node);
2802
+ return name !== null && (environment.aliases.has(name) || resolveTypeAlias(environment.sourceCode, node) !== null) && !isInsideTypeAliasDeclaration(node);
2803
+ }
2804
+ function shouldReportType(node, environment) {
2805
+ if (isPlainAliasConsumerUse(node, environment)) return false;
2806
+ if (classifyUnsafeDictionary(node, environment) === null) return false;
2807
+ let current = node.parent;
2808
+ while (current.type !== "Program") {
2809
+ if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) return false;
2810
+ current = current.parent;
2811
+ }
2812
+ return true;
2813
+ }
2814
+ /** Disallow dictionary contracts whose direct value type is an unsafe escape hatch. */
2815
+ const noUnsafeDictionaryTypeRule = defineRule({
2816
+ meta: {
2817
+ type: "problem",
2818
+ docs: { description: "Disallow dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches." },
2819
+ messages: { unsafeDictionary: "This dictionary's {{value}} value type gives callers no concrete value contract. Use an owner/schema-derived value type; parse external payloads before insertion." }
2820
+ },
2821
+ createOnce(context) {
2822
+ let environment;
2823
+ const report = (node, value) => {
2824
+ context.report({
2825
+ node,
2826
+ messageId: "unsafeDictionary",
2827
+ data: { value }
2828
+ });
2829
+ };
2830
+ const reportIfUnsafe = (node) => {
2831
+ if (!shouldReportType(node, environment)) return;
2832
+ const unsafe = classifyUnsafeDictionary(node, environment);
2833
+ if (unsafe === null) return;
2834
+ report(node, unsafe.unsafeValue);
2835
+ };
2836
+ return {
2837
+ Program(node) {
2838
+ environment = createTypeEnvironment(node, context.sourceCode);
2839
+ },
2840
+ TSTypeReference: reportIfUnsafe,
2841
+ TSTypeLiteral: reportIfUnsafe,
2842
+ TSMappedType: reportIfUnsafe,
2843
+ TSIndexSignature(node) {
2844
+ if (node.parent.type === "TSTypeLiteral") return;
2845
+ const unsafe = classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment);
2846
+ if (unsafe !== null) report(node, unsafe.unsafeValue);
2847
+ }
2848
+ };
2849
+ }
2850
+ });
2851
+
2852
+ //#endregion
2853
+ //#region src/rules/no-widen-then-assert.ts
2854
+ const functionBoundaryTypes = new Set([
2855
+ "ArrowFunctionExpression",
2856
+ "FunctionDeclaration",
2857
+ "FunctionExpression",
2858
+ "TSDeclareFunction",
2859
+ "TSEmptyBodyFunctionExpression"
2860
+ ]);
2861
+ function unwrapExpressionParentheses(expression) {
2862
+ let current = expression;
2863
+ while (current.type === "ParenthesizedExpression") current = current.expression;
2864
+ return current;
2865
+ }
2866
+ function unwrapTypeParentheses(type) {
2867
+ let current = type;
2868
+ while (current.type === "TSParenthesizedType") current = current.typeAnnotation;
2869
+ return current;
2870
+ }
2871
+ function typeReferenceName(type) {
2872
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
2873
+ }
2874
+ function isUnknownOrAnyType(type) {
2875
+ const unwrapped = unwrapTypeParentheses(type);
2876
+ return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
2877
+ }
2878
+ function isBroadRecordKeyType(type) {
2879
+ const unwrapped = unwrapTypeParentheses(type);
2880
+ if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
2881
+ if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
2882
+ return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
2883
+ }
2884
+ function isBroadRecordType(type) {
2885
+ const unwrapped = unwrapTypeParentheses(type);
2886
+ if (unwrapped.type === "TSTypeReference") {
2887
+ if (typeReferenceName(unwrapped) === "Readonly") {
2888
+ const [inner] = unwrapped.typeArguments?.params ?? [];
2889
+ return inner !== void 0 && isBroadRecordType(inner);
2890
+ }
2891
+ if (typeReferenceName(unwrapped) !== "Record") return false;
2892
+ const parameters = unwrapped.typeArguments?.params ?? [];
2893
+ return parameters.length === 2 && parameters[0] !== void 0 && parameters[1] !== void 0 && isBroadRecordKeyType(parameters[0]) && isUnknownOrAnyType(parameters[1]);
2894
+ }
2895
+ if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
2896
+ const [member] = unwrapped.members;
2897
+ const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
2898
+ return member?.type === "TSIndexSignature" && member.parameters.length === 1 && parameter !== void 0 && isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
2899
+ }
2900
+ function broadTypeKind(type) {
2901
+ const unwrapped = unwrapTypeParentheses(type);
2902
+ if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
2903
+ if (unwrapped.type === "TSObjectKeyword") return "object";
2904
+ return isBroadRecordType(unwrapped) ? "record" : null;
2905
+ }
2906
+ function assertedExpression(node) {
2907
+ return unwrapExpressionParentheses(node.expression);
2908
+ }
2909
+ function assertionFromExpression(expression) {
2910
+ const unwrapped = unwrapExpressionParentheses(expression);
2911
+ return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion" ? unwrapped : null;
2912
+ }
2913
+ function normalizedTypeText(sourceText, type) {
2914
+ return sourceText.slice(type.range[0], type.range[1]).replaceAll(/\s+/gu, "");
2915
+ }
2916
+ function typesHaveSameSyntax({ left, right, sourceText }) {
2917
+ return left !== null && normalizedTypeText(sourceText, unwrapTypeParentheses(left)) === normalizedTypeText(sourceText, unwrapTypeParentheses(right));
2918
+ }
2919
+ function isDefinitelyObjectType(type) {
2920
+ const unwrapped = unwrapTypeParentheses(type);
2921
+ if (unwrapped.type === "TSArrayType" || unwrapped.type === "TSConstructorType" || unwrapped.type === "TSFunctionType" || unwrapped.type === "TSMappedType" || unwrapped.type === "TSObjectKeyword" || unwrapped.type === "TSTupleType") return true;
2922
+ if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.length > 0;
2923
+ if (unwrapped.type === "TSIntersectionType") return unwrapped.types.every(isDefinitelyObjectType);
2924
+ if (unwrapped.type === "TSTypeOperator") return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
2925
+ return false;
2926
+ }
2927
+ function isDefinitelyNarrowerRecordType(type) {
2928
+ const unwrapped = unwrapTypeParentheses(type);
2929
+ if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
2930
+ if (unwrapped.type !== "TSTypeReference") return false;
2931
+ if (typeReferenceName(unwrapped) === "Readonly") {
2932
+ const [inner] = unwrapped.typeArguments?.params ?? [];
2933
+ return inner !== void 0 && isDefinitelyNarrowerRecordType(inner);
2934
+ }
2935
+ if (typeReferenceName(unwrapped) !== "Record") return false;
2936
+ const parameters = unwrapped.typeArguments?.params ?? [];
2937
+ return parameters.length === 2 && parameters[1] !== void 0 && !isUnknownOrAnyType(parameters[1]);
2938
+ }
2939
+ function functionBoundary(node) {
2940
+ let current = node.parent;
2941
+ while (current !== null && current.type !== "Program") {
2942
+ if (functionBoundaryTypes.has(current.type)) return current;
2943
+ current = current.parent;
2944
+ }
2945
+ return null;
2946
+ }
2947
+ function resolvedVariableForIdentifier(scopes, identifier) {
2948
+ for (const scope of scopes) {
2949
+ const reference = scope.references.find((candidate) => candidate.identifier.range[0] === identifier.range[0] && candidate.identifier.range[1] === identifier.range[1]);
2950
+ if (reference !== void 0) return reference.resolved;
2951
+ }
2952
+ return null;
2953
+ }
2954
+ function variableDeclarator(variable) {
2955
+ for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return definition.node;
2956
+ return null;
2957
+ }
2958
+ function knownValueEvidence({ boundary, expression, scopes, visitedVariables }) {
2959
+ const unwrapped = unwrapExpressionParentheses(expression);
2960
+ if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
2961
+ if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
2962
+ return { type: unwrapped.typeAnnotation };
2963
+ }
2964
+ if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return { type: null };
2965
+ if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return { type: null };
2966
+ if (unwrapped.type !== "Identifier") return null;
2967
+ const variable = resolvedVariableForIdentifier(scopes, unwrapped);
2968
+ if (variable === null || visitedVariables.has(variable)) return null;
2969
+ const annotatedIdentifier = variable.identifiers.find((identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== void 0);
2970
+ const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
2971
+ if (annotation !== void 0 && annotatedIdentifier !== void 0) {
2972
+ if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) return null;
2973
+ return { type: annotation };
2974
+ }
2975
+ const declarator = variableDeclarator(variable);
2976
+ if (declarator?.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const" || declarator.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init) || functionBoundary(declarator) !== boundary) return null;
2977
+ return knownValueEvidence({
2978
+ scopes,
2979
+ boundary,
2980
+ expression: declarator.init,
2981
+ visitedVariables: new Set([...visitedVariables, variable])
2982
+ });
2983
+ }
2984
+ function widenedBinding(variable, scopes) {
2985
+ const declarator = variableDeclarator(variable);
2986
+ if (declarator?.parent.type !== "VariableDeclaration" || declarator.parent.kind !== "const" || declarator.id.type !== "Identifier" || declarator.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
2987
+ const boundary = functionBoundary(declarator);
2988
+ const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
2989
+ const initializerAssertion = assertionFromExpression(declarator.init);
2990
+ const initializerBroadKind = initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
2991
+ const broadKind = (declaredType === void 0 ? null : broadTypeKind(declaredType)) ?? initializerBroadKind;
2992
+ if (broadKind === null) return null;
2993
+ const evidence = knownValueEvidence({
2994
+ expression: initializerAssertion !== null && initializerBroadKind !== null ? assertedExpression(initializerAssertion) : declarator.init,
2995
+ scopes,
2996
+ boundary,
2997
+ visitedVariables: new Set([variable])
2998
+ });
2999
+ return evidence === null ? null : {
3000
+ broadKind,
3001
+ evidence,
3002
+ declaredAt: declarator.range[1],
3003
+ boundary
3004
+ };
3005
+ }
3006
+ function assertionIsNarrower({ assertedType, broadKind, evidence, sourceText }) {
3007
+ if (broadTypeKind(assertedType) !== null) return false;
3008
+ if (broadKind === "top") return true;
3009
+ if (typesHaveSameSyntax({
3010
+ left: evidence.type,
3011
+ right: assertedType,
3012
+ sourceText
3013
+ })) return true;
3014
+ if (broadKind === "object") return isDefinitelyObjectType(assertedType);
3015
+ return isDefinitelyNarrowerRecordType(assertedType);
3016
+ }
3017
+ /** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
3018
+ const noWidenThenAssertRule = defineRule({
3019
+ meta: {
3020
+ type: "problem",
3021
+ docs: { description: "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type." },
3022
+ messages: { widenThenAssert: "Binding \"{{name}}\" discards type evidence and later recreates it with an assertion. Keep the precise type from initialization through use; parse boundary input once." }
3023
+ },
3024
+ createOnce(context) {
3025
+ let scopes = [];
3026
+ const checkAssertion = (node) => {
3027
+ const expression = assertedExpression(node);
3028
+ if (expression.type !== "Identifier") return;
3029
+ const variable = resolvedVariableForIdentifier(scopes, expression);
3030
+ if (variable === null) return;
3031
+ const widened = widenedBinding(variable, scopes);
3032
+ if (widened === null || node.range[0] <= widened.declaredAt || functionBoundary(node) !== widened.boundary || !assertionIsNarrower({
3033
+ assertedType: node.typeAnnotation,
3034
+ broadKind: widened.broadKind,
3035
+ evidence: widened.evidence,
3036
+ sourceText: context.sourceCode.text
3037
+ })) return;
3038
+ context.report({
3039
+ node,
3040
+ messageId: "widenThenAssert",
3041
+ data: { name: expression.name }
3042
+ });
3043
+ };
3044
+ return {
3045
+ Program() {
3046
+ scopes = context.sourceCode.scopeManager.scopes;
3047
+ },
3048
+ TSAsExpression: checkAssertion,
3049
+ TSTypeAssertion: checkAssertion
3050
+ };
3051
+ }
3052
+ });
3053
+
3054
+ //#endregion
3055
+ //#region src/rules/prefer-forwarded-props-order.ts
3056
+ function isPropsSpread(attribute) {
3057
+ return attribute.type === "JSXSpreadAttribute" && attribute.argument.type === "Identifier" && attribute.argument.name === "props";
3058
+ }
3059
+ /** Put forwarded props before component-controlled JSX attributes. */
3060
+ const preferForwardedPropsOrderRule = defineRule({
3061
+ meta: {
3062
+ type: "suggestion",
3063
+ docs: { description: "Require forwarded props before component-controlled JSX attributes." },
3064
+ messages: { propsFirst: "Spread `{...props}` before component-controlled attributes so the component retains its owned values." }
3065
+ },
3066
+ createOnce(context) {
3067
+ return { JSXOpeningElement(node) {
3068
+ const propsIndex = node.attributes.findIndex(isPropsSpread);
3069
+ if (propsIndex <= 0) return;
3070
+ context.report({
3071
+ node: node.attributes[propsIndex] ?? node,
3072
+ messageId: "propsFirst"
3073
+ });
3074
+ } };
3075
+ }
3076
+ });
3077
+
3078
+ //#endregion
3079
+ //#region src/rules/prefer-hook-order.ts
3080
+ const HOOK_ORDER = new Map([
3081
+ ["use", 0],
3082
+ ["useContext", 0],
3083
+ ["useReducer", 1],
3084
+ ["useRef", 1],
3085
+ ["useState", 1],
3086
+ ["useCallback", 2],
3087
+ ["useMemo", 2],
3088
+ ["useEffect", 3],
3089
+ ["useInsertionEffect", 3],
3090
+ ["useLayoutEffect", 3]
3091
+ ]);
3092
+ function directCallOf(statement) {
3093
+ if (statement.type === "ExpressionStatement" && statement.expression.type === "CallExpression") return {
3094
+ bindingName: null,
3095
+ call: statement.expression
3096
+ };
3097
+ if (statement.type !== "VariableDeclaration") return null;
3098
+ const declaration = statement.declarations[0];
3099
+ if (declaration?.init?.type !== "CallExpression") return null;
3100
+ return {
3101
+ bindingName: declaration.id.type === "Identifier" ? declaration.id.name : null,
3102
+ call: declaration.init
3103
+ };
3104
+ }
3105
+ function hookNameOf(call) {
3106
+ return call.callee.type === "Identifier" ? call.callee.name : null;
3107
+ }
3108
+ /** Keep built-in hooks in context, state, derivation, and effect order. */
3109
+ const preferHookOrderRule = defineRule({
3110
+ meta: {
3111
+ type: "suggestion",
3112
+ docs: { description: "Order built-in React hooks by context, state, derivation, and effect role." },
3113
+ messages: { hookOrder: "Move `{{hook}}` before later hook families so hooks read as context, state and refs, derivations, then effects." }
3114
+ },
3115
+ createOnce(context) {
3116
+ const checkFunction = (node) => {
3117
+ if (node.body === null || node.body.type !== "BlockStatement") return;
3118
+ let latestOrder = -1;
3119
+ const derivationBindings = /* @__PURE__ */ new Set();
3120
+ for (const statement of node.body.body) {
3121
+ const directCall = directCallOf(statement);
3122
+ if (directCall === null) continue;
3123
+ const { bindingName, call } = directCall;
3124
+ const hookName = hookNameOf(call);
3125
+ const order = hookName === null ? void 0 : HOOK_ORDER.get(hookName);
3126
+ if (order === void 0) continue;
3127
+ if (order === 2 && bindingName !== null) derivationBindings.add(bindingName);
3128
+ if (order < latestOrder) {
3129
+ const seed = call.arguments[0];
3130
+ if (order === 1 && seed?.type === "Identifier" && derivationBindings.has(seed.name)) continue;
3131
+ context.report({
3132
+ node: call,
3133
+ messageId: "hookOrder",
3134
+ data: { hook: hookName }
3135
+ });
3136
+ continue;
3137
+ }
3138
+ latestOrder = order;
3139
+ }
3140
+ };
3141
+ return {
3142
+ ArrowFunctionExpression: checkFunction,
3143
+ FunctionDeclaration: checkFunction,
3144
+ FunctionExpression: checkFunction
3145
+ };
3146
+ }
3147
+ });
3148
+
3149
+ //#endregion
3150
+ //#region src/rules/prefer-jsx-boolean-and.ts
3151
+ const BOOLEAN_BINARY_OPERATORS = new Set([
3152
+ "!=",
3153
+ "!==",
3154
+ "<",
3155
+ "<=",
3156
+ "==",
3157
+ "===",
3158
+ ">",
3159
+ ">=",
3160
+ "in",
3161
+ "instanceof"
3162
+ ]);
3163
+ function unwrapExpression(expression) {
3164
+ if (expression.type === "ParenthesizedExpression" || expression.type === "TSAsExpression" || expression.type === "TSNonNullExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSTypeAssertion") return unwrapExpression(expression.expression);
3165
+ return expression;
3166
+ }
3167
+ function propertyNameOf$1(key) {
3168
+ if (key.type === "Identifier") return key.name;
3169
+ if (key.type === "Literal" && typeof key.value === "string") return key.value;
3170
+ return null;
3171
+ }
3172
+ function propertyTypeInMembers(members, propertyName) {
3173
+ for (const member of members) if (member.type === "TSPropertySignature" && !member.computed && propertyNameOf$1(member.key) === propertyName) return member.typeAnnotation?.typeAnnotation ?? null;
3174
+ return null;
3175
+ }
3176
+ function propertyTypeOf({ propertyName, resolvingAliases = /* @__PURE__ */ new Set(), sourceCode, type }) {
3177
+ if (type.type === "TSParenthesizedType") return propertyTypeOf({
3178
+ propertyName,
3179
+ resolvingAliases,
3180
+ sourceCode,
3181
+ type: type.typeAnnotation
3182
+ });
3183
+ if (type.type === "TSTypeLiteral") return propertyTypeInMembers(type.members, propertyName);
3184
+ if (type.type !== "TSTypeReference") return null;
3185
+ const alias = resolveTypeAlias(sourceCode, type);
3186
+ if (alias !== null && !resolvingAliases.has(alias)) return propertyTypeOf({
3187
+ propertyName,
3188
+ resolvingAliases: new Set([...resolvingAliases, alias]),
3189
+ sourceCode,
3190
+ type: alias.typeAnnotation
3191
+ });
3192
+ for (const declaration of resolveTypeInterfaces(sourceCode, type)) {
3193
+ const propertyType = propertyTypeInMembers(declaration.body.body, propertyName);
3194
+ if (propertyType !== null) return propertyType;
3195
+ }
3196
+ return null;
3197
+ }
3198
+ function destructuredTypeOf(sourceCode, identifier) {
3199
+ let pattern = identifier;
3200
+ let propertyName = null;
3201
+ while (pattern.parent.type === "AssignmentPattern" || pattern.parent.type === "Property") {
3202
+ pattern = pattern.parent;
3203
+ if (pattern.type === "Property") propertyName = propertyNameOf$1(pattern.key);
3204
+ }
3205
+ if (propertyName === null || pattern.parent.type !== "ObjectPattern" || pattern.parent.typeAnnotation === null || pattern.parent.typeAnnotation === void 0) return null;
3206
+ return propertyTypeOf({
3207
+ propertyName,
3208
+ sourceCode,
3209
+ type: pattern.parent.typeAnnotation.typeAnnotation
3210
+ });
3211
+ }
3212
+ function declaredTypeOf(sourceCode, identifier) {
3213
+ const variable = resolveVariable(sourceCode, identifier);
3214
+ if (variable === null) return null;
3215
+ for (const definition of variable.defs) {
3216
+ if (definition.name.typeAnnotation != null) return definition.name.typeAnnotation.typeAnnotation;
3217
+ const destructuredType = destructuredTypeOf(sourceCode, definition.name);
3218
+ if (destructuredType !== null) return destructuredType;
3219
+ }
3220
+ return null;
3221
+ }
3222
+ function declaredTypeOfExpression(sourceCode, expression) {
3223
+ const unwrapped = unwrapExpression(expression);
3224
+ if (unwrapped.type === "Identifier") return declaredTypeOf(sourceCode, unwrapped);
3225
+ if (unwrapped.type !== "MemberExpression" || unwrapped.computed || unwrapped.property.type !== "Identifier") return null;
3226
+ const objectType = declaredTypeOfExpression(sourceCode, unwrapped.object);
3227
+ return objectType === null ? null : propertyTypeOf({
3228
+ propertyName: unwrapped.property.name,
3229
+ sourceCode,
3230
+ type: objectType
3231
+ });
3232
+ }
3233
+ function guardTypeOfType({ resolvingAliases = /* @__PURE__ */ new Set(), sourceCode, type }) {
3234
+ if (type.type === "TSParenthesizedType") return guardTypeOfType({
3235
+ resolvingAliases,
3236
+ sourceCode,
3237
+ type: type.typeAnnotation
3238
+ });
3239
+ if (type.type === "TSBooleanKeyword") return "boolean";
3240
+ if (type.type === "TSNumberKeyword") return "number";
3241
+ if (type.type === "TSStringKeyword") return "string";
3242
+ if (type.type === "TSLiteralType" && "value" in type.literal) {
3243
+ if (typeof type.literal.value === "boolean") return "boolean";
3244
+ if (typeof type.literal.value === "number") return "number";
3245
+ if (typeof type.literal.value === "string") return "string";
3246
+ }
3247
+ if (type.type === "TSUnionType") {
3248
+ const memberTypes = new Set(type.types.filter((member) => member.type !== "TSNullKeyword" && member.type !== "TSUndefinedKeyword" && member.type !== "TSVoidKeyword").map((member) => guardTypeOfType({
3249
+ resolvingAliases,
3250
+ sourceCode,
3251
+ type: member
3252
+ })));
3253
+ if (memberTypes.size === 1) return memberTypes.values().next().value ?? "unknown";
3254
+ if (memberTypes.has("number") || memberTypes.has("string")) return "non-boolean";
3255
+ return "unknown";
3256
+ }
3257
+ if (type.type === "TSIntersectionType") {
3258
+ const primitiveTypes = new Set(type.types.map((member) => guardTypeOfType({
3259
+ resolvingAliases,
3260
+ sourceCode,
3261
+ type: member
3262
+ })).filter((member) => member !== "unknown"));
3263
+ return primitiveTypes.size === 1 ? primitiveTypes.values().next().value ?? "unknown" : "unknown";
3264
+ }
3265
+ if (type.type === "TSTypeReference") {
3266
+ const alias = resolveTypeAlias(sourceCode, type);
3267
+ if (alias !== null && !resolvingAliases.has(alias)) return guardTypeOfType({
3268
+ resolvingAliases: new Set([...resolvingAliases, alias]),
3269
+ sourceCode,
3270
+ type: alias.typeAnnotation
3271
+ });
3272
+ }
3273
+ return "unknown";
3274
+ }
3275
+ function guardTypeOfExpression(sourceCode, expression) {
3276
+ const unwrapped = unwrapExpression(expression);
3277
+ if (unwrapped.type === "Literal") {
3278
+ if (typeof unwrapped.value === "boolean") return "boolean";
3279
+ if (typeof unwrapped.value === "number") return "number";
3280
+ if (typeof unwrapped.value === "string") return "string";
3281
+ }
3282
+ if (unwrapped.type === "UnaryExpression" && unwrapped.operator === "!") return "boolean";
3283
+ if (unwrapped.type === "BinaryExpression" && BOOLEAN_BINARY_OPERATORS.has(unwrapped.operator)) return "boolean";
3284
+ if (unwrapped.type === "LogicalExpression") {
3285
+ const leftType = guardTypeOfExpression(sourceCode, unwrapped.left);
3286
+ return leftType === guardTypeOfExpression(sourceCode, unwrapped.right) ? leftType : "unknown";
3287
+ }
3288
+ if (unwrapped.type === "ConditionalExpression") {
3289
+ const consequentType = guardTypeOfExpression(sourceCode, unwrapped.consequent);
3290
+ return consequentType === guardTypeOfExpression(sourceCode, unwrapped.alternate) ? consequentType : "unknown";
3291
+ }
3292
+ const declaredType = declaredTypeOfExpression(sourceCode, unwrapped);
3293
+ return declaredType === null ? "unknown" : guardTypeOfType({
3294
+ sourceCode,
3295
+ type: declaredType
3296
+ });
3297
+ }
3298
+ function isNull(expression) {
3299
+ const unwrapped = unwrapExpression(expression);
3300
+ return unwrapped.type === "Literal" && unwrapped.value === null;
3301
+ }
3302
+ function needsConditionParentheses(expression) {
3303
+ const unwrapped = unwrapExpression(expression);
3304
+ return unwrapped.type === "AssignmentExpression" || unwrapped.type === "ConditionalExpression" || unwrapped.type === "SequenceExpression" || unwrapped.type === "LogicalExpression" && unwrapped.operator !== "&&";
3305
+ }
3306
+ function conditionTextOf(sourceCode, expression) {
3307
+ const text = sourceCode.getText(expression);
3308
+ return needsConditionParentheses(expression) ? `(${text})` : text;
3309
+ }
3310
+ function negatedConditionTextOf(sourceCode, expression) {
3311
+ const unwrapped = unwrapExpression(expression);
3312
+ const text = sourceCode.getText(expression);
3313
+ if (unwrapped.type === "CallExpression" || unwrapped.type === "Identifier" || unwrapped.type === "Literal" || unwrapped.type === "MemberExpression" || unwrapped.type === "UnaryExpression") return `!${text}`;
3314
+ return `!(${text})`;
3315
+ }
3316
+ function renderedTextOf(sourceCode, expression) {
3317
+ const unwrapped = unwrapExpression(expression);
3318
+ const text = sourceCode.getText(expression);
3319
+ return unwrapped.type === "AssignmentExpression" || unwrapped.type === "ConditionalExpression" || unwrapped.type === "SequenceExpression" || unwrapped.type === "LogicalExpression" && unwrapped.operator !== "&&" ? `(${text})` : text;
3320
+ }
3321
+ /** Require boolean guards and canonical `&&` JSX conditionals. */
3322
+ const preferJsxBooleanAndRule = defineRule({
3323
+ meta: {
3324
+ type: "suggestion",
3325
+ docs: { description: "Require boolean JSX guards and replace boolean null-branch conditionals with logical AND." },
3326
+ fixable: "code",
3327
+ messages: {
3328
+ explicitPredicate: "Use an explicit {{type}} predicate before conditionally rendering JSX.",
3329
+ preferAnd: "Replace this boolean null-branch conditional with logical AND."
3330
+ }
3331
+ },
3332
+ createOnce(context) {
3333
+ const checkLogicalExpression = (expression) => {
3334
+ if (expression.parent.type !== "JSXExpressionContainer" || expression.operator !== "&&") return;
3335
+ const guardType = guardTypeOfExpression(context.sourceCode, expression.left);
3336
+ if (guardType === "non-boolean" || guardType === "number" || guardType === "string") context.report({
3337
+ node: expression.left,
3338
+ messageId: "explicitPredicate",
3339
+ data: { type: guardType }
3340
+ });
3341
+ };
3342
+ const checkConditionalExpression = (expression) => {
3343
+ if (expression.parent.type !== "JSXExpressionContainer") return;
3344
+ const isConsequentNull = isNull(expression.consequent);
3345
+ if (isConsequentNull === isNull(expression.alternate)) return;
3346
+ if (guardTypeOfExpression(context.sourceCode, expression.test) !== "boolean") return;
3347
+ const unwrappedTest = unwrapExpression(expression.test);
3348
+ if (isConsequentNull && unwrappedTest.type === "UnaryExpression" && unwrappedTest.operator === "!" && guardTypeOfExpression(context.sourceCode, unwrappedTest.argument) !== "boolean") return;
3349
+ let condition = expression.test;
3350
+ let isNegated = isConsequentNull;
3351
+ if (isConsequentNull && unwrappedTest.type === "UnaryExpression" && unwrappedTest.operator === "!") {
3352
+ condition = unwrappedTest.argument;
3353
+ isNegated = false;
3354
+ }
3355
+ const rendered = isConsequentNull ? expression.alternate : expression.consequent;
3356
+ const replacement = `${isNegated ? negatedConditionTextOf(context.sourceCode, condition) : conditionTextOf(context.sourceCode, condition)} && ${renderedTextOf(context.sourceCode, rendered)}`;
3357
+ context.report({
3358
+ node: expression,
3359
+ messageId: "preferAnd",
3360
+ fix(fixer) {
3361
+ return fixer.replaceText(expression, replacement);
3362
+ }
3363
+ });
3364
+ };
3365
+ return {
3366
+ ConditionalExpression: checkConditionalExpression,
3367
+ LogicalExpression: checkLogicalExpression
3368
+ };
3369
+ }
3370
+ });
3371
+
3372
+ //#endregion
3373
+ //#region src/rules/prefer-options-parameter.ts
3374
+ const ContextOptionsSchema = ruleContextOptionsSchema(z.object({ allowFunctionNames: z.array(z.string()).optional() }));
3375
+ /** Require repository-owned named callables with 3+ inputs to use options. */
3376
+ const preferOptionsParameterRule = defineRule({
3377
+ meta: {
3378
+ type: "suggestion",
3379
+ docs: { description: "Require repository-owned named callables with three or more inputs to use one options object." },
3380
+ messages: { preferOptions: "Function `{{functionName}}` has {{parameterCount}} parameters. Replace them with one named options object." },
3381
+ schema: [{
3382
+ type: "object",
3383
+ properties: { allowFunctionNames: {
3384
+ type: "array",
3385
+ items: {
3386
+ type: "string",
3387
+ minLength: 1
3388
+ },
3389
+ uniqueItems: true
3390
+ } },
3391
+ additionalProperties: false
3392
+ }],
3393
+ defaultOptions: [{ allowFunctionNames: [] }]
3394
+ },
3395
+ createOnce(context) {
3396
+ const checkFunction = (node) => {
3397
+ const functionName$1 = getOwnedFunctionName(node);
3398
+ const parameterCount = node.params.filter((parameter) => !(parameter.type === "Identifier" && parameter.name === "this")).length;
3399
+ if (functionName$1 === null || parameterCount < 3) return;
3400
+ const rawOptions = context.options;
3401
+ const parsedOptions = ContextOptionsSchema.safeParse(rawOptions);
3402
+ if ((parsedOptions.success ? parsedOptions.data : void 0)?.allowFunctionNames?.includes(functionName$1) === true) return;
3403
+ context.report({
3404
+ node,
3405
+ messageId: "preferOptions",
3406
+ data: {
3407
+ functionName: functionName$1,
3408
+ parameterCount
3409
+ }
3410
+ });
3411
+ };
3412
+ return {
3413
+ ArrowFunctionExpression: checkFunction,
3414
+ FunctionDeclaration: checkFunction,
3415
+ FunctionExpression: checkFunction,
3416
+ TSCallSignatureDeclaration: checkFunction,
3417
+ TSConstructSignatureDeclaration: checkFunction,
3418
+ TSConstructorType: checkFunction,
3419
+ TSDeclareFunction: checkFunction,
3420
+ TSEmptyBodyFunctionExpression: checkFunction,
3421
+ TSFunctionType: checkFunction,
3422
+ TSMethodSignature: checkFunction
3423
+ };
3424
+ }
3425
+ });
3426
+
3427
+ //#endregion
3428
+ //#region src/rules/prefer-react-props-reference.ts
3429
+ const HTTP_METHOD_EXPORT_NAMES = new Set([
3430
+ "DELETE",
3431
+ "GET",
3432
+ "HEAD",
3433
+ "OPTIONS",
3434
+ "PATCH",
3435
+ "POST",
3436
+ "PUT"
3437
+ ]);
3438
+ function componentNameOf(node) {
3439
+ if (node.type !== "ArrowFunctionExpression") return node.id?.name ?? null;
3440
+ if (node.parent.type === "VariableDeclarator" && node.parent.id.type === "Identifier") return node.parent.id.name;
3441
+ return null;
3442
+ }
3443
+ function isComponent(node) {
3444
+ const name = componentNameOf(node);
3445
+ return name !== null && !HTTP_METHOD_EXPORT_NAMES.has(name) && /^\p{Lu}/u.test(name);
3446
+ }
3447
+ function propertyNameOf(key) {
3448
+ if (key.type === "Identifier") return key.name;
3449
+ if (key.type === "Literal" && typeof key.value === "string") return key.value;
3450
+ return null;
3451
+ }
3452
+ function membersHaveChildren(members) {
3453
+ return members.some((member) => member.type === "TSPropertySignature" && !member.computed && propertyNameOf(member.key) === "children");
3454
+ }
3455
+ function typeHasChildren({ resolvingAliases = /* @__PURE__ */ new Set(), sourceCode, type }) {
3456
+ if (type.type === "TSParenthesizedType") return typeHasChildren({
3457
+ resolvingAliases,
3458
+ sourceCode,
3459
+ type: type.typeAnnotation
3460
+ });
3461
+ if (type.type === "TSIntersectionType") return type.types.some((member) => typeHasChildren({
3462
+ resolvingAliases,
3463
+ sourceCode,
3464
+ type: member
3465
+ }));
3466
+ if (type.type === "TSTypeLiteral") return membersHaveChildren(type.members);
3467
+ if (type.type !== "TSTypeReference") return false;
3468
+ const alias = resolveTypeAlias(sourceCode, type);
3469
+ if (alias !== null && !resolvingAliases.has(alias) && typeHasChildren({
3470
+ resolvingAliases: new Set([...resolvingAliases, alias]),
3471
+ sourceCode,
3472
+ type: alias.typeAnnotation
3473
+ })) return true;
3474
+ return resolveTypeInterfaces(sourceCode, type).some((declaration) => membersHaveChildren(declaration.body.body));
3475
+ }
3476
+ function isChildrenReference(identifier) {
3477
+ return identifier.parent.type === "MemberExpression" && !identifier.parent.computed && identifier.parent.object === identifier && identifier.parent.property.type === "Identifier" && identifier.parent.property.name === "children";
3478
+ }
3479
+ function patternBinding(property) {
3480
+ if (property.value.type === "Identifier") return property.value;
3481
+ if (property.value.type === "AssignmentPattern" && property.value.left.type === "Identifier") return property.value.left;
3482
+ return null;
3483
+ }
3484
+ function canDestructure(sourceCode, pattern) {
3485
+ if (pattern.properties.length === 0) return false;
3486
+ const rest = pattern.properties.find((property) => property.type === "RestElement");
3487
+ if (rest !== void 0) {
3488
+ if (rest.argument.type !== "Identifier") return false;
3489
+ return resolveVariable(sourceCode, rest.argument)?.references.some((reference) => reference.identifier.parent.type === "JSXSpreadAttribute" && reference.identifier.parent.argument === reference.identifier) ?? false;
3490
+ }
3491
+ return pattern.properties.every((property) => {
3492
+ if (property.type !== "Property") return false;
3493
+ const binding = patternBinding(property);
3494
+ if (binding === null) return false;
3495
+ return (resolveVariable(sourceCode, binding)?.references.filter((reference) => reference.isRead()).length ?? 0) >= 3;
3496
+ });
3497
+ }
3498
+ /** Keep React props available through one named component boundary. */
3499
+ const preferReactPropsReferenceRule = defineRule({
3500
+ meta: {
3501
+ type: "suggestion",
3502
+ docs: { description: "Keep React component props behind one named parameter and bounded body-local destructuring." },
3503
+ messages: {
3504
+ canonicalName: "Name the React component parameter `props` so prop access has one canonical form.",
3505
+ childrenReference: "Render `props.children` explicitly when the component props contract declares children.",
3506
+ destructureLocally: "Keep prop access as `props.X`; destructure inside the body only to strip owned fields before forwarding or when every field is read at least three times.",
3507
+ nameProps: "Accept one named props parameter and reference fields through it; destructure component-owned fields inside the body when forwarding requires it."
3508
+ }
3509
+ },
3510
+ createOnce(context) {
3511
+ const checkComponent = (node) => {
3512
+ if (!isComponent(node)) return;
3513
+ const parameter = node.params[0];
3514
+ if (parameter?.type === "ObjectPattern") {
3515
+ context.report({
3516
+ node: parameter,
3517
+ messageId: "nameProps"
3518
+ });
3519
+ return;
3520
+ }
3521
+ if (parameter?.type !== "Identifier") return;
3522
+ if (parameter.name !== "props") {
3523
+ context.report({
3524
+ node: parameter,
3525
+ messageId: "canonicalName"
3526
+ });
3527
+ return;
3528
+ }
3529
+ const variable = resolveVariable(context.sourceCode, parameter);
3530
+ for (const reference of variable?.references ?? []) {
3531
+ const parent = reference.identifier.parent;
3532
+ if (parent.type === "VariableDeclarator" && parent.init === reference.identifier && parent.id.type === "ObjectPattern" && !canDestructure(context.sourceCode, parent.id)) context.report({
3533
+ node: parent.id,
3534
+ messageId: "destructureLocally"
3535
+ });
3536
+ }
3537
+ const annotation = parameter.typeAnnotation?.typeAnnotation;
3538
+ if (annotation === void 0 || !typeHasChildren({
3539
+ sourceCode: context.sourceCode,
3540
+ type: annotation
3541
+ })) return;
3542
+ if (variable !== null && !variable.references.some((reference) => isChildrenReference(reference.identifier))) context.report({
3543
+ node: parameter,
3544
+ messageId: "childrenReference"
3545
+ });
3546
+ };
3547
+ return {
3548
+ ArrowFunctionExpression: checkComponent,
3549
+ FunctionDeclaration: checkComponent,
3550
+ FunctionExpression: checkComponent
3551
+ };
3552
+ }
3553
+ });
3554
+
3555
+ //#endregion
3556
+ //#region src/rules/prefer-switch-discriminator-chain.ts
3557
+ const MINIMUM_BRANCH_COUNT = 4;
3558
+ function discriminatorKey(node) {
3559
+ return node.type === "Identifier" ? node.name : null;
3560
+ }
3561
+ function comparisonDiscriminator(node) {
3562
+ if (node.type !== "BinaryExpression" || node.operator !== "===") return null;
3563
+ if (node.right.type === "Literal") return discriminatorKey(node.left);
3564
+ if (node.left.type === "Literal") return discriminatorKey(node.right);
3565
+ return null;
3566
+ }
3567
+ function discriminatorChain(node) {
3568
+ let branchCount = 0;
3569
+ let current = node;
3570
+ let discriminator = null;
3571
+ while (current !== null) {
3572
+ const branchDiscriminator = comparisonDiscriminator(current.test);
3573
+ if (branchDiscriminator === null) return null;
3574
+ if (discriminator !== null && discriminator !== branchDiscriminator) return null;
3575
+ discriminator = branchDiscriminator;
3576
+ branchCount += 1;
3577
+ current = current.alternate?.type === "IfStatement" ? current.alternate : null;
3578
+ }
3579
+ return discriminator === null ? null : {
3580
+ branchCount,
3581
+ discriminator
3582
+ };
3583
+ }
3584
+ /** Prefer a switch when repeated equality branches dispatch on one value. */
3585
+ const preferSwitchDiscriminatorChainRule = defineRule({
3586
+ meta: {
3587
+ type: "suggestion",
3588
+ docs: { description: "Require a switch for four or more equality branches on one discriminator." },
3589
+ messages: { preferSwitch: "This chain has {{branchCount}} equality branches on one discriminator. Replace it with a switch so the finite dispatch structure is explicit." }
3590
+ },
3591
+ createOnce(context) {
3592
+ return { IfStatement(node) {
3593
+ if (node.parent.type === "IfStatement" && node.parent.alternate === node) return;
3594
+ const chain = discriminatorChain(node);
3595
+ if (chain === null || chain.branchCount < MINIMUM_BRANCH_COUNT) return;
3596
+ context.report({
3597
+ node,
3598
+ messageId: "preferSwitch",
3599
+ data: { branchCount: chain.branchCount }
3600
+ });
3601
+ } };
3602
+ }
3603
+ });
3604
+
3605
+ //#endregion
3606
+ //#region src/rules/prefer-top-level-function-declarations.ts
3607
+ function isFunctionExpression(node) {
3608
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") return true;
3609
+ if (node.type === "ParenthesizedExpression" || node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSTypeAssertion") return isFunctionExpression(node.expression);
3610
+ return false;
3611
+ }
3612
+ function isTopLevelVariable(node) {
3613
+ if (node.id.type !== "Identifier") return false;
3614
+ const declaration = node.parent;
3615
+ if (declaration.type !== "VariableDeclaration") return false;
3616
+ return declaration.parent.type === "Program" || declaration.parent.type === "ExportNamedDeclaration" && declaration.parent.parent.type === "Program";
3617
+ }
3618
+ /** Prefer hoistable declarations for repository-owned top-level functions. */
3619
+ const preferTopLevelFunctionDeclarationsRule = defineRule({
3620
+ meta: {
3621
+ type: "suggestion",
3622
+ docs: { description: "Require function declarations for direct top-level function bindings and named default exports." },
3623
+ messages: {
3624
+ anonymousDefaultExport: "Name this default-exported function with a function declaration so stack frames and searches identify its owner.",
3625
+ topLevelBinding: "Top-level function `{{functionName}}` uses a function expression. Replace it with a function declaration so its owner is explicit and hoistable."
3626
+ }
3627
+ },
3628
+ createOnce(context) {
3629
+ return {
3630
+ ExportDefaultDeclaration(node) {
3631
+ if (node.declaration.type === "ArrowFunctionExpression" || node.declaration.type === "FunctionExpression" || node.declaration.type === "FunctionDeclaration" && node.declaration.id === null || (node.declaration.type === "ParenthesizedExpression" || node.declaration.type === "TSAsExpression" || node.declaration.type === "TSNonNullExpression" || node.declaration.type === "TSSatisfiesExpression" || node.declaration.type === "TSTypeAssertion") && isFunctionExpression(node.declaration)) context.report({
3632
+ node,
3633
+ messageId: "anonymousDefaultExport"
3634
+ });
3635
+ },
3636
+ VariableDeclarator(node) {
3637
+ if (node.init === null || !isTopLevelVariable(node) || !isFunctionExpression(node.init)) return;
3638
+ context.report({
3639
+ node,
3640
+ messageId: "topLevelBinding",
3641
+ data: { functionName: node.id.name }
3642
+ });
3643
+ }
3644
+ };
3645
+ }
3646
+ });
3647
+
3648
+ //#endregion
3649
+ //#region src/rules/require-lint-suppression-reason.ts
3650
+ const suppressionDirectivePattern = /^(?:eslint|oxlint)-disable(?:-next-line|-line)?(?:\s|$)/u;
3651
+ const suppressionReasonPattern = /\s--\s+\S/u;
3652
+ /** Require an explicit forcing reason on ESLint and Oxlint suppressions. */
3653
+ const requireLintSuppressionReasonRule = defineRule({
3654
+ meta: {
3655
+ type: "suggestion",
3656
+ docs: { description: "Require ESLint and Oxlint disable directives to state their forcing reason." },
3657
+ messages: { missingReason: "Add a reason after `--` that explains why this lint suppression is required." }
3658
+ },
3659
+ createOnce(context) {
3660
+ return { Program() {
3661
+ for (const comment of context.sourceCode.getAllComments()) {
3662
+ const directive = comment.value.trim();
3663
+ if (suppressionDirectivePattern.test(directive) && !suppressionReasonPattern.test(directive)) context.report({
3664
+ loc: context.sourceCode.getLoc(comment),
3665
+ messageId: "missingReason"
3666
+ });
3667
+ }
3668
+ } };
3669
+ }
3670
+ });
3671
+
3672
+ //#endregion
3673
+ //#region src/rules/require-repository-test-subject.ts
3674
+ const runtimeRepositoryPrefixes = ["cloudflare:"];
3675
+ function hasRuntimeImport(node) {
3676
+ if (node.importKind === "type") return false;
3677
+ if (node.specifiers.length === 0) return true;
3678
+ return node.specifiers.some((specifier) => specifier.type !== "ImportSpecifier" || specifier.importKind !== "type");
3679
+ }
3680
+ /** Require behavioral tests to exercise repository-owned code. */
3681
+ const requireRepositoryTestSubjectRule = defineRule({
3682
+ meta: {
3683
+ type: "problem",
3684
+ docs: { description: "Require files containing test cases to import a repository-owned subject." },
3685
+ messages: { missingSubject: "This test file exercises no repository-owned subject. Remove the test or import the behavior it protects." },
3686
+ schema: repositoryModuleRuleSchema,
3687
+ defaultOptions: [{ internalModulePrefixes: [] }]
3688
+ },
3689
+ createOnce(context) {
3690
+ let firstTestCall = null;
3691
+ let hasRepositoryImport = false;
3692
+ let internalModulePrefixes = [];
3693
+ function isRepositorySpecifier(specifier) {
3694
+ return isRepositoryOwnedModuleSpecifier({
3695
+ additionalPrefixes: runtimeRepositoryPrefixes,
3696
+ internalModulePrefixes,
3697
+ specifier
3698
+ });
3699
+ }
3700
+ return {
3701
+ "Program"() {
3702
+ firstTestCall = null;
3703
+ hasRepositoryImport = false;
3704
+ internalModulePrefixes = getInternalModulePrefixes(context.options);
3705
+ },
3706
+ "ImportDeclaration"(node) {
3707
+ if (typeof node.source.value === "string" && isRepositorySpecifier(node.source.value) && hasRuntimeImport(node)) hasRepositoryImport = true;
3708
+ },
3709
+ "ImportExpression"(node) {
3710
+ if (node.source.type === "Literal" && typeof node.source.value === "string" && isRepositorySpecifier(node.source.value)) hasRepositoryImport = true;
3711
+ },
3712
+ "CallExpression"(node) {
3713
+ if (firstTestCall === null && getTestFrameworkCall(context.sourceCode, node)?.kind === "test") firstTestCall = node;
3714
+ },
3715
+ "Program:exit"() {
3716
+ if (firstTestCall !== null && !hasRepositoryImport) context.report({
3717
+ node: firstTestCall,
3718
+ messageId: "missingSubject"
3719
+ });
3720
+ }
3721
+ };
3722
+ }
3723
+ });
3724
+
3725
+ //#endregion
3726
+ //#region src/rules/require-safety-comment-for-type-assertion.ts
3727
+ const commentOwnerKinds = new Set([
3728
+ "ExpressionStatement",
3729
+ "PropertyDefinition",
3730
+ "ReturnStatement",
3731
+ "ThrowStatement",
3732
+ "VariableDeclaration"
3733
+ ]);
3734
+ function isConstAssertion(node) {
3735
+ return node.typeAnnotation.type === "TSTypeReference" && node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name === "const";
3736
+ }
3737
+ function hasSafetyComment(sourceCode, node) {
3738
+ let current = node;
3739
+ while (true) {
3740
+ if (sourceCode.getCommentsBefore(current).some((comment) => comment.range[1] <= node.range[0] && /\bSAFETY\s*:/u.test(comment.value))) return true;
3741
+ if (commentOwnerKinds.has(current.type) || current.parent.type === "Program") return false;
3742
+ current = current.parent;
3743
+ }
3744
+ }
3745
+ /** Require every non-const type assertion to state the invariant TypeScript cannot express. */
3746
+ const requireSafetyCommentForTypeAssertionRule = defineRule({
3747
+ meta: {
3748
+ type: "problem",
3749
+ docs: { description: "Require a nearby SAFETY comment for every outermost TypeScript type assertion except const assertions." },
3750
+ messages: { missingSafetyComment: "This type assertion has no `SAFETY:` justification. State the checked invariant immediately before the assertion or its containing statement." }
3751
+ },
3752
+ createOnce(context) {
3753
+ const checkAssertion = (node) => {
3754
+ if (isConstAssertion(node) || !isOutermostTypeAssertion(node) || hasSafetyComment(context.sourceCode, node)) return;
3755
+ context.report({
3756
+ node,
3757
+ messageId: "missingSafetyComment"
3758
+ });
3759
+ };
3760
+ return {
3761
+ TSAsExpression: checkAssertion,
3762
+ TSTypeAssertion: checkAssertion
3763
+ };
3764
+ }
3765
+ });
3766
+
3767
+ //#endregion
3768
+ //#region src/rules/require-special-comment-tag.ts
3769
+ const SPECIAL_TAG = /^\s*(fixme|hack|note|todo)\b(?!:)/iu;
3770
+ /** Require canonical uppercase tags and a colon on special comments. */
3771
+ const requireSpecialCommentTagRule = defineRule({
3772
+ meta: {
3773
+ type: "suggestion",
3774
+ docs: { description: "Require canonical uppercase tags and a colon on special comments." },
3775
+ fixable: "code",
3776
+ messages: { invalidTag: "Write the special comment tag as `{{tag}}:` before its context." }
3777
+ },
3778
+ createOnce(context) {
3779
+ return { Program() {
3780
+ for (const comment of context.sourceCode.getAllComments()) {
3781
+ const match = SPECIAL_TAG.exec(comment.value);
3782
+ if (match?.[1] === void 0) continue;
3783
+ const matchedTag = match[1];
3784
+ const tag = matchedTag.toUpperCase();
3785
+ const leadingLength = comment.value.length - comment.value.trimStart().length;
3786
+ context.report({
3787
+ loc: {
3788
+ start: comment.loc.start,
3789
+ end: comment.loc.end
3790
+ },
3791
+ messageId: "invalidTag",
3792
+ data: { tag },
3793
+ fix(fixer) {
3794
+ const start = comment.range[0] + 2 + leadingLength;
3795
+ return fixer.replaceTextRange([start, start + matchedTag.length], `${tag}:`);
3796
+ }
3797
+ });
3798
+ }
3799
+ } };
3800
+ }
3801
+ });
3802
+
3803
+ //#endregion
3804
+ //#region src/index.ts
3805
+ const meta = {
3806
+ name: "@utilfirst/eslint-plugin",
3807
+ version
3808
+ };
3809
+ const antiSlopPlugin = eslintCompatPlugin({
3810
+ meta,
3811
+ rules: {
3812
+ "no-call-count-only-test": noCallCountOnlyTestRule,
3813
+ "no-chained-type-assertions": noChainedTypeAssertionsRule,
3814
+ "no-conditional-undefined-properties": noConditionalUndefinedPropertiesRule,
3815
+ "no-enum-declarations": noEnumDeclarationsRule,
3816
+ "no-imported-constant-restatement": noImportedConstantRestatementRule,
3817
+ "no-known-value-widening": noKnownValueWideningRule,
3818
+ "no-module-mocking": noModuleMockingRule,
3819
+ "no-negated-throw-assertion": noNegatedThrowAssertionRule,
3820
+ "no-object-parameters": noObjectParametersRule,
3821
+ "no-positional-boolean-parameters": noPositionalBooleanParametersRule,
3822
+ "no-promise-settlement-only-assertion": noPromiseSettlementOnlyAssertionRule,
3823
+ "no-reflect-apply": noReflectApplyRule,
3824
+ "no-reflect-get": noReflectGetRule,
3825
+ "no-test-snapshots": noTestSnapshotsRule,
3826
+ "no-truthy-falsy-assertion": noTruthyFalsyAssertionRule,
3827
+ "no-uncontrolled-time-in-test": noUncontrolledTimeInTestRule,
3828
+ "no-unknown-parameters": noUnknownParametersRule,
3829
+ "no-unknown-returns": noUnknownReturnsRule,
3830
+ "no-unknown-type-aliases": noUnknownTypeAliasesRule,
3831
+ "no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
3832
+ "no-unhandled-detached-promises": noUnhandledDetachedPromisesRule,
3833
+ "no-widen-then-assert": noWidenThenAssertRule,
3834
+ "prefer-forwarded-props-order": preferForwardedPropsOrderRule,
3835
+ "prefer-hook-order": preferHookOrderRule,
3836
+ "prefer-jsx-boolean-and": preferJsxBooleanAndRule,
3837
+ "prefer-options-parameter": preferOptionsParameterRule,
3838
+ "prefer-react-props-reference": preferReactPropsReferenceRule,
3839
+ "prefer-switch-discriminator-chain": preferSwitchDiscriminatorChainRule,
3840
+ "prefer-top-level-function-declarations": preferTopLevelFunctionDeclarationsRule,
3841
+ "require-lint-suppression-reason": requireLintSuppressionReasonRule,
3842
+ "require-repository-test-subject": requireRepositoryTestSubjectRule,
3843
+ "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
3844
+ "require-special-comment-tag": requireSpecialCommentTagRule
3845
+ }
3846
+ });
3847
+ function assertEslintCompatibleRules(candidateRules) {
3848
+ for (const candidateRule of Object.values(candidateRules)) if (candidateRule.create === void 0) throw new Error("ESLint compatibility adapter did not install `create`");
3849
+ }
3850
+ assertEslintCompatibleRules(antiSlopPlugin.rules);
3851
+ const antiSlopRules = antiSlopPlugin.rules;
3852
+ const rules = {
3853
+ "consistent-blank-lines": consistentBlankLines,
3854
+ ...antiSlopRules
3855
+ };
3856
+ const recommendedRules = Object.fromEntries(Object.keys(rules).map((ruleName) => [`utilfirst/${ruleName}`, "error"]));
3857
+ const plugin = {
3858
+ meta,
3859
+ rules,
3860
+ configs: { recommended: {} }
3861
+ };
3862
+ plugin.configs.recommended = {
3863
+ plugins: { utilfirst: plugin },
3864
+ rules: recommendedRules
3865
+ };
3866
+ var src_default = plugin;
3867
+
3868
+ //#endregion
3869
+ export { src_default as t };