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.
- package/README.md +8 -2
- package/dist/{chunk-I4RZLVE2.js → chunk-5GVEUYZF.js} +850 -367
- package/dist/chunk-5GVEUYZF.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +68 -11
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-I4RZLVE2.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
|
}
|
|
@@ -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
|
-
|
|
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
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
|
|
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,
|
|
@@ -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
|
}
|
|
@@ -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
|
|
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) => !
|
|
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,7 +1240,13 @@ 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
|
];
|
|
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
|
-
"
|
|
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
|
|
1084
|
-
import "
|
|
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
|
|
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/
|
|
1120
|
-
var
|
|
1423
|
+
// src/collect/base-registry.ts
|
|
1424
|
+
var BaseRegistry = class {
|
|
1121
1425
|
entries = [];
|
|
1122
1426
|
byHash = /* @__PURE__ */ new Map();
|
|
1123
|
-
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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/
|
|
1160
|
-
var
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
this.
|
|
1165
|
-
|
|
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
|
-
|
|
1477
|
+
byName.set(entry.name, list);
|
|
1169
1478
|
}
|
|
1170
1479
|
list.push(entry);
|
|
1171
1480
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
-
|
|
1176
|
-
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
|
+
});
|
|
1177
1498
|
}
|
|
1178
1499
|
getByName(name) {
|
|
1179
1500
|
return this.entries.filter((e) => e.name === name);
|
|
1180
1501
|
}
|
|
1181
1502
|
getNameCollisionGroups() {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
1229
|
-
const mods =
|
|
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 ===
|
|
1644
|
+
return mods.some((m) => m.kind === ts27.SyntaxKind.ExportKeyword);
|
|
1232
1645
|
}
|
|
1233
1646
|
function collectTypes(node, file, sourceFile, registry) {
|
|
1234
|
-
if (
|
|
1235
|
-
const line =
|
|
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 (
|
|
1239
|
-
const line =
|
|
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 (
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
}
|
|
1251
|
-
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)) {
|
|
1252
1708
|
const exported = isExported(node);
|
|
1253
1709
|
for (const decl of node.declarationList.declarations) {
|
|
1254
|
-
if (decl.initializer &&
|
|
1710
|
+
if (decl.initializer && ts27.isArrowFunction(decl.initializer) && ts27.isIdentifier(decl.name)) {
|
|
1255
1711
|
const arrow = decl.initializer;
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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 (
|
|
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 &&
|
|
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 (
|
|
1828
|
+
if (ts27.isExportDeclaration(node) && node.moduleSpecifier && ts27.isStringLiteral(node.moduleSpecifier)) {
|
|
1300
1829
|
const moduleSource = node.moduleSpecifier.text;
|
|
1301
|
-
if (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 (!
|
|
1875
|
+
if (!ts27.isCallExpression(node)) return;
|
|
1315
1876
|
let calleeName = null;
|
|
1316
|
-
if (
|
|
1877
|
+
if (ts27.isIdentifier(node.expression)) {
|
|
1317
1878
|
calleeName = node.expression.text;
|
|
1318
|
-
} else if (
|
|
1879
|
+
} else if (ts27.isPropertyAccessExpression(node.expression)) {
|
|
1319
1880
|
calleeName = node.expression.name.text;
|
|
1320
1881
|
}
|
|
1321
1882
|
if (calleeName) {
|
|
1322
|
-
const line =
|
|
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 &
|
|
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 =
|
|
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 ===
|
|
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 =
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1947
|
+
configPath = ts28.findConfigFile(dirname2(files[0]), ts28.sys.fileExists, "tsconfig.json");
|
|
1392
1948
|
}
|
|
1393
1949
|
if (configPath) {
|
|
1394
|
-
const configFile =
|
|
1395
|
-
const parsed =
|
|
1396
|
-
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({
|
|
1397
1953
|
rootNames: files,
|
|
1398
1954
|
options: { ...parsed.options, skipLibCheck: true }
|
|
1399
1955
|
});
|
|
1400
1956
|
}
|
|
1401
|
-
return
|
|
1957
|
+
return ts28.createProgram({
|
|
1402
1958
|
rootNames: files,
|
|
1403
1959
|
options: {
|
|
1404
|
-
target:
|
|
1405
|
-
module:
|
|
1406
|
-
moduleResolution:
|
|
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
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
const
|
|
1486
|
-
if (
|
|
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
|
|
1538
|
-
const commentsOnLine = byEndLine.get(
|
|
2020
|
+
function lastCommentOnLine(line, byEndLine) {
|
|
2021
|
+
const commentsOnLine = byEndLine.get(line);
|
|
1539
2022
|
if (commentsOnLine === void 0 || commentsOnLine.length === 0) return null;
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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
|
|
1549
|
-
if (
|
|
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
|
|
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(
|
|
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-
|
|
2201
|
+
//# sourceMappingURL=chunk-5GVEUYZF.js.map
|