tamperward 2.25.0 → 2.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +139 -27
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -3456,9 +3456,62 @@ function assertedType(node) {
|
|
|
3456
3456
|
while (ts.isParenthesizedTypeNode(t)) t = t.type;
|
|
3457
3457
|
return t;
|
|
3458
3458
|
}
|
|
3459
|
-
function
|
|
3459
|
+
function bareKind(type) {
|
|
3460
|
+
let x = type;
|
|
3461
|
+
while (ts.isParenthesizedTypeNode(x)) x = x.type;
|
|
3462
|
+
switch (x.kind) {
|
|
3463
|
+
case ts.SyntaxKind.AnyKeyword:
|
|
3464
|
+
return "any";
|
|
3465
|
+
case ts.SyntaxKind.NeverKeyword:
|
|
3466
|
+
return "never";
|
|
3467
|
+
case ts.SyntaxKind.UnknownKeyword:
|
|
3468
|
+
return "unknown";
|
|
3469
|
+
case ts.SyntaxKind.ObjectKeyword:
|
|
3470
|
+
return "objectish";
|
|
3471
|
+
}
|
|
3472
|
+
if (ts.isTypeLiteralNode(x) && x.members.length === 0) return "empty";
|
|
3473
|
+
if (ts.isTypeReferenceNode(x) && ts.isIdentifier(x.typeName) && !x.typeArguments) {
|
|
3474
|
+
if (x.typeName.text === "Object") return "objectish";
|
|
3475
|
+
if (x.typeName.text === "const") return "const";
|
|
3476
|
+
}
|
|
3477
|
+
return null;
|
|
3478
|
+
}
|
|
3479
|
+
function kindOfType(type, resolveName) {
|
|
3480
|
+
const bare = bareKind(type);
|
|
3481
|
+
if (bare) return bare;
|
|
3482
|
+
let x = type;
|
|
3483
|
+
while (ts.isParenthesizedTypeNode(x)) x = x.type;
|
|
3484
|
+
if (ts.isTypeReferenceNode(x) && ts.isIdentifier(x.typeName) && !x.typeArguments) return resolveName(x.typeName.text);
|
|
3485
|
+
return null;
|
|
3486
|
+
}
|
|
3487
|
+
function buildAliasMap(sf) {
|
|
3488
|
+
const decls = /* @__PURE__ */ new Map();
|
|
3489
|
+
const collect2 = (node) => {
|
|
3490
|
+
if (ts.isTypeAliasDeclaration(node) && (!node.typeParameters || node.typeParameters.length === 0)) decls.set(node.name.text, node.type);
|
|
3491
|
+
ts.forEachChild(node, collect2);
|
|
3492
|
+
};
|
|
3493
|
+
collect2(sf);
|
|
3494
|
+
const resolve16 = (name, seen) => {
|
|
3495
|
+
if (seen.has(name)) return null;
|
|
3496
|
+
seen.add(name);
|
|
3497
|
+
const t = decls.get(name);
|
|
3498
|
+
return t ? kindOfType(t, (n) => resolve16(n, seen)) : null;
|
|
3499
|
+
};
|
|
3500
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
3501
|
+
for (const name of decls.keys()) {
|
|
3502
|
+
const k = resolve16(name, /* @__PURE__ */ new Set());
|
|
3503
|
+
if (k) resolved.set(name, k);
|
|
3504
|
+
}
|
|
3505
|
+
return resolved;
|
|
3506
|
+
}
|
|
3507
|
+
function assertedLaunderKind(node, aliasMap) {
|
|
3508
|
+
return kindOfType(assertedType(node), (n) => aliasMap.get(n) ?? null);
|
|
3509
|
+
}
|
|
3510
|
+
function isDoubleCast(node, aliasMap) {
|
|
3460
3511
|
const inner = unparenthesized(node.expression);
|
|
3461
|
-
|
|
3512
|
+
if (!(ts.isAsExpression(inner) || ts.isTypeAssertionExpression(inner))) return false;
|
|
3513
|
+
const k = assertedLaunderKind(inner, aliasMap ?? EMPTY_ALIASES);
|
|
3514
|
+
return k != null && DOUBLE_CAST_INNER.has(k);
|
|
3462
3515
|
}
|
|
3463
3516
|
function surfaceOf(path, src) {
|
|
3464
3517
|
const sf = parseSource(path, src, scriptKind(path));
|
|
@@ -3467,20 +3520,26 @@ function surfaceOf(path, src) {
|
|
|
3467
3520
|
if (diagnostics.length > 0) return null;
|
|
3468
3521
|
const surface = { type: 0, nonNull: 0, assertions: [] };
|
|
3469
3522
|
const lines = src.split("\n");
|
|
3523
|
+
const aliasMap = buildAliasMap(sf);
|
|
3470
3524
|
const record = (kind, node) => {
|
|
3471
3525
|
const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
3472
3526
|
surface.assertions.push({ kind, line, text: node.getText(sf).replace(/\s+/g, " "), lineText: (lines[line - 1] ?? "").trim() });
|
|
3473
3527
|
if (kind === "type") surface.type++;
|
|
3474
3528
|
else surface.nonNull++;
|
|
3475
3529
|
};
|
|
3530
|
+
const launderInner = /* @__PURE__ */ new Set();
|
|
3531
|
+
const collectInners = (node) => {
|
|
3532
|
+
if ((ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) && isDoubleCast(node, aliasMap)) launderInner.add(unparenthesized(node.expression));
|
|
3533
|
+
ts.forEachChild(node, collectInners);
|
|
3534
|
+
};
|
|
3535
|
+
collectInners(sf);
|
|
3476
3536
|
const isRow4OrHonest = (node) => {
|
|
3477
|
-
const
|
|
3478
|
-
|
|
3479
|
-
return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && type.typeName.text === "const";
|
|
3537
|
+
const k = assertedLaunderKind(node, aliasMap);
|
|
3538
|
+
return k === "any" || k === "never" || k === "unknown" || k === "const";
|
|
3480
3539
|
};
|
|
3481
3540
|
const visit = (node) => {
|
|
3482
3541
|
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
3483
|
-
if (!isDoubleCast(node) && !isRow4OrHonest(node)) record("type", node);
|
|
3542
|
+
if (!isDoubleCast(node, aliasMap) && !isRow4OrHonest(node) && !launderInner.has(node)) record("type", node);
|
|
3484
3543
|
} else if (ts.isNonNullExpression(node)) {
|
|
3485
3544
|
record("non-null", node);
|
|
3486
3545
|
}
|
|
@@ -3515,7 +3574,7 @@ function unmatchedAfter(before, after) {
|
|
|
3515
3574
|
}
|
|
3516
3575
|
return out3;
|
|
3517
3576
|
}
|
|
3518
|
-
var RULE2, DECLARATION, OUT_OF_BUDGET_PATH, GENERATED_HEADER, HEADER_LINES, tsCastGrowth;
|
|
3577
|
+
var RULE2, DECLARATION, OUT_OF_BUDGET_PATH, GENERATED_HEADER, HEADER_LINES, DOUBLE_CAST_INNER, EMPTY_ALIASES, tsCastGrowth;
|
|
3519
3578
|
var init_ts_cast_growth = __esm({
|
|
3520
3579
|
"src/detectors/ts-cast-growth.ts"() {
|
|
3521
3580
|
"use strict";
|
|
@@ -3529,6 +3588,8 @@ var init_ts_cast_growth = __esm({
|
|
|
3529
3588
|
OUT_OF_BUDGET_PATH = /(?:^|\/)(?:node_modules|vendor|third_party|dist|build|generated|__generated__)\/|\.(?:generated|gen)\.[cm]?[jt]sx?$/;
|
|
3530
3589
|
GENERATED_HEADER = /@generated\b|\bAUTO-?GENERATED\b|\bDO NOT EDIT\b/i;
|
|
3531
3590
|
HEADER_LINES = 20;
|
|
3591
|
+
DOUBLE_CAST_INNER = /* @__PURE__ */ new Set(["any", "never", "unknown", "empty", "objectish"]);
|
|
3592
|
+
EMPTY_ALIASES = /* @__PURE__ */ new Map();
|
|
3532
3593
|
tsCastGrowth = {
|
|
3533
3594
|
id: RULE2,
|
|
3534
3595
|
surface: ["file"],
|
|
@@ -3570,13 +3631,51 @@ var init_ts_cast_growth = __esm({
|
|
|
3570
3631
|
});
|
|
3571
3632
|
|
|
3572
3633
|
// src/detectors/ts-any-cast.ts
|
|
3573
|
-
function
|
|
3574
|
-
|
|
3634
|
+
function commentIsLiveDirective(text) {
|
|
3635
|
+
let inner = text;
|
|
3636
|
+
if (inner.startsWith("//")) inner = inner.replace(/^\/+/, "");
|
|
3637
|
+
else inner = inner.replace(/^\/\*+/, "").replace(/\*+\/$/, "");
|
|
3638
|
+
for (const raw of inner.split("\n")) {
|
|
3639
|
+
if (DIRECTIVE_START.test(raw.replace(/^\s*\*+/, "").trim())) return true;
|
|
3640
|
+
}
|
|
3641
|
+
return false;
|
|
3642
|
+
}
|
|
3643
|
+
function countLiveDirectives(src) {
|
|
3644
|
+
let n = 0;
|
|
3645
|
+
try {
|
|
3646
|
+
const scanner = ts.createScanner(
|
|
3647
|
+
ts.ScriptTarget.Latest,
|
|
3648
|
+
/* skipTrivia */
|
|
3649
|
+
false,
|
|
3650
|
+
ts.LanguageVariant.Standard,
|
|
3651
|
+
src
|
|
3652
|
+
);
|
|
3653
|
+
let tok = scanner.scan();
|
|
3654
|
+
while (tok !== ts.SyntaxKind.EndOfFileToken) {
|
|
3655
|
+
if (tok === ts.SyntaxKind.SingleLineCommentTrivia || tok === ts.SyntaxKind.MultiLineCommentTrivia) {
|
|
3656
|
+
if (commentIsLiveDirective(scanner.getTokenText())) n++;
|
|
3657
|
+
}
|
|
3658
|
+
tok = scanner.scan();
|
|
3659
|
+
}
|
|
3660
|
+
} catch {
|
|
3661
|
+
}
|
|
3662
|
+
return n;
|
|
3663
|
+
}
|
|
3664
|
+
function countAny(src, path) {
|
|
3665
|
+
const r = { cast: 0, never: 0, broad: 0, double: 0, suppress: 0, jsdoc: 0 };
|
|
3666
|
+
r.suppress = countLiveDirectives(src);
|
|
3667
|
+
r.jsdoc = JS_FILE.test(path) ? countMatches(src, JSDOC_ANY_CAST) : 0;
|
|
3575
3668
|
try {
|
|
3576
3669
|
const sf = parseSource("f.ts", src);
|
|
3577
3670
|
if (!sf) return r;
|
|
3671
|
+
const aliasMap = buildAliasMap(sf);
|
|
3578
3672
|
const visit = (node) => {
|
|
3579
|
-
if (
|
|
3673
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
3674
|
+
const k = assertedLaunderKind(node, aliasMap);
|
|
3675
|
+
if (k === "any") r.cast++;
|
|
3676
|
+
else if (k === "never") r.never++;
|
|
3677
|
+
if (isDoubleCast(node, aliasMap)) r.double++;
|
|
3678
|
+
}
|
|
3580
3679
|
if (node.kind === ts.SyntaxKind.AnyKeyword) {
|
|
3581
3680
|
let target = node;
|
|
3582
3681
|
let p = target.parent;
|
|
@@ -3585,8 +3684,7 @@ function countAny(src) {
|
|
|
3585
3684
|
p = p.parent;
|
|
3586
3685
|
}
|
|
3587
3686
|
const isCast = p && ts.isAsExpression(p) && p.type === target || p && ts.isTypeAssertionExpression(p) && p.type === target;
|
|
3588
|
-
if (isCast) r.
|
|
3589
|
-
else r.broad++;
|
|
3687
|
+
if (!isCast) r.broad++;
|
|
3590
3688
|
}
|
|
3591
3689
|
ts.forEachChild(node, visit);
|
|
3592
3690
|
};
|
|
@@ -3595,22 +3693,33 @@ function countAny(src) {
|
|
|
3595
3693
|
}
|
|
3596
3694
|
return r;
|
|
3597
3695
|
}
|
|
3598
|
-
function
|
|
3696
|
+
function lineHasLiveDirective(line) {
|
|
3697
|
+
return countLiveDirectives(line) > 0 || FRAGMENT_DIRECTIVE.test(line);
|
|
3698
|
+
}
|
|
3699
|
+
function lineHasRow4Cast(line, aliasMap) {
|
|
3599
3700
|
const sf = parseSource("line.ts", line);
|
|
3600
3701
|
if (!sf) return false;
|
|
3601
3702
|
let found = false;
|
|
3602
3703
|
const visit = (n) => {
|
|
3603
3704
|
if (found) return;
|
|
3604
|
-
if (
|
|
3605
|
-
|
|
3606
|
-
|
|
3705
|
+
if (ts.isAsExpression(n) || ts.isTypeAssertionExpression(n)) {
|
|
3706
|
+
const k = assertedLaunderKind(n, aliasMap);
|
|
3707
|
+
if (k === "any" || k === "never" || isDoubleCast(n, aliasMap)) {
|
|
3708
|
+
found = true;
|
|
3709
|
+
return;
|
|
3710
|
+
}
|
|
3607
3711
|
}
|
|
3608
3712
|
ts.forEachChild(n, visit);
|
|
3609
3713
|
};
|
|
3610
3714
|
visit(sf);
|
|
3611
3715
|
return found;
|
|
3612
3716
|
}
|
|
3613
|
-
|
|
3717
|
+
function aliasMapForAddedLines(c) {
|
|
3718
|
+
const snippet = addedLines(c).map((l) => l.content).join("\n");
|
|
3719
|
+
const sf = parseSource("lines.ts", snippet);
|
|
3720
|
+
return sf ? buildAliasMap(sf) : /* @__PURE__ */ new Map();
|
|
3721
|
+
}
|
|
3722
|
+
var BLOCK_RULE, WARN_RULE, JSDOC_ANY_CAST, JS_FILE, countMatches, DIRECTIVE_START, NARROW_LINE, BROAD_LINE, FRAGMENT_DIRECTIVE, BLOCK_REMEDIATION, WARN_REMEDIATION, tsAnyCast;
|
|
3614
3723
|
var init_ts_any_cast = __esm({
|
|
3615
3724
|
"src/detectors/ts-any-cast.ts"() {
|
|
3616
3725
|
"use strict";
|
|
@@ -3622,13 +3731,13 @@ var init_ts_any_cast = __esm({
|
|
|
3622
3731
|
init_ts_cast_growth();
|
|
3623
3732
|
BLOCK_RULE = "ts-any-cast";
|
|
3624
3733
|
WARN_RULE = "ts-any-launder";
|
|
3625
|
-
|
|
3626
|
-
SUPPRESS = /@ts-(?:ignore|expect-error|nocheck)\b/g;
|
|
3627
|
-
JSDOC_ANY_CAST = /\/\*\*\s*@type\s*\{\s*any\s*\}\s*\*\/\s*\(/g;
|
|
3734
|
+
JSDOC_ANY_CAST = /\/\*\*\s*@type\s*\{\s*(?:any|\*|\?)\s*\}\s*\*\/\s*\(/g;
|
|
3628
3735
|
JS_FILE = /\.(?:js|jsx|mjs|cjs)$/;
|
|
3629
3736
|
countMatches = (s, re) => (s.match(re) || []).length;
|
|
3630
|
-
|
|
3737
|
+
DIRECTIVE_START = /^@ts-(?:ignore|expect-error|nocheck)\b/;
|
|
3738
|
+
NARROW_LINE = /\bas\s+\(*\s*any\b|<\s*any\s*>/;
|
|
3631
3739
|
BROAD_LINE = /:\s*any\b|<[^<>]*\bany\b[^<>]*>/;
|
|
3740
|
+
FRAGMENT_DIRECTIVE = /^\s*\*?\s*@ts-(?:ignore|expect-error|nocheck)\b/;
|
|
3632
3741
|
BLOCK_REMEDIATION = "Fix the underlying type instead of silencing the checker; do not cast to `any`.";
|
|
3633
3742
|
WARN_REMEDIATION = "Prefer a precise type or `unknown` + a guard over `any` here \u2014 flagged for review.";
|
|
3634
3743
|
tsAnyCast = {
|
|
@@ -3642,15 +3751,17 @@ var init_ts_any_cast = __esm({
|
|
|
3642
3751
|
const inTest = protectedCategory(c.path, policy) === "tests";
|
|
3643
3752
|
if (c.after != null && c.op !== "delete") {
|
|
3644
3753
|
const before = c.before ?? "";
|
|
3645
|
-
const a = countAny(c.after);
|
|
3646
|
-
const b = countAny(before);
|
|
3754
|
+
const a = countAny(c.after, c.path);
|
|
3755
|
+
const b = countAny(before, c.path);
|
|
3647
3756
|
const dCast = a.cast - b.cast;
|
|
3757
|
+
const dNever = a.never - b.never;
|
|
3648
3758
|
const dBroad = a.broad - b.broad;
|
|
3649
|
-
const dDouble =
|
|
3650
|
-
const dSuppr =
|
|
3651
|
-
const dJsdoc =
|
|
3759
|
+
const dDouble = a.double - b.double;
|
|
3760
|
+
const dSuppr = a.suppress - b.suppress;
|
|
3761
|
+
const dJsdoc = a.jsdoc - b.jsdoc;
|
|
3652
3762
|
const blockReasons = [];
|
|
3653
3763
|
if (dCast > 0) blockReasons.push("`as any` cast");
|
|
3764
|
+
if (dNever > 0) blockReasons.push("`as never` cast");
|
|
3654
3765
|
if (dDouble > 0) blockReasons.push("`as unknown as` double cast");
|
|
3655
3766
|
if (dSuppr > 0) blockReasons.push("@ts-ignore/@ts-expect-error/@ts-nocheck suppression");
|
|
3656
3767
|
if (dJsdoc > 0) blockReasons.push("JSDoc `@type {any}` cast");
|
|
@@ -3679,8 +3790,9 @@ var init_ts_any_cast = __esm({
|
|
|
3679
3790
|
continue;
|
|
3680
3791
|
}
|
|
3681
3792
|
const jsdocCast = JS_FILE.test(c.path) ? new RegExp(JSDOC_ANY_CAST.source) : null;
|
|
3793
|
+
const aliasMap = aliasMapForAddedLines(c);
|
|
3682
3794
|
for (const l of addedLines(c)) {
|
|
3683
|
-
if (NARROW_LINE.test(l.content) ||
|
|
3795
|
+
if (NARROW_LINE.test(l.content) || lineHasRow4Cast(l.content, aliasMap) || lineHasLiveDirective(l.content) || jsdocCast && jsdocCast.test(l.content)) {
|
|
3684
3796
|
out3.push(
|
|
3685
3797
|
makeFinding(inTest ? WARN_RULE : BLOCK_RULE, policy, {
|
|
3686
3798
|
file: c.path,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.25.
|
|
3
|
+
"version": "2.25.1",
|
|
4
4
|
"description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "hexrift",
|