unguard 0.9.0 → 0.10.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.
@@ -215,6 +215,12 @@ function isNullishLiteral(node) {
215
215
  if (ts3.isIdentifier(node) && node.text === "undefined") return true;
216
216
  return false;
217
217
  }
218
+ function getFunctionBodyStatements(node) {
219
+ if (!ts3.isFunctionDeclaration(node) && !ts3.isArrowFunction(node)) return null;
220
+ if (!node.body || !ts3.isBlock(node.body)) return null;
221
+ if (node.body.statements.length === 0) return null;
222
+ return { statements: node.body.statements, fn: node };
223
+ }
218
224
 
219
225
  // src/rules/ts/no-double-negation-coercion.ts
220
226
  var noDoubleNegationCoercion = {
@@ -463,79 +469,83 @@ var noExplicitAnyAnnotation = {
463
469
  }
464
470
  };
465
471
 
466
- // src/rules/ts/no-inline-type-in-params.ts
467
- import * as ts14 from "typescript";
468
- function isTopLevelFunctionParam(source, offset) {
469
- let depth = 0;
470
- for (let i = offset - 1; i >= 0; i--) {
471
- if (source[i] === "}") depth++;
472
- if (source[i] === "{") {
473
- if (depth === 0) {
474
- const before = source.slice(Math.max(0, i - 150), i).trimEnd();
475
- if (/(\)|\bfunction\b.*\))\s*$/.test(before)) {
476
- depth--;
477
- continue;
478
- }
479
- return false;
480
- }
481
- depth--;
482
- }
472
+ // src/rules/types.ts
473
+ function isTSRule(r) {
474
+ return "kind" in r && r.kind === "ts";
475
+ }
476
+ function reportDuplicateGroup(group, ruleId, severity, formatOther, formatMessage, diagnostics) {
477
+ const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
478
+ for (const entry of sorted.slice(1)) {
479
+ const others = sorted.filter((e) => e !== entry).map(formatOther).join(", ");
480
+ diagnostics.push({
481
+ ruleId,
482
+ severity,
483
+ message: formatMessage(entry, others),
484
+ file: entry.file,
485
+ line: entry.line,
486
+ column: 1
487
+ });
483
488
  }
484
- return true;
485
489
  }
