unguard 0.8.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.
- package/README.md +80 -20
- package/dist/{chunk-YYX4R25B.js → chunk-5GVEUYZF.js} +1090 -408
- package/dist/chunk-5GVEUYZF.js.map +1 -0
- package/dist/cli.js +198 -50
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +95 -13
- package/dist/index.js +5 -1
- package/package.json +7 -3
- package/dist/chunk-YYX4R25B.js.map +0 -1
|
@@ -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/
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
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 (!
|
|
511
|
-
if (!
|
|
512
|
-
if (node.expression.type.kind !==
|
|
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
|
|
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 (!
|
|
526
|
-
if (node.operatorToken.kind !==
|
|
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 (
|
|
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 (
|
|
544
|
+
if (ts15.isBinaryExpression(left) && isNullCheck(left)) {
|
|
535
545
|
const checked = getNullCheckedIdentifier(left);
|
|
536
546
|
if (checked && accessesIdentifier(right, checked)) {
|
|
537
|
-
const identNode =
|
|
538
|
-
if (
|
|
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
|
|
557
|
+
return ts15.isIdentifier(root) && root.text === name;
|
|
548
558
|
}
|
|
549
559
|
function getExpressionRoot(node) {
|
|
550
|
-
if (
|
|
551
|
-
if (
|
|
552
|
-
if (
|
|
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 !==
|
|
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 (
|
|
562
|
-
if (
|
|
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
|
|
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
|
-
|
|
575
|
-
if (
|
|
576
|
-
const stmts =
|
|
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 || !
|
|
588
|
+
if (firstStmt === void 0 || !ts16.isExpressionStatement(firstStmt)) return;
|
|
580
589
|
const expr = firstStmt.expression;
|
|
581
|
-
if (!
|
|
590
|
+
if (!ts16.isBinaryExpression(expr) || expr.operatorToken.kind !== ts16.SyntaxKind.EqualsToken) return;
|
|
582
591
|
const right = expr.right;
|
|
583
|
-
if (!
|
|
584
|
-
if (!
|
|
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 =
|
|
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
|
|
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
|
-
|
|
601
|
-
if (
|
|
602
|
-
const stmts =
|
|
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 || !
|
|
613
|
+
if (firstStmt === void 0 || !ts17.isIfStatement(firstStmt)) return;
|
|
606
614
|
const test = firstStmt.expression;
|
|
607
615
|
let guardedName = null;
|
|
608
|
-
if (
|
|
609
|
-
if (
|
|
616
|
+
if (ts17.isPrefixUnaryExpression(test) && test.operator === ts17.SyntaxKind.ExclamationToken) {
|
|
617
|
+
if (ts17.isIdentifier(test.operand)) guardedName = test.operand.text;
|
|
610
618
|
}
|
|
611
|
-
if (
|
|
619
|
+
if (ts17.isBinaryExpression(test)) {
|
|
612
620
|
const op = test.operatorToken.kind;
|
|
613
|
-
if (op ===
|
|
614
|
-
if (
|
|
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 =
|
|
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 =
|
|
624
|
-
(p) =>
|
|
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
|
}
|
|
@@ -630,70 +638,67 @@ var preferRequiredParamWithGuard = {
|
|
|
630
638
|
// src/rules/cross-file/duplicate-type-declaration.ts
|
|
631
639
|
var duplicateTypeDeclaration = {
|
|
632
640
|
id: "duplicate-type-declaration",
|
|
633
|
-
severity: "
|
|
641
|
+
severity: "warning",
|
|
634
642
|
message: "Identical type shape declared in multiple files; consolidate to a single definition",
|
|
635
643
|
analyze(project) {
|
|
636
644
|
const diagnostics = [];
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
|
662
|
+
import * as ts18 from "typescript";
|
|
659
663
|
var duplicateFunctionDeclaration = {
|
|
660
664
|
id: "duplicate-function-declaration",
|
|
661
|
-
severity: "
|
|
665
|
+
severity: "warning",
|
|
662
666
|
message: "Identical function body declared in multiple files; consolidate to a single definition",
|
|
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
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
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 || !
|
|
696
|
-
return
|
|
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
|
|
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 (!
|
|
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 (
|
|
749
|
-
return
|
|
753
|
+
if (ts19.isFunctionDeclaration(node) || ts19.isFunctionExpression(node) || ts19.isArrowFunction(node)) return false;
|
|
754
|
+
return ts19.forEachChild(node, visit) ?? false;
|
|
750
755
|
}
|
|
751
|
-
return
|
|
756
|
+
return ts19.forEachChild(block, visit) ?? false;
|
|
752
757
|
}
|
|
753
758
|
function hasReturn(block) {
|
|
754
|
-
return walkBlock(block, (n) =>
|
|
759
|
+
return walkBlock(block, (n) => ts19.isReturnStatement(n));
|
|
755
760
|
}
|
|
756
761
|
function hasThrow(block) {
|
|
757
|
-
return walkBlock(block, (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 (!
|
|
767
|
+
if (!ts19.isCallExpression(n)) return false;
|
|
763
768
|
const callee = n.expression;
|
|
764
|
-
if (!
|
|
765
|
-
if (!
|
|
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
|
|
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 (!
|
|
783
|
+
if (!ts20.isCatchClause(node)) return;
|
|
779
784
|
if (!node.variableDeclaration) return;
|
|
780
785
|
const param = node.variableDeclaration.name;
|
|
781
|
-
if (!
|
|
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 (
|
|
789
|
-
if (
|
|
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
|
-
|
|
801
|
+
ts20.forEachChild(node, visit);
|
|
797
802
|
}
|
|
798
|
-
|
|
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 (
|
|
808
|
-
return
|
|
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 (
|
|
817
|
+
if (ts20.isObjectLiteralExpression(arg)) {
|
|
813
818
|
for (const prop of arg.properties) {
|
|
814
|
-
if (
|
|
815
|
-
if (
|
|
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
|
|
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 ===
|
|
848
|
+
const val = arg.kind === ts21.SyntaxKind.NullKeyword ? "null" : "undefined";
|
|
844
849
|
diagnostics.push({
|
|
845
850
|
ruleId: this.id,
|
|
846
851
|
severity: this.severity,
|
|
@@ -862,7 +867,7 @@ import { dirname, resolve } from "path";
|
|
|
862
867
|
var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
863
868
|
var duplicateFunctionName = {
|
|
864
869
|
id: "duplicate-function-name",
|
|
865
|
-
severity: "
|
|
870
|
+
severity: "warning",
|
|
866
871
|
message: "Same function name exported from multiple files; consolidate or rename to avoid ambiguity",
|
|
867
872
|
analyze(project) {
|
|
868
873
|
const diagnostics = [];
|
|
@@ -890,18 +895,14 @@ var duplicateFunctionName = {
|
|
|
890
895
|
);
|
|
891
896
|
});
|
|
892
897
|
if (hasImportLink) continue;
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
}
|
|
@@ -911,16 +912,16 @@ function resolveCandidates(fromFile, specifier) {
|
|
|
911
912
|
const candidates = [base];
|
|
912
913
|
for (const ext of EXTENSIONS) {
|
|
913
914
|
candidates.push(base + ext);
|
|
914
|
-
candidates.push(resolve(base,
|
|
915
|
+
candidates.push(resolve(base, `index${ext}`));
|
|
915
916
|
}
|
|
916
917
|
return candidates;
|
|
917
918
|
}
|
|
918
919
|
|
|
919
920
|
// src/rules/cross-file/duplicate-type-name.ts
|
|
920
|
-
import * as
|
|
921
|
+
import * as ts22 from "typescript";
|
|
921
922
|
var duplicateTypeName = {
|
|
922
923
|
id: "duplicate-type-name",
|
|
923
|
-
severity: "
|
|
924
|
+
severity: "warning",
|
|
924
925
|
message: "Same type name exported from multiple files; consolidate or rename to avoid ambiguity",
|
|
925
926
|
analyze(project) {
|
|
926
927
|
const diagnostics = [];
|
|
@@ -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) => !
|
|
932
|
+
(e) => !ts22.isTypeLiteralNode(e.node) && !ts22.isInterfaceDeclaration(e.node)
|
|
932
933
|
);
|
|
933
934
|
if (hasInferredType) continue;
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
|
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 (!
|
|
960
|
-
if (node.expression.kind !==
|
|
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
|
-
|
|
1230
|
+
duplicateInlineTypeInParams,
|
|
980
1231
|
noTypeAssertion,
|
|
981
1232
|
noRedundantExistenceGuard,
|
|
982
1233
|
preferDefaultParamValue,
|
|
@@ -989,21 +1240,108 @@ var allRules = [
|
|
|
989
1240
|
explicitNullArg,
|
|
990
1241
|
duplicateFunctionName,
|
|
991
1242
|
duplicateTypeName,
|
|
992
|
-
|
|
1243
|
+
duplicateConstantDeclaration,
|
|
1244
|
+
noDynamicImport,
|
|
1245
|
+
nearDuplicateFunction,
|
|
1246
|
+
trivialWrapper,
|
|
1247
|
+
unusedExport,
|
|
1248
|
+
duplicateFile,
|
|
1249
|
+
duplicateStatementSequence
|
|
993
1250
|
];
|
|
1251
|
+
var ruleMetadata = {
|
|
1252
|
+
"no-any-cast": { category: "type-evasion", tags: ["safety"] },
|
|
1253
|
+
"no-explicit-any-annotation": { category: "type-evasion", tags: ["safety"] },
|
|
1254
|
+
"no-type-assertion": { category: "type-evasion", tags: ["safety"] },
|
|
1255
|
+
"no-ts-ignore": { category: "type-evasion", tags: ["safety"] },
|
|
1256
|
+
"no-optional-property-access": { category: "defensive-code", tags: ["type-aware"] },
|
|
1257
|
+
"no-optional-element-access": { category: "defensive-code", tags: ["type-aware"] },
|
|
1258
|
+
"no-optional-call": { category: "defensive-code", tags: ["type-aware"] },
|
|
1259
|
+
"no-nullish-coalescing": { category: "defensive-code", tags: ["type-aware"] },
|
|
1260
|
+
"no-logical-or-fallback": { category: "defensive-code", tags: ["type-aware"] },
|
|
1261
|
+
"no-null-ternary-normalization": { category: "defensive-code", tags: ["type-aware"] },
|
|
1262
|
+
"no-non-null-assertion": { category: "defensive-code", tags: ["type-aware"] },
|
|
1263
|
+
"no-double-negation-coercion": { category: "defensive-code", tags: ["readability"] },
|
|
1264
|
+
"no-redundant-existence-guard": { category: "defensive-code", tags: ["type-aware"] },
|
|
1265
|
+
"no-empty-catch": { category: "error-handling", tags: ["safety"] },
|
|
1266
|
+
"no-catch-return": { category: "error-handling", tags: ["safety"] },
|
|
1267
|
+
"no-error-rewrap": { category: "error-handling", tags: ["safety"] },
|
|
1268
|
+
"duplicate-inline-type-in-params": { category: "cross-file", tags: ["duplicate", "api"] },
|
|
1269
|
+
"prefer-default-param-value": { category: "interface-design", tags: ["api"] },
|
|
1270
|
+
"prefer-required-param-with-guard": { category: "interface-design", tags: ["api"] },
|
|
1271
|
+
"duplicate-type-declaration": { category: "cross-file", tags: ["duplicate"] },
|
|
1272
|
+
"duplicate-type-name": { category: "cross-file", tags: ["duplicate"] },
|
|
1273
|
+
"duplicate-function-declaration": { category: "cross-file", tags: ["duplicate"] },
|
|
1274
|
+
"duplicate-function-name": { category: "cross-file", tags: ["duplicate"] },
|
|
1275
|
+
"duplicate-constant-declaration": { category: "cross-file", tags: ["duplicate"] },
|
|
1276
|
+
"optional-arg-always-used": { category: "cross-file", tags: ["api"] },
|
|
1277
|
+
"explicit-null-arg": { category: "cross-file", tags: ["api"] },
|
|
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"] }
|
|
1284
|
+
};
|
|
1285
|
+
function getRuleMetadata(ruleId) {
|
|
1286
|
+
const metadata = ruleMetadata[ruleId];
|
|
1287
|
+
if (metadata !== void 0) return metadata;
|
|
1288
|
+
return { category: "cross-file", tags: [] };
|
|
1289
|
+
}
|
|
994
1290
|
|
|
995
|
-
// src/
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1291
|
+
// src/scan/config.ts
|
|
1292
|
+
var BUILTIN_IGNORE = [
|
|
1293
|
+
"**/node_modules/**",
|
|
1294
|
+
"**/dist/**",
|
|
1295
|
+
"**/.git/**",
|
|
1296
|
+
"**/*.d.ts",
|
|
1297
|
+
"**/*.d.cts",
|
|
1298
|
+
"**/*.d.mts"
|
|
1299
|
+
];
|
|
1300
|
+
var GENERATED_IGNORE = [
|
|
1301
|
+
"**/*.gen.*",
|
|
1302
|
+
"**/*.generated.*"
|
|
1303
|
+
];
|
|
1304
|
+
var DEFAULT_FAIL_ON = "info";
|
|
1305
|
+
function resolveScanConfig(options) {
|
|
1306
|
+
const paths = options.paths.length > 0 ? options.paths : ["."];
|
|
1307
|
+
const ignore2 = [...BUILTIN_IGNORE, ...GENERATED_IGNORE, ...options.ignore ?? []];
|
|
1308
|
+
return {
|
|
1309
|
+
paths,
|
|
1310
|
+
strict: options.strict ?? false,
|
|
1311
|
+
rules: options.rules ? [...options.rules] : null,
|
|
1312
|
+
ignore: ignore2,
|
|
1313
|
+
rulePolicy: toRulePolicyEntries(options.rulePolicy),
|
|
1314
|
+
showSeverities: normalizeSeveritySet(options.showSeverities),
|
|
1315
|
+
failOn: options.failOn ?? DEFAULT_FAIL_ON,
|
|
1316
|
+
useGitIgnore: options.useGitIgnore ?? true
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function toRulePolicyEntries(policy) {
|
|
1320
|
+
if (policy === void 0) return [];
|
|
1321
|
+
if (Array.isArray(policy)) return [...policy];
|
|
1322
|
+
const entries = [];
|
|
1323
|
+
for (const [selector, severity] of Object.entries(policy)) {
|
|
1324
|
+
entries.push({ selector, severity });
|
|
1325
|
+
}
|
|
1326
|
+
return entries;
|
|
1327
|
+
}
|
|
1328
|
+
function normalizeSeveritySet(levels) {
|
|
1329
|
+
if (levels === void 0 || levels.length === 0) return null;
|
|
1330
|
+
return new Set(levels);
|
|
1331
|
+
}
|
|
1332
|
+
function isRulePolicySeverity(value) {
|
|
1333
|
+
return value === "off" || value === "info" || value === "warning" || value === "error";
|
|
1334
|
+
}
|
|
1335
|
+
function isSeverity(value) {
|
|
1336
|
+
return value === "info" || value === "warning" || value === "error";
|
|
1337
|
+
}
|
|
1338
|
+
function isFailOn(value) {
|
|
1339
|
+
return value === "none" || isSeverity(value);
|
|
1002
1340
|
}
|
|
1003
1341
|
|
|
1004
1342
|
// src/collect/index.ts
|
|
1005
|
-
import * as
|
|
1006
|
-
import "
|
|
1343
|
+
import * as ts27 from "typescript";
|
|
1344
|
+
import { createHash as createHash2 } from "crypto";
|
|
1007
1345
|
|
|
1008
1346
|
// src/utils/hash.ts
|
|
1009
1347
|
import { createHash } from "crypto";
|
|
@@ -1033,18 +1371,60 @@ function normalizeTypeNode(node, sourceFile) {
|
|
|
1033
1371
|
return node.getText(sourceFile).replace(/\s+/g, " ").trim();
|
|
1034
1372
|
}
|
|
1035
1373
|
function hashFunctionBody(node, sourceFile) {
|
|
1036
|
-
const
|
|
1037
|
-
const normalized = bodyText.replace(/\s+/g, " ").trim();
|
|
1374
|
+
const normalized = stripComments(node.getText(sourceFile));
|
|
1038
1375
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
1039
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
|
+
}
|
|
1040
1422
|
|
|
1041
|
-
// src/collect/
|
|
1042
|
-
var
|
|
1423
|
+
// src/collect/base-registry.ts
|
|
1424
|
+
var BaseRegistry = class {
|
|
1043
1425
|
entries = [];
|
|
1044
1426
|
byHash = /* @__PURE__ */ new Map();
|
|
1045
|
-
|
|
1046
|
-
const hash = hashTypeShape(typeNode, sourceFile);
|
|
1047
|
-
const entry = { name, file, line, hash, node: typeNode, exported };
|
|
1427
|
+
addEntry(entry, hash) {
|
|
1048
1428
|
this.entries.push(entry);
|
|
1049
1429
|
let list = this.byHash.get(hash);
|
|
1050
1430
|
if (list === void 0) {
|
|
@@ -1059,81 +1439,169 @@ var TypeRegistry = class {
|
|
|
1059
1439
|
getAll() {
|
|
1060
1440
|
return this.entries;
|
|
1061
1441
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
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);
|
|
1072
1451
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
});
|
|
1452
|
+
list.push(entry);
|
|
1453
|
+
}
|
|
1454
|
+
getSecondaryDuplicateGroups() {
|
|
1455
|
+
return [...this.bySecondaryHash.values()].filter((group) => group.length > 1);
|
|
1078
1456
|
}
|
|
1079
1457
|
};
|
|
1080
1458
|
|
|
1081
|
-
// src/collect/
|
|
1082
|
-
var
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
this.
|
|
1087
|
-
|
|
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);
|
|
1088
1475
|
if (list === void 0) {
|
|
1089
1476
|
list = [];
|
|
1090
|
-
|
|
1477
|
+
byName.set(entry.name, list);
|
|
1091
1478
|
}
|
|
1092
1479
|
list.push(entry);
|
|
1093
1480
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
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);
|
|
1096
1492
|
}
|
|
1097
|
-
|
|
1098
|
-
return this.
|
|
1493
|
+
getNearDuplicateGroups() {
|
|
1494
|
+
return this.getSecondaryDuplicateGroups().filter((group) => {
|
|
1495
|
+
const hashes = new Set(group.map((e) => e.hash));
|
|
1496
|
+
return hashes.size > 1;
|
|
1497
|
+
});
|
|
1099
1498
|
}
|
|
1100
1499
|
getByName(name) {
|
|
1101
1500
|
return this.entries.filter((e) => e.name === name);
|
|
1102
1501
|
}
|
|
1103
1502
|
getNameCollisionGroups() {
|
|
1104
|
-
|
|
1105
|
-
for (const entry of this.entries) {
|
|
1106
|
-
if (!entry.exported) continue;
|
|
1107
|
-
let list = byName.get(entry.name);
|
|
1108
|
-
if (list === void 0) {
|
|
1109
|
-
list = [];
|
|
1110
|
-
byName.set(entry.name, list);
|
|
1111
|
-
}
|
|
1112
|
-
list.push(entry);
|
|
1113
|
-
}
|
|
1114
|
-
return [...byName.values()].filter((group) => {
|
|
1115
|
-
if (group.length < 2) return false;
|
|
1116
|
-
const files = new Set(group.map((e) => e.file));
|
|
1117
|
-
return files.size > 1;
|
|
1118
|
-
});
|
|
1503
|
+
return getExportedNameCollisions(this.entries);
|
|
1119
1504
|
}
|
|
1120
1505
|
};
|
|
1121
1506
|
|
|
1507
|
+
// src/collect/constant-registry.ts
|
|
1508
|
+
var ConstantRegistry = class extends BaseRegistry {
|
|
1509
|
+
add(entry) {
|
|
1510
|
+
this.addEntry(entry, entry.valueHash);
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
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
|
+
|
|
1122
1578
|
// src/collect/index.ts
|
|
1123
|
-
function collectProject(program) {
|
|
1579
|
+
function collectProject(program, tsRules) {
|
|
1124
1580
|
const checker = program.getTypeChecker();
|
|
1125
1581
|
const types = new TypeRegistry();
|
|
1126
1582
|
const functions = new FunctionRegistry();
|
|
1583
|
+
const constants = new ConstantRegistry();
|
|
1127
1584
|
const callSites = [];
|
|
1128
1585
|
const imports = [];
|
|
1586
|
+
const statementSequences = new StatementSequenceRegistry();
|
|
1587
|
+
const inlineParamTypes = new InlineParamTypeRegistry();
|
|
1129
1588
|
const fileMap = /* @__PURE__ */ new Map();
|
|
1589
|
+
const diagnostics = [];
|
|
1130
1590
|
for (const sourceFile of program.getSourceFiles()) {
|
|
1131
1591
|
let visit2 = function(node) {
|
|
1132
1592
|
collectTypes(node, file, sourceFile, types);
|
|
1133
1593
|
collectFunctions(node, file, sourceFile, checker, functions);
|
|
1594
|
+
collectConstants(node, file, sourceFile, constants);
|
|
1134
1595
|
collectCallSites(node, file, sourceFile, checker, callSites);
|
|
1596
|
+
collectStatementSequences(node, file, sourceFile, statementSequences);
|
|
1597
|
+
collectInlineParamTypes(node, file, sourceFile, inlineParamTypes);
|
|
1135
1598
|
collectImports(node, file, imports);
|
|
1136
|
-
|
|
1599
|
+
if (ruleContexts) {
|
|
1600
|
+
for (const { rule, ctx } of ruleContexts) {
|
|
1601
|
+
rule.visit(node, ctx);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
ts27.forEachChild(node, visit2);
|
|
1137
1605
|
};
|
|
1138
1606
|
var visit = visit2;
|
|
1139
1607
|
const file = sourceFile.fileName;
|
|
@@ -1142,48 +1610,166 @@ function collectProject(program) {
|
|
|
1142
1610
|
const source = sourceFile.getFullText();
|
|
1143
1611
|
const comments = collectAllComments(sourceFile);
|
|
1144
1612
|
fileMap.set(file, { source, sourceFile, comments });
|
|
1145
|
-
|
|
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);
|
|
1146
1629
|
}
|
|
1147
|
-
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
|
+
);
|
|
1148
1639
|
}
|
|
1149
1640
|
function isExported(node) {
|
|
1150
|
-
if (!
|
|
1151
|
-
const mods =
|
|
1641
|
+
if (!ts27.canHaveModifiers(node)) return false;
|
|
1642
|
+
const mods = ts27.getModifiers(node);
|
|
1152
1643
|
if (mods === void 0) return false;
|
|
1153
|
-
return mods.some((m) => m.kind ===
|
|
1644
|
+
return mods.some((m) => m.kind === ts27.SyntaxKind.ExportKeyword);
|
|
1154
1645
|
}
|
|
1155
1646
|
function collectTypes(node, file, sourceFile, registry) {
|
|
1156
|
-
if (
|
|
1157
|
-
const line =
|
|
1647
|
+
if (ts27.isTypeAliasDeclaration(node)) {
|
|
1648
|
+
const line = ts27.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
1158
1649
|
registry.add(node.name.text, file, line, node.type, sourceFile, isExported(node));
|
|
1159
1650
|
}
|
|
1160
|
-
if (
|
|
1161
|
-
const line =
|
|
1651
|
+
if (ts27.isInterfaceDeclaration(node)) {
|
|
1652
|
+
const line = ts27.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
1162
1653
|
registry.add(node.name.text, file, line, node, sourceFile, isExported(node));
|
|
1163
1654
|
}
|
|
1164
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
|
+
}
|
|
1165
1699
|
function collectFunctions(node, file, sourceFile, checker, registry) {
|
|
1166
|
-
if (
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
}
|
|
1173
|
-
if (
|
|
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)) {
|
|
1174
1708
|
const exported = isExported(node);
|
|
1175
1709
|
for (const decl of node.declarationList.declarations) {
|
|
1176
|
-
if (decl.initializer &&
|
|
1710
|
+
if (decl.initializer && ts27.isArrowFunction(decl.initializer) && ts27.isIdentifier(decl.name)) {
|
|
1177
1711
|
const arrow = decl.initializer;
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
+
}
|
|
1184
1727
|
}
|
|
1185
1728
|
}
|
|
1186
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
|
+
}
|
|
1187
1773
|
}
|
|
1188
1774
|
function extractParams(parameters, sourceFile) {
|
|
1189
1775
|
return parameters.map((p) => {
|
|
@@ -1194,8 +1780,29 @@ function extractParams(parameters, sourceFile) {
|
|
|
1194
1780
|
return { name, optional, hasDefault, typeText };
|
|
1195
1781
|
});
|
|
1196
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
|
+
}
|
|
1197
1804
|
function collectImports(node, file, imports) {
|
|
1198
|
-
if (
|
|
1805
|
+
if (ts27.isImportDeclaration(node) && ts27.isStringLiteral(node.moduleSpecifier)) {
|
|
1199
1806
|
const moduleSource = node.moduleSpecifier.text;
|
|
1200
1807
|
const clause = node.importClause;
|
|
1201
1808
|
if (!clause) return;
|
|
@@ -1207,7 +1814,7 @@ function collectImports(node, file, imports) {
|
|
|
1207
1814
|
source: moduleSource
|
|
1208
1815
|
});
|
|
1209
1816
|
}
|
|
1210
|
-
if (clause.namedBindings &&
|
|
1817
|
+
if (clause.namedBindings && ts27.isNamedImports(clause.namedBindings)) {
|
|
1211
1818
|
for (const el of clause.namedBindings.elements) {
|
|
1212
1819
|
imports.push({
|
|
1213
1820
|
file,
|
|
@@ -1218,9 +1825,9 @@ function collectImports(node, file, imports) {
|
|
|
1218
1825
|
}
|
|
1219
1826
|
}
|
|
1220
1827
|
}
|
|
1221
|
-
if (
|
|
1828
|
+
if (ts27.isExportDeclaration(node) && node.moduleSpecifier && ts27.isStringLiteral(node.moduleSpecifier)) {
|
|
1222
1829
|
const moduleSource = node.moduleSpecifier.text;
|
|
1223
|
-
if (node.exportClause &&
|
|
1830
|
+
if (node.exportClause && ts27.isNamedExports(node.exportClause)) {
|
|
1224
1831
|
for (const el of node.exportClause.elements) {
|
|
1225
1832
|
imports.push({
|
|
1226
1833
|
file,
|
|
@@ -1232,20 +1839,52 @@ function collectImports(node, file, imports) {
|
|
|
1232
1839
|
}
|
|
1233
1840
|
}
|
|
1234
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
|
+
}
|
|
1235
1874
|
function collectCallSites(node, file, sourceFile, checker, sites) {
|
|
1236
|
-
if (!
|
|
1875
|
+
if (!ts27.isCallExpression(node)) return;
|
|
1237
1876
|
let calleeName = null;
|
|
1238
|
-
if (
|
|
1877
|
+
if (ts27.isIdentifier(node.expression)) {
|
|
1239
1878
|
calleeName = node.expression.text;
|
|
1240
|
-
} else if (
|
|
1879
|
+
} else if (ts27.isPropertyAccessExpression(node.expression)) {
|
|
1241
1880
|
calleeName = node.expression.name.text;
|
|
1242
1881
|
}
|
|
1243
1882
|
if (calleeName) {
|
|
1244
|
-
const line =
|
|
1883
|
+
const line = ts27.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile)).line + 1;
|
|
1245
1884
|
let symbol;
|
|
1246
1885
|
try {
|
|
1247
1886
|
symbol = checker.getSymbolAtLocation(node.expression);
|
|
1248
|
-
if (symbol && symbol.flags &
|
|
1887
|
+
if (symbol && symbol.flags & ts27.SymbolFlags.Alias) {
|
|
1249
1888
|
symbol = checker.getAliasedSymbol(symbol);
|
|
1250
1889
|
}
|
|
1251
1890
|
} catch {
|
|
@@ -1265,12 +1904,12 @@ function collectAllComments(sourceFile) {
|
|
|
1265
1904
|
const source = sourceFile.getFullText();
|
|
1266
1905
|
const seen = /* @__PURE__ */ new Set();
|
|
1267
1906
|
function visit(node) {
|
|
1268
|
-
const leading =
|
|
1907
|
+
const leading = ts27.getLeadingCommentRanges(source, node.getFullStart());
|
|
1269
1908
|
if (leading) {
|
|
1270
1909
|
for (const r of leading) {
|
|
1271
1910
|
if (seen.has(r.pos)) continue;
|
|
1272
1911
|
seen.add(r.pos);
|
|
1273
|
-
const isLine = r.kind ===
|
|
1912
|
+
const isLine = r.kind === ts27.SyntaxKind.SingleLineCommentTrivia;
|
|
1274
1913
|
comments.push({
|
|
1275
1914
|
type: isLine ? "Line" : "Block",
|
|
1276
1915
|
value: source.slice(r.pos + 2, isLine ? r.end : r.end - 2),
|
|
@@ -1279,12 +1918,12 @@ function collectAllComments(sourceFile) {
|
|
|
1279
1918
|
});
|
|
1280
1919
|
}
|
|
1281
1920
|
}
|
|
1282
|
-
const trailing =
|
|
1921
|
+
const trailing = ts27.getTrailingCommentRanges(source, node.getEnd());
|
|
1283
1922
|
if (trailing) {
|
|
1284
1923
|
for (const r of trailing) {
|
|
1285
1924
|
if (seen.has(r.pos)) continue;
|
|
1286
1925
|
seen.add(r.pos);
|
|
1287
|
-
const isLine = r.kind ===
|
|
1926
|
+
const isLine = r.kind === ts27.SyntaxKind.SingleLineCommentTrivia;
|
|
1288
1927
|
comments.push({
|
|
1289
1928
|
type: isLine ? "Line" : "Block",
|
|
1290
1929
|
value: source.slice(r.pos + 2, isLine ? r.end : r.end - 2),
|
|
@@ -1293,34 +1932,34 @@ function collectAllComments(sourceFile) {
|
|
|
1293
1932
|
});
|
|
1294
1933
|
}
|
|
1295
1934
|
}
|
|
1296
|
-
|
|
1935
|
+
ts27.forEachChild(node, visit);
|
|
1297
1936
|
}
|
|
1298
1937
|
visit(sourceFile);
|
|
1299
1938
|
return comments;
|
|
1300
1939
|
}
|
|
1301
1940
|
|
|
1302
1941
|
// src/typecheck/program.ts
|
|
1303
|
-
import * as
|
|
1942
|
+
import * as ts28 from "typescript";
|
|
1304
1943
|
import { dirname as dirname2 } from "path";
|
|
1305
1944
|
function createProgramFromFiles(files) {
|
|
1306
1945
|
let configPath;
|
|
1307
1946
|
if (files.length > 0) {
|
|
1308
|
-
configPath =
|
|
1947
|
+
configPath = ts28.findConfigFile(dirname2(files[0]), ts28.sys.fileExists, "tsconfig.json");
|
|
1309
1948
|
}
|
|
1310
1949
|
if (configPath) {
|
|
1311
|
-
const configFile =
|
|
1312
|
-
const parsed =
|
|
1313
|
-
return
|
|
1950
|
+
const configFile = ts28.readConfigFile(configPath, ts28.sys.readFile);
|
|
1951
|
+
const parsed = ts28.parseJsonConfigFileContent(configFile.config, ts28.sys, dirname2(configPath));
|
|
1952
|
+
return ts28.createProgram({
|
|
1314
1953
|
rootNames: files,
|
|
1315
1954
|
options: { ...parsed.options, skipLibCheck: true }
|
|
1316
1955
|
});
|
|
1317
1956
|
}
|
|
1318
|
-
return
|
|
1957
|
+
return ts28.createProgram({
|
|
1319
1958
|
rootNames: files,
|
|
1320
1959
|
options: {
|
|
1321
|
-
target:
|
|
1322
|
-
module:
|
|
1323
|
-
moduleResolution:
|
|
1960
|
+
target: ts28.ScriptTarget.ESNext,
|
|
1961
|
+
module: ts28.ModuleKind.ESNext,
|
|
1962
|
+
moduleResolution: ts28.ModuleResolutionKind.Bundler,
|
|
1324
1963
|
strict: true,
|
|
1325
1964
|
skipLibCheck: true,
|
|
1326
1965
|
noEmit: true
|
|
@@ -1328,166 +1967,72 @@ function createProgramFromFiles(files) {
|
|
|
1328
1967
|
});
|
|
1329
1968
|
}
|
|
1330
1969
|
|
|
1331
|
-
// src/
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
const
|
|
1335
|
-
const contexts = rules.map((rule) => ({
|
|
1336
|
-
rule,
|
|
1337
|
-
ctx: buildContext(rule, sourceFile, checker, source, filename, diagnostics)
|
|
1338
|
-
}));
|
|
1339
|
-
function visit(node) {
|
|
1340
|
-
for (const { rule, ctx } of contexts) {
|
|
1341
|
-
rule.visit(node, ctx);
|
|
1342
|
-
}
|
|
1343
|
-
ts28.forEachChild(node, visit);
|
|
1344
|
-
}
|
|
1345
|
-
visit(sourceFile);
|
|
1346
|
-
return diagnostics;
|
|
1347
|
-
}
|
|
1348
|
-
function buildContext(rule, sourceFile, checker, source, filename, diagnostics) {
|
|
1349
|
-
return {
|
|
1350
|
-
filename,
|
|
1351
|
-
source,
|
|
1352
|
-
sourceFile,
|
|
1353
|
-
checker,
|
|
1354
|
-
report(node, message) {
|
|
1355
|
-
const { line, character } = ts28.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
1356
|
-
diagnostics.push({
|
|
1357
|
-
ruleId: rule.id,
|
|
1358
|
-
severity: rule.severity,
|
|
1359
|
-
message: message ?? rule.message,
|
|
1360
|
-
file: filename,
|
|
1361
|
-
line: line + 1,
|
|
1362
|
-
column: character + 1
|
|
1363
|
-
});
|
|
1364
|
-
},
|
|
1365
|
-
reportAtOffset(offset, message) {
|
|
1366
|
-
const { line, character } = ts28.getLineAndCharacterOfPosition(sourceFile, offset);
|
|
1367
|
-
diagnostics.push({
|
|
1368
|
-
ruleId: rule.id,
|
|
1369
|
-
severity: rule.severity,
|
|
1370
|
-
message: message ?? rule.message,
|
|
1371
|
-
file: filename,
|
|
1372
|
-
line: line + 1,
|
|
1373
|
-
column: character + 1
|
|
1374
|
-
});
|
|
1375
|
-
},
|
|
1376
|
-
isNullable(node) {
|
|
1377
|
-
const type = checker.getTypeAtLocation(node);
|
|
1378
|
-
return isNullableType(checker, type);
|
|
1379
|
-
},
|
|
1380
|
-
isExternal(node) {
|
|
1381
|
-
const type = checker.getTypeAtLocation(node);
|
|
1382
|
-
const symbol = type.getSymbol();
|
|
1383
|
-
if (!symbol) return false;
|
|
1384
|
-
const declarations = symbol.getDeclarations();
|
|
1385
|
-
if (!declarations || declarations.length === 0) return false;
|
|
1386
|
-
return declarations.some((d) => isFromNodeModules(d));
|
|
1387
|
-
}
|
|
1388
|
-
};
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
// src/engine.ts
|
|
1392
|
-
async function scan(options) {
|
|
1393
|
-
const patterns = options.paths.length > 0 ? options.paths : ["."];
|
|
1394
|
-
const globs = patterns.map((p) => {
|
|
1395
|
-
if (p === ".") return `./**/*.{ts,cts,mts,tsx}`;
|
|
1396
|
-
if (p.endsWith("/")) return `${p}**/*.{ts,cts,mts,tsx}`;
|
|
1397
|
-
if (!p.includes("*") && !p.endsWith(".ts") && !p.endsWith(".tsx") && !p.endsWith(".cts") && !p.endsWith(".mts")) {
|
|
1398
|
-
return `${p}/**/*.{ts,cts,mts,tsx}`;
|
|
1399
|
-
}
|
|
1400
|
-
return p;
|
|
1401
|
-
});
|
|
1402
|
-
const files = await fg(globs, {
|
|
1403
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/*.d.ts", "**/*.d.cts", "**/*.d.mts"],
|
|
1404
|
-
absolute: true
|
|
1405
|
-
});
|
|
1406
|
-
const activeRules = allRules.filter((r) => {
|
|
1407
|
-
if (options.rules && !options.rules.includes(r.id)) return false;
|
|
1408
|
-
return true;
|
|
1409
|
-
});
|
|
1410
|
-
const tsRules = activeRules.filter(isTSRule);
|
|
1411
|
-
const crossFileRules = activeRules.filter((r) => !isTSRule(r));
|
|
1412
|
-
const diagnostics = [];
|
|
1970
|
+
// src/scan/analyze.ts
|
|
1971
|
+
function analyzeFiles(files, rules) {
|
|
1972
|
+
const tsRules = rules.filter(isTSRule);
|
|
1973
|
+
const crossFileRules = rules.filter((r) => !isTSRule(r));
|
|
1413
1974
|
const program = files.length > 0 ? createProgramFromFiles(files) : null;
|
|
1414
|
-
if (
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
diagnostics.push(...tsDiags);
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
if (crossFileRules.length > 0 && program) {
|
|
1426
|
-
const projectIndex = collectProject(program);
|
|
1427
|
-
for (const rule of crossFileRules) {
|
|
1428
|
-
const crossDiags = rule.analyze(projectIndex);
|
|
1429
|
-
for (const d of crossDiags) {
|
|
1430
|
-
const fileData = projectIndex.files.get(d.file);
|
|
1431
|
-
if (fileData !== void 0) annotate([d], fileData.comments, fileData.source);
|
|
1432
|
-
}
|
|
1433
|
-
diagnostics.push(...crossDiags);
|
|
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);
|
|
1434
1982
|
}
|
|
1983
|
+
diagnostics.push(...crossDiagnostics);
|
|
1435
1984
|
}
|
|
1985
|
+
return dedupeDiagnostics(diagnostics);
|
|
1986
|
+
}
|
|
1987
|
+
function dedupeDiagnostics(diagnostics) {
|
|
1436
1988
|
const seen = /* @__PURE__ */ new Set();
|
|
1437
1989
|
const deduped = [];
|
|
1438
|
-
for (const
|
|
1439
|
-
const key = `${
|
|
1990
|
+
for (const diagnostic of diagnostics) {
|
|
1991
|
+
const key = `${diagnostic.file}:${diagnostic.line}:${diagnostic.ruleId}`;
|
|
1440
1992
|
if (seen.has(key)) continue;
|
|
1441
1993
|
seen.add(key);
|
|
1442
|
-
deduped.push(
|
|
1443
|
-
}
|
|
1444
|
-
if (options.strict) {
|
|
1445
|
-
for (const d of deduped) {
|
|
1446
|
-
d.severity = "error";
|
|
1447
|
-
}
|
|
1994
|
+
deduped.push(diagnostic);
|
|
1448
1995
|
}
|
|
1449
|
-
return
|
|
1996
|
+
return deduped;
|
|
1450
1997
|
}
|
|
1451
1998
|
function annotate(diagnostics, comments, source) {
|
|
1452
1999
|
if (comments.length === 0 || diagnostics.length === 0) return;
|
|
1453
2000
|
const byEndLine = /* @__PURE__ */ new Map();
|
|
1454
|
-
for (const
|
|
1455
|
-
const endLine = lineAt(source,
|
|
2001
|
+
for (const comment of comments) {
|
|
2002
|
+
const endLine = lineAt(source, comment.end);
|
|
1456
2003
|
let list = byEndLine.get(endLine);
|
|
1457
2004
|
if (list === void 0) {
|
|
1458
2005
|
list = [];
|
|
1459
2006
|
byEndLine.set(endLine, list);
|
|
1460
2007
|
}
|
|
1461
|
-
list.push(
|
|
2008
|
+
list.push(comment);
|
|
1462
2009
|
}
|
|
1463
|
-
for (const
|
|
1464
|
-
const inline = findInlineComment(
|
|
2010
|
+
for (const diagnostic of diagnostics) {
|
|
2011
|
+
const inline = findInlineComment(diagnostic.line, byEndLine);
|
|
1465
2012
|
if (inline !== null) {
|
|
1466
|
-
|
|
2013
|
+
diagnostic.annotation = inline;
|
|
1467
2014
|
continue;
|
|
1468
2015
|
}
|
|
1469
|
-
const above = collectAnnotation(
|
|
1470
|
-
if (above !== null)
|
|
2016
|
+
const above = collectAnnotation(diagnostic.line - 1, byEndLine, source);
|
|
2017
|
+
if (above !== null) diagnostic.annotation = above;
|
|
1471
2018
|
}
|
|
1472
2019
|
}
|
|
1473
|
-
function
|
|
1474
|
-
const commentsOnLine = byEndLine.get(
|
|
2020
|
+
function lastCommentOnLine(line, byEndLine) {
|
|
2021
|
+
const commentsOnLine = byEndLine.get(line);
|
|
1475
2022
|
if (commentsOnLine === void 0 || commentsOnLine.length === 0) return null;
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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;
|
|
1479
2028
|
const text = comment.value.trim();
|
|
1480
2029
|
if (text.startsWith("@expect")) return null;
|
|
1481
2030
|
return text;
|
|
1482
2031
|
}
|
|
1483
2032
|
function collectAnnotation(commentEndLine, byEndLine, source) {
|
|
1484
|
-
const
|
|
1485
|
-
if (
|
|
1486
|
-
|
|
1487
|
-
if (comment === void 0) return null;
|
|
1488
|
-
if (comment.type === "Block") {
|
|
1489
|
-
return cleanBlockComment(comment.value);
|
|
1490
|
-
}
|
|
2033
|
+
const comment = lastCommentOnLine(commentEndLine, byEndLine);
|
|
2034
|
+
if (comment === null) return null;
|
|
2035
|
+
if (comment.type === "Block") return cleanBlockComment(comment.value);
|
|
1491
2036
|
const lines = [comment.value.trim()];
|
|
1492
2037
|
let prevLine = commentEndLine - 1;
|
|
1493
2038
|
for (; ; ) {
|
|
@@ -1512,8 +2057,145 @@ function lineAt(source, offset) {
|
|
|
1512
2057
|
return line;
|
|
1513
2058
|
}
|
|
1514
2059
|
|
|
2060
|
+
// src/scan/discover.ts
|
|
2061
|
+
import fg from "fast-glob";
|
|
2062
|
+
import ignore from "ignore";
|
|
2063
|
+
import { existsSync, readFileSync } from "fs";
|
|
2064
|
+
import { relative, resolve as resolve2, sep } from "path";
|
|
2065
|
+
async function discoverFiles(config) {
|
|
2066
|
+
const globs = expandGlobs(config.paths);
|
|
2067
|
+
const discoveredFiles = await fg(globs, {
|
|
2068
|
+
ignore: config.ignore,
|
|
2069
|
+
absolute: true
|
|
2070
|
+
});
|
|
2071
|
+
if (!config.useGitIgnore) return discoveredFiles;
|
|
2072
|
+
return applyGitIgnore(discoveredFiles);
|
|
2073
|
+
}
|
|
2074
|
+
function expandGlobs(paths) {
|
|
2075
|
+
return paths.map((p) => {
|
|
2076
|
+
if (p === ".") return "./**/*.{ts,cts,mts,tsx}";
|
|
2077
|
+
if (p.endsWith("/")) return `${p}**/*.{ts,cts,mts,tsx}`;
|
|
2078
|
+
if (!p.includes("*") && !p.endsWith(".ts") && !p.endsWith(".tsx") && !p.endsWith(".cts") && !p.endsWith(".mts")) {
|
|
2079
|
+
return `${p}/**/*.{ts,cts,mts,tsx}`;
|
|
2080
|
+
}
|
|
2081
|
+
return p;
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
function applyGitIgnore(files) {
|
|
2085
|
+
const gitIgnorePath = resolve2(process.cwd(), ".gitignore");
|
|
2086
|
+
if (!existsSync(gitIgnorePath)) return files;
|
|
2087
|
+
const matcher = ignore().add(readFileSync(gitIgnorePath, "utf8"));
|
|
2088
|
+
return files.filter((file) => {
|
|
2089
|
+
const rel = relative(process.cwd(), file);
|
|
2090
|
+
if (rel.startsWith("..")) return true;
|
|
2091
|
+
const normalized = rel.split(sep).join("/");
|
|
2092
|
+
return !matcher.ignores(normalized);
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/scan/policy.ts
|
|
2097
|
+
function buildRuleDescriptors(rules) {
|
|
2098
|
+
return rules.map((rule) => {
|
|
2099
|
+
const metadata = getRuleMetadata(rule.id);
|
|
2100
|
+
return {
|
|
2101
|
+
rule,
|
|
2102
|
+
category: metadata.category,
|
|
2103
|
+
tags: metadata.tags
|
|
2104
|
+
};
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
function resolveActiveRules(descriptors, config) {
|
|
2108
|
+
const selectedRules = config.rules ? new Set(config.rules) : null;
|
|
2109
|
+
const active = [];
|
|
2110
|
+
for (const descriptor of descriptors) {
|
|
2111
|
+
if (selectedRules && !selectedRules.has(descriptor.rule.id)) continue;
|
|
2112
|
+
const resolvedSeverity = resolveRuleSeverity(descriptor, config);
|
|
2113
|
+
if (resolvedSeverity === "off") continue;
|
|
2114
|
+
if (descriptor.rule.severity === resolvedSeverity) {
|
|
2115
|
+
active.push(descriptor.rule);
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
active.push({ ...descriptor.rule, severity: resolvedSeverity });
|
|
2119
|
+
}
|
|
2120
|
+
return active;
|
|
2121
|
+
}
|
|
2122
|
+
function resolveRuleSeverity(descriptor, config) {
|
|
2123
|
+
let severity = descriptor.rule.severity;
|
|
2124
|
+
for (const entry of config.rulePolicy) {
|
|
2125
|
+
if (!matchesSelector(descriptor, entry.selector)) continue;
|
|
2126
|
+
severity = entry.severity;
|
|
2127
|
+
}
|
|
2128
|
+
if (severity === "off") return "off";
|
|
2129
|
+
if (config.strict) return "error";
|
|
2130
|
+
return severity;
|
|
2131
|
+
}
|
|
2132
|
+
function matchesSelector(descriptor, selector) {
|
|
2133
|
+
if (selector.startsWith("category:")) {
|
|
2134
|
+
return descriptor.category === selector.slice("category:".length);
|
|
2135
|
+
}
|
|
2136
|
+
if (selector.startsWith("tag:")) {
|
|
2137
|
+
return descriptor.tags.includes(selector.slice("tag:".length));
|
|
2138
|
+
}
|
|
2139
|
+
if (selector === descriptor.rule.id) return true;
|
|
2140
|
+
if (!selector.includes("*")) return false;
|
|
2141
|
+
const regex = new RegExp(`^${escapeRegex(selector).replaceAll("\\*", ".*")}$`);
|
|
2142
|
+
return regex.test(descriptor.rule.id);
|
|
2143
|
+
}
|
|
2144
|
+
function escapeRegex(value) {
|
|
2145
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2146
|
+
}
|
|
2147
|
+
function finalizeScanResult(diagnostics, fileCount, config) {
|
|
2148
|
+
const visibleDiagnostics = config.showSeverities ? diagnostics.filter((d) => config.showSeverities?.has(d.severity)) : diagnostics;
|
|
2149
|
+
return {
|
|
2150
|
+
diagnostics,
|
|
2151
|
+
visibleDiagnostics,
|
|
2152
|
+
fileCount,
|
|
2153
|
+
exitCode: computeExitCode(diagnostics, config.failOn)
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
function toScanResult(execution) {
|
|
2157
|
+
return {
|
|
2158
|
+
diagnostics: execution.diagnostics,
|
|
2159
|
+
fileCount: execution.fileCount
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
function computeExitCode(diagnostics, failOn) {
|
|
2163
|
+
if (failOn === "none") return 0;
|
|
2164
|
+
const hasError = diagnostics.some((d) => d.severity === "error");
|
|
2165
|
+
const hasWarning = diagnostics.some((d) => d.severity === "warning");
|
|
2166
|
+
const hasInfo = diagnostics.some((d) => d.severity === "info");
|
|
2167
|
+
if (failOn === "error") return hasError ? 2 : 0;
|
|
2168
|
+
if (failOn === "warning") {
|
|
2169
|
+
if (hasError) return 2;
|
|
2170
|
+
return hasWarning ? 1 : 0;
|
|
2171
|
+
}
|
|
2172
|
+
if (hasError) return 2;
|
|
2173
|
+
if (hasWarning || hasInfo) return 1;
|
|
2174
|
+
return 0;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/engine.ts
|
|
2178
|
+
async function executeScan(options) {
|
|
2179
|
+
const config = resolveScanConfig(options);
|
|
2180
|
+
const files = await discoverFiles(config);
|
|
2181
|
+
const descriptors = buildRuleDescriptors(allRules);
|
|
2182
|
+
const activeRules = resolveActiveRules(descriptors, config);
|
|
2183
|
+
const diagnostics = analyzeFiles(files, activeRules);
|
|
2184
|
+
return finalizeScanResult(diagnostics, files.length, config);
|
|
2185
|
+
}
|
|
2186
|
+
async function scan(options) {
|
|
2187
|
+
const execution = await executeScan(options);
|
|
2188
|
+
return toScanResult(execution);
|
|
2189
|
+
}
|
|
2190
|
+
|
|
1515
2191
|
export {
|
|
1516
2192
|
allRules,
|
|
2193
|
+
getRuleMetadata,
|
|
2194
|
+
toRulePolicyEntries,
|
|
2195
|
+
isRulePolicySeverity,
|
|
2196
|
+
isSeverity,
|
|
2197
|
+
isFailOn,
|
|
2198
|
+
executeScan,
|
|
1517
2199
|
scan
|
|
1518
2200
|
};
|
|
1519
|
-
//# sourceMappingURL=chunk-
|
|
2201
|
+
//# sourceMappingURL=chunk-5GVEUYZF.js.map
|