tamperward 2.24.3 → 2.24.5

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 +572 -42
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1536,11 +1536,86 @@ function gitSubcommand(toks) {
1536
1536
  }
1537
1537
  return null;
1538
1538
  }
1539
- var GIT_OPT_WITH_ARG;
1539
+ function shellWord(t) {
1540
+ let out3 = "";
1541
+ let single = false;
1542
+ let double = false;
1543
+ for (let i = 0; i < t.length; i++) {
1544
+ const ch = t[i];
1545
+ if (single) {
1546
+ if (ch === "'") single = false;
1547
+ else out3 += ch;
1548
+ continue;
1549
+ }
1550
+ if (ch === "\\" && i + 1 < t.length && (!double || /["\\$`]/.test(t[i + 1]))) {
1551
+ out3 += t[++i];
1552
+ continue;
1553
+ }
1554
+ if (ch === "'" && !double) {
1555
+ single = true;
1556
+ continue;
1557
+ }
1558
+ if (ch === '"') {
1559
+ double = !double;
1560
+ continue;
1561
+ }
1562
+ out3 += ch;
1563
+ }
1564
+ return out3;
1565
+ }
1566
+ function expandBraces(t) {
1567
+ const m = t.match(/^([^{]*)\{([^{}]*)\}(.*)$/);
1568
+ if (!m) return [t];
1569
+ return m[2].split(",").flatMap((alt) => expandBraces(`${m[1]}${alt}${m[3]}`));
1570
+ }
1571
+ function globToRegExp(pattern) {
1572
+ const classes = [];
1573
+ const src = pattern.replace(/^\.\//, "").replace(/\[([^\][]*)\]/g, (_, body) => {
1574
+ classes.push(`[${body.replace(/^!/, "^").replace(/\\/g, "\\\\")}]`);
1575
+ return "";
1576
+ }).replace(/[.+^${}()|\\[\]]/g, "\\$&").replace(/\*\*\/?/g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\0/g, "(?:.*/)?").replace(//g, () => classes.shift() ?? "");
1577
+ return new RegExp(`^(?:\\./)?${src}/?$`);
1578
+ }
1579
+ function expandGlob(t, listing) {
1580
+ const alts = expandBraces(t);
1581
+ if (!alts.some((a) => GLOB_CHARS.test(a))) return alts;
1582
+ const out3 = /* @__PURE__ */ new Set();
1583
+ for (const alt of alts) {
1584
+ if (!GLOB_CHARS.test(alt)) {
1585
+ out3.add(alt);
1586
+ continue;
1587
+ }
1588
+ let re;
1589
+ try {
1590
+ re = globToRegExp(alt);
1591
+ } catch {
1592
+ out3.add(alt);
1593
+ continue;
1594
+ }
1595
+ const depth = alt.replace(/^\.\//, "").replace(/\/+$/, "").split("/").length;
1596
+ for (const f of listing) {
1597
+ if (re.test(f)) {
1598
+ out3.add(f);
1599
+ continue;
1600
+ }
1601
+ const parts = f.split("/");
1602
+ if (parts.length > depth && !alt.includes("**")) {
1603
+ const dir = parts.slice(0, depth).join("/");
1604
+ if (re.test(dir)) out3.add(dir);
1605
+ }
1606
+ }
1607
+ }
1608
+ return [...out3];
1609
+ }
1610
+ function isGlob(t) {
1611
+ return GLOB_CHARS.test(t) || /\{[^{}]*\}/.test(t);
1612
+ }
1613
+ var GIT_OPT_WITH_ARG, GLOB_CHARS;
1540
1614
  var init_command = __esm({
1541
1615
  "src/detectors/command.ts"() {
1542
1616
  "use strict";
1543
1617
  GIT_OPT_WITH_ARG = /* @__PURE__ */ new Set(["-c", "-C", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env"]);
1618
+ GLOB_CHARS = /[*?[]/;
1544
1619
  }
1545
1620
  });
1546
1621
 
@@ -2850,16 +2925,20 @@ function shellWritesHook(seg, toks, policy, ctx) {
2850
2925
  return null;
2851
2926
  }
2852
2927
  }
2853
- function xargsWritesHook(toks, fed, policy, ctx) {
2928
+ function xargsCommand(toks) {
2854
2929
  const at = toks.findIndex((t) => t === "xargs" || t.endsWith("/xargs"));
2855
2930
  if (at === -1) return null;
2931
+ let i = at + 1;
2932
+ while (i < toks.length && toks[i].startsWith("-")) i += /^-(?:I|n|L|P|d|a|s)$/.test(toks[i]) ? 2 : 1;
2933
+ return { cmd: (toks[i] ?? "").replace(/^.*\//, ""), args: toks.slice(i + 1), at: i };
2934
+ }
2935
+ function xargsWritesHook(toks, fed, policy, ctx) {
2936
+ const run = xargsCommand(toks);
2937
+ if (run === null) return null;
2856
2938
  const { hookish } = hookTests(policy, ctx);
2857
2939
  const fedHook = fed.some(hookish);
2858
2940
  if (!fedHook) return null;
2859
- let i = at + 1;
2860
- while (i < toks.length && toks[i].startsWith("-")) i += /^-(?:I|n|L|P|d|a|s)$/.test(toks[i]) ? 2 : 1;
2861
- const cmd = (toks[i] ?? "").replace(/^.*\//, "");
2862
- const args = toks.slice(i + 1);
2941
+ const { cmd, args, at: i } = run;
2863
2942
  if (["rm", "unlink", "shred", "truncate", "mv", "tee", "sponge"].includes(cmd)) return `xargs ${cmd} rewrites or removes a protected hook named earlier in the line`;
2864
2943
  if (cmd === "chmod" && chmodDropsExec(toks.slice(i))) return "xargs chmod removes execute permission from a hook named earlier in the line";
2865
2944
  if ((cmd === "sed" || cmd === "perl") && args.some((a) => /^-[A-Za-z]*i|^--in-place/.test(a))) return `xargs ${cmd} -i rewrites a protected hook named earlier in the line`;
@@ -3819,6 +3898,47 @@ function matchesOutsideString(p, content, lang) {
3819
3898
  }
3820
3899
  return false;
3821
3900
  }
3901
+ function afterLinesOf(c) {
3902
+ const out3 = /* @__PURE__ */ new Map();
3903
+ if (c.after != null) {
3904
+ c.after.split("\n").forEach((l, i) => out3.set(i + 1, l));
3905
+ return out3;
3906
+ }
3907
+ for (const h of c.hunks) for (const l of h.lines) if (l.newLine != null && l.type !== "del") out3.set(l.newLine, l.content);
3908
+ return out3;
3909
+ }
3910
+ function contextSkipHits(c, lang) {
3911
+ if (lang !== "go" && lang !== "rs") return [];
3912
+ const lines = afterLinesOf(c);
3913
+ const hits = [];
3914
+ for (const l of addedLines(c)) {
3915
+ if (l.newLine == null) continue;
3916
+ if (lang === "go") {
3917
+ const m = GO_TEST_MAIN.exec(l.content);
3918
+ if (!m) continue;
3919
+ const runs = new RegExp("\\b" + m[1] + "\\.Run\\(\\)");
3920
+ let ran = false;
3921
+ for (const [, text] of lines) if (runs.test(text) && !isCommentLine(text.trim(), lang)) ran = true;
3922
+ if (!ran) hits.push({ line: l.newLine, why: `a TestMain that never calls ${m[1]}.Run() (no test in the package runs)`, evidence: l.content.trim() });
3923
+ continue;
3924
+ }
3925
+ const cfg = RS_CFG.exec(l.content);
3926
+ if (!cfg || cfg[1].trim() === "test") continue;
3927
+ let first = l.newLine;
3928
+ while (RS_ATTR.test(lines.get(first - 1) ?? "")) first--;
3929
+ let at = l.newLine + 1;
3930
+ while (RS_ATTR.test(lines.get(at) ?? "") || lines.has(at) && !/\S/.test(lines.get(at) ?? "")) at++;
3931
+ const item = lines.get(at) ?? "";
3932
+ let besideTest = false;
3933
+ for (let i = first; i < at; i++) if (RS_TEST_ATTR.test(lines.get(i) ?? "")) besideTest = true;
3934
+ if (RS_MOD.test(item)) {
3935
+ hits.push({ line: l.newLine, why: "a #[cfg(\u2026)] on the test module (the tests are compiled out under every ordinary configuration)", evidence: l.content.trim() });
3936
+ } else if (besideTest && RS_FN.test(item)) {
3937
+ hits.push({ line: l.newLine, why: "a #[cfg(\u2026)] beside #[test] (the test is compiled out under every ordinary configuration)", evidence: l.content.trim() });
3938
+ }
3939
+ }
3940
+ return hits;
3941
+ }
3822
3942
  function scriptKind2(path) {
3823
3943
  if (/\.tsx$/i.test(path)) return ts.ScriptKind.TSX;
3824
3944
  if (/\.jsx$/i.test(path)) return ts.ScriptKind.JSX;
@@ -4147,7 +4267,7 @@ function astSkipHits(c, resolvers) {
4147
4267
  }
4148
4268
  return { hits, authoritative: true, unowned: analysis.unowned };
4149
4269
  }
4150
- var RULE4, acc, JS_RUNNER, JS_MOD, GO_SKIP_PATTERNS, PY_MARK, PY_CONFTEST_PATTERNS, PY_MARK_ALIAS, PY_DECORATOR, PATTERNS2, JS_RUNNERS, JS_DEFAULT_RUNNER_MODULES, JS_TEST_MODULES, RUNNER2, NON_RUNNER, UNKNOWN, withCause, MODULE_EXTENSIONS, RunnerResolver, ModuleBinder, NO_AST, testSkip;
4270
+ var RULE4, acc, JS_RUNNER, JS_MOD, JVM_CONDITIONAL, GO_SKIP_PATTERNS, PY_MARK, PY_CONFTEST_PATTERNS, PY_MARK_ALIAS, PY_DECORATOR, PATTERNS2, JS_RUNNERS, JS_DEFAULT_RUNNER_MODULES, JS_TEST_MODULES, GO_TEST_MAIN, RS_CFG, RS_ATTR, RS_TEST_ATTR, RS_MOD, RS_FN, RUNNER2, NON_RUNNER, UNKNOWN, withCause, MODULE_EXTENSIONS, RunnerResolver, ModuleBinder, NO_AST, testSkip;
4151
4271
  var init_test_skip = __esm({
4152
4272
  "src/detectors/test-skip.ts"() {
4153
4273
  "use strict";
@@ -4162,6 +4282,7 @@ var init_test_skip = __esm({
4162
4282
  acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
4163
4283
  JS_RUNNER = "\\b(?:it|test|describe|suite)";
4164
4284
  JS_MOD = "concurrent|sequential|shuffle|serial|parallel";
4285
+ JVM_CONDITIONAL = /@(?:[\w.]+\.)?(?:EnabledIf|DisabledIf)\w*\b/;
4165
4286
  GO_SKIP_PATTERNS = [
4166
4287
  { re: /\b\w+\.Skip(?:f|Now)?\(/, why: "a runtime t.Skip()/Skipf()/SkipNow() call" },
4167
4288
  { re: /\bT\(\)\.Skip(?:f|Now)?\(/, why: "a runtime T().Skip()/Skipf()/SkipNow() call (testify suite)" }
@@ -4228,7 +4349,10 @@ var init_test_skip = __esm({
4228
4349
  ...GO_SKIP_PATTERNS,
4229
4350
  // #441: any receiver, and testify's T()
4230
4351
  { re: /\bif\s+testing\.Short\(\)/, why: "a testing.Short() guard (the body is skipped under -short)" },
4231
- { re: /^\s*\/\/\s*(?:go:build|\+build)\s+ignore\b/, why: "a build-ignore constraint (the file is excluded from the test run)", comment: true }
4352
+ // Any constraint on a `_test.go` — `ignore`, `integration`, `never`, `!ci` — takes
4353
+ // the file out of the default `go test ./...` run; the tag's name is the agent's
4354
+ // to choose (#431).
4355
+ { re: /^\s*\/\/\s*(?:go:build|\+build)\b/, why: "a build constraint on a test file (the file is excluded from the default test run)", comment: true }
4232
4356
  ],
4233
4357
  rs: [
4234
4358
  { re: /#\[ignore\b/, why: "an #[ignore] attribute (the test no longer runs by default)" },
@@ -4239,14 +4363,19 @@ var init_test_skip = __esm({
4239
4363
  { re: /(?:^|[\s;{(])x(?:it|describe|context|specify|example)\s*[('"]/, why: "an x-prefixed disabled example" },
4240
4364
  { re: /(?:^|[\s;{(])f(?:it|describe|context)\s*[('"]/, why: "an f-prefixed focused example (narrows the suite)" },
4241
4365
  { re: /\b(?:skip|pending):\s*(?:true|['"])/, why: "a skip/pending metadata flag" },
4242
- { re: /^\s*(?:it|specify|example)\s+(['"])(?:(?!\1).)*\1\s*$/, why: "an example with no block (RSpec reports it as pending)" }
4366
+ { re: /^\s*(?:it|specify|example)\s+(['"])(?:(?!\1).)*\1\s*$/, why: "an example with no block (RSpec reports it as pending)" },
4367
+ // `it 'x', if: false do` — RSpec's conditional filter with a constant that never
4368
+ // lets the example run (#431). `if: ENV['SLOW']` is a real condition and passes.
4369
+ { re: /(?:^|[\s;{(])(?:it|specify|example|describe|context|scenario|feature)\s*[('"].*(?:\bif:\s*false\b|\bunless:\s*true\b|:if\s*=>\s*false\b|:unless\s*=>\s*true\b)/, why: "a constant-false if:/unless: filter (the example never runs)" }
4243
4370
  ],
4244
4371
  java: [
4245
4372
  { re: /@(?:[\w.]+\.)?(?:Ignore|Disabled)\b/, why: "an @Ignore/@Disabled annotation" },
4373
+ { re: JVM_CONDITIONAL, why: "a conditional-execution annotation (@EnabledIf\u2026 / @DisabledIf\u2026: the test runs only when the condition allows)" },
4246
4374
  { re: /\bassume(?:True\(\s*false|False\(\s*true)\s*[,)]/, why: "an assumption that never holds (the test aborts as skipped)" }
4247
4375
  ],
4248
4376
  kt: [
4249
4377
  { re: /@(?:[\w.]+\.)?(?:Ignore|Disabled)\b/, why: "an @Ignore/@Disabled annotation" },
4378
+ { re: JVM_CONDITIONAL, why: "a conditional-execution annotation (@EnabledIf\u2026 / @DisabledIf\u2026: the test runs only when the condition allows)" },
4250
4379
  { re: /\bassume(?:True\(\s*false|False\(\s*true)\s*[,)]/, why: "an assumption that never holds (the test aborts as skipped)" }
4251
4380
  ],
4252
4381
  php: [{ re: /\bmarkTest(?:Skipped|Incomplete)\(/, why: "a markTestSkipped()/markTestIncomplete() call" }],
@@ -4268,6 +4397,12 @@ var init_test_skip = __esm({
4268
4397
  "bun:test",
4269
4398
  "@playwright/test"
4270
4399
  ]);
4400
+ GO_TEST_MAIN = /^func\s+TestMain\s*\(\s*(\w+)\s+\*testing\.M\s*\)/;
4401
+ RS_CFG = /^\s*#\[cfg\((.*)\)\]\s*$/;
4402
+ RS_ATTR = /^\s*#!?\[/;
4403
+ RS_TEST_ATTR = /^\s*#\[[\w:]*test\b/;
4404
+ RS_MOD = /^\s*(?:pub(?:\([^)]*\))?\s+)?mod\s+\w+/;
4405
+ RS_FN = /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+\w+/;
4271
4406
  RUNNER2 = { kind: "runner", causes: [] };
4272
4407
  NON_RUNNER = { kind: "non-runner" };
4273
4408
  UNKNOWN = { kind: "unknown" };
@@ -4559,6 +4694,18 @@ var init_test_skip = __esm({
4559
4694
  );
4560
4695
  }
4561
4696
  }
4697
+ for (const hit of contextSkipHits(c, lang)) {
4698
+ astHitLines.add(hit.line);
4699
+ out3.push(
4700
+ makeFinding(RULE4, policy, {
4701
+ file: c.path,
4702
+ line: hit.line,
4703
+ message: `Test skipped or narrowed: ${hit.why}.`,
4704
+ evidence: hit.evidence,
4705
+ remediation: "Make the test pass rather than skipping it. If it is genuinely obsolete, a human must sign off."
4706
+ })
4707
+ );
4708
+ }
4562
4709
  if (lang === "py") {
4563
4710
  for (const hit of pyAliasDecoratorHits(c)) {
4564
4711
  if (hit.line != null) astHitLines.add(hit.line);
@@ -7957,6 +8104,289 @@ They WILL FAIL in v10.0.0
7957
8104
  }
7958
8105
  });
7959
8106
 
8107
+ // src/detectors/reachability.ts
8108
+ function constTruth(expr) {
8109
+ const v = constValue(expr);
8110
+ if (v === null) return null;
8111
+ return v.kind === "unknown" ? v.truthy : Boolean(v.value);
8112
+ }
8113
+ function constValue(expr) {
8114
+ if (ts.isParenthesizedExpression(expr)) return constValue(expr.expression);
8115
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) return { kind: "value", value: true };
8116
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) return { kind: "value", value: false };
8117
+ if (expr.kind === ts.SyntaxKind.NullKeyword) return { kind: "value", value: null };
8118
+ if (ts.isIdentifier(expr)) {
8119
+ if (expr.text === "undefined") return { kind: "value", value: void 0 };
8120
+ if (expr.text === "NaN") return { kind: "value", value: NaN };
8121
+ return null;
8122
+ }
8123
+ if (ts.isNumericLiteral(expr)) return { kind: "value", value: Number(expr.text) };
8124
+ if (ts.isBigIntLiteral(expr)) return { kind: "unknown", truthy: !/^0+n$/.test(expr.text) };
8125
+ if (ts.isStringLiteralLike(expr)) return { kind: "value", value: expr.text };
8126
+ if (ts.isVoidExpression(expr)) return { kind: "value", value: void 0 };
8127
+ if (ts.isPrefixUnaryExpression(expr)) {
8128
+ if (expr.operator === ts.SyntaxKind.ExclamationToken) {
8129
+ const inner = constTruth(expr.operand);
8130
+ return inner === null ? null : { kind: "value", value: !inner };
8131
+ }
8132
+ if (expr.operator === ts.SyntaxKind.MinusToken || expr.operator === ts.SyntaxKind.PlusToken) {
8133
+ const inner = constValue(expr.operand);
8134
+ if (inner === null || inner.kind !== "value" || typeof inner.value !== "number") return null;
8135
+ return { kind: "value", value: expr.operator === ts.SyntaxKind.MinusToken ? -inner.value : inner.value };
8136
+ }
8137
+ return null;
8138
+ }
8139
+ if (ts.isBinaryExpression(expr)) {
8140
+ const op = expr.operatorToken.kind;
8141
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
8142
+ const l2 = constTruth(expr.left);
8143
+ const r2 = constTruth(expr.right);
8144
+ const and = op === ts.SyntaxKind.AmpersandAmpersandToken;
8145
+ if (and) {
8146
+ if (l2 === false || r2 === false) return { kind: "unknown", truthy: false };
8147
+ if (l2 === true) return r2 === null ? null : { kind: "unknown", truthy: r2 };
8148
+ if (r2 === true) return l2 === null ? null : { kind: "unknown", truthy: l2 };
8149
+ return null;
8150
+ }
8151
+ if (l2 === true || r2 === true) return { kind: "unknown", truthy: true };
8152
+ if (l2 === false) return r2 === null ? null : { kind: "unknown", truthy: r2 };
8153
+ if (r2 === false) return l2 === null ? null : { kind: "unknown", truthy: l2 };
8154
+ return null;
8155
+ }
8156
+ const l = constValue(expr.left);
8157
+ const r = constValue(expr.right);
8158
+ if (l === null || r === null || l.kind !== "value" || r.kind !== "value") return null;
8159
+ const a = l.value;
8160
+ const b = r.value;
8161
+ switch (op) {
8162
+ case ts.SyntaxKind.EqualsEqualsEqualsToken:
8163
+ return { kind: "value", value: a === b };
8164
+ case ts.SyntaxKind.ExclamationEqualsEqualsToken:
8165
+ return { kind: "value", value: a !== b };
8166
+ case ts.SyntaxKind.EqualsEqualsToken:
8167
+ return { kind: "value", value: a == b };
8168
+ case ts.SyntaxKind.ExclamationEqualsToken:
8169
+ return { kind: "value", value: a != b };
8170
+ default:
8171
+ break;
8172
+ }
8173
+ if (typeof a === "number" && typeof b === "number") {
8174
+ switch (op) {
8175
+ case ts.SyntaxKind.LessThanToken:
8176
+ return { kind: "value", value: a < b };
8177
+ case ts.SyntaxKind.LessThanEqualsToken:
8178
+ return { kind: "value", value: a <= b };
8179
+ case ts.SyntaxKind.GreaterThanToken:
8180
+ return { kind: "value", value: a > b };
8181
+ case ts.SyntaxKind.GreaterThanEqualsToken:
8182
+ return { kind: "value", value: a >= b };
8183
+ default:
8184
+ break;
8185
+ }
8186
+ }
8187
+ return null;
8188
+ }
8189
+ return null;
8190
+ }
8191
+ function isTerminal(stmt) {
8192
+ return ts.isReturnStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBreakStatement(stmt) || ts.isContinueStatement(stmt);
8193
+ }
8194
+ function namedFunctionBody(node) {
8195
+ if (ts.isFunctionDeclaration(node) && node.name && node.body) return { name: node.name.text, body: node.body, owner: node };
8196
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
8197
+ let init = node.initializer;
8198
+ while (ts.isParenthesizedExpression(init) || ts.isAsExpression(init) || ts.isSatisfiesExpression(init)) init = init.expression;
8199
+ if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) return { name: node.name.text, body: init.body, owner: node };
8200
+ }
8201
+ return null;
8202
+ }
8203
+ function isExportedDeclaration(node) {
8204
+ const stmt = ts.isVariableDeclaration(node) ? node.parent.parent : node;
8205
+ if (ts.isExportAssignment(stmt)) return true;
8206
+ return ts.canHaveModifiers(stmt) && (ts.getModifiers(stmt) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
8207
+ }
8208
+ function unreachableNodes(sf) {
8209
+ const dead = /* @__PURE__ */ new Set();
8210
+ const markBranches = (node) => {
8211
+ if (ts.isIfStatement(node)) {
8212
+ const t = constTruth(node.expression);
8213
+ if (t === false) dead.add(node.thenStatement);
8214
+ else if (t === true && node.elseStatement) dead.add(node.elseStatement);
8215
+ } else if (ts.isWhileStatement(node) || ts.isForStatement(node)) {
8216
+ const cond = ts.isWhileStatement(node) ? node.expression : node.condition;
8217
+ if (cond && constTruth(cond) === false) dead.add(node.statement);
8218
+ } else if (ts.isConditionalExpression(node)) {
8219
+ const t = constTruth(node.condition);
8220
+ if (t === false) dead.add(node.whenTrue);
8221
+ else if (t === true) dead.add(node.whenFalse);
8222
+ } else if (ts.isBinaryExpression(node)) {
8223
+ const op = node.operatorToken.kind;
8224
+ const l = constTruth(node.left);
8225
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken && l === false || op === ts.SyntaxKind.BarBarToken && l === true) {
8226
+ dead.add(node.right);
8227
+ }
8228
+ }
8229
+ if (ts.isSourceFile(node) || ts.isBlock(node) || ts.isModuleBlock(node) || ts.isCaseClause(node) || ts.isDefaultClause(node)) {
8230
+ let ended = false;
8231
+ for (const stmt of node.statements) {
8232
+ if (ended && !ts.isFunctionDeclaration(stmt)) dead.add(stmt);
8233
+ if (isTerminal(stmt)) ended = true;
8234
+ }
8235
+ }
8236
+ ts.forEachChild(node, markBranches);
8237
+ };
8238
+ markBranches(sf);
8239
+ const candidates = [];
8240
+ const collect2 = (node) => {
8241
+ const fn = namedFunctionBody(node);
8242
+ if (fn && !isExportedDeclaration(fn.owner)) candidates.push(fn);
8243
+ ts.forEachChild(node, collect2);
8244
+ };
8245
+ collect2(sf);
8246
+ if (candidates.length === 0) return dead;
8247
+ const inDead = (node) => {
8248
+ for (let n = node; n; n = n.parent) if (dead.has(n)) return true;
8249
+ return false;
8250
+ };
8251
+ const within2 = (node, owner) => node.pos >= owner.pos && node.end <= owner.end;
8252
+ for (let round = 0; round < MAX_ROUNDS; round++) {
8253
+ const live = /* @__PURE__ */ new Map();
8254
+ const gather = (node) => {
8255
+ if (dead.has(node)) return;
8256
+ if (ts.isIdentifier(node)) {
8257
+ const list3 = live.get(node.text);
8258
+ if (list3) list3.push(node);
8259
+ else live.set(node.text, [node]);
8260
+ }
8261
+ ts.forEachChild(node, gather);
8262
+ };
8263
+ gather(sf);
8264
+ let changed = false;
8265
+ for (const fn of candidates) {
8266
+ if (dead.has(fn.body) || inDead(fn.owner)) continue;
8267
+ const refs = (live.get(fn.name) ?? []).filter((id) => !within2(id, fn.owner));
8268
+ if (refs.length === 0) {
8269
+ dead.add(fn.body);
8270
+ changed = true;
8271
+ }
8272
+ }
8273
+ if (!changed) break;
8274
+ }
8275
+ return dead;
8276
+ }
8277
+ function literalInteriors(sf) {
8278
+ const out3 = [];
8279
+ const visit = (node) => {
8280
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
8281
+ out3.push([node.getStart(sf) + 1, Math.max(node.getStart(sf) + 1, node.getEnd() - 1)]);
8282
+ } else if (ts.isTemplateHead(node) || ts.isTemplateMiddle(node)) {
8283
+ out3.push([node.getStart(sf) + 1, Math.max(node.getStart(sf) + 1, node.getEnd() - 2)]);
8284
+ } else if (ts.isTemplateTail(node)) {
8285
+ out3.push([node.getStart(sf) + 1, Math.max(node.getStart(sf) + 1, node.getEnd() - 1)]);
8286
+ }
8287
+ ts.forEachChild(node, visit);
8288
+ };
8289
+ visit(sf);
8290
+ return out3;
8291
+ }
8292
+ function partitionJs(src) {
8293
+ const sf = parseSource("spec.ts", src);
8294
+ if (!sf) return null;
8295
+ const dead = unreachableNodes(sf);
8296
+ const deadRanges = [];
8297
+ for (const n of dead) deadRanges.push([n.getStart(sf), n.getEnd()]);
8298
+ const literals3 = literalInteriors(sf);
8299
+ const mask = new Uint8Array(src.length);
8300
+ for (const [s, e] of deadRanges) for (let i = s; i < e; i++) mask[i] = 1;
8301
+ for (const [s, e] of literals3) for (let i = s; i < e; i++) if (mask[i] === 0) mask[i] = 2;
8302
+ const live = src.split("");
8303
+ const hidden = src.split("");
8304
+ const inLiteral = new Uint8Array(src.length);
8305
+ for (let i = 0; i < src.length; i++) {
8306
+ if (mask[i] === 1) {
8307
+ if (live[i] !== "\n") live[i] = " ";
8308
+ } else if (mask[i] === 2) {
8309
+ inLiteral[i] = 1;
8310
+ } else if (hidden[i] !== "\n") hidden[i] = " ";
8311
+ }
8312
+ let lineStart = 0;
8313
+ for (let i = 0; i <= src.length; i++) {
8314
+ if (i === src.length || src[i] === "\n") {
8315
+ let all = i > lineStart;
8316
+ for (let j = lineStart; j < i && all; j++) if (mask[j] !== 2 && /\S/.test(src[j])) all = false;
8317
+ if (all) blankRange(live, lineStart, i);
8318
+ lineStart = i + 1;
8319
+ }
8320
+ }
8321
+ return { live: live.join(""), hidden: hidden.join(""), inLiteral };
8322
+ }
8323
+ function partitionPy(src) {
8324
+ const lines = src.split("\n");
8325
+ const dead = new Array(lines.length).fill(false);
8326
+ let killIndent = null;
8327
+ let suiteIndent = null;
8328
+ for (let i = 0; i < lines.length; i++) {
8329
+ const l = lines[i];
8330
+ if (!/\S/.test(l) || /^\s*#/.test(l)) continue;
8331
+ const indent = PY_INDENT(l);
8332
+ if (killIndent !== null && indent < killIndent) killIndent = null;
8333
+ if (suiteIndent !== null && indent <= suiteIndent) suiteIndent = null;
8334
+ if (killIndent !== null && indent >= killIndent || suiteIndent !== null && indent > suiteIndent) {
8335
+ dead[i] = true;
8336
+ continue;
8337
+ }
8338
+ if (PY_TERMINAL.test(l)) killIndent = indent;
8339
+ else if (PY_CONST_FALSE.test(l)) suiteIndent = indent;
8340
+ }
8341
+ const live = lines.map((l, i) => dead[i] ? "" : l).join("\n");
8342
+ const hidden = lines.map((l, i) => dead[i] ? l : "").join("\n");
8343
+ return { live, hidden, inLiteral: new Uint8Array(live.length) };
8344
+ }
8345
+ function partition(src, path) {
8346
+ const lang = langOf(path);
8347
+ if (lang === "js" || lang === null) {
8348
+ const p = partitionJs(src);
8349
+ if (p) return p;
8350
+ } else if (lang === "py") {
8351
+ return partitionPy(src);
8352
+ }
8353
+ return { live: src, hidden: "", inLiteral: new Uint8Array(src.length) };
8354
+ }
8355
+ function pytestCollectedDefs(src) {
8356
+ let n = 0;
8357
+ const stack = [];
8358
+ for (const line of src.split("\n")) {
8359
+ if (!/\S/.test(line) || /^\s*#/.test(line)) continue;
8360
+ const indent = PY_INDENT(line);
8361
+ while (stack.length && indent <= stack[stack.length - 1].indent) stack.pop();
8362
+ const cls = PY_CLASS.exec(line);
8363
+ if (cls) {
8364
+ stack.push({ indent, collected: PY_COLLECTED_CLASS(cls[1], cls[2] ?? "") });
8365
+ continue;
8366
+ }
8367
+ if (PY_TEST_DEF.test(line) && stack.every((c) => c.collected)) n++;
8368
+ }
8369
+ return n;
8370
+ }
8371
+ var MAX_ROUNDS, blankRange, PY_INDENT, PY_TERMINAL, PY_CONST_FALSE, PY_CLASS, PY_TEST_DEF, PY_COLLECTED_CLASS;
8372
+ var init_reachability = __esm({
8373
+ "src/detectors/reachability.ts"() {
8374
+ "use strict";
8375
+ init_ts_lazy();
8376
+ init_files();
8377
+ MAX_ROUNDS = 8;
8378
+ blankRange = (chars, start, end) => {
8379
+ for (let i = start; i < end && i < chars.length; i++) if (chars[i] !== "\n") chars[i] = " ";
8380
+ };
8381
+ PY_INDENT = (line) => line.length - line.trimStart().length;
8382
+ PY_TERMINAL = /^\s*(?:return\b|raise\b)/;
8383
+ PY_CONST_FALSE = /^\s*(?:if|while)\s+(?:False|0|None|not\s+True)\s*:/;
8384
+ PY_CLASS = /^\s*class\s+(\w+)\s*(?:\(([^)]*)\))?\s*:/;
8385
+ PY_TEST_DEF = /^\s*(?:async\s+)?def\s+test\w*\s*\(/;
8386
+ PY_COLLECTED_CLASS = (name, bases) => /^Test/.test(name) || /Test/.test(bases);
8387
+ }
8388
+ });
8389
+
7960
8390
  // src/detectors/test-deletion.ts
7961
8391
  function calleeName(expr) {
7962
8392
  if (ts.isIdentifier(expr)) return expr.text;
@@ -8006,6 +8436,7 @@ function eachRows(call) {
8006
8436
  }
8007
8437
  function countTests(src, path = "spec.ts", substantiveOnly = false) {
8008
8438
  const lang = langOf(path);
8439
+ if (lang === "py") return { min: pytestCollectedDefs(src), open: false };
8009
8440
  if (lang && lang !== "js") {
8010
8441
  const re = TEST_DEFS[lang];
8011
8442
  let n2 = 0;
@@ -8016,7 +8447,9 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
8016
8447
  if (!sf) return { min: 0, open: true };
8017
8448
  let n = 0;
8018
8449
  let open = false;
8450
+ const dead = unreachableNodes(sf);
8019
8451
  const visit = (node, mult) => {
8452
+ if (dead.has(node)) return;
8020
8453
  if (ts.isForOfStatement(node)) {
8021
8454
  const r = loopRows(node.expression);
8022
8455
  if (r.open) open = true;
@@ -8193,11 +8626,13 @@ var init_test_deletion = __esm({
8193
8626
  init_policy();
8194
8627
  init_finding();
8195
8628
  init_command();
8629
+ init_hook_wiring();
8196
8630
  init_files();
8197
8631
  init_repo();
8198
8632
  init_suite_config();
8199
8633
  init_invocation();
8200
8634
  init_narrow();
8635
+ init_reachability();
8201
8636
  init_runner_chain();
8202
8637
  RULE9 = "test-deletion";
8203
8638
  JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
@@ -8328,16 +8763,31 @@ var init_test_deletion = __esm({
8328
8763
  }
8329
8764
  } else {
8330
8765
  let root = "";
8766
+ const fed = [];
8331
8767
  for (const seg of segments(c.raw)) {
8332
- const toks = tokens(seg).map(unquote);
8768
+ const toks = tokens(seg).map(shellWord);
8333
8769
  const cdTo = toks[0] === "cd" ? toks[1] : null;
8334
8770
  if (cdTo !== null) {
8335
8771
  root = cdTo === void 0 || /^[/~$]/.test(cdTo) || root === null ? null : joinRoot(root, cdTo);
8772
+ fed.push(...toks);
8336
8773
  continue;
8337
8774
  }
8338
8775
  const inRepo = (t) => root ? `${root}/${t}` : t;
8339
8776
  const listing = trackedFiles(ctx);
8340
- const testToks = toks.filter((t) => {
8777
+ const whole = (t) => /^(?:\.|\.\/|\*|\.\/\*)$/.test(t);
8778
+ const expand = (raw) => {
8779
+ const t = raw.replace(/^(?:of|if)=/, "");
8780
+ if (!isGlob(t) || whole(t)) return [t];
8781
+ if (listing && root !== null) return expandGlob(inRepo(t), listing).map((p) => root ? p.slice(root.length + 1) : p);
8782
+ return expandBraces(t).flatMap((alt) => {
8783
+ if (!isGlob(alt)) return [alt];
8784
+ const base = alt.split("/").pop() ?? "";
8785
+ if (/test|spec/i.test(base.replace(/[*?]|\[[^\]]*\]/g, ""))) return [alt.replace(/\*\*\//g, "").replace(/\*/g, "a").replace(/\?/g, "t")];
8786
+ const dir = alt.includes("/") ? alt.replace(/\/[^/]*$/, "") : "";
8787
+ return dir && !isGlob(dir) && containsProtected(dir, policy, "tests", ctx, "snapshots") ? [dir] : [];
8788
+ });
8789
+ };
8790
+ const specPath = (t) => {
8341
8791
  const resolved = root !== null && /(?:^|\/)\.{1,2}(?:\/|$)/.test(t) ? joinRoot(root, t) : null;
8342
8792
  const probe = resolved ?? t;
8343
8793
  if (!isSpec(probe)) return false;
@@ -8345,56 +8795,82 @@ var init_test_deletion = __esm({
8345
8795
  const looksLikeFile = /\.[A-Za-z0-9]+$/.test(probe.split("/").pop() ?? "");
8346
8796
  if (looksLikeFile || listing.includes(resolved ?? inRepo(t))) return true;
8347
8797
  return containsProtected(resolved ?? inRepo(t), policy, "tests", ctx, "snapshots");
8348
- });
8798
+ };
8349
8799
  const rootHasProtected = () => listing ? listing.some(isSpec) : true;
8350
- const dirToks = toks.filter((t) => {
8351
- if (t.startsWith("-") || testToks.includes(t)) return false;
8352
- const whole = /^(?:\.|\.\/|\*|\.\/\*)$/.test(t);
8353
- if (!whole && /[*?{}[\]]/.test(t)) return false;
8354
- if (root === null) return whole ? false : containsProtected(t, policy, "tests", ctx, "snapshots");
8355
- const dir = whole ? root : joinRoot(root, t);
8800
+ const dirPath = (t) => {
8801
+ if (t.startsWith("-")) return false;
8802
+ const isWhole = whole(t);
8803
+ if (!isWhole && isGlob(t)) return false;
8804
+ if (root === null) return isWhole ? false : containsProtected(t, policy, "tests", ctx, "snapshots");
8805
+ const dir = isWhole ? root : joinRoot(root, t);
8356
8806
  if (dir === null) return false;
8357
8807
  if (dir === "") return rootHasProtected();
8358
8808
  return containsProtected(dir, policy, "tests", ctx, "snapshots");
8359
- });
8809
+ };
8810
+ const xargs = xargsCommand(toks);
8811
+ const source = xargs ? [...fed, ...toks] : toks;
8812
+ fed.push(...toks);
8813
+ const testToks = source.filter((t) => expand(t).some(specPath));
8814
+ const dirToks = source.filter((t) => !t.startsWith("-") && !testToks.includes(t) && expand(t).some(dirPath));
8360
8815
  if (testToks.length === 0 && dirToks.length === 0) continue;
8361
8816
  const named = [...testToks, ...dirToks];
8362
- const cmd = toks[0] ?? "";
8817
+ let at = 0;
8818
+ while (at < toks.length && WRAPPERS.test(toks[at])) at++;
8819
+ const cmdToks = toks.slice(at);
8820
+ const cmd = xargs ? xargs.cmd : (cmdToks[0] ?? "").replace(/^.*\//, "");
8821
+ const args = xargs ? xargs.args : cmdToks.slice(1);
8822
+ const inPlace = (a) => /^-[A-Za-z]*i|^--in-place/.test(a);
8823
+ const isDir = (p) => listing ? listing.some((f) => f.startsWith(inRepo(p).replace(/\/+$/, "") + "/")) : /\/$/.test(p);
8824
+ const exists = (p) => !listing || root === null || listing.includes(inRepo(p.replace(/^\.\//, "")));
8825
+ const ARG_TAKING = /^-(?:t|m|o|g|S|e)$|^--(?:target-directory|mode|owner|group|suffix|rsh|exclude|include|files-from)$/;
8826
+ const positional = args.filter((a, i) => a !== "--" && !a.startsWith("-") && !ARG_TAKING.test(args[i - 1] ?? ""));
8363
8827
  let why = null;
8364
8828
  let evidence = seg;
8365
- const redirectTarget = seg.match(/>\s*(\S+)/)?.[1];
8366
- const redirectsOntoTest = redirectTarget ? isSpec(unquote(redirectTarget)) : false;
8829
+ const redirectsOntoTest = [...seg.matchAll(REDIRECT_TARGET)].some((m) => isSpec(shellWord(m[1])));
8367
8830
  const findDeletes = cmd === "find" && /\s(?:-delete\b|-exec\s+(?:rm|unlink)\b|-execdir\s+(?:rm|unlink)\b)/.test(seg);
8368
- const gitRestoresOld = cmd === "git" && /^git\s+(?:checkout|restore)\b/.test(seg) && (() => {
8831
+ const gitSub = cmd === "git" ? gitSubcommand(cmdToks) : null;
8832
+ const gitRestoresOld = (gitSub === "checkout" || gitSub === "restore") && (() => {
8369
8833
  const src = seg.match(/--source[= ]\s*(\S+)/)?.[1];
8370
- const revIdx = toks.findIndex((t, i) => i >= 2 && !t.startsWith("-") && !named.includes(t) && t !== "--");
8371
- const rev2 = src ?? (revIdx >= 0 ? toks[revIdx] : void 0);
8834
+ const revIdx = cmdToks.findIndex((t, i) => i >= 2 && !t.startsWith("-") && !named.includes(t) && t !== "--");
8835
+ const rev2 = src ?? (revIdx >= 0 ? cmdToks[revIdx] : void 0);
8372
8836
  if (!rev2 || rev2 === "HEAD" || rev2 === "@" || revIsHead(rev2, ctx) === true) return false;
8373
8837
  if (src || seg.includes(" -- ")) return true;
8374
- return revIdx >= 0 && toks.some((t, i) => i > revIdx && named.includes(t));
8838
+ return revIdx >= 0 && cmdToks.some((t, i) => i > revIdx && named.includes(t));
8375
8839
  })();
8376
- if (/\brm\b/.test(seg) && !/^(?:git|npm|yarn|pnpm|bun)\b/.test(seg)) {
8377
- why = "rm deletes a test file";
8378
- } else if (cmd === "git" && /^git\s+rm\b/.test(seg)) {
8840
+ const dests = cmd === "cp" || cmd === "install" || cmd === "rsync" || cmd === "ln" || cmd === "mv" ? destinations(cmd, args, isDir) : [];
8841
+ const overwrites = dests.some((d) => isSpec(d) && exists(d)) && positional.some((p) => !dests.includes(p) && !isSpec(p));
8842
+ if (xargs) {
8843
+ if (cmd === "rm" || cmd === "unlink" || cmd === "shred") why = `xargs ${cmd} deletes a test file named earlier in the line`;
8844
+ else if (cmd === "truncate") why = "xargs truncate empties a test file named earlier in the line";
8845
+ else if (cmd === "mv") why = "xargs mv moves a test file named earlier in the line";
8846
+ else if (cmd === "tee" || cmd === "sponge") why = `xargs ${cmd} rewrites a test file named earlier in the line`;
8847
+ else if ((cmd === "sed" || cmd === "perl") && args.some(inPlace)) why = `xargs ${cmd} -i rewrites a test file named earlier in the line`;
8848
+ } else if (cmd === "rm" || cmd === "unlink" || cmd === "shred") {
8849
+ why = `${cmd} deletes a test file`;
8850
+ } else if (gitSub === "rm") {
8379
8851
  why = "git rm deletes a test file";
8380
8852
  } else if (cmd === "sed" && SED_INPLACE.test(seg)) {
8381
8853
  why = "sed -i rewrites a test file in place";
8382
8854
  } else if (cmd === "perl" && /(?:^|\s)-[a-zA-Z]*i\b|\s-i\S*/.test(seg)) {
8383
8855
  why = "perl -i rewrites a test file in place";
8384
- } else if (/\btruncate\b/.test(seg)) {
8856
+ } else if (cmd === "truncate") {
8385
8857
  why = "truncate empties a test file";
8386
8858
  } else if (redirectsOntoTest) {
8387
8859
  why = "a redirect overwrites/empties a test file";
8388
- } else if (cmd === "cp" && testToks.length > 0 && isSpec(toks[toks.length - 1] ?? "") && !isSpec(toks[toks.length - 2] ?? "")) {
8389
- why = "cp overwrites a test file with something else";
8390
- } else if (cmd === "tee" && !toks.includes("-a") && !toks.includes("--append")) {
8860
+ } else if (cmd !== "mv" && overwrites) {
8861
+ why = `${cmd} overwrites a test file with something else`;
8862
+ } else if (cmd === "tee" && !args.includes("-a") && !args.includes("--append")) {
8391
8863
  why = "tee overwrites a test file";
8864
+ } else if (cmd === "sponge" && !args.includes("-a")) {
8865
+ why = "sponge rewrites a test file in place";
8866
+ } else if (cmd === "dd" && args.some((a) => a.startsWith("of=") && isSpec(a.slice(3)))) {
8867
+ why = "dd rewrites a test file";
8392
8868
  } else if (findDeletes) {
8393
8869
  why = "find -delete removes test files";
8394
8870
  } else if (gitRestoresOld) {
8395
8871
  why = "git checkout <rev> replaces a test file with an older version";
8396
- } else if (/\bmv\b/.test(seg)) {
8397
- const dest = toks[toks.length - 1];
8872
+ } else if (cmd === "mv") {
8873
+ const dest = positional[positional.length - 1];
8398
8874
  if (dest && testToks.length > 0 && !isSpec(dest)) {
8399
8875
  why = "mv renames a test file out of the test glob";
8400
8876
  evidence = `${testToks[0]} \u2192 ${dest}`;
@@ -8404,6 +8880,8 @@ var init_test_deletion = __esm({
8404
8880
  } else if (dest && testToks.length === 0 && dirToks[0] !== dest && leavesGlob(dirToks[0] ?? "", dest, policy, ctx)) {
8405
8881
  why = "mv renames a test directory so its specs leave the test glob";
8406
8882
  evidence = `${dirToks[0]} \u2192 ${dest}`;
8883
+ } else if (overwrites) {
8884
+ why = "mv overwrites a test file with something else";
8407
8885
  }
8408
8886
  }
8409
8887
  if (why) {
@@ -8464,7 +8942,10 @@ function significantLinesOrdered(src, path) {
8464
8942
  }
8465
8943
  return out3;
8466
8944
  }
8467
- var RULE10, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, testContentRemoval;
8945
+ function liveSignificantLines(src, path) {
8946
+ return significantLinesOrdered(partition(src, path).live, path);
8947
+ }
8948
+ var RULE10, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, KeptPool, testContentRemoval;
8468
8949
  var init_test_content_removal = __esm({
8469
8950
  "src/detectors/test-content-removal.ts"() {
8470
8951
  "use strict";
@@ -8473,22 +8954,58 @@ var init_test_content_removal = __esm({
8473
8954
  init_finding();
8474
8955
  init_test_deletion();
8475
8956
  init_files();
8957
+ init_reachability();
8476
8958
  RULE10 = "test-content-removal";
8477
8959
  MIN_REMOVED_LINES = 3;
8478
8960
  MIN_REMOVED_ROWS = 2;
8479
8961
  ws = (s) => s.replace(/\s+/g, "");
8480
8962
  TEST_DIR_FILE = /(?:^|\/)(?:test|tests|__tests__|spec|specs)\//;
8963
+ KeptPool = class {
8964
+ text = "";
8965
+ inLiteral = [];
8966
+ addLive(src, path) {
8967
+ const p = partition(src, path);
8968
+ const lang = langOf(path);
8969
+ let lineStart = 0;
8970
+ for (let i = 0; i <= p.live.length; i++) {
8971
+ if (i < p.live.length && p.live[i] !== "\n") continue;
8972
+ const line = p.live.slice(lineStart, i);
8973
+ if (isSignificantLine(line.trim(), lang)) {
8974
+ for (let j = lineStart; j < i; j++) {
8975
+ if (/\s/.test(p.live[j])) continue;
8976
+ this.text += p.live[j];
8977
+ this.inLiteral.push(p.inLiteral[j]);
8978
+ }
8979
+ }
8980
+ lineStart = i + 1;
8981
+ }
8982
+ }
8983
+ /** Text added outside any spec (a case table moved to `test/fixtures/`): code. */
8984
+ addCode(ws2) {
8985
+ this.text += ws2;
8986
+ for (let i = 0; i < ws2.length; i++) this.inLiteral.push(0);
8987
+ }
8988
+ keeps(wsLine) {
8989
+ if (wsLine.length === 0) return true;
8990
+ let at = this.text.indexOf(wsLine);
8991
+ while (at !== -1) {
8992
+ if (this.inLiteral[at] === 0) return true;
8993
+ at = this.text.indexOf(wsLine, at + 1);
8994
+ }
8995
+ return false;
8996
+ }
8997
+ };
8481
8998
  testContentRemoval = {
8482
8999
  id: RULE10,
8483
9000
  surface: ["file"],
8484
9001
  certainty: "mechanical",
8485
9002
  run(changes, policy) {
8486
9003
  const out3 = [];
8487
- let keptPool = "";
9004
+ const keptPool = new KeptPool();
8488
9005
  let addedPool = "";
8489
9006
  for (const c of changes) {
8490
9007
  if (c.kind !== "file" || c.after == null) continue;
8491
- if (isProtected(c.path, policy, "tests")) keptPool += ws(significantLinesOrdered(c.after, c.path).join("\n"));
9008
+ if (isProtected(c.path, policy, "tests")) keptPool.addLive(c.after, c.path);
8492
9009
  let addedHere = "";
8493
9010
  if (c.op === "add") addedHere = ws(c.after);
8494
9011
  else if (c.before != null) {
@@ -8496,7 +9013,7 @@ var init_test_content_removal = __esm({
8496
9013
  addedHere = ws(c.after.split("\n").filter((l) => !had.has(l.trim())).join("\n"));
8497
9014
  }
8498
9015
  addedPool += addedHere;
8499
- if (!isProtected(c.path, policy, "tests") && TEST_DIR_FILE.test(c.path) && langOf(c.path) === "js") keptPool += addedHere;
9016
+ if (!isProtected(c.path, policy, "tests") && TEST_DIR_FILE.test(c.path) && langOf(c.path) === "js") keptPool.addCode(addedHere);
8500
9017
  }
8501
9018
  for (const c of changes) {
8502
9019
  if (c.kind !== "file" || c.op !== "modify" && c.op !== "rename" || c.before == null || c.after == null) continue;
@@ -8505,16 +9022,18 @@ var init_test_content_removal = __esm({
8505
9022
  const blocksBefore = countTests(c.before, c.path);
8506
9023
  const blocksAfter = countTests(c.after, c.path);
8507
9024
  if (blocksAfter.min < blocksBefore.min && !blocksAfter.open) continue;
8508
- const beforeSig = significantLinesOrdered(c.before, c.path);
8509
- const afterSig = significantLinesOrdered(c.after, c.path);
9025
+ const beforeSig = liveSignificantLines(c.before, c.path);
9026
+ const afterSig = liveSignificantLines(c.after, c.path);
8510
9027
  const netDrop = beforeSig.length - afterSig.length;
8511
9028
  const afterSet = new Set(afterSig);
8512
9029
  const gone = [];
8513
9030
  for (const line of new Set(beforeSig)) {
8514
9031
  if (afterSet.has(line)) continue;
8515
- if (keptPool.includes(ws(line))) continue;
9032
+ if (keptPool.keeps(ws(line))) continue;
8516
9033
  gone.push(line);
8517
9034
  }
9035
+ const hiddenPool = ws(partition(c.after, c.path).hidden);
9036
+ const hidden = gone.filter((line) => hiddenPool.includes(ws(line)));
8518
9037
  const table = eachTables(c.before, c.path);
8519
9038
  const tableAfter = eachTables(c.after, c.path);
8520
9039
  const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
@@ -8529,6 +9048,17 @@ var init_test_content_removal = __esm({
8529
9048
  );
8530
9049
  continue;
8531
9050
  }
9051
+ if (hidden.length >= MIN_REMOVED_LINES) {
9052
+ out3.push(
9053
+ makeFinding(RULE10, policy, {
9054
+ file: c.path,
9055
+ message: `Test content made unreachable in a surviving spec: ${hidden.length} significant line(s) kept only where they cannot run (a string, a template literal, a dead branch, after a return).`,
9056
+ evidence: hidden.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
9057
+ remediation: "Fix the code under test. Assertions parked in a string or behind a constant-false branch or an early return no longer execute; the spec is weakened exactly as if they were deleted."
9058
+ })
9059
+ );
9060
+ continue;
9061
+ }
8532
9062
  if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
8533
9063
  out3.push(
8534
9064
  makeFinding(RULE10, policy, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.24.3",
3
+ "version": "2.24.5",
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",