486
- var noInlineTypeInParams = {
487
- kind: "ts",
488
- id: "no-inline-type-in-params",
489
- severity: "info",
490
- message: "Inline type literal in annotation; extract to a named type for reuse and clarity",
491
- visit(node, ctx) {
492
- if (!ts14.isTypeLiteralNode(node)) return;
493
- const parent = node.parent;
494
- if (!parent || !ts14.isParameter(parent)) return;
495
- if (parent.type !== node) return;
496
- const offset = node.getStart(ctx.sourceFile);
497
- if (!isTopLevelFunctionParam(ctx.source, offset)) return;
498
- ctx.report(node);
490
+
491
+ // src/rules/cross-file/duplicate-inline-type-in-params.ts
492
+ var duplicateInlineTypeInParams = {
493
+ id: "duplicate-inline-type-in-params",
494
+ severity: "warning",
495
+ message: "Same inline param type shape appears in multiple places; extract to a shared named type",
496
+ analyze(project) {
497
+ const diagnostics = [];
498
+ for (const group of project.inlineParamTypes.getDuplicateGroups()) {
499
+ reportDuplicateGroup(
500
+ group,
501
+ this.id,
502
+ this.severity,
503
+ (e) => `${e.typeText} (${e.file}:${e.line})`,
504
+ (e, others) => `Inline param type \`${e.typeText}\` also appears at: ${others}`,
505
+ diagnostics
506
+ );
507
+ }
508
+ return diagnostics;
499
509
  }
500
510
  };
501
511
 
502
512
  // src/rules/ts/no-type-assertion.ts
503
- import * as ts15 from "typescript";
513
+ import * as ts14 from "typescript";
504
514
  var noTypeAssertion = {
505
515
  kind: "ts",
506
516
  id: "no-type-assertion",
507
517
  severity: "error",
508
518
  message: "Double type assertion (`as unknown as T`) circumvents the type system; fix the upstream type or use a type guard",
509
519
  visit(node, ctx) {
510
- if (!ts15.isAsExpression(node)) return;
511
- if (!ts15.isAsExpression(node.expression)) return;
512
- if (node.expression.type.kind !== ts15.SyntaxKind.UnknownKeyword) return;
520
+ if (!ts14.isAsExpression(node)) return;
521
+ if (!ts14.isAsExpression(node.expression)) return;
522
+ if (node.expression.type.kind !== ts14.SyntaxKind.UnknownKeyword) return;
513
523
  ctx.report(node);
514
524
  }
515
525
  };
516
526
 
517
527
  // src/rules/ts/no-redundant-existence-guard.ts
518
- import * as ts16 from "typescript";
528
+ import * as ts15 from "typescript";
519
529
  var noRedundantExistenceGuard = {
520
530
  kind: "ts",
521
531
  id: "no-redundant-existence-guard",
522
532
  severity: "warning",
523
533
  message: "Redundant existence guard (obj && obj.prop) on a non-nullable type; remove the guard or fix the type upstream",
524
534
  visit(node, ctx) {
525
- if (!ts16.isBinaryExpression(node)) return;
526
- if (node.operatorToken.kind !== ts16.SyntaxKind.AmpersandAmpersandToken) return;
535
+ if (!ts15.isBinaryExpression(node)) return;
536
+ if (node.operatorToken.kind !== ts15.SyntaxKind.AmpersandAmpersandToken) return;
527
537
  const left = node.left;
528
538
  const right = node.right;
529
- if (ts16.isIdentifier(left) && accessesIdentifier(right, left.text)) {
539
+ if (ts15.isIdentifier(left) && accessesIdentifier(right, left.text)) {
530
540
  if (ctx.isNullable(left)) return;
531
541
  ctx.report(node);
532
542
  return;
533
543
  }
534
- if (ts16.isBinaryExpression(left) && isNullCheck(left)) {
544
+ if (ts15.isBinaryExpression(left) && isNullCheck(left)) {
535
545
  const checked = getNullCheckedIdentifier(left);
536
546
  if (checked && accessesIdentifier(right, checked)) {
537
- const identNode = ts16.isIdentifier(left.left) ? left.left : left.right;
538
- if (ts16.isIdentifier(identNode) && identNode.text === checked && !ctx.isNullable(identNode)) {
547
+ const identNode = ts15.isIdentifier(left.left) ? left.left : left.right;
548
+ if (ts15.isIdentifier(identNode) && identNode.text === checked && !ctx.isNullable(identNode)) {
539
549
  ctx.report(node);
540
550
  }
541
551
  }
@@ -544,84 +554,82 @@ var noRedundantExistenceGuard = {
544
554
  };
545
555
  function accessesIdentifier(expr, name) {
546
556
  const root = getExpressionRoot(expr);
547
- return ts16.isIdentifier(root) && root.text === name;
557
+ return ts15.isIdentifier(root) && root.text === name;
548
558
  }
549
559
  function getExpressionRoot(node) {
550
- if (ts16.isPropertyAccessExpression(node)) return getExpressionRoot(node.expression);
551
- if (ts16.isElementAccessExpression(node)) return getExpressionRoot(node.expression);
552
- if (ts16.isCallExpression(node)) return getExpressionRoot(node.expression);
560
+ if (ts15.isPropertyAccessExpression(node)) return getExpressionRoot(node.expression);
561
+ if (ts15.isElementAccessExpression(node)) return getExpressionRoot(node.expression);
562
+ if (ts15.isCallExpression(node)) return getExpressionRoot(node.expression);
553
563
  return node;
554
564
  }
555
565
  function isNullCheck(expr) {
556
566
  const op = expr.operatorToken.kind;
557
- if (op !== ts16.SyntaxKind.ExclamationEqualsToken && op !== ts16.SyntaxKind.ExclamationEqualsEqualsToken) return false;
567
+ if (op !== ts15.SyntaxKind.ExclamationEqualsToken && op !== ts15.SyntaxKind.ExclamationEqualsEqualsToken) return false;
558
568
  return isNullishLiteral(expr.right) || isNullishLiteral(expr.left);
559
569
  }
560
570
  function getNullCheckedIdentifier(expr) {
561
- if (ts16.isIdentifier(expr.left) && isNullishLiteral(expr.right)) return expr.left.text;
562
- if (ts16.isIdentifier(expr.right) && isNullishLiteral(expr.left)) return expr.right.text;
571
+ if (ts15.isIdentifier(expr.left) && isNullishLiteral(expr.right)) return expr.left.text;
572
+ if (ts15.isIdentifier(expr.right) && isNullishLiteral(expr.left)) return expr.right.text;
563
573
  return null;
564
574
  }
565
575
 
566
576
  // src/rules/ts/prefer-default-param-value.ts
567
- import * as ts17 from "typescript";
577
+ import * as ts16 from "typescript";
568
578
  var preferDefaultParamValue = {
569
579
  kind: "ts",
570
580
  id: "prefer-default-param-value",
571
581
  severity: "info",
572
582
  message: "Use a default parameter value instead of reassigning from nullish coalescing inside the body",
573
583
  visit(node, ctx) {
574
- if (!ts17.isFunctionDeclaration(node) && !ts17.isArrowFunction(node)) return;
575
- if (!node.body || !ts17.isBlock(node.body)) return;
576
- const stmts = node.body.statements;
577
- if (stmts.length === 0) return;
584
+ const result = getFunctionBodyStatements(node);
585
+ if (result === null) return;
586
+ const { statements: stmts, fn } = result;
578
587
  const firstStmt = stmts[0];
579
- if (firstStmt === void 0 || !ts17.isExpressionStatement(firstStmt)) return;
588
+ if (firstStmt === void 0 || !ts16.isExpressionStatement(firstStmt)) return;
580
589
  const expr = firstStmt.expression;
581
- if (!ts17.isBinaryExpression(expr) || expr.operatorToken.kind !== ts17.SyntaxKind.EqualsToken) return;
590
+ if (!ts16.isBinaryExpression(expr) || expr.operatorToken.kind !== ts16.SyntaxKind.EqualsToken) return;
582
591
  const right = expr.right;
583
- if (!ts17.isBinaryExpression(right) || right.operatorToken.kind !== ts17.SyntaxKind.QuestionQuestionToken) return;
584
- if (!ts17.isIdentifier(expr.left) || !ts17.isIdentifier(right.left)) return;
592
+ if (!ts16.isBinaryExpression(right) || right.operatorToken.kind !== ts16.SyntaxKind.QuestionQuestionToken) return;
593
+ if (!ts16.isIdentifier(expr.left) || !ts16.isIdentifier(right.left)) return;
585
594
  if (expr.left.text !== right.left.text) return;
586
595
  const paramName = expr.left.text;
587
- const isParam = node.parameters.some((p) => ts17.isIdentifier(p.name) && p.name.text === paramName);
596
+ const isParam = fn.parameters.some((p) => ts16.isIdentifier(p.name) && p.name.text === paramName);
588
597
  if (isParam) ctx.report(firstStmt);
589
598
  }
590
599
  };
591
600
 
592
601
  // src/rules/ts/prefer-required-param-with-guard.ts
593
- import * as ts18 from "typescript";
602
+ import * as ts17 from "typescript";
594
603
  var preferRequiredParamWithGuard = {
595
604
  kind: "ts",
596
605
  id: "prefer-required-param-with-guard",
597
606
  severity: "info",
598
607
  message: "Optional param with immediate guard (if (!param) return/throw); make it required instead",
599
608
  visit(node, ctx) {
600
- if (!ts18.isFunctionDeclaration(node) && !ts18.isArrowFunction(node)) return;
601
- if (!node.body || !ts18.isBlock(node.body)) return;
602
- const stmts = node.body.statements;
603
- if (stmts.length === 0) return;
609
+ const result = getFunctionBodyStatements(node);
610
+ if (result === null) return;
611
+ const { statements: stmts, fn } = result;
604
612
  const firstStmt = stmts[0];
605
- if (firstStmt === void 0 || !ts18.isIfStatement(firstStmt)) return;
613
+ if (firstStmt === void 0 || !ts17.isIfStatement(firstStmt)) return;
606
614
  const test = firstStmt.expression;
607
615
  let guardedName = null;
608
- if (ts18.isPrefixUnaryExpression(test) && test.operator === ts18.SyntaxKind.ExclamationToken) {
609
- if (ts18.isIdentifier(test.operand)) guardedName = test.operand.text;
616
+ if (ts17.isPrefixUnaryExpression(test) && test.operator === ts17.SyntaxKind.ExclamationToken) {
617
+ if (ts17.isIdentifier(test.operand)) guardedName = test.operand.text;
610
618
  }
611
- if (ts18.isBinaryExpression(test)) {
619
+ if (ts17.isBinaryExpression(test)) {
612
620
  const op = test.operatorToken.kind;
613
- if (op === ts18.SyntaxKind.EqualsEqualsEqualsToken || op === ts18.SyntaxKind.EqualsEqualsToken) {
614
- if (ts18.isIdentifier(test.left) && ts18.isIdentifier(test.right) && test.right.text === "undefined") {
621
+ if (op === ts17.SyntaxKind.EqualsEqualsEqualsToken || op === ts17.SyntaxKind.EqualsEqualsToken) {
622
+ if (ts17.isIdentifier(test.left) && ts17.isIdentifier(test.right) && test.right.text === "undefined") {
615
623
  guardedName = test.left.text;
616
624
  }
617
625
  }
618
626
  }
619
627
  if (!guardedName) return;
620
628
  const consequent = firstStmt.thenStatement;
621
- const isGuard = ts18.isReturnStatement(consequent) || ts18.isThrowStatement(consequent) || ts18.isBlock(consequent) && consequent.statements.length === 1 && consequent.statements[0] !== void 0 && (ts18.isReturnStatement(consequent.statements[0]) || ts18.isThrowStatement(consequent.statements[0]));
629
+ const isGuard = ts17.isReturnStatement(consequent) || ts17.isThrowStatement(consequent) || ts17.isBlock(consequent) && consequent.statements.length === 1 && consequent.statements[0] !== void 0 && (ts17.isReturnStatement(consequent.statements[0]) || ts17.isThrowStatement(consequent.statements[0]));
622
630
  if (!isGuard) return;
623
- const isOptional = node.parameters.some(
624
- (p) => ts18.isIdentifier(p.name) && p.name.text === guardedName && p.questionToken !== void 0
631
+ const isOptional = fn.parameters.some(
632
+ (p) => ts17.isIdentifier(p.name) && p.name.text === guardedName && p.questionToken !== void 0
625
633
  );
626
634
  if (isOptional) ctx.report(firstStmt);
627
635
  }
@@ -637,25 +645,21 @@ var duplicateTypeDeclaration = {
637
645
  for (const group of project.types.getDuplicateGroups()) {
638
646
  const files = new Set(group.map((e) => e.file));
639
647
  if (files.size < 2) continue;
640
- const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
641
- for (const entry of sorted.slice(1)) {
642
- const others = sorted.filter((e) => e !== entry).map((e) => `${e.name} (${e.file}:${e.line})`).join(", ");
643
- diagnostics.push({
644
- ruleId: this.id,
645
- severity: this.severity,
646
- message: `Type "${entry.name}" has identical shape to: ${others}`,
647
- file: entry.file,
648
- line: entry.line,
649
- column: 1
650
- });
651
- }
648
+ reportDuplicateGroup(
649
+ group,
650
+ this.id,
651
+ this.severity,
652
+ (e) => `${e.name} (${e.file}:${e.line})`,
653
+ (e, others) => `Type "${e.name}" has identical shape to: ${others}`,
654
+ diagnostics
655
+ );
652
656
  }
653
657
  return diagnostics;
654
658
  }
655
659
  };
656
660
 
657
661
  // src/rules/cross-file/duplicate-function-declaration.ts
658
- import * as ts19 from "typescript";
662
+ import * as ts18 from "typescript";
659
663
  var duplicateFunctionDeclaration = {
660
664
  id: "duplicate-function-declaration",
661
665
  severity: "warning",
@@ -663,37 +667,38 @@ var duplicateFunctionDeclaration = {
663
667
  analyze(project) {
664
668
  const diagnostics = [];
665
669
  for (const group of project.functions.getDuplicateGroups()) {
666
- const files = new Set(group.map((e) => e.file));
667
- if (files.size < 2) continue;
668
670
  const first = group[0];
669
- if (first !== void 0 && isSetter(first.node)) continue;
670
- const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
671
- for (const entry of sorted.slice(1)) {
672
- const others = sorted.filter((e) => e !== entry).map((e) => `${e.name} (${e.file}:${e.line})`).join(", ");
673
- diagnostics.push({
674
- ruleId: this.id,
675
- severity: this.severity,
676
- message: `Function "${entry.name}" has identical body to: ${others}`,
677
- file: entry.file,
678
- line: entry.line,
679
- column: 1
680
- });
681
- }
671
+ if (first === void 0) continue;
672
+ if (isSingleStatement(first.node)) continue;
673
+ if (isSetter(first.node)) continue;
674
+ reportDuplicateGroup(
675
+ group,
676
+ this.id,
677
+ this.severity,
678
+ (e) => `${e.name} (${e.file}:${e.line})`,
679
+ (e, others) => `Function "${e.name}" has identical body to: ${others}`,
680
+ diagnostics
681
+ );
682
682
  }
683
683
  return diagnostics;
684
684
  }
685
685
  };
686
+ function getBodyBlock(node) {
687
+ if (ts18.isFunctionDeclaration(node) || ts18.isFunctionExpression(node)) return node.body;
688
+ if (ts18.isArrowFunction(node)) return ts18.isBlock(node.body) ? node.body : void 0;
689
+ if (ts18.isMethodDeclaration(node)) return node.body;
690
+ return void 0;
691
+ }
692
+ function isSingleStatement(node) {
693
+ const body = getBodyBlock(node);
694
+ return body !== void 0 && body.statements.length <= 1;
695
+ }
686
696
  function isSetter(node) {
687
- let body;
688
- if (ts19.isFunctionDeclaration(node) || ts19.isFunctionExpression(node)) {
689
- body = node.body;
690
- } else if (ts19.isArrowFunction(node)) {
691
- body = ts19.isBlock(node.body) ? node.body : void 0;
692
- }
697
+ const body = getBodyBlock(node);
693
698
  if (!body || body.statements.length !== 1) return false;
694
699
  const stmt = body.statements[0];
695
- if (stmt === void 0 || !ts19.isExpressionStatement(stmt)) return false;
696
- return ts19.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts19.SyntaxKind.EqualsToken;
700
+ if (stmt === void 0 || !ts18.isExpressionStatement(stmt)) return false;
701
+ return ts18.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts18.SyntaxKind.EqualsToken;
697
702
  }
698
703
 
699
704
  // src/rules/cross-file/optional-arg-always-used.ts
@@ -728,14 +733,14 @@ var optionalArgAlwaysUsed = {
728
733
  };
729
734
 
730
735
  // src/rules/ts/no-catch-return.ts
731
- import * as ts20 from "typescript";
736
+ import * as ts19 from "typescript";
732
737
  var noCatchReturn = {
733
738
  kind: "ts",
734
739
  id: "no-catch-return",
735
740
  severity: "warning",
736
741
  message: "Catch block silently returns a fallback value with no logging; rethrow, log the error, or let it propagate",
737
742
  visit(node, ctx) {
738
- if (!ts20.isCatchClause(node)) return;
743
+ if (!ts19.isCatchClause(node)) return;
739
744
  const block = node.block;
740
745
  if (hasReturn(block) && !hasThrow(block) && !hasLogging(block)) {
741
746
  ctx.report(node);
@@ -745,57 +750,57 @@ var noCatchReturn = {
745
750
  function walkBlock(block, predicate) {
746
751
  function visit(node) {
747
752
  if (predicate(node)) return true;
748
- if (ts20.isFunctionDeclaration(node) || ts20.isFunctionExpression(node) || ts20.isArrowFunction(node)) return false;
749
- return ts20.forEachChild(node, visit) ?? false;
753
+ if (ts19.isFunctionDeclaration(node) || ts19.isFunctionExpression(node) || ts19.isArrowFunction(node)) return false;
754
+ return ts19.forEachChild(node, visit) ?? false;
750
755
  }
751
- return ts20.forEachChild(block, visit) ?? false;
756
+ return ts19.forEachChild(block, visit) ?? false;
752
757
  }
753
758
  function hasReturn(block) {
754
- return walkBlock(block, (n) => ts20.isReturnStatement(n));
759
+ return walkBlock(block, (n) => ts19.isReturnStatement(n));
755
760
  }
756
761
  function hasThrow(block) {
757
- return walkBlock(block, (n) => ts20.isThrowStatement(n));
762
+ return walkBlock(block, (n) => ts19.isThrowStatement(n));
758
763
  }
759
764
  var LOG_OBJECTS = /* @__PURE__ */ new Set(["console", "logger", "log"]);
760
765
  function hasLogging(block) {
761
766
  return walkBlock(block, (n) => {
762
- if (!ts20.isCallExpression(n)) return false;
767
+ if (!ts19.isCallExpression(n)) return false;
763
768
  const callee = n.expression;
764
- if (!ts20.isPropertyAccessExpression(callee)) return false;
765
- if (!ts20.isIdentifier(callee.expression)) return false;
769
+ if (!ts19.isPropertyAccessExpression(callee)) return false;
770
+ if (!ts19.isIdentifier(callee.expression)) return false;
766
771
  return LOG_OBJECTS.has(callee.expression.text);
767
772
  });
768
773
  }
769
774
 
770
775
  // src/rules/ts/no-error-rewrap.ts
771
- import * as ts21 from "typescript";
776
+ import * as ts20 from "typescript";
772
777
  var noErrorRewrap = {
773
778
  kind: "ts",
774
779
  id: "no-error-rewrap",
775
780
  severity: "error",
776
781
  message: "Re-wrapped error loses the original stack trace and type; use { cause: originalError } to preserve the error chain",
777
782
  visit(node, ctx) {
778
- if (!ts21.isCatchClause(node)) return;
783
+ if (!ts20.isCatchClause(node)) return;
779
784
  if (!node.variableDeclaration) return;
780
785
  const param = node.variableDeclaration.name;
781
- if (!ts21.isIdentifier(param)) return;
786
+ if (!ts20.isIdentifier(param)) return;
782
787
  const catchName = param.text;
783
788
  findRewraps(node.block, catchName, ctx);
784
789
  }
785
790
  };
786
791
  function findRewraps(block, catchName, ctx) {
787
792
  function visit(node) {
788
- if (ts21.isFunctionDeclaration(node) || ts21.isFunctionExpression(node) || ts21.isArrowFunction(node)) return;
789
- if (ts21.isThrowStatement(node) && node.expression && ts21.isNewExpression(node.expression)) {
793
+ if (ts20.isFunctionDeclaration(node) || ts20.isFunctionExpression(node) || ts20.isArrowFunction(node)) return;
794
+ if (ts20.isThrowStatement(node) && node.expression && ts20.isNewExpression(node.expression)) {
790
795
  const args = node.expression.arguments;
791
796
  if (args && args.length > 0 && referencesName(args, catchName) && !hasCauseArg(args)) {
792
797
  ctx.report(node);
793
798
  }
794
799
  return;
795
800
  }
796
- ts21.forEachChild(node, visit);
801
+ ts20.forEachChild(node, visit);
797
802
  }
798
- ts21.forEachChild(block, visit);
803
+ ts20.forEachChild(block, visit);
799
804
  }
800
805
  function referencesName(args, name) {
801
806
  for (const arg of args) {
@@ -804,15 +809,15 @@ function referencesName(args, name) {
804
809
  return false;
805
810
  }
806
811
  function containsIdentifier(node, name) {
807
- if (ts21.isIdentifier(node) && node.text === name) return true;
808
- return ts21.forEachChild(node, (child) => containsIdentifier(child, name) || void 0) ?? false;
812
+ if (ts20.isIdentifier(node) && node.text === name) return true;
813
+ return ts20.forEachChild(node, (child) => containsIdentifier(child, name) || void 0) ?? false;
809
814
  }
810
815
  function hasCauseArg(args) {
811
816
  for (const arg of args) {
812
- if (ts21.isObjectLiteralExpression(arg)) {
817
+ if (ts20.isObjectLiteralExpression(arg)) {
813
818
  for (const prop of arg.properties) {
814
- if (ts21.isPropertyAssignment(prop) && ts21.isIdentifier(prop.name) && prop.name.text === "cause") return true;
815
- if (ts21.isShorthandPropertyAssignment(prop) && prop.name.text === "cause") return true;
819
+ if (ts20.isPropertyAssignment(prop) && ts20.isIdentifier(prop.name) && prop.name.text === "cause") return true;
820
+ if (ts20.isShorthandPropertyAssignment(prop) && prop.name.text === "cause") return true;
816
821
  }
817
822
  }
818
823
  }
@@ -820,7 +825,7 @@ function hasCauseArg(args) {
820
825
  }
821
826
 
822
827
  // src/rules/cross-file/explicit-null-arg.ts
823
- import * as ts22 from "typescript";
828
+ import * as ts21 from "typescript";
824
829
  var explicitNullArg = {
825
830
  id: "explicit-null-arg",
826
831
  severity: "warning",
@@ -840,7 +845,7 @@ var explicitNullArg = {
840
845
  const arg = site.node.arguments[i];
841
846
  if (arg === void 0) continue;
842
847
  if (isNullishLiteral(arg)) {
843
- const val = arg.kind === ts22.SyntaxKind.NullKeyword ? "null" : "undefined";
848
+ const val = arg.kind === ts21.SyntaxKind.NullKeyword ? "null" : "undefined";
844
849
  diagnostics.push({
845
850
  ruleId: this.id,
846
851
  severity: this.severity,
@@ -890,18 +895,14 @@ var duplicateFunctionName = {
890
895
  );
891
896
  });
892
897
  if (hasImportLink) continue;
893
- const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
894
- for (const entry of sorted.slice(1)) {
895
- const others = sorted.filter((e) => e !== entry).map((e) => `${e.file}:${e.line}`).join(", ");
896
- diagnostics.push({
897
- ruleId: this.id,
898
- severity: this.severity,
899
- message: `Exported function "${entry.name}" also defined in: ${others}`,
900
- file: entry.file,
901
- line: entry.line,
902
- column: 1
903
- });
904
- }
898
+ reportDuplicateGroup(
899
+ group,
900
+ this.id,
901
+ this.severity,
902
+ (e) => `${e.file}:${e.line}`,
903
+ (e, others) => `Exported function "${e.name}" also defined in: ${others}`,
904
+ diagnostics
905
+ );
905
906
  }
906
907
  return diagnostics;
907
908
  }
@@ -917,7 +918,7 @@ function resolveCandidates(fromFile, specifier) {
917
918
  }
918
919
 
919
920
  // src/rules/cross-file/duplicate-type-name.ts
920
- import * as ts23 from "typescript";
921
+ import * as ts22 from "typescript";
921
922
  var duplicateTypeName = {
922
923
  id: "duplicate-type-name",
923
924
  severity: "warning",
@@ -928,40 +929,290 @@ var duplicateTypeName = {
928
929
  const hashes = new Set(group.map((e) => e.hash));
929
930
  if (hashes.size === 1) continue;
930
931
  const hasInferredType = group.some(
931
- (e) => !ts23.isTypeLiteralNode(e.node) && !ts23.isInterfaceDeclaration(e.node)
932
+ (e) => !ts22.isTypeLiteralNode(e.node) && !ts22.isInterfaceDeclaration(e.node)
932
933
  );
933
934
  if (hasInferredType) continue;
934
- const sorted = [...group].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
935
- for (const entry of sorted.slice(1)) {
936
- const others = sorted.filter((e) => e !== entry).map((e) => `${e.file}:${e.line}`).join(", ");
937
- diagnostics.push({
938
- ruleId: this.id,
939
- severity: this.severity,
940
- message: `Exported type "${entry.name}" also defined in: ${others}`,
941
- file: entry.file,
942
- line: entry.line,
943
- column: 1
944
- });
945
- }
935
+ reportDuplicateGroup(
936
+ group,
937
+ this.id,
938
+ this.severity,
939
+ (e) => `${e.file}:${e.line}`,
940
+ (e, others) => `Exported type "${e.name}" also defined in: ${others}`,
941
+ diagnostics
942
+ );
943
+ }
944
+ return diagnostics;
945
+ }
946
+ };
947
+
948
+ // src/rules/cross-file/duplicate-constant-declaration.ts
949
+ var duplicateConstantDeclaration = {
950
+ id: "duplicate-constant-declaration",
951
+ severity: "warning",
952
+ message: "Identical constant value declared in multiple files; consolidate to a single definition",
953
+ analyze(project) {
954
+ const diagnostics = [];
955
+ for (const group of project.constants.getDuplicateGroups()) {
956
+ const files = new Set(group.map((e) => e.file));
957
+ if (files.size < 2) continue;
958
+ reportDuplicateGroup(
959
+ group,
960
+ this.id,
961
+ this.severity,
962
+ (e) => `${e.name} (${e.file}:${e.line})`,
963
+ (e, others) => `Constant "${e.name}" has identical value \`${e.valueText}\` to: ${others}`,
964
+ diagnostics
965
+ );
946
966
  }
947
967
  return diagnostics;
948
968
  }
949
969
  };
950
970
 
951
971
  // src/rules/ts/no-dynamic-import.ts
952
- import * as ts24 from "typescript";
972
+ import * as ts23 from "typescript";
953
973
  var noDynamicImport = {
954
974
  kind: "ts",
955
975
  id: "no-dynamic-import",
956
976
  severity: "error",
957
977
  message: "Dynamic import() breaks static analysis and hides dependencies; use a static import instead",
958
978
  visit(node, ctx) {
959
- if (!ts24.isCallExpression(node)) return;
960
- if (node.expression.kind !== ts24.SyntaxKind.ImportKeyword) return;
979
+ if (!ts23.isCallExpression(node)) return;
980
+ if (node.expression.kind !== ts23.SyntaxKind.ImportKeyword) return;
961
981
  ctx.report(node);
962
982
  }
963
983
  };
964
984
 
985
+ // src/rules/cross-file/near-duplicate-function.ts
986
+ var nearDuplicateFunction = {
987
+ id: "near-duplicate-function",
988
+ severity: "warning",
989
+ message: "Near-duplicate function bodies across files; consider parameterizing",
990
+ analyze(project) {
991
+ const diagnostics = [];
992
+ for (const group of project.functions.getNearDuplicateGroups()) {
993
+ const MIN_NORMALIZED_BODY = 32;
994
+ if (group.every((e) => e.normalizedBodyLength < MIN_NORMALIZED_BODY)) continue;
995
+ reportDuplicateGroup(
996
+ group,
997
+ this.id,
998
+ this.severity,
999
+ (e) => `${e.name} (${e.file}:${e.line})`,
1000
+ (e, others) => `Function "${e.name}" is near-duplicate of: ${others}`,
1001
+ diagnostics
1002
+ );
1003
+ }
1004
+ return diagnostics;
1005
+ }
1006
+ };
1007
+
1008
+ // src/rules/cross-file/trivial-wrapper.ts
1009
+ import * as ts24 from "typescript";
1010
+ function getFunctionBody(node) {
1011
+ if (ts24.isFunctionDeclaration(node) || ts24.isFunctionExpression(node)) {
1012
+ return node.body;
1013
+ }
1014
+ if (ts24.isArrowFunction(node)) {
1015
+ return node.body;
1016
+ }
1017
+ if (ts24.isMethodDeclaration(node)) {
1018
+ return node.body;
1019
+ }
1020
+ return void 0;
1021
+ }
1022
+ function getCalleeName(expr) {
1023
+ if (ts24.isIdentifier(expr)) return expr.text;
1024
+ if (ts24.isPropertyAccessExpression(expr)) return expr.name.text;
1025
+ return void 0;
1026
+ }
1027
+ function getTrivialCallTarget(fn) {
1028
+ const body = getFunctionBody(fn.node);
1029
+ if (!body) return null;
1030
+ let callExpr;
1031
+ if (ts24.isBlock(body)) {
1032
+ if (body.statements.length !== 1) return null;
1033
+ const stmt = body.statements[0];
1034
+ if (!stmt || !ts24.isReturnStatement(stmt) || !stmt.expression) return null;
1035
+ if (!ts24.isCallExpression(stmt.expression)) return null;
1036
+ callExpr = stmt.expression;
1037
+ } else {
1038
+ if (!ts24.isCallExpression(body)) return null;
1039
+ callExpr = body;
1040
+ }
1041
+ const calleeName = getCalleeName(callExpr.expression);
1042
+ if (!calleeName) return null;
1043
+ const paramNames = fn.params.map((p) => p.name);
1044
+ const args = callExpr.arguments;
1045
+ for (const arg of args) {
1046
+ if (!ts24.isIdentifier(arg)) return null;
1047
+ if (!paramNames.includes(arg.text)) return null;
1048
+ }
1049
+ return { calleeName };
1050
+ }
1051
+ var trivialWrapper = {
1052
+ id: "trivial-wrapper",
1053
+ severity: "info",
1054
+ message: "Function is a trivial wrapper that delegates without transformation; consider using the target directly",
1055
+ analyze(project) {
1056
+ const diagnostics = [];
1057
+ const knownFunctions = new Set(
1058
+ project.functions.getAll().map((f) => f.name)
1059
+ );
1060
+ for (const fn of project.functions.getAll()) {
1061
+ const target = getTrivialCallTarget(fn);
1062
+ if (!target) continue;
1063
+ if (!knownFunctions.has(target.calleeName)) continue;
1064
+ if (fn.name === target.calleeName) continue;
1065
+ diagnostics.push({
1066
+ ruleId: this.id,
1067
+ severity: this.severity,
1068
+ message: `Function "${fn.name}" trivially wraps "${target.calleeName}" without transformation`,
1069
+ file: fn.file,
1070
+ line: fn.line,
1071
+ column: 1
1072
+ });
1073
+ }
1074
+ return diagnostics;
1075
+ }
1076
+ };
1077
+
1078
+ // src/rules/cross-file/unused-export.ts
1079
+ var unusedExport = {
1080
+ id: "unused-export",
1081
+ severity: "info",
1082
+ message: "Exported function has no usages within the project",
1083
+ analyze(project) {
1084
+ const diagnostics = [];
1085
+ const usedDeclarations = /* @__PURE__ */ new Set();
1086
+ const usedNamesByFile = /* @__PURE__ */ new Map();
1087
+ for (const site of project.callSites) {
1088
+ if (site.symbol?.valueDeclaration) {
1089
+ usedDeclarations.add(site.symbol.valueDeclaration);
1090
+ }
1091
+ let files = usedNamesByFile.get(site.calleeName);
1092
+ if (!files) {
1093
+ files = /* @__PURE__ */ new Set();
1094
+ usedNamesByFile.set(site.calleeName, files);
1095
+ }
1096
+ files.add(site.file);
1097
+ }
1098
+ for (const imp of project.imports) {
1099
+ let files = usedNamesByFile.get(imp.importedName);
1100
+ if (!files) {
1101
+ files = /* @__PURE__ */ new Set();
1102
+ usedNamesByFile.set(imp.importedName, files);
1103
+ }
1104
+ files.add(imp.file);
1105
+ }
1106
+ const importedClassNames = buildImportedClassNames(project);
1107
+ for (const fn of project.functions.getAll()) {
1108
+ if (!fn.exported) continue;
1109
+ if (isEntryPoint(fn.file)) continue;
1110
+ if (fn.implementsInterface) continue;
1111
+ if (fn.className && isClassImported(fn.className, fn.file, importedClassNames)) continue;
1112
+ if (fn.symbol?.valueDeclaration && usedDeclarations.has(fn.symbol.valueDeclaration)) continue;
1113
+ const nameUsers = usedNamesByFile.get(fn.name);
1114
+ if (nameUsers) {
1115
+ const hasExternalUser = [...nameUsers].some((f) => f !== fn.file);
1116
+ if (hasExternalUser) continue;
1117
+ }
1118
+ diagnostics.push({
1119
+ ruleId: this.id,
1120
+ severity: this.severity,
1121
+ message: `Exported function "${fn.name}" has no usages in the project`,
1122
+ file: fn.file,
1123
+ line: fn.line,
1124
+ column: 1
1125
+ });
1126
+ }
1127
+ return diagnostics;
1128
+ }
1129
+ };
1130
+ function isEntryPoint(file) {
1131
+ if (/\/index\.[cm]?[jt]sx?$/.test(file)) return true;
1132
+ if (/\/bin\//.test(file)) return true;
1133
+ return false;
1134
+ }
1135
+ function buildImportedClassNames(project) {
1136
+ const map = /* @__PURE__ */ new Map();
1137
+ for (const imp of project.imports) {
1138
+ let files = map.get(imp.importedName);
1139
+ if (!files) {
1140
+ files = /* @__PURE__ */ new Set();
1141
+ map.set(imp.importedName, files);
1142
+ }
1143
+ files.add(imp.file);
1144
+ }
1145
+ return map;
1146
+ }
1147
+ function isClassImported(className, declFile, importedNames) {
1148
+ const importers = importedNames.get(className);
1149
+ if (!importers) return false;
1150
+ return [...importers].some((f) => f !== declFile);
1151
+ }
1152
+
1153
+ // src/rules/cross-file/duplicate-file.ts
1154
+ var duplicateFile = {
1155
+ id: "duplicate-file",
1156
+ severity: "warning",
1157
+ message: "File has identical content to another file; one is likely dead code",
1158
+ analyze(project) {
1159
+ const diagnostics = [];
1160
+ for (const files of project.fileHashes.values()) {
1161
+ if (files.length < 2) continue;
1162
+ const sorted = [...files].sort();
1163
+ for (const file of sorted.slice(1)) {
1164
+ const others = sorted.filter((f) => f !== file).join(", ");
1165
+ diagnostics.push({
1166
+ ruleId: this.id,
1167
+ severity: this.severity,
1168
+ message: `File is identical to: ${others}`,
1169
+ file,
1170
+ line: 1,
1171
+ column: 1
1172
+ });
1173
+ }
1174
+ }
1175
+ return diagnostics;
1176
+ }
1177
+ };
1178
+
1179
+ // src/rules/cross-file/duplicate-statement-sequence.ts
1180
+ var duplicateStatementSequence = {
1181
+ id: "duplicate-statement-sequence",
1182
+ severity: "info",
1183
+ message: "Repeated statement sequence; consider extracting to a shared helper",
1184
+ analyze(project) {
1185
+ const diagnostics = [];
1186
+ const MIN_NORMALIZED_BODY = 128;
1187
+ for (const group of project.statementSequences.getNormalizedDuplicateGroups()) {
1188
+ if (group.every((e) => e.normalizedBodyLength < MIN_NORMALIZED_BODY)) continue;
1189
+ const byLocation = /* @__PURE__ */ new Map();
1190
+ for (const entry of group) {
1191
+ const key = `${entry.file}:${entry.line}`;
1192
+ const existing = byLocation.get(key);
1193
+ if (existing === void 0 || entry.statementCount > existing.statementCount) {
1194
+ byLocation.set(key, entry);
1195
+ }
1196
+ }
1197
+ const deduped = [...byLocation.values()];
1198
+ if (deduped.length < 2) continue;
1199
+ const sorted = deduped.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
1200
+ for (const entry of sorted.slice(1)) {
1201
+ const others = sorted.filter((e) => e !== entry).map((e) => `${e.file}:${e.line}`).join(", ");
1202
+ diagnostics.push({
1203
+ ruleId: this.id,
1204
+ severity: this.severity,
1205
+ message: `Statement sequence (${entry.statementCount} statements) duplicated at: ${others}`,
1206
+ file: entry.file,
1207
+ line: entry.line,
1208
+ column: 1
1209
+ });
1210
+ }
1211
+ }
1212
+ return diagnostics;
1213
+ }
1214
+ };
1215
+
965
1216
  // src/rules/index.ts
966
1217
  var allRules = [
967
1218
  noEmptyCatch,
@@ -976,7 +1227,7 @@ var allRules = [
976
1227
  noNullTernaryNormalization,
977
1228
  noAnyCast,
978
1229
  noExplicitAnyAnnotation,
979
- noInlineTypeInParams,
1230
+ duplicateInlineTypeInParams,
980
1231
  noTypeAssertion,
981
1232
  noRedundantExistenceGuard,
982
1233
  preferDefaultParamValue,
@@ -989,7 +1240,13 @@ var allRules = [
989
1240
  explicitNullArg,
990
1241
  duplicateFunctionName,
991
1242
  duplicateTypeName,
992
- noDynamicImport
1243
+ duplicateConstantDeclaration,
1244
+ noDynamicImport,
1245
+ nearDuplicateFunction,
1246
+ trivialWrapper,
1247
+ unusedExport,
1248
+ duplicateFile,
1249
+ duplicateStatementSequence
993
1250
  ];
994
1251
  var ruleMetadata = {
995
1252
  "no-any-cast": { category: "type-evasion", tags: ["safety"] },
@@ -1008,16 +1265,22 @@ var ruleMetadata = {
1008
1265
  "no-empty-catch": { category: "error-handling", tags: ["safety"] },
1009
1266
  "no-catch-return": { category: "error-handling", tags: ["safety"] },
1010
1267
  "no-error-rewrap": { category: "error-handling", tags: ["safety"] },
1011
- "no-inline-type-in-params": { category: "interface-design", tags: ["api"] },
1268
+ "duplicate-inline-type-in-params": { category: "cross-file", tags: ["duplicate", "api"] },
1012
1269
  "prefer-default-param-value": { category: "interface-design", tags: ["api"] },
1013
1270
  "prefer-required-param-with-guard": { category: "interface-design", tags: ["api"] },
1014
1271
  "duplicate-type-declaration": { category: "cross-file", tags: ["duplicate"] },
1015
1272
  "duplicate-type-name": { category: "cross-file", tags: ["duplicate"] },
1016
1273
  "duplicate-function-declaration": { category: "cross-file", tags: ["duplicate"] },
1017
1274
  "duplicate-function-name": { category: "cross-file", tags: ["duplicate"] },
1275
+ "duplicate-constant-declaration": { category: "cross-file", tags: ["duplicate"] },
1018
1276
  "optional-arg-always-used": { category: "cross-file", tags: ["api"] },
1019
1277
  "explicit-null-arg": { category: "cross-file", tags: ["api"] },
1020
- "no-dynamic-import": { category: "imports", tags: ["safety"] }
1278
+ "no-dynamic-import": { category: "imports", tags: ["safety"] },
1279
+ "near-duplicate-function": { category: "cross-file", tags: ["duplicate"] },
1280
+ "trivial-wrapper": { category: "cross-file", tags: ["duplicate"] },
1281
+ "unused-export": { category: "cross-file", tags: ["api"] },
1282
+ "duplicate-file": { category: "cross-file", tags: ["duplicate"] },
1283
+ "duplicate-statement-sequence": { category: "cross-file", tags: ["duplicate"] }
1021
1284
  };
1022
1285
  function getRuleMetadata(ruleId) {
1023
1286
  const metadata = ruleMetadata[ruleId];
@@ -1076,12 +1339,9 @@ function isFailOn(value) {
1076
1339
  return value === "none" || isSeverity(value);
1077
1340
  }
1078
1341
 
1079
- // src/scan/analyze.ts
1080
- import { readFileSync as readFileSync2 } from "fs";
1081
-
1082
1342
  // src/collect/index.ts
1083
- import * as ts26 from "typescript";
1084
- import "fs";
1343
+ import * as ts27 from "typescript";
1344
+ import { createHash as createHash2 } from "crypto";
1085
1345
 
1086
1346
  // src/utils/hash.ts
1087
1347
  import { createHash } from "crypto";
@@ -1111,18 +1371,60 @@ function normalizeTypeNode(node, sourceFile) {
1111
1371
  return node.getText(sourceFile).replace(/\s+/g, " ").trim();
1112
1372
  }
1113
1373
  function hashFunctionBody(node, sourceFile) {
1114
- const bodyText = node.getText(sourceFile);
1115
- const normalized = bodyText.replace(/\s+/g, " ").trim();
1374
+ const normalized = stripComments(node.getText(sourceFile));
1116
1375
  return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
1117
1376
  }
1377
+ function bodyTextLength(node, sourceFile) {
1378
+ return stripComments(node.getText(sourceFile)).length;
1379
+ }
1380
+ function stripComments(text) {
1381
+ return text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
1382
+ }
1383
+ function normalizeBody(node, sourceFile, paramNames) {
1384
+ let text = node.getText(sourceFile);
1385
+ text = text.replace(/\/\/[^\n]*/g, "");
1386
+ text = text.replace(/\/\*[\s\S]*?\*\//g, "");
1387
+ text = text.replace(/"(?:[^"\\]|\\.)*"/g, '"__STR__"');
1388
+ text = text.replace(/'(?:[^'\\]|\\.)*'/g, '"__STR__"');
1389
+ text = text.replace(/`(?:[^`\\]|\\.)*`/g, '"__STR__"');
1390
+ text = text.replace(/\b\d+(?:\.\d+)?\b/g, "__NUM__");
1391
+ for (let i = 0; i < paramNames.length; i++) {
1392
+ const name = paramNames[i];
1393
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1394
+ text = text.replace(new RegExp(`\\b${escaped}\\b`, "g"), `$${i}`);
1395
+ }
1396
+ text = text.replace(/\bthis\./g, "$_.");
1397
+ text = text.replace(/\$\d+\./g, "$_.");
1398
+ text = text.replace(/\s+/g, " ").trim();
1399
+ return text;
1400
+ }
1401
+ function normalizeText(text) {
1402
+ let t = text.replace(/\/\/[^\n]*/g, "");
1403
+ t = t.replace(/\/\*[\s\S]*?\*\//g, "");
1404
+ t = t.replace(/"(?:[^"\\]|\\.)*"/g, '"__STR__"');
1405
+ t = t.replace(/'(?:[^'\\]|\\.)*'/g, '"__STR__"');
1406
+ t = t.replace(/`(?:[^`\\]|\\.)*`/g, '"__STR__"');
1407
+ t = t.replace(/\b\d+(?:\.\d+)?\b/g, "__NUM__");
1408
+ t = t.replace(/\bthis\./g, "$_.");
1409
+ t = t.replace(/\s+/g, " ").trim();
1410
+ return t;
1411
+ }
1412
+ function hashText(text) {
1413
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
1414
+ }
1415
+ function hashFunctionBodyNormalized(node, sourceFile, paramNames) {
1416
+ const text = normalizeBody(node, sourceFile, paramNames);
1417
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
1418
+ }
1419
+ function normalizedBodyTextLength(node, sourceFile, paramNames) {
1420
+ return normalizeBody(node, sourceFile, paramNames).length;
1421
+ }
1118
1422
 
1119
- // src/collect/type-registry.ts
1120
- var TypeRegistry = class {
1423
+ // src/collect/base-registry.ts
1424
+ var BaseRegistry = class {
1121
1425
  entries = [];
1122
1426
  byHash = /* @__PURE__ */ new Map();
1123
- add(name, file, line, typeNode, sourceFile, exported) {
1124
- const hash = hashTypeShape(typeNode, sourceFile);
1125
- const entry = { name, file, line, hash, node: typeNode, exported };
1427
+ addEntry(entry, hash) {
1126
1428
  this.entries.push(entry);
1127
1429
  let list = this.byHash.get(hash);
1128
1430
  if (list === void 0) {
@@ -1137,81 +1439,169 @@ var TypeRegistry = class {
1137
1439
  getAll() {
1138
1440
  return this.entries;
1139
1441
  }
1140
- getNameCollisionGroups() {
1141
- const byName = /* @__PURE__ */ new Map();
1142
- for (const entry of this.entries) {
1143
- if (!entry.exported) continue;
1144
- let list = byName.get(entry.name);
1145
- if (list === void 0) {
1146
- list = [];
1147
- byName.set(entry.name, list);
1148
- }
1149
- list.push(entry);
1442
+ };
1443
+ var DualHashRegistry = class extends BaseRegistry {
1444
+ bySecondaryHash = /* @__PURE__ */ new Map();
1445
+ addWithSecondary(entry, primaryHash, secondaryHash) {
1446
+ this.addEntry(entry, primaryHash);
1447
+ let list = this.bySecondaryHash.get(secondaryHash);
1448
+ if (list === void 0) {
1449
+ list = [];
1450
+ this.bySecondaryHash.set(secondaryHash, list);
1150
1451
  }
1151
- return [...byName.values()].filter((group) => {
1152
- if (group.length < 2) return false;
1153
- const files = new Set(group.map((e) => e.file));
1154
- return files.size > 1;
1155
- });
1452
+ list.push(entry);
1453
+ }
1454
+ getSecondaryDuplicateGroups() {
1455
+ return [...this.bySecondaryHash.values()].filter((group) => group.length > 1);
1156
1456
  }
1157
1457
  };
1158
1458
 
1159
- // src/collect/function-registry.ts
1160
- var FunctionRegistry = class {
1161
- entries = [];
1162
- byHash = /* @__PURE__ */ new Map();
1163
- add(entry) {
1164
- this.entries.push(entry);
1165
- let list = this.byHash.get(entry.hash);
1459
+ // src/collect/type-registry.ts
1460
+ var TypeRegistry = class extends BaseRegistry {
1461
+ add(name, file, line, typeNode, sourceFile, exported) {
1462
+ const hash = hashTypeShape(typeNode, sourceFile);
1463
+ const entry = { name, file, line, hash, node: typeNode, exported };
1464
+ this.addEntry(entry, hash);
1465
+ }
1466
+ getNameCollisionGroups() {
1467
+ return getExportedNameCollisions(this.entries);
1468
+ }
1469
+ };
1470
+ function getExportedNameCollisions(entries) {
1471
+ const byName = /* @__PURE__ */ new Map();
1472
+ for (const entry of entries) {
1473
+ if (!entry.exported) continue;
1474
+ let list = byName.get(entry.name);
1166
1475
  if (list === void 0) {
1167
1476
  list = [];
1168
- this.byHash.set(entry.hash, list);
1477
+ byName.set(entry.name, list);
1169
1478
  }
1170
1479
  list.push(entry);
1171
1480
  }
1172
- getDuplicateGroups() {
1173
- return [...this.byHash.values()].filter((group) => group.length > 1);
1481
+ return [...byName.values()].filter((group) => {
1482
+ if (group.length < 2) return false;
1483
+ const files = new Set(group.map((e) => e.file));
1484
+ return files.size > 1;
1485
+ });
1486
+ }
1487
+
1488
+ // src/collect/function-registry.ts
1489
+ var FunctionRegistry = class extends DualHashRegistry {
1490
+ add(entry) {
1491
+ this.addWithSecondary(entry, entry.hash, entry.normalizedHash);
1174
1492
  }
1175
- getAll() {
1176
- return this.entries;
1493
+ getNearDuplicateGroups() {
1494
+ return this.getSecondaryDuplicateGroups().filter((group) => {
1495
+ const hashes = new Set(group.map((e) => e.hash));
1496
+ return hashes.size > 1;
1497
+ });
1177
1498
  }
1178
1499
  getByName(name) {
1179
1500
  return this.entries.filter((e) => e.name === name);
1180
1501
  }
1181
1502
  getNameCollisionGroups() {
1182
- const byName = /* @__PURE__ */ new Map();
1183
- for (const entry of this.entries) {
1184
- if (!entry.exported) continue;
1185
- let list = byName.get(entry.name);
1186
- if (list === void 0) {
1187
- list = [];
1188
- byName.set(entry.name, list);
1189
- }
1190
- list.push(entry);
1191
- }
1192
- return [...byName.values()].filter((group) => {
1193
- if (group.length < 2) return false;
1194
- const files = new Set(group.map((e) => e.file));
1195
- return files.size > 1;
1196
- });
1503
+ return getExportedNameCollisions(this.entries);
1504
+ }
1505
+ };
1506
+
1507
+ // src/collect/constant-registry.ts
1508
+ var ConstantRegistry = class extends BaseRegistry {
1509
+ add(entry) {
1510
+ this.addEntry(entry, entry.valueHash);
1197
1511
  }
1198
1512
  };
1199
1513
 
1514
+ // src/collect/statement-sequence-registry.ts
1515
+ var StatementSequenceRegistry = class extends DualHashRegistry {
1516
+ add(entry) {
1517
+ this.addWithSecondary(entry, entry.hash, entry.normalizedHash);
1518
+ }
1519
+ getNormalizedDuplicateGroups() {
1520
+ return this.getSecondaryDuplicateGroups();
1521
+ }
1522
+ };
1523
+
1524
+ // src/collect/inline-type-registry.ts
1525
+ var InlineParamTypeRegistry = class extends BaseRegistry {
1526
+ add(file, line, typeNode, sourceFile) {
1527
+ const hash = hashTypeShape(typeNode, sourceFile);
1528
+ const typeText = typeNode.getText(sourceFile).replace(/\s+/g, " ").trim();
1529
+ this.addEntry({ file, line, hash, typeText, node: typeNode }, hash);
1530
+ }
1531
+ };
1532
+
1533
+ // src/typecheck/walk.ts
1534
+ import * as ts26 from "typescript";
1535
+ function buildContext(rule, sourceFile, checker, source, filename, diagnostics) {
1536
+ return {
1537
+ filename,
1538
+ source,
1539
+ sourceFile,
1540
+ checker,
1541
+ report(node, message) {
1542
+ const { line, character } = ts26.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
1543
+ diagnostics.push({
1544
+ ruleId: rule.id,
1545
+ severity: rule.severity,
1546
+ message: message ?? rule.message,
1547
+ file: filename,
1548
+ line: line + 1,
1549
+ column: character + 1
1550
+ });
1551
+ },
1552
+ reportAtOffset(offset, message) {
1553
+ const { line, character } = ts26.getLineAndCharacterOfPosition(sourceFile, offset);
1554
+ diagnostics.push({
1555
+ ruleId: rule.id,
1556
+ severity: rule.severity,
1557
+ message: message ?? rule.message,
1558
+ file: filename,
1559
+ line: line + 1,
1560
+ column: character + 1
1561
+ });
1562
+ },
1563
+ isNullable(node) {
1564
+ const type = checker.getTypeAtLocation(node);
1565
+ return isNullableType(checker, type);
1566
+ },
1567
+ isExternal(node) {
1568
+ const type = checker.getTypeAtLocation(node);
1569
+ const symbol = type.getSymbol();
1570
+ if (!symbol) return false;
1571
+ const declarations = symbol.getDeclarations();
1572
+ if (!declarations || declarations.length === 0) return false;
1573
+ return declarations.some((d) => isFromNodeModules(d));
1574
+ }
1575
+ };
1576
+ }
1577
+
1200
1578
  // src/collect/index.ts
1201
- function collectProject(program) {
1579
+ function collectProject(program, tsRules) {
1202
1580
  const checker = program.getTypeChecker();
1203
1581
  const types = new TypeRegistry();
1204
1582
  const functions = new FunctionRegistry();
1583
+ const constants = new ConstantRegistry();
1205
1584
  const callSites = [];
1206
1585
  const imports = [];
1586
+ const statementSequences = new StatementSequenceRegistry();
1587
+ const inlineParamTypes = new InlineParamTypeRegistry();
1207
1588
  const fileMap = /* @__PURE__ */ new Map();
1589
+ const diagnostics = [];
1208
1590
  for (const sourceFile of program.getSourceFiles()) {
1209
1591
  let visit2 = function(node) {
1210
1592
  collectTypes(node, file, sourceFile, types);
1211
1593
  collectFunctions(node, file, sourceFile, checker, functions);
1594
+ collectConstants(node, file, sourceFile, constants);
1212
1595
  collectCallSites(node, file, sourceFile, checker, callSites);
1596
+ collectStatementSequences(node, file, sourceFile, statementSequences);
1597
+ collectInlineParamTypes(node, file, sourceFile, inlineParamTypes);
1213
1598
  collectImports(node, file, imports);
1214
- ts26.forEachChild(node, visit2);
1599
+ if (ruleContexts) {
1600
+ for (const { rule, ctx } of ruleContexts) {
1601
+ rule.visit(node, ctx);
1602
+ }
1603
+ }
1604
+ ts27.forEachChild(node, visit2);
1215
1605
  };
1216
1606
  var visit = visit2;
1217
1607
  const file = sourceFile.fileName;
@@ -1220,48 +1610,166 @@ function collectProject(program) {
1220
1610
  const source = sourceFile.getFullText();
1221
1611
  const comments = collectAllComments(sourceFile);
1222
1612
  fileMap.set(file, { source, sourceFile, comments });
1223
- ts26.forEachChild(sourceFile, visit2);
1613
+ const ruleContexts = tsRules?.map((rule) => ({
1614
+ rule,
1615
+ ctx: buildContext(rule, sourceFile, checker, source, file, diagnostics)
1616
+ }));
1617
+ ts27.forEachChild(sourceFile, visit2);
1618
+ }
1619
+ const fileHashes = /* @__PURE__ */ new Map();
1620
+ for (const [file, { source }] of fileMap) {
1621
+ const normalized = source.replace(/\s+/g, " ").trim();
1622
+ const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
1623
+ let list = fileHashes.get(hash);
1624
+ if (list === void 0) {
1625
+ list = [];
1626
+ fileHashes.set(hash, list);
1627
+ }
1628
+ list.push(file);
1224
1629
  }
1225
- return { types, functions, callSites, imports, files: fileMap };
1630
+ return { index: { types, functions, constants, callSites, imports, files: fileMap, fileHashes, statementSequences, inlineParamTypes }, diagnostics };
1631
+ }
1632
+ function hasNonPublicModifier(node) {
1633
+ if (!ts27.canHaveModifiers(node)) return false;
1634
+ const mods = ts27.getModifiers(node);
1635
+ if (mods === void 0) return false;
1636
+ return mods.some(
1637
+ (m) => m.kind === ts27.SyntaxKind.PrivateKeyword || m.kind === ts27.SyntaxKind.ProtectedKeyword
1638
+ );
1226
1639
  }
1227
1640
  function isExported(node) {
1228
- if (!ts26.canHaveModifiers(node)) return false;
1229
- const mods = ts26.getModifiers(node);
1641
+ if (!ts27.canHaveModifiers(node)) return false;
1642
+ const mods = ts27.getModifiers(node);
1230
1643
  if (mods === void 0) return false;
1231
- return mods.some((m) => m.kind === ts26.SyntaxKind.ExportKeyword);
1644
+ return mods.some((m) => m.kind === ts27.SyntaxKind.ExportKeyword);
1232
1645
  }
1233
1646
  function collectTypes(node, file, sourceFile, registry) {
1234
- if (ts26.isTypeAliasDeclaration(node)) {
1235
- const line = ts26.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1647
+ if (ts27.isTypeAliasDeclaration(node)) {
1648
+ const line = ts27.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1236
1649
  registry.add(node.name.text, file, line, node.type, sourceFile, isExported(node));
1237
1650
  }
1238
- if (ts26.isInterfaceDeclaration(node)) {
1239
- const line = ts26.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1651
+ if (ts27.isInterfaceDeclaration(node)) {
1652
+ const line = ts27.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1240
1653
  registry.add(node.name.text, file, line, node, sourceFile, isExported(node));
1241
1654
  }
1242
1655
  }
1656
+ function isConstantValue(node) {
1657
+ if (ts27.isStringLiteral(node)) return true;
1658
+ if (ts27.isNumericLiteral(node)) return true;
1659
+ if (ts27.isNoSubstitutionTemplateLiteral(node)) return true;
1660
+ if (ts27.isPrefixUnaryExpression(node)) return isConstantValue(node.operand);
1661
+ if (ts27.isBinaryExpression(node)) return isConstantValue(node.left) && isConstantValue(node.right);
1662
+ return false;
1663
+ }
1664
+ function collectConstants(node, file, sourceFile, registry) {
1665
+ if (!ts27.isVariableStatement(node)) return;
1666
+ if (!(node.declarationList.flags & ts27.NodeFlags.Const)) return;
1667
+ const exported = isExported(node);
1668
+ for (const decl of node.declarationList.declarations) {
1669
+ if (!decl.initializer || !ts27.isIdentifier(decl.name)) continue;
1670
+ if (!isConstantValue(decl.initializer)) continue;
1671
+ const valueText = decl.initializer.getText(sourceFile).replace(/\s+/g, " ").trim();
1672
+ const valueHash = createHash2("sha256").update(valueText).digest("hex").slice(0, 16);
1673
+ const line = ts27.getLineAndCharacterOfPosition(sourceFile, decl.getStart(sourceFile)).line + 1;
1674
+ registry.add({ name: decl.name.text, file, line, valueHash, valueText, exported });
1675
+ }
1676
+ }
1677
+ function buildFunctionEntry(body, parameters, sourceFile, file, lineNode, name, extra) {
1678
+ const line = ts27.getLineAndCharacterOfPosition(sourceFile, lineNode.getStart(sourceFile)).line + 1;
1679
+ const params = extractParams(parameters, sourceFile);
1680
+ const paramNames = params.map((p) => p.name);
1681
+ const hash = hashFunctionBody(body, sourceFile);
1682
+ const normalizedHash = hashFunctionBodyNormalized(body, sourceFile, paramNames);
1683
+ const bodyLength = bodyTextLength(body, sourceFile);
1684
+ const normalizedBodyLength = normalizedBodyTextLength(body, sourceFile, paramNames);
1685
+ return {
1686
+ name,
1687
+ file,
1688
+ line,
1689
+ hash,
1690
+ normalizedHash,
1691
+ params,
1692
+ node: extra.node ?? body,
1693
+ exported: extra.exported ?? false,
1694
+ bodyLength,
1695
+ normalizedBodyLength,
1696
+ ...extra
1697
+ };
1698
+ }
1243
1699
  function collectFunctions(node, file, sourceFile, checker, registry) {
1244
- if (ts26.isFunctionDeclaration(node) && node.name && node.body) {
1245
- const line = ts26.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1246
- const params = extractParams(node.parameters, sourceFile);
1247
- const hash = hashFunctionBody(node.body, sourceFile);
1248
- const symbol = checker.getSymbolAtLocation(node.name);
1249
- registry.add({ name: node.name.text, file, line, hash, params, node, exported: isExported(node), symbol });
1250
- }
1251
- if (ts26.isVariableStatement(node)) {
1700
+ if (ts27.isFunctionDeclaration(node) && node.name && node.body) {
1701
+ registry.add(buildFunctionEntry(node.body, node.parameters, sourceFile, file, node, node.name.text, {
1702
+ exported: isExported(node),
1703
+ symbol: checker.getSymbolAtLocation(node.name),
1704
+ node
1705
+ }));
1706
+ }
1707
+ if (ts27.isVariableStatement(node)) {
1252
1708
  const exported = isExported(node);
1253
1709
  for (const decl of node.declarationList.declarations) {
1254
- if (decl.initializer && ts26.isArrowFunction(decl.initializer) && ts26.isIdentifier(decl.name)) {
1710
+ if (decl.initializer && ts27.isArrowFunction(decl.initializer) && ts27.isIdentifier(decl.name)) {
1255
1711
  const arrow = decl.initializer;
1256
- const body = ts26.isBlock(arrow.body) ? arrow.body : arrow.body;
1257
- const line = ts26.getLineAndCharacterOfPosition(sourceFile, decl.getStart(sourceFile)).line + 1;
1258
- const params = extractParams(arrow.parameters, sourceFile);
1259
- const hash = hashFunctionBody(body, sourceFile);
1260
- const symbol = checker.getSymbolAtLocation(decl.name);
1261
- registry.add({ name: decl.name.text, file, line, hash, params, node: arrow, exported, symbol });
1712
+ registry.add(buildFunctionEntry(arrow.body, arrow.parameters, sourceFile, file, decl, decl.name.text, {
1713
+ exported,
1714
+ symbol: checker.getSymbolAtLocation(decl.name),
1715
+ node: arrow
1716
+ }));
1717
+ }
1718
+ if (decl.initializer && ts27.isFunctionExpression(decl.initializer) && ts27.isIdentifier(decl.name)) {
1719
+ const fn = decl.initializer;
1720
+ if (fn.body) {
1721
+ registry.add(buildFunctionEntry(fn.body, fn.parameters, sourceFile, file, decl, decl.name.text, {
1722
+ exported,
1723
+ symbol: checker.getSymbolAtLocation(decl.name),
1724
+ node: fn
1725
+ }));
1726
+ }
1262
1727
  }
1263
1728
  }
1264
1729
  }
1730
+ if (ts27.isPropertyAssignment(node) && (ts27.isIdentifier(node.name) || ts27.isStringLiteral(node.name))) {
1731
+ const init = node.initializer;
1732
+ const propName = node.name.text;
1733
+ if (ts27.isArrowFunction(init)) {
1734
+ registry.add(buildFunctionEntry(init.body, init.parameters, sourceFile, file, node, propName, { node: init }));
1735
+ }
1736
+ if (ts27.isFunctionExpression(init) && init.body) {
1737
+ registry.add(buildFunctionEntry(init.body, init.parameters, sourceFile, file, node, propName, { node: init }));
1738
+ }
1739
+ }
1740
+ if (ts27.isArrowFunction(node) || ts27.isFunctionExpression(node)) {
1741
+ const parent = node.parent;
1742
+ if (ts27.isVariableDeclaration(parent) && ts27.isIdentifier(parent.name)) {
1743
+ } else if (ts27.isPropertyAssignment(parent)) {
1744
+ } else {
1745
+ const body = ts27.isArrowFunction(node) ? node.body : node.body;
1746
+ if (body) {
1747
+ const MIN_ANON_BODY = 64;
1748
+ if (bodyTextLength(body, sourceFile) >= MIN_ANON_BODY) {
1749
+ const name = deriveAnonymousName(node, sourceFile);
1750
+ registry.add(buildFunctionEntry(body, node.parameters, sourceFile, file, node, name, { node }));
1751
+ }
1752
+ }
1753
+ }
1754
+ }
1755
+ if (ts27.isMethodDeclaration(node) && node.body && ts27.isIdentifier(node.name)) {
1756
+ const parent = node.parent;
1757
+ if (ts27.isClassDeclaration(parent)) {
1758
+ if (hasNonPublicModifier(node)) return;
1759
+ const className = parent.name ? parent.name.text : "<anonymous>";
1760
+ const name = `${className}.${node.name.text}`;
1761
+ const implementsInterface = parent.heritageClauses?.some(
1762
+ (c) => c.token === ts27.SyntaxKind.ImplementsKeyword
1763
+ ) ?? false;
1764
+ registry.add(buildFunctionEntry(node.body, node.parameters, sourceFile, file, node, name, {
1765
+ exported: isExported(parent),
1766
+ symbol: checker.getSymbolAtLocation(node.name),
1767
+ node,
1768
+ className,
1769
+ implementsInterface
1770
+ }));
1771
+ }
1772
+ }
1265
1773
  }
1266
1774
  function extractParams(parameters, sourceFile) {
1267
1775
  return parameters.map((p) => {
@@ -1272,8 +1780,29 @@ function extractParams(parameters, sourceFile) {
1272
1780
  return { name, optional, hasDefault, typeText };
1273
1781
  });
1274
1782
  }
1783
+ function deriveAnonymousName(node, sourceFile) {
1784
+ const parent = node.parent;
1785
+ const line = ts27.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1786
+ if (ts27.isCallExpression(parent)) {
1787
+ const grandparent = parent.parent;
1788
+ if (ts27.isPropertyAssignment(grandparent) && ts27.isIdentifier(grandparent.name)) {
1789
+ return grandparent.name.text;
1790
+ }
1791
+ let calleeName = null;
1792
+ if (ts27.isIdentifier(parent.expression)) {
1793
+ calleeName = parent.expression.text;
1794
+ } else if (ts27.isPropertyAccessExpression(parent.expression)) {
1795
+ calleeName = parent.expression.name.text;
1796
+ }
1797
+ if (calleeName) {
1798
+ const argIndex = parent.arguments.indexOf(node);
1799
+ if (argIndex >= 0) return `${calleeName}.$arg${argIndex}`;
1800
+ }
1801
+ }
1802
+ return `<anonymous>:${line}`;
1803
+ }
1275
1804
  function collectImports(node, file, imports) {
1276
- if (ts26.isImportDeclaration(node) && ts26.isStringLiteral(node.moduleSpecifier)) {
1805
+ if (ts27.isImportDeclaration(node) && ts27.isStringLiteral(node.moduleSpecifier)) {
1277
1806
  const moduleSource = node.moduleSpecifier.text;
1278
1807
  const clause = node.importClause;
1279
1808
  if (!clause) return;
@@ -1285,7 +1814,7 @@ function collectImports(node, file, imports) {
1285
1814
  source: moduleSource
1286
1815
  });
1287
1816
  }
1288
- if (clause.namedBindings && ts26.isNamedImports(clause.namedBindings)) {
1817
+ if (clause.namedBindings && ts27.isNamedImports(clause.namedBindings)) {
1289
1818
  for (const el of clause.namedBindings.elements) {
1290
1819
  imports.push({
1291
1820
  file,
@@ -1296,9 +1825,9 @@ function collectImports(node, file, imports) {
1296
1825
  }
1297
1826
  }
1298
1827
  }
1299
- if (ts26.isExportDeclaration(node) && node.moduleSpecifier && ts26.isStringLiteral(node.moduleSpecifier)) {
1828
+ if (ts27.isExportDeclaration(node) && node.moduleSpecifier && ts27.isStringLiteral(node.moduleSpecifier)) {
1300
1829
  const moduleSource = node.moduleSpecifier.text;
1301
- if (node.exportClause && ts26.isNamedExports(node.exportClause)) {
1830
+ if (node.exportClause && ts27.isNamedExports(node.exportClause)) {
1302
1831
  for (const el of node.exportClause.elements) {
1303
1832
  imports.push({
1304
1833
  file,
@@ -1310,20 +1839,52 @@ function collectImports(node, file, imports) {
1310
1839
  }
1311
1840
  }
1312
1841
  }
1842
+ function collectStatementSequences(node, file, sourceFile, registry) {
1843
+ if (!ts27.isBlock(node)) return;
1844
+ const stmts = node.statements;
1845
+ const n = stmts.length;
1846
+ if (n < 3) return;
1847
+ const MAX_WINDOW = Math.min(n, 5);
1848
+ for (let size = 3; size <= MAX_WINDOW; size++) {
1849
+ for (let start = 0; start + size <= n; start++) {
1850
+ const window = stmts.slice(start, start + size);
1851
+ const texts = window.map((s) => s.getText(sourceFile));
1852
+ const joined = texts.join("\n");
1853
+ const normalized = normalizeText(joined);
1854
+ if (normalized.length < 128) continue;
1855
+ const hash = hashText(joined.replace(/\s+/g, " ").trim());
1856
+ const normalizedHash = hashText(normalized);
1857
+ const firstStmt = window[0];
1858
+ const lastStmt = window[window.length - 1];
1859
+ if (firstStmt === void 0 || lastStmt === void 0) continue;
1860
+ const line = ts27.getLineAndCharacterOfPosition(sourceFile, firstStmt.getStart(sourceFile)).line + 1;
1861
+ const endLine = ts27.getLineAndCharacterOfPosition(sourceFile, lastStmt.getEnd()).line + 1;
1862
+ registry.add({ file, line, endLine, hash, normalizedHash, statementCount: size, normalizedBodyLength: normalized.length });
1863
+ }
1864
+ }
1865
+ }
1866
+ function collectInlineParamTypes(node, file, sourceFile, registry) {
1867
+ if (!ts27.isTypeLiteralNode(node)) return;
1868
+ const parent = node.parent;
1869
+ if (!parent || !ts27.isParameter(parent)) return;
1870
+ if (parent.type !== node) return;
1871
+ const line = ts27.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1872
+ registry.add(file, line, node, sourceFile);
1873
+ }
1313
1874
  function collectCallSites(node, file, sourceFile, checker, sites) {
1314
- if (!ts26.isCallExpression(node)) return;
1875
+ if (!ts27.isCallExpression(node)) return;
1315
1876
  let calleeName = null;
1316
- if (ts26.isIdentifier(node.expression)) {
1877
+ if (ts27.isIdentifier(node.expression)) {
1317
1878
  calleeName = node.expression.text;
1318
- } else if (ts26.isPropertyAccessExpression(node.expression)) {
1879
+ } else if (ts27.isPropertyAccessExpression(node.expression)) {
1319
1880
  calleeName = node.expression.name.text;
1320
1881
  }
1321
1882
  if (calleeName) {
1322
- const line = ts26.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1883
+ const line = ts27.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
1323
1884
  let symbol;
1324
1885
  try {
1325
1886
  symbol = checker.getSymbolAtLocation(node.expression);
1326
- if (symbol && symbol.flags & ts26.SymbolFlags.Alias) {
1887
+ if (symbol && symbol.flags & ts27.SymbolFlags.Alias) {
1327
1888
  symbol = checker.getAliasedSymbol(symbol);
1328
1889
  }
1329
1890
  } catch {
@@ -1343,12 +1904,12 @@ function collectAllComments(sourceFile) {
1343
1904
  const source = sourceFile.getFullText();
1344
1905
  const seen = /* @__PURE__ */ new Set();
1345
1906
  function visit(node) {
1346
- const leading = ts26.getLeadingCommentRanges(source, node.getFullStart());
1907
+ const leading = ts27.getLeadingCommentRanges(source, node.getFullStart());
1347
1908
  if (leading) {
1348
1909
  for (const r of leading) {
1349
1910
  if (seen.has(r.pos)) continue;
1350
1911
  seen.add(r.pos);
1351
- const isLine = r.kind === ts26.SyntaxKind.SingleLineCommentTrivia;
1912
+ const isLine = r.kind === ts27.SyntaxKind.SingleLineCommentTrivia;
1352
1913
  comments.push({
1353
1914
  type: isLine ? "Line" : "Block",
1354
1915
  value: source.slice(r.pos + 2, isLine ? r.end : r.end - 2),
@@ -1357,12 +1918,12 @@ function collectAllComments(sourceFile) {
1357
1918
  });
1358
1919
  }
1359
1920
  }
1360
- const trailing = ts26.getTrailingCommentRanges(source, node.getEnd());
1921
+ const trailing = ts27.getTrailingCommentRanges(source, node.getEnd());
1361
1922
  if (trailing) {
1362
1923
  for (const r of trailing) {
1363
1924
  if (seen.has(r.pos)) continue;
1364
1925
  seen.add(r.pos);
1365
- const isLine = r.kind === ts26.SyntaxKind.SingleLineCommentTrivia;
1926
+ const isLine = r.kind === ts27.SyntaxKind.SingleLineCommentTrivia;
1366
1927
  comments.push({
1367
1928
  type: isLine ? "Line" : "Block",
1368
1929
  value: source.slice(r.pos + 2, isLine ? r.end : r.end - 2),
@@ -1371,39 +1932,34 @@ function collectAllComments(sourceFile) {
1371
1932
  });
1372
1933
  }
1373
1934
  }
1374
- ts26.forEachChild(node, visit);
1935
+ ts27.forEachChild(node, visit);
1375
1936
  }
1376
1937
  visit(sourceFile);
1377
1938
  return comments;
1378
1939
  }
1379
1940
 
1380
- // src/rules/types.ts
1381
- function isTSRule(r) {
1382
- return "kind" in r && r.kind === "ts";
1383
- }
1384
-
1385
1941
  // src/typecheck/program.ts
1386
- import * as ts27 from "typescript";
1942
+ import * as ts28 from "typescript";
1387
1943
  import { dirname as dirname2 } from "path";
1388
1944
  function createProgramFromFiles(files) {
1389
1945
  let configPath;
1390
1946
  if (files.length > 0) {
1391
- configPath = ts27.findConfigFile(dirname2(files[0]), ts27.sys.fileExists, "tsconfig.json");
1947
+ configPath = ts28.findConfigFile(dirname2(files[0]), ts28.sys.fileExists, "tsconfig.json");
1392
1948
  }
1393
1949
  if (configPath) {
1394
- const configFile = ts27.readConfigFile(configPath, ts27.sys.readFile);
1395
- const parsed = ts27.parseJsonConfigFileContent(configFile.config, ts27.sys, dirname2(configPath));
1396
- return ts27.createProgram({
1950
+ const configFile = ts28.readConfigFile(configPath, ts28.sys.readFile);
1951
+ const parsed = ts28.parseJsonConfigFileContent(configFile.config, ts28.sys, dirname2(configPath));
1952
+ return ts28.createProgram({
1397
1953
  rootNames: files,
1398
1954
  options: { ...parsed.options, skipLibCheck: true }
1399
1955
  });
1400
1956
  }
1401
- return ts27.createProgram({
1957
+ return ts28.createProgram({
1402
1958
  rootNames: files,
1403
1959
  options: {
1404
- target: ts27.ScriptTarget.ESNext,
1405
- module: ts27.ModuleKind.ESNext,
1406
- moduleResolution: ts27.ModuleResolutionKind.Bundler,
1960
+ target: ts28.ScriptTarget.ESNext,
1961
+ module: ts28.ModuleKind.ESNext,
1962
+ moduleResolution: ts28.ModuleResolutionKind.Bundler,
1407
1963
  strict: true,
1408
1964
  skipLibCheck: true,
1409
1965
  noEmit: true
@@ -1411,93 +1967,20 @@ function createProgramFromFiles(files) {
1411
1967
  });
1412
1968
  }
1413
1969
 
1414
- // src/typecheck/walk.ts
1415
- import * as ts28 from "typescript";
1416
- function runTSRules(rules, sourceFile, checker, source, filename) {
1417
- const diagnostics = [];
1418
- const contexts = rules.map((rule) => ({
1419
- rule,
1420
- ctx: buildContext(rule, sourceFile, checker, source, filename, diagnostics)
1421
- }));
1422
- function visit(node) {
1423
- for (const { rule, ctx } of contexts) {
1424
- rule.visit(node, ctx);
1425
- }
1426
- ts28.forEachChild(node, visit);
1427
- }
1428
- visit(sourceFile);
1429
- return diagnostics;
1430
- }
1431
- function buildContext(rule, sourceFile, checker, source, filename, diagnostics) {
1432
- return {
1433
- filename,
1434
- source,
1435
- sourceFile,
1436
- checker,
1437
- report(node, message) {
1438
- const { line, character } = ts28.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
1439
- diagnostics.push({
1440
- ruleId: rule.id,
1441
- severity: rule.severity,
1442
- message: message ?? rule.message,
1443
- file: filename,
1444
- line: line + 1,
1445
- column: character + 1
1446
- });
1447
- },
1448
- reportAtOffset(offset, message) {
1449
- const { line, character } = ts28.getLineAndCharacterOfPosition(sourceFile, offset);
1450
- diagnostics.push({
1451
- ruleId: rule.id,
1452
- severity: rule.severity,
1453
- message: message ?? rule.message,
1454
- file: filename,
1455
- line: line + 1,
1456
- column: character + 1
1457
- });
1458
- },
1459
- isNullable(node) {
1460
- const type = checker.getTypeAtLocation(node);
1461
- return isNullableType(checker, type);
1462
- },
1463
- isExternal(node) {
1464
- const type = checker.getTypeAtLocation(node);
1465
- const symbol = type.getSymbol();
1466
- if (!symbol) return false;
1467
- const declarations = symbol.getDeclarations();
1468
- if (!declarations || declarations.length === 0) return false;
1469
- return declarations.some((d) => isFromNodeModules(d));
1470
- }
1471
- };
1472
- }
1473
-
1474
1970
  // src/scan/analyze.ts
1475
1971
  function analyzeFiles(files, rules) {
1476
1972
  const tsRules = rules.filter(isTSRule);
1477
1973
  const crossFileRules = rules.filter((r) => !isTSRule(r));
1478
- const diagnostics = [];
1479
1974
  const program = files.length > 0 ? createProgramFromFiles(files) : null;
1480
- if (!program) return diagnostics;
1481
- if (tsRules.length > 0) {
1482
- const checker = program.getTypeChecker();
1483
- for (const file of files) {
1484
- const source = readFileSync2(file, "utf8");
1485
- const sourceFile = program.getSourceFile(file);
1486
- if (sourceFile) {
1487
- diagnostics.push(...runTSRules(tsRules, sourceFile, checker, source, file));
1488
- }
1489
- }
1490
- }
1491
- if (crossFileRules.length > 0) {
1492
- const projectIndex = collectProject(program);
1493
- for (const rule of crossFileRules) {
1494
- const crossDiagnostics = rule.analyze(projectIndex);
1495
- for (const diagnostic of crossDiagnostics) {
1496
- const fileData = projectIndex.files.get(diagnostic.file);
1497
- if (fileData !== void 0) annotate([diagnostic], fileData.comments, fileData.source);
1498
- }
1499
- diagnostics.push(...crossDiagnostics);
1975
+ if (!program) return [];
1976
+ const { index, diagnostics } = collectProject(program, tsRules);
1977
+ for (const rule of crossFileRules) {
1978
+ const crossDiagnostics = rule.analyze(index);
1979
+ for (const diagnostic of crossDiagnostics) {
1980
+ const fileData = index.files.get(diagnostic.file);
1981
+ if (fileData !== void 0) annotate([diagnostic], fileData.comments, fileData.source);
1500
1982
  }
1983
+ diagnostics.push(...crossDiagnostics);
1501
1984
  }
1502
1985
  return dedupeDiagnostics(diagnostics);
1503
1986
  }
@@ -1534,21 +2017,21 @@ function annotate(diagnostics, comments, source) {
1534
2017
  if (above !== null) diagnostic.annotation = above;
1535
2018
  }
1536
2019
  }
1537
- function findInlineComment(diagLine, byEndLine) {
1538
- const commentsOnLine = byEndLine.get(diagLine);
2020
+ function lastCommentOnLine(line, byEndLine) {
2021
+ const commentsOnLine = byEndLine.get(line);
1539
2022
  if (commentsOnLine === void 0 || commentsOnLine.length === 0) return null;
1540
- const comment = commentsOnLine.at(-1);
1541
- if (comment === void 0) return null;
1542
- if (comment.type !== "Line") return null;
2023
+ return commentsOnLine.at(-1) ?? null;
2024
+ }
2025
+ function findInlineComment(diagLine, byEndLine) {
2026
+ const comment = lastCommentOnLine(diagLine, byEndLine);
2027
+ if (comment === null || comment.type !== "Line") return null;
1543
2028
  const text = comment.value.trim();
1544
2029
  if (text.startsWith("@expect")) return null;
1545
2030
  return text;
1546
2031
  }
1547
2032
  function collectAnnotation(commentEndLine, byEndLine, source) {
1548
- const commentsOnLine = byEndLine.get(commentEndLine);
1549
- if (commentsOnLine === void 0 || commentsOnLine.length === 0) return null;
1550
- const comment = commentsOnLine.at(-1);
1551
- if (comment === void 0) return null;
2033
+ const comment = lastCommentOnLine(commentEndLine, byEndLine);
2034
+ if (comment === null) return null;
1552
2035
  if (comment.type === "Block") return cleanBlockComment(comment.value);
1553
2036
  const lines = [comment.value.trim()];
1554
2037
  let prevLine = commentEndLine - 1;
@@ -1577,7 +2060,7 @@ function lineAt(source, offset) {
1577
2060
  // src/scan/discover.ts
1578
2061
  import fg from "fast-glob";
1579
2062
  import ignore from "ignore";
1580
- import { existsSync, readFileSync as readFileSync3 } from "fs";
2063
+ import { existsSync, readFileSync } from "fs";
1581
2064
  import { relative, resolve as resolve2, sep } from "path";
1582
2065
  async function discoverFiles(config) {
1583
2066
  const globs = expandGlobs(config.paths);
@@ -1601,7 +2084,7 @@ function expandGlobs(paths) {
1601
2084
  function applyGitIgnore(files) {
1602
2085
  const gitIgnorePath = resolve2(process.cwd(), ".gitignore");
1603
2086
  if (!existsSync(gitIgnorePath)) return files;
1604
- const matcher = ignore().add(readFileSync3(gitIgnorePath, "utf8"));
2087
+ const matcher = ignore().add(readFileSync(gitIgnorePath, "utf8"));
1605
2088
  return files.filter((file) => {
1606
2089
  const rel = relative(process.cwd(), file);
1607
2090
  if (rel.startsWith("..")) return true;
@@ -1715,4 +2198,4 @@ export {
1715
2198
  executeScan,
1716
2199
  scan
1717
2200
  };
1718
- //# sourceMappingURL=chunk-I4RZLVE2.js.map
2201
+ //# sourceMappingURL=chunk-5GVEUYZF.js.map