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.
Files changed (2) hide show
  1. package/dist/cli/index.js +139 -27
  2. 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 isDoubleCast(node) {
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
- return (ts.isAsExpression(inner) || ts.isTypeAssertionExpression(inner)) && assertedType(inner).kind === ts.SyntaxKind.UnknownKeyword;
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 type = assertedType(node);
3478
- if (type.kind === ts.SyntaxKind.AnyKeyword || type.kind === ts.SyntaxKind.UnknownKeyword) return true;
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 countAny(src) {
3574
- const r = { cast: 0, broad: 0, double: 0 };
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 ((ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) && isDoubleCast(node)) r.double++;
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.cast++;
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 lineHasDoubleCast(line) {
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 ((ts.isAsExpression(n) || ts.isTypeAssertionExpression(n)) && isDoubleCast(n)) {
3605
- found = true;
3606
- return;
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
- var BLOCK_RULE, WARN_RULE, DOUBLE_CAST, SUPPRESS, JSDOC_ANY_CAST, JS_FILE, countMatches, NARROW_LINE, BROAD_LINE, BLOCK_REMEDIATION, WARN_REMEDIATION, tsAnyCast;
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
- DOUBLE_CAST = /\bas\s+\(*\s*unknown\s*\)*\s+as\b/g;
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
- NARROW_LINE = /\bas\s+\(*\s*any\b|<\s*any\s*>|@ts-(?:ignore|expect-error|nocheck)\b/;
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 = Math.max(a.double - b.double, countMatches(c.after, DOUBLE_CAST) - countMatches(before, DOUBLE_CAST));
3650
- const dSuppr = countMatches(c.after, SUPPRESS) - countMatches(before, SUPPRESS);
3651
- const dJsdoc = JS_FILE.test(c.path) ? countMatches(c.after, JSDOC_ANY_CAST) - countMatches(before, JSDOC_ANY_CAST) : 0;
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) || lineHasDoubleCast(l.content) || jsdocCast && jsdocCast.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.0",
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",