@utilfirst/eslint-plugin 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { defineRule, eslintCompatPlugin } from "@oxlint/plugins";
1
2
  import { AST_NODE_TYPES } from "@typescript-eslint/utils";
3
+ import { z } from "zod";
2
4
 
3
5
  //#region package.json
4
- var version = "0.1.1";
6
+ var version = "0.2.0";
5
7
 
6
8
  //#endregion
7
9
  //#region src/rules/consistent-blank-lines.ts
@@ -61,20 +63,30 @@ const consistentBlankLines = {
61
63
  url: "https://github.com/utilfirst/utilfirst-eslint-plugin/blob/main/docs/rules/consistent-blank-lines.md"
62
64
  },
63
65
  fixable: "whitespace",
66
+ defaultOptions: [],
64
67
  schema: [],
65
68
  messages: {
66
69
  extra: "Unexpected blank line between items that continue the same paragraph.",
67
70
  missing: "Expected a blank line between items that start a new paragraph."
68
71
  }
69
72
  },
70
- defaultOptions: [],
71
73
  create(context) {
72
74
  const sourceCode = context.sourceCode;
73
75
  function checkBlock(statements) {
74
76
  for (let i = 0; i < statements.length - 1; i++) {
75
77
  const prev = statements[i];
76
78
  const next = statements[i + 1];
77
- if (prev && next) checkPair(prev, next, sourceCode.getCommentsBefore(next)[0] ?? next, () => sameParagraph(prev, next, sourceCode), () => isImportPair(prev, next) || isReExport(prev) && isReExport(next) || isExpressionStatementPair(prev, next));
79
+ if (prev && next) checkPair({
80
+ effectiveStart: sourceCode.getCommentsBefore(next)[0] ?? next,
81
+ isSameParagraph: () => sameParagraph({
82
+ next,
83
+ prev,
84
+ sourceCode
85
+ }),
86
+ next,
87
+ prev,
88
+ shouldPreserveExtra: () => isImportPair(prev, next) || isReExport(prev) && isReExport(next) || isExpressionStatementPair(prev, next)
89
+ });
78
90
  }
79
91
  }
80
92
  function checkJsxChildren(children) {
@@ -98,11 +110,21 @@ const consistentBlankLines = {
98
110
  if (prevSibling && nextSibling) {
99
111
  const { node: prev } = prevSibling;
100
112
  const { leading, node: next } = nextSibling;
101
- checkPair(prev, next, leading[0] ?? next, () => sameJsxParagraph(prev, next, leading), () => false);
113
+ checkPair({
114
+ effectiveStart: leading[0] ?? next,
115
+ isSameParagraph: () => sameJsxParagraph({
116
+ leading,
117
+ next,
118
+ prev
119
+ }),
120
+ next,
121
+ prev,
122
+ shouldPreserveExtra: () => false
123
+ });
102
124
  }
103
125
  }
104
126
  }
105
- function checkPair(prev, next, effectiveStart, isSameParagraph, shouldPreserveExtra) {
127
+ function checkPair({ effectiveStart, isSameParagraph, next, prev, shouldPreserveExtra }) {
106
128
  const prevEndLine = prev.loc.end.line;
107
129
  const nextStartLine = effectiveStart.loc.start.line;
108
130
  if (nextStartLine - prevEndLine < 1) return;
@@ -149,19 +171,27 @@ const consistentBlankLines = {
149
171
  };
150
172
  }
151
173
  };
152
- function sameParagraph(prev, next, sourceCode) {
174
+ function sameParagraph({ next, prev, sourceCode }) {
153
175
  if (hasMultiLineLeadingComment(next, sourceCode)) return false;
154
176
  const prevHook = isHookStatement(prev);
155
177
  const nextHook = isHookStatement(next);
156
- if (prevHook || nextHook) return prevHook && nextHook && isMatchingVarDeclPair(prev, next);
157
- return sharesNameFlow(prev, next, sourceCode) || sameShape(prev, next);
178
+ if (prevHook || nextHook) return prevHook && nextHook && isMatchingVarDeclPair({
179
+ next,
180
+ prev,
181
+ shouldMatchInitializers: false
182
+ });
183
+ return sharesNameFlow({
184
+ next,
185
+ prev,
186
+ sourceCode
187
+ }) || statementsBelongTogether(prev, next);
158
188
  }
159
189
  function isHookStatement(stmt) {
160
190
  if (stmt.type === AST_NODE_TYPES.VariableDeclaration && stmt.declarations.length === 1) return isHookCall(stmt.declarations[0].init);
161
191
  if (stmt.type === AST_NODE_TYPES.ExpressionStatement) return isHookCall(stmt.expression);
162
192
  return false;
163
193
  }
164
- function sharesNameFlow(prev, next, sourceCode) {
194
+ function sharesNameFlow({ next, prev, sourceCode }) {
165
195
  if (isMultiLine(prev)) return false;
166
196
  const nextType = unwrapExport(next).type;
167
197
  if (isMultiLine(next) && HEAVY_NEXT_TYPES.has(nextType)) return false;
@@ -173,12 +203,15 @@ function sharesNameFlow(prev, next, sourceCode) {
173
203
  return false;
174
204
  }
175
205
  function isHookCall(node) {
176
- return node?.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.Identifier && /^use[A-Z]/.test(node.callee.name);
206
+ return node?.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.Identifier && /^use[A-Z]/u.test(node.callee.name);
177
207
  }
178
- function sameShape(prev, next) {
208
+ function statementsBelongTogether(prev, next) {
179
209
  if (isImportPair(prev, next)) return true;
180
210
  if (isReExport(prev) && isReExport(next)) return true;
181
- if (isMatchingVarDeclPair(prev, next)) return true;
211
+ if (isMatchingVarDeclPair({
212
+ next,
213
+ prev
214
+ })) return true;
182
215
  if (isMatchingTypeAliasPair(prev, next)) return true;
183
216
  if (FLOW_PREV_TYPES.has(prev.type) && FLOW_NEXT_TYPES.has(next.type)) {
184
217
  if (HEAVY_NEXT_TYPES.has(next.type) && isMultiLine(next)) return false;
@@ -199,14 +232,14 @@ function isExpressionStatementPair(prev, next) {
199
232
  function isReExport(stmt) {
200
233
  return stmt.type === AST_NODE_TYPES.ExportNamedDeclaration && stmt.source !== null || stmt.type === AST_NODE_TYPES.ExportAllDeclaration;
201
234
  }
202
- function isMatchingVarDeclPair(prev, next) {
235
+ function isMatchingVarDeclPair({ next, prev, shouldMatchInitializers = true }) {
203
236
  const previousDeclaration = unwrapExport(prev);
204
237
  const nextDeclaration = unwrapExport(next);
205
238
  if (previousDeclaration.type !== AST_NODE_TYPES.VariableDeclaration || nextDeclaration.type !== AST_NODE_TYPES.VariableDeclaration) return false;
206
239
  if (!isConstOrLet(previousDeclaration) || !isConstOrLet(nextDeclaration)) return false;
207
240
  if (isMultiLine(prev) || isMultiLine(next)) return false;
208
241
  if (previousDeclaration.declarations.length !== 1 || nextDeclaration.declarations.length !== 1) return false;
209
- if (!initsMatchByShape(previousDeclaration.declarations[0].init, nextDeclaration.declarations[0].init)) return false;
242
+ if (shouldMatchInitializers && !initializersBelongTogether(previousDeclaration.declarations[0].init, nextDeclaration.declarations[0].init)) return false;
210
243
  return prev.type === AST_NODE_TYPES.ExportNamedDeclaration === (next.type === AST_NODE_TYPES.ExportNamedDeclaration);
211
244
  }
212
245
  function isMatchingTypeAliasPair(prev, next) {
@@ -223,13 +256,10 @@ function unwrapExport(stmt) {
223
256
  function isConstOrLet(decl) {
224
257
  return decl.kind === "const" || decl.kind === "let";
225
258
  }
226
- function initsMatchByShape(prevInit, nextInit) {
259
+ function initializersBelongTogether(prevInit, nextInit) {
227
260
  const prevCall = asCall(prevInit);
228
261
  const nextCall = asCall(nextInit);
229
- if (prevCall && nextCall) {
230
- if (calleesEqual(prevCall.callee, nextCall.callee)) return true;
231
- return prevCall.arguments.length === 0 && nextCall.arguments.length === 0;
232
- }
262
+ if (prevCall && nextCall) return calleesEqual(prevCall.callee, nextCall.callee);
233
263
  if (prevCall || nextCall) return false;
234
264
  return true;
235
265
  }
@@ -246,6 +276,8 @@ function calleesEqual(a, b) {
246
276
  if (a.computed !== b.computed) return false;
247
277
  if (!calleesEqual(a.object, b.object)) return false;
248
278
  if (!a.computed && a.property.type === AST_NODE_TYPES.Identifier && b.property.type === AST_NODE_TYPES.Identifier) return a.property.name === b.property.name;
279
+ if (a.computed && a.property.type === AST_NODE_TYPES.Identifier && b.property.type === AST_NODE_TYPES.Identifier) return a.property.name === b.property.name;
280
+ if (a.computed && a.property.type === AST_NODE_TYPES.Literal && b.property.type === AST_NODE_TYPES.Literal) return a.property.value === b.property.value;
249
281
  return false;
250
282
  }
251
283
  if (a.type === AST_NODE_TYPES.CallExpression && b.type === AST_NODE_TYPES.CallExpression) return calleesEqual(a.callee, b.callee);
@@ -257,7 +289,7 @@ function isGuardIf(stmt) {
257
289
  function blockAlwaysTerminates(node) {
258
290
  if (TERMINATING_STATEMENT_TYPES.has(node.type)) return true;
259
291
  if (node.type === AST_NODE_TYPES.BlockStatement) {
260
- const last = node.body[node.body.length - 1];
292
+ const last = node.body.at(-1);
261
293
  return last ? blockAlwaysTerminates(last) : false;
262
294
  }
263
295
  if (node.type === AST_NODE_TYPES.IfStatement) {
@@ -304,6 +336,7 @@ function collectBindingNames(node, set) {
304
336
  case AST_NODE_TYPES.AssignmentPattern:
305
337
  collectBindingNames(node.left, set);
306
338
  break;
339
+ default: break;
307
340
  }
308
341
  }
309
342
  function collectAssignmentRoots(target, set) {
@@ -319,16 +352,22 @@ function collectReferencedNames(root, sourceCode) {
319
352
  const set = /* @__PURE__ */ new Set();
320
353
  walk(root, (node) => {
321
354
  if (node.type === AST_NODE_TYPES.Identifier && !isDeclarationOrPropertyKey(node)) {
322
- if (resolvesOutsideRoot(node, root, sourceCode)) set.add(node.name);
355
+ if (resolvesOutsideRoot({
356
+ idNode: node,
357
+ root,
358
+ sourceCode
359
+ })) set.add(node.name);
323
360
  } else if (node.type === AST_NODE_TYPES.ThisExpression) {
324
361
  if (thisResolvesOutsideRoot(node, root)) set.add("this");
325
- } else if (node.type === AST_NODE_TYPES.JSXIdentifier && isJsxComponentIdentifier(node)) {
326
- if (resolvesOutsideRoot(node, root, sourceCode)) set.add(node.name);
327
- }
362
+ } else if (node.type === AST_NODE_TYPES.JSXIdentifier && isJsxComponentIdentifier(node) && resolvesOutsideRoot({
363
+ idNode: node,
364
+ root,
365
+ sourceCode
366
+ })) set.add(node.name);
328
367
  });
329
368
  return set;
330
369
  }
331
- function resolvesOutsideRoot(idNode, root, sourceCode) {
370
+ function resolvesOutsideRoot({ idNode, root, sourceCode }) {
332
371
  let scope = sourceCode.getScope(idNode);
333
372
  while (scope) {
334
373
  const variable = scope.variables.find((v) => v.name === idNode.name);
@@ -349,7 +388,7 @@ function thisResolvesOutsideRoot(thisNode, root) {
349
388
  return true;
350
389
  }
351
390
  function isJsxComponentIdentifier(node) {
352
- if (!/^[A-Z]/.test(node.name)) return false;
391
+ if (!/^[A-Z]/u.test(node.name)) return false;
353
392
  const parent = node.parent;
354
393
  if (parent.type === AST_NODE_TYPES.JSXOpeningElement && parent.name === node) return true;
355
394
  if (parent.type === AST_NODE_TYPES.JSXClosingElement && parent.name === node) return true;
@@ -379,7 +418,9 @@ function isInBindingPosition(idNode) {
379
418
  let cur = idNode;
380
419
  let parent = parentOf(cur);
381
420
  while (parent) {
382
- if (FN_DECL_TYPES.has(parent.type) && "params" in parent && Array.isArray(parent.params) && parent.params.includes(cur)) return true;
421
+ if (FN_DECL_TYPES.has(parent.type) && "params" in parent && Array.isArray(parent.params)) {
422
+ for (const parameter of parent.params) if (parameter === cur) return true;
423
+ }
383
424
  if (parent.type === AST_NODE_TYPES.ObjectPattern || parent.type === AST_NODE_TYPES.ArrayPattern) return true;
384
425
  if (parent.type === AST_NODE_TYPES.Property) {
385
426
  if (parent.computed && parent.key === cur) return false;
@@ -402,9 +443,9 @@ function isInBindingPosition(idNode) {
402
443
  }
403
444
  return false;
404
445
  }
405
- function sameJsxParagraph(prev, next, leading) {
446
+ function sameJsxParagraph({ leading, next, prev }) {
406
447
  const firstLeading = leading[0];
407
- const lastLeading = leading[leading.length - 1];
448
+ const lastLeading = leading.at(-1);
408
449
  if (firstLeading && lastLeading && lastLeading.loc.end.line > firstLeading.loc.start.line) return false;
409
450
  if (siblingsIncludeLiteralText(next)) return true;
410
451
  return !(isMultiLine(prev) || isMultiLine(next));
@@ -422,22 +463,30 @@ function siblingsIncludeLiteralText(node) {
422
463
  return false;
423
464
  }
424
465
  function expressionYieldsStringLiteral(expr) {
425
- if (expr.type === AST_NODE_TYPES.Literal) return typeof expr.value === "string";
466
+ if (expr.type === AST_NODE_TYPES.Literal) return isString$2(expr.value);
426
467
  if (expr.type === AST_NODE_TYPES.TemplateLiteral) return true;
427
468
  if (expr.type === AST_NODE_TYPES.LogicalExpression) return expressionYieldsStringLiteral(expr.right);
428
469
  if (expr.type === AST_NODE_TYPES.ConditionalExpression) return expressionYieldsStringLiteral(expr.consequent) || expressionYieldsStringLiteral(expr.alternate);
429
470
  return false;
430
471
  }
472
+ function isString$2(value) {
473
+ return typeof value === "string";
474
+ }
475
+ function isNode(value) {
476
+ return value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
477
+ }
478
+ function isArray(value) {
479
+ return Array.isArray(value);
480
+ }
431
481
  function walk(node, fn) {
432
- if (!node || typeof node !== "object" || typeof node.type !== "string") return;
433
482
  fn(node);
434
- const nodeFields = node;
435
- for (const key of Object.keys(nodeFields)) {
483
+ const nodeEntries = Object.entries(node);
484
+ for (const [key, child] of nodeEntries) {
436
485
  if (SKIP_KEYS.has(key)) continue;
437
486
  if (key === "body" && OPAQUE_BODY_TYPES.has(node.type)) continue;
438
- const child = nodeFields[key];
439
- if (Array.isArray(child)) for (const c of child) walk(c, fn);
440
- else walk(child, fn);
487
+ if (isArray(child)) {
488
+ for (const childNode of child) if (isNode(childNode)) walk(childNode, fn);
489
+ } else if (isNode(child)) walk(child, fn);
441
490
  }
442
491
  }
443
492
  function isMultiLine(node) {
@@ -447,23 +496,1940 @@ function hasMultiLineLeadingComment(node, sourceCode) {
447
496
  const comments = sourceCode.getCommentsBefore(node);
448
497
  if (comments.length === 0) return false;
449
498
  const first = comments[0];
450
- const last = comments[comments.length - 1];
499
+ const last = comments.at(-1);
451
500
  return Boolean(first && last && last.loc.end.line > first.loc.start.line);
452
501
  }
453
502
 
454
503
  //#endregion
455
- //#region src/index.ts
456
- const plugin = {
504
+ //#region src/rules/no-chained-type-assertions.ts
505
+ function isTypeAssertionExpression(node) {
506
+ return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
507
+ }
508
+ function unwrapParenthesizedExpression(expression) {
509
+ let current = expression;
510
+ while (current.type === "ParenthesizedExpression") current = current.expression;
511
+ return current;
512
+ }
513
+ function isConstAssertion$1(node) {
514
+ const { typeAnnotation } = node;
515
+ return typeAnnotation.type === "TSTypeReference" && typeAnnotation.typeName.type === "Identifier" && typeAnnotation.typeName.name === "const";
516
+ }
517
+ function isOutermostAssertionInChain(node) {
518
+ let current = node;
519
+ let parent = node.parent;
520
+ while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
521
+ current = parent;
522
+ parent = parent.parent;
523
+ }
524
+ return !isTypeAssertionExpression(parent) || parent.expression !== current;
525
+ }
526
+ function isForbiddenAssertionChain(node) {
527
+ let assertionCount = 0;
528
+ let hasNonConstAssertion = false;
529
+ let current = node;
530
+ while (isTypeAssertionExpression(current)) {
531
+ assertionCount += 1;
532
+ hasNonConstAssertion ||= !isConstAssertion$1(current);
533
+ current = unwrapParenthesizedExpression(current.expression);
534
+ }
535
+ return assertionCount > 1 && hasNonConstAssertion;
536
+ }
537
+ /** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
538
+ const noChainedTypeAssertionsRule = defineRule({
539
+ meta: {
540
+ type: "problem",
541
+ docs: { description: "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains." },
542
+ messages: { chained: "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it." }
543
+ },
544
+ createOnce(context) {
545
+ const checkTypeAssertion = (node) => {
546
+ if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return;
547
+ context.report({
548
+ node,
549
+ messageId: "chained"
550
+ });
551
+ };
552
+ return {
553
+ TSAsExpression: checkTypeAssertion,
554
+ TSTypeAssertion: checkTypeAssertion
555
+ };
556
+ }
557
+ });
558
+
559
+ //#endregion
560
+ //#region src/shared/scope.ts
561
+ function resolveVariable(sourceCode, identifier) {
562
+ let scope = sourceCode.getScope(identifier);
563
+ while (scope !== null) {
564
+ const variable = scope.set.get(identifier.name);
565
+ if (variable !== void 0) return variable;
566
+ scope = scope.upper;
567
+ }
568
+ return null;
569
+ }
570
+
571
+ //#endregion
572
+ //#region src/rules/no-conditional-undefined-properties.ts
573
+ function unwrapParentheses(expression) {
574
+ let current = expression;
575
+ while (current.type === "ParenthesizedExpression") current = current.expression;
576
+ return current;
577
+ }
578
+ function isUndefinedExpression(sourceCode, expression) {
579
+ const unwrapped = unwrapParentheses(expression);
580
+ const undefinedVariable = unwrapped.type === "Identifier" && unwrapped.name === "undefined" ? resolveVariable(sourceCode, unwrapped) : void 0;
581
+ return unwrapped.type === "Identifier" && unwrapped.name === "undefined" && (undefinedVariable === null || undefinedVariable?.defs.length === 0) || unwrapped.type === "UnaryExpression" && unwrapped.operator === "void";
582
+ }
583
+ function hasConditionalUndefinedValue(sourceCode, value) {
584
+ const unwrapped = unwrapParentheses(value);
585
+ if (unwrapped.type !== "ConditionalExpression") return false;
586
+ return [unwrapped.consequent, unwrapped.alternate].some((branch) => isUndefinedExpression(sourceCode, branch) || hasConditionalUndefinedValue(sourceCode, branch));
587
+ }
588
+ function isObjectExpressionProperty(node) {
589
+ return node.type === "Property" && node.parent.type === "ObjectExpression";
590
+ }
591
+ /** Disallow conditional undefined values that retain an optional property. */
592
+ const noConditionalUndefinedPropertiesRule = defineRule({
593
+ meta: {
594
+ type: "problem",
595
+ docs: { description: "Disallow object properties whose conditional value is undefined." },
596
+ messages: { conditionalUndefined: "This conditional keeps the property present with an undefined value. Build a typed object and add the property only when present." }
597
+ },
598
+ createOnce(context) {
599
+ return { Property(node) {
600
+ if (isObjectExpressionProperty(node) && node.kind === "init" && !node.method && hasConditionalUndefinedValue(context.sourceCode, node.value)) context.report({
601
+ node,
602
+ messageId: "conditionalUndefined"
603
+ });
604
+ } };
605
+ }
606
+ });
607
+
608
+ //#endregion
609
+ //#region src/rules/no-enum-declarations.ts
610
+ function isInsideAmbientModule(node) {
611
+ let current = node.parent;
612
+ while (current.type !== "Program") {
613
+ if (current.type === "TSModuleDeclaration" && current.declare) return true;
614
+ current = current.parent;
615
+ }
616
+ return false;
617
+ }
618
+ /** Prefer literal unions or constant objects over repository-owned TypeScript enums. */
619
+ const noEnumDeclarationsRule = defineRule({
620
+ meta: {
621
+ type: "suggestion",
622
+ docs: { description: "Disallow repository-owned enum declarations while preserving ambient enums." },
623
+ messages: { enumDeclaration: "Replace this enum with a literal union or an inferred constant object. Keep ambient enums only when their boundary requires them." }
624
+ },
625
+ create(context) {
626
+ const isDeclarationFile = /\.d\.[cm]?ts$/u.test(context.filename);
627
+ return { TSEnumDeclaration(node) {
628
+ if (!node.declare && !isDeclarationFile && !isInsideAmbientModule(node)) context.report({
629
+ node,
630
+ messageId: "enumDeclaration"
631
+ });
632
+ } };
633
+ }
634
+ });
635
+
636
+ //#endregion
637
+ //#region src/shared/dictionary-types.ts
638
+ const BUILT_INS = new Set([
639
+ "Record",
640
+ "Readonly",
641
+ "Partial",
642
+ "Required",
643
+ "Pick",
644
+ "Omit",
645
+ "PropertyKey",
646
+ "NonNullable"
647
+ ]);
648
+ const TRANSPARENT_WRAPPERS = new Set([
649
+ "Readonly",
650
+ "Partial",
651
+ "Required",
652
+ "NonNullable"
653
+ ]);
654
+ function declaredStatement(statement) {
655
+ return statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration ?? null : statement;
656
+ }
657
+ function createTypeEnvironment(program) {
658
+ const aliases = /* @__PURE__ */ new Map();
659
+ const interfaces = /* @__PURE__ */ new Map();
660
+ const shadowedBuiltIns = /* @__PURE__ */ new Set();
661
+ for (const statement of program.body) {
662
+ const declaration = declaredStatement(statement);
663
+ if (declaration?.type === "ImportDeclaration") {
664
+ for (const specifier of declaration.specifiers) if (BUILT_INS.has(specifier.local.name)) shadowedBuiltIns.add(specifier.local.name);
665
+ continue;
666
+ }
667
+ if (declaration?.type === "TSTypeAliasDeclaration") {
668
+ if (aliases.get(declaration.id.name) === void 0) aliases.set(declaration.id.name, declaration);
669
+ else shadowedBuiltIns.add(declaration.id.name);
670
+ if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
671
+ continue;
672
+ }
673
+ if (declaration?.type === "TSInterfaceDeclaration") {
674
+ const declarations = interfaces.get(declaration.id.name) ?? [];
675
+ declarations.push(declaration);
676
+ interfaces.set(declaration.id.name, declarations);
677
+ if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
678
+ continue;
679
+ }
680
+ if (declaration?.type === "TSEnumDeclaration") {
681
+ if (BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
682
+ continue;
683
+ }
684
+ if ((declaration?.type === "ClassDeclaration" || declaration?.type === "FunctionDeclaration") && declaration.id !== null && BUILT_INS.has(declaration.id.name)) shadowedBuiltIns.add(declaration.id.name);
685
+ }
686
+ return {
687
+ aliases,
688
+ interfaces,
689
+ shadowedBuiltIns
690
+ };
691
+ }
692
+ function typeReferenceName$2(type) {
693
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
694
+ }
695
+ function isBuiltIn(name, environment) {
696
+ return BUILT_INS.has(name) && !environment.shadowedBuiltIns.has(name);
697
+ }
698
+ function isUnappliedReferenceTo(type, name) {
699
+ const unwrapped = unwrapTransparentType(type);
700
+ return unwrapped.type === "TSTypeReference" && typeReferenceName$2(unwrapped) === name && (unwrapped.typeArguments?.params.length ?? 0) === 0;
701
+ }
702
+ function unwrapTransparentType(type) {
703
+ let current = type;
704
+ while (current.type === "TSParenthesizedType" || current.type === "TSTypeOperator" && current.operator === "readonly") current = current.typeAnnotation;
705
+ return current;
706
+ }
707
+ function isNeverType(type) {
708
+ return unwrapTransparentType(type).type === "TSNeverKeyword";
709
+ }
710
+ function optionalPropertyTypeAnnotation(member) {
711
+ return member.typeAnnotation;
712
+ }
713
+ function optionalMappedTypeAnnotation(type) {
714
+ return type.typeAnnotation;
715
+ }
716
+ function isEffectivelyEmptyMember(member) {
717
+ if (member.type !== "TSPropertySignature" || !member.optional) return false;
718
+ const typeAnnotation = optionalPropertyTypeAnnotation(member);
719
+ return typeAnnotation !== null && typeAnnotation !== void 0 && isNeverType(typeAnnotation.typeAnnotation);
720
+ }
721
+ function isEffectivelyEmptyTypeLiteral(type) {
722
+ return type.members.length === 0 || type.members.every(isEffectivelyEmptyMember);
723
+ }
724
+ function isEffectivelyEmptyInterface(declarations) {
725
+ if (declarations.length !== 1) return false;
726
+ const [type] = declarations;
727
+ return type?.extends.length === 0 && (type.body.body.length === 0 || type.body.body.every(isEffectivelyEmptyMember));
728
+ }
729
+ function resolvedSubstitutionArgument({ base, resolving = /* @__PURE__ */ new Set(), type }) {
730
+ const unwrapped = unwrapTransparentType(type);
731
+ if (unwrapped.type !== "TSTypeReference") return type;
732
+ const name = typeReferenceName$2(unwrapped);
733
+ if (name === null || resolving.has(name)) return type;
734
+ const substitution = base.get(name);
735
+ if (substitution === void 0) return type;
736
+ return resolvedSubstitutionArgument({
737
+ base,
738
+ resolving: new Set([...resolving, name]),
739
+ type: substitution
740
+ });
741
+ }
742
+ function aliasSubstitution({ alias, base, type }) {
743
+ const parameters = alias.typeParameters?.params ?? [];
744
+ const arguments_ = type.typeArguments?.params ?? [];
745
+ const next = new Map(base);
746
+ for (const [index, parameter] of parameters.entries()) {
747
+ const argument = arguments_[index] ?? parameter.default;
748
+ if (argument === null) return null;
749
+ next.set(parameter.name.name, resolvedSubstitutionArgument({
750
+ base: next,
751
+ type: argument
752
+ }));
753
+ }
754
+ return next;
755
+ }
756
+ function unsafeDirectValue({ environment, resolvingAliases, substitutions, type }) {
757
+ const unwrapped = unwrapTransparentType(type);
758
+ if (unwrapped.type === "TSUnknownKeyword") return "unknown";
759
+ if (unwrapped.type === "TSAnyKeyword") return "any";
760
+ if (unwrapped.type === "TSObjectKeyword") return "object";
761
+ if (unwrapped.type === "TSTypeLiteral" && isEffectivelyEmptyTypeLiteral(unwrapped)) return "empty-object";
762
+ if (unwrapped.type === "TSUnionType") return unwrapped.types.some((member) => unsafeDirectValue({
763
+ environment,
764
+ type: member,
765
+ substitutions,
766
+ resolvingAliases
767
+ }) !== null) ? "union" : null;
768
+ if (unwrapped.type === "TSIntersectionType") {
769
+ const unsafeMembers = unwrapped.types.map((member) => unsafeDirectValue({
770
+ environment,
771
+ resolvingAliases,
772
+ substitutions,
773
+ type: member
774
+ }));
775
+ if (unsafeMembers.includes("any")) return "any";
776
+ return unsafeMembers.length > 0 && unsafeMembers.every((member) => member !== null) ? unsafeMembers[0] ?? null : null;
777
+ }
778
+ if (unwrapped.type !== "TSTypeReference") return null;
779
+ const name = typeReferenceName$2(unwrapped);
780
+ if (name === null) return null;
781
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
782
+ const wrapped = unwrapped.typeArguments?.params[0];
783
+ return wrapped === void 0 ? null : unsafeDirectValue({
784
+ environment,
785
+ type: wrapped,
786
+ substitutions,
787
+ resolvingAliases
788
+ });
789
+ }
790
+ const substitution = substitutions.get(name);
791
+ if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : unsafeDirectValue({
792
+ environment,
793
+ type: substitution,
794
+ substitutions,
795
+ resolvingAliases
796
+ });
797
+ const interfaceDeclarations = environment.interfaces.get(name);
798
+ if (interfaceDeclarations !== void 0) return isEffectivelyEmptyInterface(interfaceDeclarations) ? "empty-object" : null;
799
+ const alias = environment.aliases.get(name);
800
+ if (alias === void 0 || resolvingAliases.has(name)) return null;
801
+ const nextSubstitutions = aliasSubstitution({
802
+ alias,
803
+ base: substitutions,
804
+ type: unwrapped
805
+ });
806
+ if (nextSubstitutions === null) return null;
807
+ return unsafeDirectValue({
808
+ environment,
809
+ resolvingAliases: new Set([...resolvingAliases, name]),
810
+ substitutions: nextSubstitutions,
811
+ type: alias.typeAnnotation
812
+ });
813
+ }
814
+ function dictionaryValueTypes({ environment, resolvingAliases, substitutions, type }) {
815
+ const unwrapped = unwrapTransparentType(type);
816
+ if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.flatMap((member) => member.type === "TSIndexSignature" ? [{
817
+ type: member.typeAnnotation.typeAnnotation,
818
+ substitutions
819
+ }] : []);
820
+ if (unwrapped.type === "TSMappedType") {
821
+ const typeAnnotation = optionalMappedTypeAnnotation(unwrapped);
822
+ return typeAnnotation === null || typeAnnotation === void 0 ? [] : [{
823
+ type: typeAnnotation,
824
+ substitutions
825
+ }];
826
+ }
827
+ if (unwrapped.type !== "TSTypeReference") return [];
828
+ const name = typeReferenceName$2(unwrapped);
829
+ if (name === null) return [];
830
+ const substitution = substitutions.get(name);
831
+ if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? [] : dictionaryValueTypes({
832
+ environment,
833
+ type: substitution,
834
+ substitutions,
835
+ resolvingAliases
836
+ });
837
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
838
+ const wrapped = unwrapped.typeArguments?.params[0];
839
+ return wrapped === void 0 ? [] : dictionaryValueTypes({
840
+ environment,
841
+ type: wrapped,
842
+ substitutions,
843
+ resolvingAliases
844
+ });
845
+ }
846
+ if (name === "Record" && isBuiltIn(name, environment)) {
847
+ const value = unwrapped.typeArguments?.params[1] ?? null;
848
+ return value === null ? [] : [{
849
+ type: value,
850
+ substitutions
851
+ }];
852
+ }
853
+ if ((name === "Pick" || name === "Omit") && isBuiltIn(name, environment)) {
854
+ const source = unwrapped.typeArguments?.params[0];
855
+ return source === void 0 ? [] : dictionaryValueTypes({
856
+ environment,
857
+ type: source,
858
+ substitutions,
859
+ resolvingAliases
860
+ });
861
+ }
862
+ const alias = environment.aliases.get(name);
863
+ if (alias === void 0 || resolvingAliases.has(name)) return [];
864
+ const nextSubstitutions = aliasSubstitution({
865
+ alias,
866
+ base: substitutions,
867
+ type: unwrapped
868
+ });
869
+ if (nextSubstitutions === null) return [];
870
+ return dictionaryValueTypes({
871
+ environment,
872
+ resolvingAliases: new Set([...resolvingAliases, name]),
873
+ substitutions: nextSubstitutions,
874
+ type: alias.typeAnnotation
875
+ });
876
+ }
877
+ function classifyUnsafeDictionaryValue(valueType, environment) {
878
+ const unsafeValue = unsafeDirectValue({
879
+ environment,
880
+ resolvingAliases: /* @__PURE__ */ new Set(),
881
+ substitutions: /* @__PURE__ */ new Map(),
882
+ type: valueType
883
+ });
884
+ return unsafeValue === null ? null : {
885
+ kind: "unsafe-dictionary",
886
+ unsafeValue
887
+ };
888
+ }
889
+ function classifyUnsafeDictionary(type, environment) {
890
+ for (const valueType of dictionaryValueTypes({
891
+ environment,
892
+ resolvingAliases: /* @__PURE__ */ new Set(),
893
+ substitutions: /* @__PURE__ */ new Map(),
894
+ type
895
+ })) {
896
+ const unsafeValue = unsafeDirectValue({
897
+ environment,
898
+ resolvingAliases: /* @__PURE__ */ new Set(),
899
+ substitutions: valueType.substitutions,
900
+ type: valueType.type
901
+ });
902
+ if (unsafeValue !== null) return {
903
+ kind: "unsafe-dictionary",
904
+ unsafeValue
905
+ };
906
+ }
907
+ return null;
908
+ }
909
+ function resolvesToDictionary({ environment, resolvingAliases, substitutions, type }) {
910
+ return dictionaryValueTypes({
911
+ environment,
912
+ resolvingAliases,
913
+ substitutions,
914
+ type
915
+ }).length > 0;
916
+ }
917
+ function classifyWideningTarget(type, environment) {
918
+ const unwrapped = unwrapTransparentType(type);
919
+ if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
920
+ if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
921
+ if (unwrapped.type === "TSTypeLiteral") {
922
+ if (unwrapped.members.some((member) => member.type === "TSIndexSignature")) return { kind: "open dictionary" };
923
+ return unwrapped.members.length > 0 ? { kind: "anonymous object" } : null;
924
+ }
925
+ if (unwrapped.type === "TSMappedType") return { kind: "open dictionary" };
926
+ if (unwrapped.type !== "TSTypeReference") return null;
927
+ const name = typeReferenceName$2(unwrapped);
928
+ if (name === null) return null;
929
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
930
+ const wrapped = unwrapped.typeArguments?.params[0];
931
+ return wrapped === void 0 ? null : classifyWideningTarget(wrapped, environment);
932
+ }
933
+ if (name === "Record" && isBuiltIn(name, environment)) return { kind: "open dictionary" };
934
+ const alias = environment.aliases.get(name);
935
+ if (alias === void 0) return null;
936
+ if ((alias.typeParameters?.params.length ?? 0) > 0) {
937
+ const substitutions$1 = aliasSubstitution({
938
+ alias,
939
+ base: /* @__PURE__ */ new Map(),
940
+ type: unwrapped
941
+ });
942
+ return substitutions$1 !== null && resolvesToDictionary({
943
+ environment,
944
+ type: alias.typeAnnotation,
945
+ substitutions: substitutions$1,
946
+ resolvingAliases: new Set([name])
947
+ }) ? { kind: "generic container" } : null;
948
+ }
949
+ const substitutions = aliasSubstitution({
950
+ alias,
951
+ base: /* @__PURE__ */ new Map(),
952
+ type: unwrapped
953
+ });
954
+ if (substitutions === null) return null;
955
+ return classifyAliasBroadTarget({
956
+ environment,
957
+ type: alias.typeAnnotation,
958
+ substitutions,
959
+ resolvingAliases: new Set([name])
960
+ });
961
+ }
962
+ function isBroadMappedKey({ environment, substitutions, type }) {
963
+ const unwrapped = unwrapTransparentType(type);
964
+ if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
965
+ if (unwrapped.type === "TSUnionType") return unwrapped.types.every((member) => isBroadMappedKey({
966
+ environment,
967
+ substitutions,
968
+ type: member
969
+ }));
970
+ if (unwrapped.type !== "TSTypeReference") return false;
971
+ const name = typeReferenceName$2(unwrapped);
972
+ if (name === null) return false;
973
+ const substitution = substitutions.get(name);
974
+ if (substitution !== void 0 && !isUnappliedReferenceTo(substitution, name)) return isBroadMappedKey({
975
+ environment,
976
+ substitutions,
977
+ type: substitution
978
+ });
979
+ return name === "PropertyKey" && isBuiltIn(name, environment);
980
+ }
981
+ function classifyAliasBroadTarget({ environment, resolvingAliases, substitutions, type }) {
982
+ const unwrapped = unwrapTransparentType(type);
983
+ if (unwrapped.type === "TSUnknownKeyword") return { kind: "unknown" };
984
+ if (unwrapped.type === "TSObjectKeyword") return { kind: "object" };
985
+ if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type === "TSIndexSignature") ? { kind: "open dictionary" } : null;
986
+ if (unwrapped.type === "TSMappedType") return isBroadMappedKey({
987
+ environment,
988
+ substitutions,
989
+ type: unwrapped.constraint
990
+ }) ? { kind: "open dictionary" } : null;
991
+ if (unwrapped.type !== "TSTypeReference") return null;
992
+ const name = typeReferenceName$2(unwrapped);
993
+ if (name === null) return null;
994
+ const substitution = substitutions.get(name);
995
+ if (substitution !== void 0) return isUnappliedReferenceTo(substitution, name) ? null : classifyAliasBroadTarget({
996
+ environment,
997
+ type: substitution,
998
+ substitutions,
999
+ resolvingAliases
1000
+ });
1001
+ if (TRANSPARENT_WRAPPERS.has(name) && isBuiltIn(name, environment)) {
1002
+ const wrapped = unwrapped.typeArguments?.params[0];
1003
+ return wrapped === void 0 ? null : classifyAliasBroadTarget({
1004
+ environment,
1005
+ type: wrapped,
1006
+ substitutions,
1007
+ resolvingAliases
1008
+ });
1009
+ }
1010
+ if (name === "Record" && isBuiltIn(name, environment)) return { kind: "open dictionary" };
1011
+ const alias = environment.aliases.get(name);
1012
+ if (alias === void 0 || resolvingAliases.has(name)) return null;
1013
+ const nextSubstitutions = aliasSubstitution({
1014
+ alias,
1015
+ base: substitutions,
1016
+ type: unwrapped
1017
+ });
1018
+ if (nextSubstitutions === null) return null;
1019
+ return classifyAliasBroadTarget({
1020
+ environment,
1021
+ resolvingAliases: new Set([...resolvingAliases, name]),
1022
+ substitutions: nextSubstitutions,
1023
+ type: alias.typeAnnotation
1024
+ });
1025
+ }
1026
+ function isKnownEvidenceExpression(expression) {
1027
+ let current = expression;
1028
+ while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression" || current.type === "TSSatisfiesExpression") current = current.expression;
1029
+ if (current.type === "ObjectExpression") return true;
1030
+ 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";
1031
+ }
1032
+
1033
+ //#endregion
1034
+ //#region src/rules/no-known-value-widening.ts
1035
+ function unwrapExpression$1(expression) {
1036
+ let current = expression;
1037
+ while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") current = current.expression;
1038
+ return current;
1039
+ }
1040
+ function variableDeclarator$1(variable) {
1041
+ if (variable.defs.length !== 1) return null;
1042
+ const [definition] = variable.defs;
1043
+ return definition?.type === "Variable" && definition.node.type === "VariableDeclarator" ? definition.node : null;
1044
+ }
1045
+ function isStableConstVariable(variable, declarator) {
1046
+ return declarator.parent.type === "VariableDeclaration" && declarator.parent.kind === "const" && variable.references.every((reference) => reference.init || !reference.isWrite());
1047
+ }
1048
+ function hasKnownEvidence({ expression, sourceCode, visitedVariables = /* @__PURE__ */ new Set() }) {
1049
+ if (isKnownEvidenceExpression(expression)) return true;
1050
+ const unwrapped = unwrapExpression$1(expression);
1051
+ if (unwrapped.type !== "Identifier") return false;
1052
+ const variable = resolveVariable(sourceCode, unwrapped);
1053
+ if (variable === null || visitedVariables.has(variable)) return false;
1054
+ const declarator = variableDeclarator$1(variable);
1055
+ if (declarator === null) return false;
1056
+ if (declarator.init === null || !isStableConstVariable(variable, declarator)) return false;
1057
+ visitedVariables.add(variable);
1058
+ return hasKnownEvidence({
1059
+ expression: declarator.init,
1060
+ sourceCode,
1061
+ visitedVariables
1062
+ });
1063
+ }
1064
+ function annotationTarget(annotation, environment) {
1065
+ return annotation === null || annotation === void 0 ? null : classifyWideningTarget(annotation.typeAnnotation, environment);
1066
+ }
1067
+ function enclosingFunction(node) {
1068
+ let current = node.parent;
1069
+ while (current !== null && current.type !== "Program") {
1070
+ if (current.type === "ArrowFunctionExpression" || current.type === "FunctionDeclaration" || current.type === "FunctionExpression") return current;
1071
+ current = current.parent;
1072
+ }
1073
+ return null;
1074
+ }
1075
+ function sourceKeyName(sourceCode, key) {
1076
+ if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name;
1077
+ if (key.type === "Literal") return String(key.value);
1078
+ return sourceCode.getText(key);
1079
+ }
1080
+ function functionName(sourceCode, owner) {
1081
+ if (owner === null) return "anonymous function";
1082
+ if (owner.id !== null) return owner.id.name;
1083
+ const parent = owner.parent;
1084
+ if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") return parent.id.name;
1085
+ if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key);
1086
+ return "anonymous function";
1087
+ }
1088
+ function isDictionaryAccumulatorTarget(destination) {
1089
+ return destination.kind === "open dictionary" || destination.kind === "generic container";
1090
+ }
1091
+ function isObjectExpression(expression) {
1092
+ return unwrapExpression$1(expression).type === "ObjectExpression";
1093
+ }
1094
+ function hasParentAssertion(node) {
1095
+ return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
1096
+ }
1097
+ /** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
1098
+ const noKnownValueWideningRule = defineRule({
1099
+ meta: {
1100
+ type: "problem",
1101
+ docs: { description: "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence." },
1102
+ messages: { widening: "The explicit {{target}} type on {{subject}} discards known type evidence. Keep inference, validate with `satisfies`, or use a named owner contract." }
1103
+ },
1104
+ createOnce(context) {
1105
+ let environment = null;
1106
+ const reportFlow = ({ destination, expression, subject }) => {
1107
+ if (destination === null) return;
1108
+ if (isDictionaryAccumulatorTarget(destination) && isObjectExpression(expression)) return;
1109
+ if (!hasKnownEvidence({
1110
+ expression,
1111
+ sourceCode: context.sourceCode
1112
+ })) return;
1113
+ context.report({
1114
+ node: expression,
1115
+ messageId: "widening",
1116
+ data: {
1117
+ subject,
1118
+ target: destination.kind
1119
+ }
1120
+ });
1121
+ };
1122
+ const targetFromAnnotation = (annotation) => environment === null ? null : annotationTarget(annotation, environment);
1123
+ return {
1124
+ Program(node) {
1125
+ environment = createTypeEnvironment(node);
1126
+ },
1127
+ VariableDeclarator(node) {
1128
+ if (node.init === null || node.id.type !== "Identifier") return;
1129
+ reportFlow({
1130
+ destination: targetFromAnnotation(node.id.typeAnnotation),
1131
+ expression: node.init,
1132
+ subject: `binding \`${node.id.name}\``
1133
+ });
1134
+ },
1135
+ PropertyDefinition(node) {
1136
+ if (node.value === null) return;
1137
+ reportFlow({
1138
+ destination: targetFromAnnotation(node.typeAnnotation),
1139
+ expression: node.value,
1140
+ subject: `property \`${sourceKeyName(context.sourceCode, node.key)}\``
1141
+ });
1142
+ },
1143
+ AccessorProperty(node) {
1144
+ if (node.value === null) return;
1145
+ reportFlow({
1146
+ destination: targetFromAnnotation(node.typeAnnotation),
1147
+ expression: node.value,
1148
+ subject: `property \`${sourceKeyName(context.sourceCode, node.key)}\``
1149
+ });
1150
+ },
1151
+ AssignmentExpression(node) {
1152
+ if (node.operator !== "=" || node.left.type !== "Identifier") return;
1153
+ const variable = resolveVariable(context.sourceCode, node.left);
1154
+ if (variable === null) return;
1155
+ const declarator = variableDeclarator$1(variable);
1156
+ if (declarator?.id.type !== "Identifier") return;
1157
+ reportFlow({
1158
+ destination: targetFromAnnotation(declarator.id.typeAnnotation),
1159
+ expression: node.right,
1160
+ subject: `binding \`${declarator.id.name}\``
1161
+ });
1162
+ },
1163
+ ReturnStatement(node) {
1164
+ if (node.argument === null) return;
1165
+ const owner = enclosingFunction(node);
1166
+ reportFlow({
1167
+ destination: targetFromAnnotation(owner?.returnType),
1168
+ expression: node.argument,
1169
+ subject: `return value of \`${functionName(context.sourceCode, owner)}\``
1170
+ });
1171
+ },
1172
+ ArrowFunctionExpression(node) {
1173
+ if (node.body.type === "BlockStatement") return;
1174
+ reportFlow({
1175
+ destination: targetFromAnnotation(node.returnType),
1176
+ expression: node.body,
1177
+ subject: `return value of \`${functionName(context.sourceCode, node)}\``
1178
+ });
1179
+ },
1180
+ TSAsExpression(node) {
1181
+ if (environment === null || hasParentAssertion(node)) return;
1182
+ reportFlow({
1183
+ destination: classifyWideningTarget(node.typeAnnotation, environment),
1184
+ expression: node.expression,
1185
+ subject: "assertion"
1186
+ });
1187
+ },
1188
+ TSTypeAssertion(node) {
1189
+ if (environment === null || hasParentAssertion(node)) return;
1190
+ reportFlow({
1191
+ destination: classifyWideningTarget(node.typeAnnotation, environment),
1192
+ expression: node.expression,
1193
+ subject: "assertion"
1194
+ });
1195
+ }
1196
+ };
1197
+ }
1198
+ });
1199
+
1200
+ //#endregion
1201
+ //#region src/shared/rule-options.ts
1202
+ /** Normalize the option shapes exposed by the ESLint and Oxlint contexts. */
1203
+ function ruleContextOptionsSchema(optionsSchema) {
1204
+ return z.union([optionsSchema, z.array(optionsSchema)]).nullable().transform((options) => {
1205
+ if (options === null) return;
1206
+ if (Array.isArray(options)) return options[0];
1207
+ return options;
1208
+ });
1209
+ }
1210
+
1211
+ //#endregion
1212
+ //#region src/rules/no-module-mocking.ts
1213
+ const moduleMockMethods = new Set([
1214
+ "doMock",
1215
+ "mock",
1216
+ "unstable_mockModule"
1217
+ ]);
1218
+ const ModuleMockContextOptionsSchema = ruleContextOptionsSchema(z.object({ internalModulePrefixes: z.array(z.string()).optional() }));
1219
+ function importedName(node) {
1220
+ if (node.type !== "ImportSpecifier") return null;
1221
+ return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
1222
+ }
1223
+ function isGlobalReference(sourceCode, expression) {
1224
+ const variable = resolveVariable(sourceCode, expression);
1225
+ return variable === null || variable.defs.length === 0;
1226
+ }
1227
+ function isTestFrameworkObject(sourceCode, expression) {
1228
+ if (expression.type !== "Identifier") return false;
1229
+ if ((expression.name === "vi" || expression.name === "jest") && isGlobalReference(sourceCode, expression)) return true;
1230
+ const variable = resolveVariable(sourceCode, expression);
1231
+ if (variable === null || variable.defs.length === 0) return expression.name === "vi" || expression.name === "jest";
1232
+ return variable.defs.some((definition) => {
1233
+ if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") return false;
1234
+ const source = definition.parent.source.value;
1235
+ const name = importedName(definition.node);
1236
+ return source === "vitest" && name === "vi" || source === "@jest/globals" && name === "jest";
1237
+ });
1238
+ }
1239
+ function moduleMockCall(sourceCode, callee) {
1240
+ if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
1241
+ if (!isTestFrameworkObject(sourceCode, callee.object)) return false;
1242
+ const property = callee.property;
1243
+ let method = null;
1244
+ if (callee.computed && property.type === "Literal" && (property.value === "doMock" || property.value === "mock" || property.value === "unstable_mockModule")) method = property.value;
1245
+ else if (!callee.computed && property.type === "Identifier") method = property.name;
1246
+ return method !== null && moduleMockMethods.has(method);
1247
+ }
1248
+ function isRepositoryOwnedSpecifier(specifier, internalModulePrefixes) {
1249
+ return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("#") || internalModulePrefixes.some((prefix) => specifier.startsWith(prefix));
1250
+ }
1251
+ function isString$1(value) {
1252
+ return typeof value === "string";
1253
+ }
1254
+ /** Ban test framework mocking of repository-owned modules. */
1255
+ const noModuleMockingRule = defineRule({
1256
+ meta: {
1257
+ type: "problem",
1258
+ docs: { description: "Disallow Vitest and Jest mocking of repository-owned modules; tests must replace local dependencies through production seams." },
1259
+ messages: { moduleMock: "Replace this local module mock through a production dependency seam and a faithful test implementation." },
1260
+ schema: [{
1261
+ type: "object",
1262
+ properties: { internalModulePrefixes: {
1263
+ type: "array",
1264
+ items: {
1265
+ type: "string",
1266
+ minLength: 1
1267
+ },
1268
+ uniqueItems: true
1269
+ } },
1270
+ additionalProperties: false
1271
+ }],
1272
+ defaultOptions: [{ internalModulePrefixes: [] }]
1273
+ },
1274
+ createOnce(context) {
1275
+ return { CallExpression(node) {
1276
+ if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
1277
+ if (!moduleMockCall(context.sourceCode, node.callee)) return;
1278
+ const rawOptions = context.options;
1279
+ const parsedOptions = ModuleMockContextOptionsSchema.safeParse(rawOptions);
1280
+ const internalModulePrefixes = (parsedOptions.success ? parsedOptions.data : void 0)?.internalModulePrefixes ?? [];
1281
+ const [specifier] = node.arguments;
1282
+ if (specifier?.type !== "Literal" || !isString$1(specifier.value) || !isRepositoryOwnedSpecifier(specifier.value, internalModulePrefixes)) return;
1283
+ context.report({
1284
+ node,
1285
+ messageId: "moduleMock"
1286
+ });
1287
+ } };
1288
+ }
1289
+ });
1290
+
1291
+ //#endregion
1292
+ //#region src/shared/lexical-type-parameters.ts
1293
+ const AstNodeSchema = z.custom((value) => z.object({ type: z.string() }).safeParse(value).success);
1294
+ function collectInferTypeParameterNames({ names, node, visitorKeys }) {
1295
+ if (node.type === "TSInferType") names.add(node.typeParameter.name.name);
1296
+ const childKeys = visitorKeys[node.type] ?? [];
1297
+ for (const [key, value] of Object.entries(node)) {
1298
+ if (!childKeys.includes(key)) continue;
1299
+ const parsedNode = AstNodeSchema.safeParse(value);
1300
+ if (parsedNode.success) {
1301
+ collectInferTypeParameterNames({
1302
+ names,
1303
+ node: parsedNode.data,
1304
+ visitorKeys
1305
+ });
1306
+ continue;
1307
+ }
1308
+ if (!Array.isArray(value)) continue;
1309
+ for (const child of value) {
1310
+ const parsedChild = AstNodeSchema.safeParse(child);
1311
+ if (parsedChild.success) collectInferTypeParameterNames({
1312
+ names,
1313
+ node: parsedChild.data,
1314
+ visitorKeys
1315
+ });
1316
+ }
1317
+ }
1318
+ }
1319
+ /** Collect type binders that are in scope at a node and can shadow module aliases. */
1320
+ function lexicalTypeParameterNames(node, visitorKeys) {
1321
+ const names = /* @__PURE__ */ new Set();
1322
+ let descendant = node;
1323
+ let current = node;
1324
+ while (current.type !== "Program") {
1325
+ if ("typeParameters" in current) for (const parameter of current.typeParameters?.params ?? []) names.add(parameter.name.name);
1326
+ if (current.type === "TSMappedType" && (descendant === current.nameType || descendant === current.typeAnnotation)) names.add(current.key.name);
1327
+ if (current.type === "TSConditionalType" && descendant === current.trueType) collectInferTypeParameterNames({
1328
+ names,
1329
+ node: current.extendsType,
1330
+ visitorKeys
1331
+ });
1332
+ descendant = current;
1333
+ current = current.parent;
1334
+ }
1335
+ return names;
1336
+ }
1337
+
1338
+ //#endregion
1339
+ //#region src/shared/type-alias.ts
1340
+ function typeAliasDeclarationOf(definition) {
1341
+ const runtimeDefinition = definition;
1342
+ return runtimeDefinition.type === "Type" && runtimeDefinition.node.type === "TSTypeAliasDeclaration" ? runtimeDefinition.node : null;
1343
+ }
1344
+ function resolveTypeAlias(sourceCode, reference) {
1345
+ if (reference.typeName.type !== "Identifier") return null;
1346
+ let scope = sourceCode.getScope(reference);
1347
+ while (scope !== null) {
1348
+ const variable = scope.set.get(reference.typeName.name);
1349
+ for (const definition of variable?.defs ?? []) {
1350
+ const alias = typeAliasDeclarationOf(definition);
1351
+ if (alias !== null) return alias;
1352
+ }
1353
+ scope = scope.upper;
1354
+ }
1355
+ return null;
1356
+ }
1357
+
1358
+ //#endregion
1359
+ //#region src/rules/no-object-parameters.ts
1360
+ function parameterAnnotation$1(parameter) {
1361
+ if (parameter.type === "TSParameterProperty") return parameterAnnotation$1(parameter.parameter);
1362
+ if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation$1(parameter.argument);
1363
+ if (parameter.type === "AssignmentPattern") return parameter.left.typeAnnotation;
1364
+ return parameter.typeAnnotation;
1365
+ }
1366
+ function parameterType$2(parameter) {
1367
+ const annotation = parameterAnnotation$1(parameter);
1368
+ if (annotation === null || annotation === void 0) return null;
1369
+ const type = annotation.typeAnnotation;
1370
+ return parameter.type === "RestElement" && type.type === "TSArrayType" ? type.elementType : type;
1371
+ }
1372
+ function parameterName$2(parameter, sourceCode) {
1373
+ return parameter.type === "Identifier" ? parameter.name : sourceCode.getText(parameter).replace(/\s*:\s*object\s*$/u, "");
1374
+ }
1375
+ /** Ban the broad object type on function inputs, including local aliases to object. */
1376
+ const noObjectParametersRule = defineRule({
1377
+ meta: {
1378
+ type: "problem",
1379
+ docs: { description: "Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary." },
1380
+ messages: { objectParameter: "Parameter `{{parameter}}` uses the broad `object` type. Accept a named owner type; parse external input at its boundary before calling this function." }
1381
+ },
1382
+ createOnce(context) {
1383
+ const resolvesToObject = ({ shadowedAliases, type, visited = /* @__PURE__ */ new Set() }) => {
1384
+ if (type.type === "TSObjectKeyword") return true;
1385
+ if (type.type === "TSParenthesizedType") return resolvesToObject({
1386
+ shadowedAliases,
1387
+ type: type.typeAnnotation,
1388
+ visited
1389
+ });
1390
+ if (type.type === "TSUnionType") return type.types.some((member) => resolvesToObject({
1391
+ shadowedAliases,
1392
+ type: member,
1393
+ visited
1394
+ }));
1395
+ if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier" || (type.typeArguments?.params.length ?? 0) > 0 || visited.has(type.typeName.name) || shadowedAliases.has(type.typeName.name)) return false;
1396
+ const alias = resolveTypeAlias(context.sourceCode, type);
1397
+ if (alias === null || (alias.typeParameters?.params.length ?? 0) > 0) return false;
1398
+ const nextVisited = new Set([...visited, type.typeName.name]);
1399
+ return resolvesToObject({
1400
+ shadowedAliases,
1401
+ type: alias.typeAnnotation,
1402
+ visited: nextVisited
1403
+ });
1404
+ };
1405
+ const checkParameters = (node) => {
1406
+ const shadowedAliases = lexicalTypeParameterNames(node, context.sourceCode.visitorKeys);
1407
+ for (const parameter of node.params) {
1408
+ const type = parameterType$2(parameter);
1409
+ if (type === null) continue;
1410
+ if (!resolvesToObject({
1411
+ shadowedAliases,
1412
+ type
1413
+ })) continue;
1414
+ context.report({
1415
+ node: type,
1416
+ messageId: "objectParameter",
1417
+ data: { parameter: parameterName$2(parameter, context.sourceCode) }
1418
+ });
1419
+ }
1420
+ };
1421
+ return {
1422
+ ArrowFunctionExpression: checkParameters,
1423
+ FunctionDeclaration: checkParameters,
1424
+ FunctionExpression: checkParameters,
1425
+ TSCallSignatureDeclaration: checkParameters,
1426
+ TSConstructSignatureDeclaration: checkParameters,
1427
+ TSConstructorType: checkParameters,
1428
+ TSDeclareFunction: checkParameters,
1429
+ TSEmptyBodyFunctionExpression: checkParameters,
1430
+ TSFunctionType: checkParameters,
1431
+ TSMethodSignature: checkParameters
1432
+ };
1433
+ }
1434
+ });
1435
+
1436
+ //#endregion
1437
+ //#region src/shared/owned-function.ts
1438
+ function getOwnedFunctionName(node) {
1439
+ if (node.type === "FunctionDeclaration") return node.id?.name ?? null;
1440
+ const { parent } = node;
1441
+ if (parent.type === "VariableDeclarator" && parent.init === node && parent.id.type === "Identifier") return parent.id.name;
1442
+ if (parent.type === "Property" && parent.value === node && parent.parent.type === "ObjectExpression") return getStaticPropertyName({
1443
+ isComputed: parent.computed,
1444
+ key: parent.key
1445
+ });
1446
+ if ((parent.type === "MethodDefinition" || parent.type === "TSAbstractMethodDefinition") && parent.value === node) return getStaticPropertyName({
1447
+ isComputed: parent.computed,
1448
+ key: parent.key
1449
+ });
1450
+ if ((parent.type === "PropertyDefinition" || parent.type === "TSAbstractPropertyDefinition") && parent.value === node) return getStaticPropertyName({
1451
+ isComputed: parent.computed,
1452
+ key: parent.key
1453
+ });
1454
+ return null;
1455
+ }
1456
+ function getStaticPropertyName({ isComputed, key }) {
1457
+ if (key.type === "PrivateIdentifier") return `#${key.name}`;
1458
+ if (!isComputed && key.type === "Identifier") return key.name;
1459
+ if (key.type === "Literal") return parseStaticPropertyValue(key.value);
1460
+ return null;
1461
+ }
1462
+ function parseStaticPropertyValue(value) {
1463
+ if (typeof value === "string" || typeof value === "number") return String(value);
1464
+ return null;
1465
+ }
1466
+
1467
+ //#endregion
1468
+ //#region src/rules/no-positional-boolean-parameters.ts
1469
+ const ContextOptionsSchema$2 = ruleContextOptionsSchema(z.object({ allowFunctionNames: z.array(z.string()).optional() }));
1470
+ function annotationOf(parameter) {
1471
+ if (parameter.type === "TSParameterProperty") return annotationOf(parameter.parameter);
1472
+ if (parameter.type === "AssignmentPattern") return parameter.left.typeAnnotation;
1473
+ return parameter.typeAnnotation;
1474
+ }
1475
+ function parameterName$1(parameter, sourceCode) {
1476
+ if (parameter.type === "TSParameterProperty") return parameterName$1(parameter.parameter, sourceCode);
1477
+ if (parameter.type === "AssignmentPattern") return parameter.left.type === "Identifier" ? parameter.left.name : sourceCode.getText(parameter.left);
1478
+ return parameter.type === "Identifier" ? parameter.name : sourceCode.getText(parameter);
1479
+ }
1480
+ /** Disallow positional boolean flags on repository-owned named callables. */
1481
+ const noPositionalBooleanParametersRule = defineRule({
1482
+ meta: {
1483
+ type: "suggestion",
1484
+ docs: { description: "Disallow explicit boolean parameters on repository-owned named callables." },
1485
+ messages: { positionalBoolean: "Parameter `{{parameter}}` is a positional boolean flag on `{{functionName}}`. Replace it with a named options object." },
1486
+ schema: [{
1487
+ type: "object",
1488
+ properties: { allowFunctionNames: {
1489
+ type: "array",
1490
+ items: {
1491
+ type: "string",
1492
+ minLength: 1
1493
+ },
1494
+ uniqueItems: true
1495
+ } },
1496
+ additionalProperties: false
1497
+ }],
1498
+ defaultOptions: [{ allowFunctionNames: [] }]
1499
+ },
1500
+ createOnce(context) {
1501
+ const checkFunction = (node) => {
1502
+ const functionName$1 = getOwnedFunctionName(node);
1503
+ if (functionName$1 === null) return;
1504
+ const rawOptions = context.options;
1505
+ const parsedOptions = ContextOptionsSchema$2.safeParse(rawOptions);
1506
+ if ((parsedOptions.success ? parsedOptions.data : void 0)?.allowFunctionNames?.includes(functionName$1) === true) return;
1507
+ for (const parameter of node.params) {
1508
+ if (parameter.type === "RestElement") continue;
1509
+ const annotation = annotationOf(parameter);
1510
+ if (annotation?.typeAnnotation.type !== "TSBooleanKeyword") continue;
1511
+ context.report({
1512
+ node: annotation.typeAnnotation,
1513
+ messageId: "positionalBoolean",
1514
+ data: {
1515
+ functionName: functionName$1,
1516
+ parameter: parameterName$1(parameter, context.sourceCode)
1517
+ }
1518
+ });
1519
+ }
1520
+ };
1521
+ return {
1522
+ ArrowFunctionExpression: checkFunction,
1523
+ FunctionDeclaration: checkFunction,
1524
+ FunctionExpression: checkFunction
1525
+ };
1526
+ }
1527
+ });
1528
+
1529
+ //#endregion
1530
+ //#region src/shared/reflect-method.ts
1531
+ function isGlobalReflect(sourceCode, expression) {
1532
+ if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
1533
+ const variable = resolveVariable(sourceCode, expression);
1534
+ return variable === null || variable.defs.length === 0;
1535
+ }
1536
+ /** Reports whether a call target names one method on the global Reflect object. */
1537
+ function isGlobalReflectMethodCall({ callee, methodName, sourceCode }) {
1538
+ if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) return false;
1539
+ if (!isGlobalReflect(sourceCode, callee.object)) return false;
1540
+ const property = callee.property;
1541
+ return callee.computed ? property.type === "Literal" && property.value === methodName : property.type === "Identifier" && property.name === methodName;
1542
+ }
1543
+
1544
+ //#endregion
1545
+ //#region src/rules/no-reflect-apply.ts
1546
+ /** Ban Reflect.apply, which bypasses ordinary typed function calls. */
1547
+ const noReflectApplyRule = defineRule({
1548
+ meta: {
1549
+ type: "problem",
1550
+ docs: { description: "Disallow Reflect.apply; call typed functions directly or model dynamic dispatch behind an interface." },
1551
+ messages: { reflectApply: "Replace `Reflect.apply` with a typed function call. Model dynamic dispatch behind a named interface." }
1552
+ },
1553
+ createOnce(context) {
1554
+ return { CallExpression(node) {
1555
+ if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
1556
+ if (isGlobalReflectMethodCall({
1557
+ callee: node.callee,
1558
+ methodName: "apply",
1559
+ sourceCode: context.sourceCode
1560
+ })) context.report({
1561
+ node,
1562
+ messageId: "reflectApply"
1563
+ });
1564
+ } };
1565
+ }
1566
+ });
1567
+
1568
+ //#endregion
1569
+ //#region src/rules/no-reflect-get.ts
1570
+ /** Ban Reflect.get, which bypasses ordinary property access and useful type evidence. */
1571
+ const noReflectGetRule = defineRule({
1572
+ meta: {
1573
+ type: "problem",
1574
+ docs: { description: "Disallow Reflect.get; use typed property access or parse dynamic input into a domain type." },
1575
+ messages: { reflectGet: "Replace `Reflect.get` with typed property access. Parse dynamic input into a named domain type before reading it." }
1576
+ },
1577
+ createOnce(context) {
1578
+ return { CallExpression(node) {
1579
+ if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression") return;
1580
+ if (isGlobalReflectMethodCall({
1581
+ callee: node.callee,
1582
+ methodName: "get",
1583
+ sourceCode: context.sourceCode
1584
+ })) context.report({
1585
+ node,
1586
+ messageId: "reflectGet"
1587
+ });
1588
+ } };
1589
+ }
1590
+ });
1591
+
1592
+ //#endregion
1593
+ //#region src/rules/no-unhandled-detached-promises.ts
1594
+ function unwrapExpression(expression) {
1595
+ if (expression.type === "ChainExpression" || expression.type === "ParenthesizedExpression" || expression.type === "TSAsExpression" || expression.type === "TSNonNullExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSTypeAssertion") return unwrapExpression(expression.expression);
1596
+ return expression;
1597
+ }
1598
+ function isString(value) {
1599
+ return typeof value === "string";
1600
+ }
1601
+ function staticMemberName(expression) {
1602
+ if (!expression.computed && expression.property.type === "Identifier") return expression.property.name;
1603
+ if (expression.computed && expression.property.type === "Literal" && isString(expression.property.value)) return expression.property.value;
1604
+ return null;
1605
+ }
1606
+ function isRejectionHandler(argument) {
1607
+ if (argument === void 0 || argument.type === "SpreadElement") return false;
1608
+ const unwrapped = unwrapExpression(argument);
1609
+ if (unwrapped.type === "Identifier") return unwrapped.name !== "undefined";
1610
+ return ![
1611
+ "ArrayExpression",
1612
+ "BinaryExpression",
1613
+ "JSXElement",
1614
+ "JSXFragment",
1615
+ "Literal",
1616
+ "ObjectExpression",
1617
+ "TemplateLiteral"
1618
+ ].includes(unwrapped.type);
1619
+ }
1620
+ function hasRejectionHandler(expression) {
1621
+ const unwrapped = unwrapExpression(expression);
1622
+ if (unwrapped.type !== "CallExpression") return false;
1623
+ if (unwrapped.callee.type !== "Super" && unwrapped.callee.type !== "V8IntrinsicExpression" && "object" in unwrapped.callee && "property" in unwrapped.callee) {
1624
+ const memberName = staticMemberName(unwrapped.callee);
1625
+ if (memberName === "catch") return isRejectionHandler(unwrapped.arguments[0]);
1626
+ if (memberName === "then") return isRejectionHandler(unwrapped.arguments[1]);
1627
+ return hasRejectionHandler(unwrapped.callee.object);
1628
+ }
1629
+ return false;
1630
+ }
1631
+ /** Treat `void` calls as detached work and require a rejection handler. */
1632
+ const noUnhandledDetachedPromisesRule = defineRule({
1633
+ meta: {
1634
+ type: "problem",
1635
+ docs: { description: "Disallow void-marked detached call chains that do not install a rejection handler." },
1636
+ messages: { unhandledDetachedPromise: "A `void` call marks detached work and must handle rejection. Add `.catch(...)` or a second `.then(...)` callback at this boundary." }
1637
+ },
1638
+ createOnce(context) {
1639
+ return { UnaryExpression(node) {
1640
+ if (node.operator === "void" && unwrapExpression(node.argument).type === "CallExpression" && !hasRejectionHandler(node.argument)) context.report({
1641
+ node,
1642
+ messageId: "unhandledDetachedPromise"
1643
+ });
1644
+ } };
1645
+ }
1646
+ });
1647
+
1648
+ //#endregion
1649
+ //#region src/shared/boundary-decoder.ts
1650
+ function parameterType$1(parameter) {
1651
+ if (parameter.type === "TSParameterProperty") return parameterType$1(parameter.parameter);
1652
+ if (parameter.type === "AssignmentPattern") return parameterType$1(parameter.left);
1653
+ if (parameter.type === "RestElement") {
1654
+ const annotation = parameter.typeAnnotation;
1655
+ if (annotation?.typeAnnotation.type === "TSArrayType") return annotation.typeAnnotation.elementType;
1656
+ return parameterType$1(parameter.argument);
1657
+ }
1658
+ return parameter.typeAnnotation?.typeAnnotation ?? null;
1659
+ }
1660
+ function hasDecodedReturnType(owner) {
1661
+ const returnType = owner.returnType?.typeAnnotation;
1662
+ if (returnType === void 0) return false;
1663
+ return ![
1664
+ "TSAnyKeyword",
1665
+ "TSUndefinedKeyword",
1666
+ "TSUnknownKeyword",
1667
+ "TSVoidKeyword"
1668
+ ].includes(returnType.type);
1669
+ }
1670
+ /** Identify a boundary that converts an explicitly untrusted input into a typed result. */
1671
+ function isBoundaryDecoder(owner) {
1672
+ return owner.params.some((parameter) => parameterType$1(parameter)?.type === "TSUnknownKeyword") && hasDecodedReturnType(owner);
1673
+ }
1674
+
1675
+ //#endregion
1676
+ //#region src/rules/no-unknown-parameters.ts
1677
+ const ContextOptionsSchema$1 = ruleContextOptionsSchema(z.object({ allowParameterNames: z.array(z.string()).optional() }));
1678
+ function parameterAnnotation(parameter) {
1679
+ if (parameter.type === "TSParameterProperty") return parameterAnnotation(parameter.parameter);
1680
+ if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
1681
+ if (parameter.type === "AssignmentPattern") return parameter.left.typeAnnotation;
1682
+ return parameter.typeAnnotation;
1683
+ }
1684
+ function parameterType(parameter) {
1685
+ const annotation = parameterAnnotation(parameter);
1686
+ if (annotation === null || annotation === void 0) return null;
1687
+ const type = annotation.typeAnnotation;
1688
+ return parameter.type === "RestElement" && type.type === "TSArrayType" ? type.elementType : type;
1689
+ }
1690
+ function parameterName(parameter, sourceText) {
1691
+ if (parameter.type === "TSParameterProperty") return parameterName(parameter.parameter, sourceText);
1692
+ if (parameter.type === "AssignmentPattern") return parameterName(parameter.left, sourceText);
1693
+ if (parameter.type === "RestElement") return parameterName(parameter.argument, sourceText);
1694
+ return parameter.type === "Identifier" ? parameter.name : sourceText.replace(/\s*:\s*unknown\s*$/u, "");
1695
+ }
1696
+ /** Keep unknown inputs at explicit decoding and error-enrichment boundaries. */
1697
+ const noUnknownParametersRule = defineRule({
1698
+ meta: {
1699
+ type: "problem",
1700
+ docs: { description: "Disallow explicitly unknown parameters outside decoders and error-cause enrichment boundaries." },
1701
+ 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." },
1702
+ schema: [{
1703
+ type: "object",
1704
+ properties: { allowParameterNames: {
1705
+ type: "array",
1706
+ items: {
1707
+ type: "string",
1708
+ minLength: 1
1709
+ },
1710
+ uniqueItems: true
1711
+ } },
1712
+ additionalProperties: false
1713
+ }],
1714
+ defaultOptions: [{ allowParameterNames: [] }]
1715
+ },
1716
+ createOnce(context) {
1717
+ const checkParameters = (node) => {
1718
+ if (isBoundaryDecoder(node)) return;
1719
+ const parsedOptions = ContextOptionsSchema$1.safeParse(context.options);
1720
+ const options = parsedOptions.success ? parsedOptions.data : void 0;
1721
+ for (const parameter of node.params) {
1722
+ const type = parameterType(parameter);
1723
+ if (type?.type !== "TSUnknownKeyword") continue;
1724
+ const name = parameterName(parameter, context.sourceCode.getText(parameter));
1725
+ if (name === "cause" || options?.allowParameterNames?.includes(name) === true) continue;
1726
+ context.report({
1727
+ node: type,
1728
+ messageId: "unknownParameter",
1729
+ data: { parameter: name }
1730
+ });
1731
+ }
1732
+ };
1733
+ return {
1734
+ ArrowFunctionExpression: checkParameters,
1735
+ FunctionDeclaration: checkParameters,
1736
+ FunctionExpression: checkParameters,
1737
+ TSCallSignatureDeclaration: checkParameters,
1738
+ TSConstructSignatureDeclaration: checkParameters,
1739
+ TSConstructorType: checkParameters,
1740
+ TSDeclareFunction: checkParameters,
1741
+ TSEmptyBodyFunctionExpression: checkParameters,
1742
+ TSFunctionType: checkParameters,
1743
+ TSMethodSignature: checkParameters
1744
+ };
1745
+ }
1746
+ });
1747
+
1748
+ //#endregion
1749
+ //#region src/rules/no-unknown-returns.ts
1750
+ function referencedAliasName$1(type) {
1751
+ if (type.type === "TSParenthesizedType") return referencedAliasName$1(type.typeAnnotation);
1752
+ if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
1753
+ return (type.typeArguments?.params.length ?? 0) === 0 ? type.typeName.name : null;
1754
+ }
1755
+ /** Ban function contracts that return unknown instead of a parsed domain type. */
1756
+ const noUnknownReturnsRule = defineRule({
1757
+ meta: {
1758
+ type: "problem",
1759
+ docs: { description: "Disallow functions whose explicit return contract is unknown or Promise<unknown>." },
1760
+ messages: { unknownReturn: "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type." }
1761
+ },
1762
+ createOnce(context) {
1763
+ const resolvesToUnknown = ({ shadowedAliases, type, visited = /* @__PURE__ */ new Set() }) => {
1764
+ if (type.type === "TSUnknownKeyword") return true;
1765
+ if (type.type === "TSParenthesizedType") return resolvesToUnknown({
1766
+ shadowedAliases,
1767
+ type: type.typeAnnotation,
1768
+ visited
1769
+ });
1770
+ if (type.type === "TSUnionType") return type.types.some((member) => resolvesToUnknown({
1771
+ shadowedAliases,
1772
+ type: member,
1773
+ visited
1774
+ }));
1775
+ if (type.type === "TSTypeReference" && type.typeName.type === "Identifier" && (type.typeName.name === "Promise" || type.typeName.name === "PromiseLike")) {
1776
+ const value = type.typeArguments?.params[0];
1777
+ return value !== void 0 && resolvesToUnknown({
1778
+ shadowedAliases,
1779
+ type: value,
1780
+ visited
1781
+ });
1782
+ }
1783
+ const name = referencedAliasName$1(type);
1784
+ if (name === null || visited.has(name) || shadowedAliases.has(name)) return false;
1785
+ const alias = type.type === "TSTypeReference" ? resolveTypeAlias(context.sourceCode, type) : null;
1786
+ if (alias === null || (alias.typeParameters?.params.length ?? 0) > 0) return false;
1787
+ const nextVisited = new Set([...visited, name]);
1788
+ return resolvesToUnknown({
1789
+ shadowedAliases,
1790
+ type: alias.typeAnnotation,
1791
+ visited: nextVisited
1792
+ });
1793
+ };
1794
+ const checkReturnType = (node) => {
1795
+ const annotation = node.returnType;
1796
+ if (annotation === null || annotation === void 0) return;
1797
+ if (!resolvesToUnknown({
1798
+ shadowedAliases: lexicalTypeParameterNames(node, context.sourceCode.visitorKeys),
1799
+ type: annotation.typeAnnotation
1800
+ })) return;
1801
+ context.report({
1802
+ node: annotation.typeAnnotation,
1803
+ messageId: "unknownReturn"
1804
+ });
1805
+ };
1806
+ return {
1807
+ ArrowFunctionExpression: checkReturnType,
1808
+ FunctionDeclaration: checkReturnType,
1809
+ FunctionExpression: checkReturnType,
1810
+ TSCallSignatureDeclaration: checkReturnType,
1811
+ TSConstructSignatureDeclaration: checkReturnType,
1812
+ TSConstructorType: checkReturnType,
1813
+ TSDeclareFunction: checkReturnType,
1814
+ TSEmptyBodyFunctionExpression: checkReturnType,
1815
+ TSFunctionType: checkReturnType,
1816
+ TSMethodSignature: checkReturnType
1817
+ };
1818
+ }
1819
+ });
1820
+
1821
+ //#endregion
1822
+ //#region src/rules/no-unknown-type-aliases.ts
1823
+ function referencedAliasName(type) {
1824
+ if (type.type === "TSParenthesizedType") return referencedAliasName(type.typeAnnotation);
1825
+ if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
1826
+ return (type.typeArguments?.params.length ?? 0) === 0 ? type.typeName.name : null;
1827
+ }
1828
+ /** Ban named aliases that merely conceal TypeScript's unknown top type. */
1829
+ const noUnknownTypeAliasesRule = defineRule({
1830
+ meta: {
1831
+ type: "problem",
1832
+ docs: { description: "Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary." },
1833
+ 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." }
1834
+ },
1835
+ createOnce(context) {
1836
+ const resolvesToUnknown = (type, visited = /* @__PURE__ */ new Set()) => {
1837
+ if (type.type === "TSUnknownKeyword") return true;
1838
+ if (type.type === "TSParenthesizedType") return resolvesToUnknown(type.typeAnnotation, visited);
1839
+ if (type.type === "TSUnionType") return type.types.some((member) => resolvesToUnknown(member, visited));
1840
+ const name = referencedAliasName(type);
1841
+ if (name === null || visited.has(name)) return false;
1842
+ const alias = type.type === "TSTypeReference" ? resolveTypeAlias(context.sourceCode, type) : null;
1843
+ if (alias === null || (alias.typeParameters?.params.length ?? 0) > 0) return false;
1844
+ const nextVisited = new Set([...visited, name]);
1845
+ return resolvesToUnknown(alias.typeAnnotation, nextVisited);
1846
+ };
1847
+ return { TSTypeAliasDeclaration(node) {
1848
+ if (!resolvesToUnknown(node.typeAnnotation, new Set([node.id.name]))) return;
1849
+ context.report({
1850
+ node: node.id,
1851
+ messageId: "unknownAlias",
1852
+ data: { alias: node.id.name }
1853
+ });
1854
+ } };
1855
+ }
1856
+ });
1857
+
1858
+ //#endregion
1859
+ //#region src/rules/no-unsafe-dictionary-type.ts
1860
+ const typeNodeKinds = new Set([
1861
+ "JSDocNonNullableType",
1862
+ "JSDocNullableType",
1863
+ "JSDocUnknownType",
1864
+ "TSAnyKeyword",
1865
+ "TSArrayType",
1866
+ "TSBigIntKeyword",
1867
+ "TSBooleanKeyword",
1868
+ "TSConditionalType",
1869
+ "TSConstructorType",
1870
+ "TSFunctionType",
1871
+ "TSImportType",
1872
+ "TSIndexedAccessType",
1873
+ "TSInferType",
1874
+ "TSIntersectionType",
1875
+ "TSIntrinsicKeyword",
1876
+ "TSLiteralType",
1877
+ "TSMappedType",
1878
+ "TSNamedTupleMember",
1879
+ "TSNeverKeyword",
1880
+ "TSNullKeyword",
1881
+ "TSNumberKeyword",
1882
+ "TSObjectKeyword",
1883
+ "TSParenthesizedType",
1884
+ "TSStringKeyword",
1885
+ "TSSymbolKeyword",
1886
+ "TSTemplateLiteralType",
1887
+ "TSThisType",
1888
+ "TSTupleType",
1889
+ "TSTypeLiteral",
1890
+ "TSTypeOperator",
1891
+ "TSTypePredicate",
1892
+ "TSTypeQuery",
1893
+ "TSTypeReference",
1894
+ "TSUndefinedKeyword",
1895
+ "TSUnionType",
1896
+ "TSUnknownKeyword",
1897
+ "TSVoidKeyword"
1898
+ ]);
1899
+ function isTypeNode(node) {
1900
+ return typeNodeKinds.has(node.type);
1901
+ }
1902
+ function typeReferenceName$1(type) {
1903
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
1904
+ }
1905
+ function isInsideTypeAliasDeclaration(node) {
1906
+ let current = node.parent;
1907
+ while (current !== null && current.type !== "Program") {
1908
+ if (current.type === "TSTypeAliasDeclaration") return true;
1909
+ current = current.parent;
1910
+ }
1911
+ return false;
1912
+ }
1913
+ function isPlainAliasConsumerUse(node, environment) {
1914
+ if (node.type !== "TSTypeReference" || (node.typeArguments?.params.length ?? 0) > 0) return false;
1915
+ const name = typeReferenceName$1(node);
1916
+ return name !== null && environment.aliases.has(name) && !isInsideTypeAliasDeclaration(node);
1917
+ }
1918
+ function shouldReportType(node, environment) {
1919
+ if (isPlainAliasConsumerUse(node, environment)) return false;
1920
+ if (classifyUnsafeDictionary(node, environment) === null) return false;
1921
+ let current = node.parent;
1922
+ while (current.type !== "Program") {
1923
+ if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) return false;
1924
+ current = current.parent;
1925
+ }
1926
+ return true;
1927
+ }
1928
+ /** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
1929
+ const noUnsafeDictionaryTypeRule = defineRule({
457
1930
  meta: {
458
- name: "@utilfirst/eslint-plugin",
459
- version
1931
+ type: "problem",
1932
+ docs: { description: "Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches." },
1933
+ 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." }
460
1934
  },
461
- rules: { "consistent-blank-lines": consistentBlankLines },
462
- configs: {}
1935
+ createOnce(context) {
1936
+ let environment;
1937
+ const report = (node, value) => {
1938
+ context.report({
1939
+ node,
1940
+ messageId: "unsafeDictionary",
1941
+ data: { value }
1942
+ });
1943
+ };
1944
+ const reportIfUnsafe = (node) => {
1945
+ if (!shouldReportType(node, environment)) return;
1946
+ const unsafe = classifyUnsafeDictionary(node, environment);
1947
+ if (unsafe === null) return;
1948
+ report(node, unsafe.unsafeValue);
1949
+ };
1950
+ return {
1951
+ Program(node) {
1952
+ environment = createTypeEnvironment(node);
1953
+ },
1954
+ TSTypeReference: reportIfUnsafe,
1955
+ TSTypeLiteral: reportIfUnsafe,
1956
+ TSMappedType: reportIfUnsafe,
1957
+ TSIndexSignature(node) {
1958
+ if (node.parent.type === "TSTypeLiteral") return;
1959
+ const unsafe = classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment);
1960
+ if (unsafe !== null) report(node, unsafe.unsafeValue);
1961
+ }
1962
+ };
1963
+ }
1964
+ });
1965
+
1966
+ //#endregion
1967
+ //#region src/rules/no-widen-then-assert.ts
1968
+ const functionBoundaryTypes = new Set([
1969
+ "ArrowFunctionExpression",
1970
+ "FunctionDeclaration",
1971
+ "FunctionExpression",
1972
+ "TSDeclareFunction",
1973
+ "TSEmptyBodyFunctionExpression"
1974
+ ]);
1975
+ function unwrapExpressionParentheses(expression) {
1976
+ let current = expression;
1977
+ while (current.type === "ParenthesizedExpression") current = current.expression;
1978
+ return current;
1979
+ }
1980
+ function unwrapTypeParentheses(type) {
1981
+ let current = type;
1982
+ while (current.type === "TSParenthesizedType") current = current.typeAnnotation;
1983
+ return current;
1984
+ }
1985
+ function typeReferenceName(type) {
1986
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
1987
+ }
1988
+ function isUnknownOrAnyType(type) {
1989
+ const unwrapped = unwrapTypeParentheses(type);
1990
+ return unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword";
1991
+ }
1992
+ function isBroadRecordKeyType(type) {
1993
+ const unwrapped = unwrapTypeParentheses(type);
1994
+ if (unwrapped.type === "TSStringKeyword" || unwrapped.type === "TSNumberKeyword" || unwrapped.type === "TSSymbolKeyword") return true;
1995
+ if (unwrapped.type === "TSUnionType") return unwrapped.types.every(isBroadRecordKeyType);
1996
+ return unwrapped.type === "TSTypeReference" && typeReferenceName(unwrapped) === "PropertyKey";
1997
+ }
1998
+ function isBroadRecordType(type) {
1999
+ const unwrapped = unwrapTypeParentheses(type);
2000
+ if (unwrapped.type === "TSTypeReference") {
2001
+ if (typeReferenceName(unwrapped) === "Readonly") {
2002
+ const [inner] = unwrapped.typeArguments?.params ?? [];
2003
+ return inner !== void 0 && isBroadRecordType(inner);
2004
+ }
2005
+ if (typeReferenceName(unwrapped) !== "Record") return false;
2006
+ const parameters = unwrapped.typeArguments?.params ?? [];
2007
+ return parameters.length === 2 && parameters[0] !== void 0 && parameters[1] !== void 0 && isBroadRecordKeyType(parameters[0]) && isUnknownOrAnyType(parameters[1]);
2008
+ }
2009
+ if (unwrapped.type !== "TSTypeLiteral" || unwrapped.members.length !== 1) return false;
2010
+ const [member] = unwrapped.members;
2011
+ const [parameter] = member?.type === "TSIndexSignature" ? member.parameters : [];
2012
+ return member?.type === "TSIndexSignature" && member.parameters.length === 1 && parameter !== void 0 && isBroadRecordKeyType(parameter.typeAnnotation.typeAnnotation) && isUnknownOrAnyType(member.typeAnnotation.typeAnnotation);
2013
+ }
2014
+ function broadTypeKind(type) {
2015
+ const unwrapped = unwrapTypeParentheses(type);
2016
+ if (unwrapped.type === "TSUnknownKeyword" || unwrapped.type === "TSAnyKeyword") return "top";
2017
+ if (unwrapped.type === "TSObjectKeyword") return "object";
2018
+ return isBroadRecordType(unwrapped) ? "record" : null;
2019
+ }
2020
+ function assertedExpression(node) {
2021
+ return unwrapExpressionParentheses(node.expression);
2022
+ }
2023
+ function assertionFromExpression(expression) {
2024
+ const unwrapped = unwrapExpressionParentheses(expression);
2025
+ return unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion" ? unwrapped : null;
2026
+ }
2027
+ function normalizedTypeText(sourceText, type) {
2028
+ return sourceText.slice(type.range[0], type.range[1]).replaceAll(/\s+/gu, "");
2029
+ }
2030
+ function typesHaveSameSyntax({ left, right, sourceText }) {
2031
+ return left !== null && normalizedTypeText(sourceText, unwrapTypeParentheses(left)) === normalizedTypeText(sourceText, unwrapTypeParentheses(right));
2032
+ }
2033
+ function isDefinitelyObjectType(type) {
2034
+ const unwrapped = unwrapTypeParentheses(type);
2035
+ if (unwrapped.type === "TSArrayType" || unwrapped.type === "TSConstructorType" || unwrapped.type === "TSFunctionType" || unwrapped.type === "TSMappedType" || unwrapped.type === "TSObjectKeyword" || unwrapped.type === "TSTupleType") return true;
2036
+ if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.length > 0;
2037
+ if (unwrapped.type === "TSIntersectionType") return unwrapped.types.every(isDefinitelyObjectType);
2038
+ if (unwrapped.type === "TSTypeOperator") return unwrapped.operator === "readonly" && isDefinitelyObjectType(unwrapped.typeAnnotation);
2039
+ return false;
2040
+ }
2041
+ function isDefinitelyNarrowerRecordType(type) {
2042
+ const unwrapped = unwrapTypeParentheses(type);
2043
+ if (unwrapped.type === "TSTypeLiteral") return unwrapped.members.some((member) => member.type !== "TSIndexSignature");
2044
+ if (unwrapped.type !== "TSTypeReference") return false;
2045
+ if (typeReferenceName(unwrapped) === "Readonly") {
2046
+ const [inner] = unwrapped.typeArguments?.params ?? [];
2047
+ return inner !== void 0 && isDefinitelyNarrowerRecordType(inner);
2048
+ }
2049
+ if (typeReferenceName(unwrapped) !== "Record") return false;
2050
+ const parameters = unwrapped.typeArguments?.params ?? [];
2051
+ return parameters.length === 2 && parameters[1] !== void 0 && !isUnknownOrAnyType(parameters[1]);
2052
+ }
2053
+ function functionBoundary(node) {
2054
+ let current = node.parent;
2055
+ while (current !== null && current.type !== "Program") {
2056
+ if (functionBoundaryTypes.has(current.type)) return current;
2057
+ current = current.parent;
2058
+ }
2059
+ return null;
2060
+ }
2061
+ function resolvedVariableForIdentifier(scopes, identifier) {
2062
+ for (const scope of scopes) {
2063
+ const reference = scope.references.find((candidate) => candidate.identifier.range[0] === identifier.range[0] && candidate.identifier.range[1] === identifier.range[1]);
2064
+ if (reference !== void 0) return reference.resolved;
2065
+ }
2066
+ return null;
2067
+ }
2068
+ function variableDeclarator(variable) {
2069
+ for (const definition of variable.defs) if (definition.type === "Variable" && definition.node.type === "VariableDeclarator") return definition.node;
2070
+ return null;
2071
+ }
2072
+ function knownValueEvidence({ boundary, expression, scopes, visitedVariables }) {
2073
+ const unwrapped = unwrapExpressionParentheses(expression);
2074
+ if (unwrapped.type === "TSAsExpression" || unwrapped.type === "TSTypeAssertion") {
2075
+ if (broadTypeKind(unwrapped.typeAnnotation) !== null) return null;
2076
+ return { type: unwrapped.typeAnnotation };
2077
+ }
2078
+ if (unwrapped.type === "Literal" || unwrapped.type === "TemplateLiteral") return { type: null };
2079
+ if (unwrapped.type === "ArrayExpression" || unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "ClassExpression" || unwrapped.type === "FunctionExpression" || unwrapped.type === "NewExpression" || unwrapped.type === "ObjectExpression") return { type: null };
2080
+ if (unwrapped.type !== "Identifier") return null;
2081
+ const variable = resolvedVariableForIdentifier(scopes, unwrapped);
2082
+ if (variable === null || visitedVariables.has(variable)) return null;
2083
+ const annotatedIdentifier = variable.identifiers.find((identifier) => identifier.typeAnnotation !== null && identifier.typeAnnotation !== void 0);
2084
+ const annotation = annotatedIdentifier?.typeAnnotation?.typeAnnotation;
2085
+ if (annotation !== void 0 && annotatedIdentifier !== void 0) {
2086
+ if (functionBoundary(annotatedIdentifier) !== boundary || broadTypeKind(annotation) !== null) return null;
2087
+ return { type: annotation };
2088
+ }
2089
+ const declarator = variableDeclarator(variable);
2090
+ 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;
2091
+ return knownValueEvidence({
2092
+ scopes,
2093
+ boundary,
2094
+ expression: declarator.init,
2095
+ visitedVariables: new Set([...visitedVariables, variable])
2096
+ });
2097
+ }
2098
+ function widenedBinding(variable, scopes) {
2099
+ const declarator = variableDeclarator(variable);
2100
+ 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;
2101
+ const boundary = functionBoundary(declarator);
2102
+ const declaredType = declarator.id.typeAnnotation?.typeAnnotation;
2103
+ const initializerAssertion = assertionFromExpression(declarator.init);
2104
+ const initializerBroadKind = initializerAssertion === null ? null : broadTypeKind(initializerAssertion.typeAnnotation);
2105
+ const broadKind = (declaredType === void 0 ? null : broadTypeKind(declaredType)) ?? initializerBroadKind;
2106
+ if (broadKind === null) return null;
2107
+ const evidence = knownValueEvidence({
2108
+ expression: initializerAssertion !== null && initializerBroadKind !== null ? assertedExpression(initializerAssertion) : declarator.init,
2109
+ scopes,
2110
+ boundary,
2111
+ visitedVariables: new Set([variable])
2112
+ });
2113
+ return evidence === null ? null : {
2114
+ broadKind,
2115
+ evidence,
2116
+ declaredAt: declarator.range[1],
2117
+ boundary
2118
+ };
2119
+ }
2120
+ function assertionIsNarrower({ assertedType, broadKind, evidence, sourceText }) {
2121
+ if (broadTypeKind(assertedType) !== null) return false;
2122
+ if (broadKind === "top") return true;
2123
+ if (typesHaveSameSyntax({
2124
+ left: evidence.type,
2125
+ right: assertedType,
2126
+ sourceText
2127
+ })) return true;
2128
+ if (broadKind === "object") return isDefinitelyObjectType(assertedType);
2129
+ return isDefinitelyNarrowerRecordType(assertedType);
2130
+ }
2131
+ /** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
2132
+ const noWidenThenAssertRule = defineRule({
2133
+ meta: {
2134
+ type: "problem",
2135
+ docs: { description: "Disallow local const flows that explicitly widen a known value before asserting the widened binding to a narrower type." },
2136
+ 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." }
2137
+ },
2138
+ createOnce(context) {
2139
+ let scopes = [];
2140
+ const checkAssertion = (node) => {
2141
+ const expression = assertedExpression(node);
2142
+ if (expression.type !== "Identifier") return;
2143
+ const variable = resolvedVariableForIdentifier(scopes, expression);
2144
+ if (variable === null) return;
2145
+ const widened = widenedBinding(variable, scopes);
2146
+ if (widened === null || node.range[0] <= widened.declaredAt || functionBoundary(node) !== widened.boundary || !assertionIsNarrower({
2147
+ assertedType: node.typeAnnotation,
2148
+ broadKind: widened.broadKind,
2149
+ evidence: widened.evidence,
2150
+ sourceText: context.sourceCode.text
2151
+ })) return;
2152
+ context.report({
2153
+ node,
2154
+ messageId: "widenThenAssert",
2155
+ data: { name: expression.name }
2156
+ });
2157
+ };
2158
+ return {
2159
+ Program() {
2160
+ scopes = context.sourceCode.scopeManager.scopes;
2161
+ },
2162
+ TSAsExpression: checkAssertion,
2163
+ TSTypeAssertion: checkAssertion
2164
+ };
2165
+ }
2166
+ });
2167
+
2168
+ //#endregion
2169
+ //#region src/rules/prefer-options-parameter.ts
2170
+ const ContextOptionsSchema = ruleContextOptionsSchema(z.object({ allowFunctionNames: z.array(z.string()).optional() }));
2171
+ /** Require repository-owned named callables with 3+ inputs to use options. */
2172
+ const preferOptionsParameterRule = defineRule({
2173
+ meta: {
2174
+ type: "suggestion",
2175
+ docs: { description: "Require repository-owned named callables with three or more inputs to use one options object." },
2176
+ messages: { preferOptions: "Function `{{functionName}}` has {{parameterCount}} parameters. Replace them with one named options object." },
2177
+ schema: [{
2178
+ type: "object",
2179
+ properties: { allowFunctionNames: {
2180
+ type: "array",
2181
+ items: {
2182
+ type: "string",
2183
+ minLength: 1
2184
+ },
2185
+ uniqueItems: true
2186
+ } },
2187
+ additionalProperties: false
2188
+ }],
2189
+ defaultOptions: [{ allowFunctionNames: [] }]
2190
+ },
2191
+ createOnce(context) {
2192
+ const checkFunction = (node) => {
2193
+ const functionName$1 = getOwnedFunctionName(node);
2194
+ const parameterCount = node.params.filter((parameter) => !(parameter.type === "Identifier" && parameter.name === "this")).length;
2195
+ if (functionName$1 === null || parameterCount < 3) return;
2196
+ const rawOptions = context.options;
2197
+ const parsedOptions = ContextOptionsSchema.safeParse(rawOptions);
2198
+ if ((parsedOptions.success ? parsedOptions.data : void 0)?.allowFunctionNames?.includes(functionName$1) === true) return;
2199
+ context.report({
2200
+ node,
2201
+ messageId: "preferOptions",
2202
+ data: {
2203
+ functionName: functionName$1,
2204
+ parameterCount
2205
+ }
2206
+ });
2207
+ };
2208
+ return {
2209
+ ArrowFunctionExpression: checkFunction,
2210
+ FunctionDeclaration: checkFunction,
2211
+ FunctionExpression: checkFunction
2212
+ };
2213
+ }
2214
+ });
2215
+
2216
+ //#endregion
2217
+ //#region src/rules/prefer-switch-discriminator-chain.ts
2218
+ const MINIMUM_BRANCH_COUNT = 4;
2219
+ function discriminatorKey(node) {
2220
+ return node.type === "Identifier" ? node.name : null;
2221
+ }
2222
+ function comparisonDiscriminator(node) {
2223
+ if (node.type !== "BinaryExpression" || node.operator !== "===") return null;
2224
+ if (node.right.type === "Literal") return discriminatorKey(node.left);
2225
+ if (node.left.type === "Literal") return discriminatorKey(node.right);
2226
+ return null;
2227
+ }
2228
+ function discriminatorChain(node) {
2229
+ let branchCount = 0;
2230
+ let current = node;
2231
+ let discriminator = null;
2232
+ while (current !== null) {
2233
+ const branchDiscriminator = comparisonDiscriminator(current.test);
2234
+ if (branchDiscriminator === null) return null;
2235
+ if (discriminator !== null && discriminator !== branchDiscriminator) return null;
2236
+ discriminator = branchDiscriminator;
2237
+ branchCount += 1;
2238
+ current = current.alternate?.type === "IfStatement" ? current.alternate : null;
2239
+ }
2240
+ return discriminator === null ? null : {
2241
+ branchCount,
2242
+ discriminator
2243
+ };
2244
+ }
2245
+ /** Prefer a switch when repeated equality branches dispatch on one value. */
2246
+ const preferSwitchDiscriminatorChainRule = defineRule({
2247
+ meta: {
2248
+ type: "suggestion",
2249
+ docs: { description: "Require a switch for four or more equality branches on one discriminator." },
2250
+ messages: { preferSwitch: "This chain has {{branchCount}} equality branches on one discriminator. Replace it with a switch so the finite dispatch structure is explicit." }
2251
+ },
2252
+ createOnce(context) {
2253
+ return { IfStatement(node) {
2254
+ if (node.parent.type === "IfStatement" && node.parent.alternate === node) return;
2255
+ const chain = discriminatorChain(node);
2256
+ if (chain === null || chain.branchCount < MINIMUM_BRANCH_COUNT) return;
2257
+ context.report({
2258
+ node,
2259
+ messageId: "preferSwitch",
2260
+ data: { branchCount: chain.branchCount }
2261
+ });
2262
+ } };
2263
+ }
2264
+ });
2265
+
2266
+ //#endregion
2267
+ //#region src/rules/prefer-top-level-function-declarations.ts
2268
+ function isFunctionExpression(node) {
2269
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") return true;
2270
+ if (node.type === "ParenthesizedExpression" || node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSTypeAssertion") return isFunctionExpression(node.expression);
2271
+ return false;
2272
+ }
2273
+ function isTopLevelVariable(node) {
2274
+ if (node.id.type !== "Identifier") return false;
2275
+ const declaration = node.parent;
2276
+ if (declaration.type !== "VariableDeclaration") return false;
2277
+ return declaration.parent.type === "Program" || declaration.parent.type === "ExportNamedDeclaration" && declaration.parent.parent.type === "Program";
2278
+ }
2279
+ /** Prefer hoistable declarations for repository-owned top-level functions. */
2280
+ const preferTopLevelFunctionDeclarationsRule = defineRule({
2281
+ meta: {
2282
+ type: "suggestion",
2283
+ docs: { description: "Require function declarations for direct top-level function bindings and named default exports." },
2284
+ messages: {
2285
+ anonymousDefaultExport: "Name this default-exported function with a function declaration so stack frames and searches identify its owner.",
2286
+ topLevelBinding: "Top-level function `{{functionName}}` uses a function expression. Replace it with a function declaration so its owner is explicit and hoistable."
2287
+ }
2288
+ },
2289
+ createOnce(context) {
2290
+ return {
2291
+ ExportDefaultDeclaration(node) {
2292
+ 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({
2293
+ node,
2294
+ messageId: "anonymousDefaultExport"
2295
+ });
2296
+ },
2297
+ VariableDeclarator(node) {
2298
+ if (node.init === null || !isTopLevelVariable(node) || !isFunctionExpression(node.init)) return;
2299
+ context.report({
2300
+ node,
2301
+ messageId: "topLevelBinding",
2302
+ data: { functionName: node.id.name }
2303
+ });
2304
+ }
2305
+ };
2306
+ }
2307
+ });
2308
+
2309
+ //#endregion
2310
+ //#region src/rules/require-lint-suppression-reason.ts
2311
+ const suppressionDirectivePattern = /^(?:eslint|oxlint)-disable(?:-next-line|-line)?(?:\s|$)/u;
2312
+ const suppressionReasonPattern = /\s--\s+\S/u;
2313
+ /** Require an explicit forcing reason on ESLint and Oxlint suppressions. */
2314
+ const requireLintSuppressionReasonRule = defineRule({
2315
+ meta: {
2316
+ type: "suggestion",
2317
+ docs: { description: "Require ESLint and Oxlint disable directives to state their forcing reason." },
2318
+ messages: { missingReason: "Add a reason after `--` that explains why this lint suppression is required." }
2319
+ },
2320
+ createOnce(context) {
2321
+ return { Program() {
2322
+ for (const comment of context.sourceCode.getAllComments()) {
2323
+ const directive = comment.value.trim();
2324
+ if (suppressionDirectivePattern.test(directive) && !suppressionReasonPattern.test(directive)) context.report({
2325
+ loc: context.sourceCode.getLoc(comment),
2326
+ messageId: "missingReason"
2327
+ });
2328
+ }
2329
+ } };
2330
+ }
2331
+ });
2332
+
2333
+ //#endregion
2334
+ //#region src/rules/require-safety-comment-for-type-assertion.ts
2335
+ const commentOwnerKinds = new Set([
2336
+ "ExpressionStatement",
2337
+ "PropertyDefinition",
2338
+ "ReturnStatement",
2339
+ "ThrowStatement",
2340
+ "VariableDeclaration"
2341
+ ]);
2342
+ function isConstAssertion(node) {
2343
+ return node.typeAnnotation.type === "TSTypeReference" && node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name === "const";
2344
+ }
2345
+ function isNestedAssertion(node) {
2346
+ let current = node;
2347
+ let parent = node.parent;
2348
+ while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
2349
+ current = parent;
2350
+ parent = parent.parent;
2351
+ }
2352
+ return (parent.type === "TSAsExpression" || parent.type === "TSTypeAssertion") && parent.expression === current;
2353
+ }
2354
+ function hasSafetyComment(sourceCode, node) {
2355
+ let current = node;
2356
+ while (true) {
2357
+ if (sourceCode.getCommentsBefore(current).some((comment) => comment.range[1] <= node.range[0] && /\bSAFETY\s*:/u.test(comment.value))) return true;
2358
+ if (commentOwnerKinds.has(current.type) || current.parent.type === "Program") return false;
2359
+ current = current.parent;
2360
+ }
2361
+ }
2362
+ /** Require every non-const type assertion to state the invariant TypeScript cannot express. */
2363
+ const requireSafetyCommentForTypeAssertionRule = defineRule({
2364
+ meta: {
2365
+ type: "problem",
2366
+ docs: { description: "Require a nearby SAFETY comment for every outermost TypeScript type assertion except const assertions." },
2367
+ messages: { missingSafetyComment: "This type assertion has no `SAFETY:` justification. State the checked invariant immediately before the assertion or its containing statement." }
2368
+ },
2369
+ createOnce(context) {
2370
+ const checkAssertion = (node) => {
2371
+ if (isConstAssertion(node) || isNestedAssertion(node) || hasSafetyComment(context.sourceCode, node)) return;
2372
+ context.report({
2373
+ node,
2374
+ messageId: "missingSafetyComment"
2375
+ });
2376
+ };
2377
+ return {
2378
+ TSAsExpression: checkAssertion,
2379
+ TSTypeAssertion: checkAssertion
2380
+ };
2381
+ }
2382
+ });
2383
+
2384
+ //#endregion
2385
+ //#region src/index.ts
2386
+ const meta = {
2387
+ name: "@utilfirst/eslint-plugin",
2388
+ version
2389
+ };
2390
+ const antiSlopPlugin = eslintCompatPlugin({
2391
+ meta,
2392
+ rules: {
2393
+ "no-chained-type-assertions": noChainedTypeAssertionsRule,
2394
+ "no-conditional-undefined-properties": noConditionalUndefinedPropertiesRule,
2395
+ "no-enum-declarations": noEnumDeclarationsRule,
2396
+ "no-known-value-widening": noKnownValueWideningRule,
2397
+ "no-module-mocking": noModuleMockingRule,
2398
+ "no-object-parameters": noObjectParametersRule,
2399
+ "no-positional-boolean-parameters": noPositionalBooleanParametersRule,
2400
+ "no-reflect-apply": noReflectApplyRule,
2401
+ "no-reflect-get": noReflectGetRule,
2402
+ "no-unknown-parameters": noUnknownParametersRule,
2403
+ "no-unknown-returns": noUnknownReturnsRule,
2404
+ "no-unknown-type-aliases": noUnknownTypeAliasesRule,
2405
+ "no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
2406
+ "no-unhandled-detached-promises": noUnhandledDetachedPromisesRule,
2407
+ "no-widen-then-assert": noWidenThenAssertRule,
2408
+ "prefer-options-parameter": preferOptionsParameterRule,
2409
+ "prefer-switch-discriminator-chain": preferSwitchDiscriminatorChainRule,
2410
+ "prefer-top-level-function-declarations": preferTopLevelFunctionDeclarationsRule,
2411
+ "require-lint-suppression-reason": requireLintSuppressionReasonRule,
2412
+ "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule
2413
+ }
2414
+ });
2415
+ function assertEslintCompatibleRules(candidateRules) {
2416
+ for (const candidateRule of Object.values(candidateRules)) if (candidateRule.create === void 0) throw new Error("ESLint compatibility adapter did not install `create`");
2417
+ }
2418
+ assertEslintCompatibleRules(antiSlopPlugin.rules);
2419
+ const antiSlopRules = antiSlopPlugin.rules;
2420
+ const rules = {
2421
+ "consistent-blank-lines": consistentBlankLines,
2422
+ ...antiSlopRules
2423
+ };
2424
+ const recommendedRules = Object.fromEntries(Object.keys(rules).map((ruleName) => [`utilfirst/${ruleName}`, "error"]));
2425
+ const plugin = {
2426
+ meta,
2427
+ rules,
2428
+ configs: { recommended: {} }
463
2429
  };
464
2430
  plugin.configs.recommended = {
465
2431
  plugins: { utilfirst: plugin },
466
- rules: { "utilfirst/consistent-blank-lines": "error" }
2432
+ rules: recommendedRules
467
2433
  };
468
2434
  var src_default = plugin;
469
2435