tamperward 2.24.3 → 2.24.4

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 +417 -10
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -3819,6 +3819,47 @@ function matchesOutsideString(p, content, lang) {
3819
3819
  }
3820
3820
  return false;
3821
3821
  }
3822
+ function afterLinesOf(c) {
3823
+ const out3 = /* @__PURE__ */ new Map();
3824
+ if (c.after != null) {
3825
+ c.after.split("\n").forEach((l, i) => out3.set(i + 1, l));
3826
+ return out3;
3827
+ }
3828
+ 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);
3829
+ return out3;
3830
+ }
3831
+ function contextSkipHits(c, lang) {
3832
+ if (lang !== "go" && lang !== "rs") return [];
3833
+ const lines = afterLinesOf(c);
3834
+ const hits = [];
3835
+ for (const l of addedLines(c)) {
3836
+ if (l.newLine == null) continue;
3837
+ if (lang === "go") {
3838
+ const m = GO_TEST_MAIN.exec(l.content);
3839
+ if (!m) continue;
3840
+ const runs = new RegExp("\\b" + m[1] + "\\.Run\\(\\)");
3841
+ let ran = false;
3842
+ for (const [, text] of lines) if (runs.test(text) && !isCommentLine(text.trim(), lang)) ran = true;
3843
+ 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() });
3844
+ continue;
3845
+ }
3846
+ const cfg = RS_CFG.exec(l.content);
3847
+ if (!cfg || cfg[1].trim() === "test") continue;
3848
+ let first = l.newLine;
3849
+ while (RS_ATTR.test(lines.get(first - 1) ?? "")) first--;
3850
+ let at = l.newLine + 1;
3851
+ while (RS_ATTR.test(lines.get(at) ?? "") || lines.has(at) && !/\S/.test(lines.get(at) ?? "")) at++;
3852
+ const item = lines.get(at) ?? "";
3853
+ let besideTest = false;
3854
+ for (let i = first; i < at; i++) if (RS_TEST_ATTR.test(lines.get(i) ?? "")) besideTest = true;
3855
+ if (RS_MOD.test(item)) {
3856
+ 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() });
3857
+ } else if (besideTest && RS_FN.test(item)) {
3858
+ hits.push({ line: l.newLine, why: "a #[cfg(\u2026)] beside #[test] (the test is compiled out under every ordinary configuration)", evidence: l.content.trim() });
3859
+ }
3860
+ }
3861
+ return hits;
3862
+ }
3822
3863
  function scriptKind2(path) {
3823
3864
  if (/\.tsx$/i.test(path)) return ts.ScriptKind.TSX;
3824
3865
  if (/\.jsx$/i.test(path)) return ts.ScriptKind.JSX;
@@ -4147,7 +4188,7 @@ function astSkipHits(c, resolvers) {
4147
4188
  }
4148
4189
  return { hits, authoritative: true, unowned: analysis.unowned };
4149
4190
  }
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;
4191
+ 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
4192
  var init_test_skip = __esm({
4152
4193
  "src/detectors/test-skip.ts"() {
4153
4194
  "use strict";
@@ -4162,6 +4203,7 @@ var init_test_skip = __esm({
4162
4203
  acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
4163
4204
  JS_RUNNER = "\\b(?:it|test|describe|suite)";
4164
4205
  JS_MOD = "concurrent|sequential|shuffle|serial|parallel";
4206
+ JVM_CONDITIONAL = /@(?:[\w.]+\.)?(?:EnabledIf|DisabledIf)\w*\b/;
4165
4207
  GO_SKIP_PATTERNS = [
4166
4208
  { re: /\b\w+\.Skip(?:f|Now)?\(/, why: "a runtime t.Skip()/Skipf()/SkipNow() call" },
4167
4209
  { re: /\bT\(\)\.Skip(?:f|Now)?\(/, why: "a runtime T().Skip()/Skipf()/SkipNow() call (testify suite)" }
@@ -4228,7 +4270,10 @@ var init_test_skip = __esm({
4228
4270
  ...GO_SKIP_PATTERNS,
4229
4271
  // #441: any receiver, and testify's T()
4230
4272
  { 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 }
4273
+ // Any constraint on a `_test.go` — `ignore`, `integration`, `never`, `!ci` — takes
4274
+ // the file out of the default `go test ./...` run; the tag's name is the agent's
4275
+ // to choose (#431).
4276
+ { 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
4277
  ],
4233
4278
  rs: [
4234
4279
  { re: /#\[ignore\b/, why: "an #[ignore] attribute (the test no longer runs by default)" },
@@ -4239,14 +4284,19 @@ var init_test_skip = __esm({
4239
4284
  { re: /(?:^|[\s;{(])x(?:it|describe|context|specify|example)\s*[('"]/, why: "an x-prefixed disabled example" },
4240
4285
  { re: /(?:^|[\s;{(])f(?:it|describe|context)\s*[('"]/, why: "an f-prefixed focused example (narrows the suite)" },
4241
4286
  { 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)" }
4287
+ { re: /^\s*(?:it|specify|example)\s+(['"])(?:(?!\1).)*\1\s*$/, why: "an example with no block (RSpec reports it as pending)" },
4288
+ // `it 'x', if: false do` — RSpec's conditional filter with a constant that never
4289
+ // lets the example run (#431). `if: ENV['SLOW']` is a real condition and passes.
4290
+ { 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
4291
  ],
4244
4292
  java: [
4245
4293
  { re: /@(?:[\w.]+\.)?(?:Ignore|Disabled)\b/, why: "an @Ignore/@Disabled annotation" },
4294
+ { re: JVM_CONDITIONAL, why: "a conditional-execution annotation (@EnabledIf\u2026 / @DisabledIf\u2026: the test runs only when the condition allows)" },
4246
4295
  { re: /\bassume(?:True\(\s*false|False\(\s*true)\s*[,)]/, why: "an assumption that never holds (the test aborts as skipped)" }
4247
4296
  ],
4248
4297
  kt: [
4249
4298
  { re: /@(?:[\w.]+\.)?(?:Ignore|Disabled)\b/, why: "an @Ignore/@Disabled annotation" },
4299
+ { re: JVM_CONDITIONAL, why: "a conditional-execution annotation (@EnabledIf\u2026 / @DisabledIf\u2026: the test runs only when the condition allows)" },
4250
4300
  { re: /\bassume(?:True\(\s*false|False\(\s*true)\s*[,)]/, why: "an assumption that never holds (the test aborts as skipped)" }
4251
4301
  ],
4252
4302
  php: [{ re: /\bmarkTest(?:Skipped|Incomplete)\(/, why: "a markTestSkipped()/markTestIncomplete() call" }],
@@ -4268,6 +4318,12 @@ var init_test_skip = __esm({
4268
4318
  "bun:test",
4269
4319
  "@playwright/test"
4270
4320
  ]);
4321
+ GO_TEST_MAIN = /^func\s+TestMain\s*\(\s*(\w+)\s+\*testing\.M\s*\)/;
4322
+ RS_CFG = /^\s*#\[cfg\((.*)\)\]\s*$/;
4323
+ RS_ATTR = /^\s*#!?\[/;
4324
+ RS_TEST_ATTR = /^\s*#\[[\w:]*test\b/;
4325
+ RS_MOD = /^\s*(?:pub(?:\([^)]*\))?\s+)?mod\s+\w+/;
4326
+ RS_FN = /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+\w+/;
4271
4327
  RUNNER2 = { kind: "runner", causes: [] };
4272
4328
  NON_RUNNER = { kind: "non-runner" };
4273
4329
  UNKNOWN = { kind: "unknown" };
@@ -4559,6 +4615,18 @@ var init_test_skip = __esm({
4559
4615
  );
4560
4616
  }
4561
4617
  }
4618
+ for (const hit of contextSkipHits(c, lang)) {
4619
+ astHitLines.add(hit.line);
4620
+ out3.push(
4621
+ makeFinding(RULE4, policy, {
4622
+ file: c.path,
4623
+ line: hit.line,
4624
+ message: `Test skipped or narrowed: ${hit.why}.`,
4625
+ evidence: hit.evidence,
4626
+ remediation: "Make the test pass rather than skipping it. If it is genuinely obsolete, a human must sign off."
4627
+ })
4628
+ );
4629
+ }
4562
4630
  if (lang === "py") {
4563
4631
  for (const hit of pyAliasDecoratorHits(c)) {
4564
4632
  if (hit.line != null) astHitLines.add(hit.line);
@@ -7957,6 +8025,289 @@ They WILL FAIL in v10.0.0
7957
8025
  }
7958
8026
  });
7959
8027
 
8028
+ // src/detectors/reachability.ts
8029
+ function constTruth(expr) {
8030
+ const v = constValue(expr);
8031
+ if (v === null) return null;
8032
+ return v.kind === "unknown" ? v.truthy : Boolean(v.value);
8033
+ }
8034
+ function constValue(expr) {
8035
+ if (ts.isParenthesizedExpression(expr)) return constValue(expr.expression);
8036
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) return { kind: "value", value: true };
8037
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) return { kind: "value", value: false };
8038
+ if (expr.kind === ts.SyntaxKind.NullKeyword) return { kind: "value", value: null };
8039
+ if (ts.isIdentifier(expr)) {
8040
+ if (expr.text === "undefined") return { kind: "value", value: void 0 };
8041
+ if (expr.text === "NaN") return { kind: "value", value: NaN };
8042
+ return null;
8043
+ }
8044
+ if (ts.isNumericLiteral(expr)) return { kind: "value", value: Number(expr.text) };
8045
+ if (ts.isBigIntLiteral(expr)) return { kind: "unknown", truthy: !/^0+n$/.test(expr.text) };
8046
+ if (ts.isStringLiteralLike(expr)) return { kind: "value", value: expr.text };
8047
+ if (ts.isVoidExpression(expr)) return { kind: "value", value: void 0 };
8048
+ if (ts.isPrefixUnaryExpression(expr)) {
8049
+ if (expr.operator === ts.SyntaxKind.ExclamationToken) {
8050
+ const inner = constTruth(expr.operand);
8051
+ return inner === null ? null : { kind: "value", value: !inner };
8052
+ }
8053
+ if (expr.operator === ts.SyntaxKind.MinusToken || expr.operator === ts.SyntaxKind.PlusToken) {
8054
+ const inner = constValue(expr.operand);
8055
+ if (inner === null || inner.kind !== "value" || typeof inner.value !== "number") return null;
8056
+ return { kind: "value", value: expr.operator === ts.SyntaxKind.MinusToken ? -inner.value : inner.value };
8057
+ }
8058
+ return null;
8059
+ }
8060
+ if (ts.isBinaryExpression(expr)) {
8061
+ const op = expr.operatorToken.kind;
8062
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
8063
+ const l2 = constTruth(expr.left);
8064
+ const r2 = constTruth(expr.right);
8065
+ const and = op === ts.SyntaxKind.AmpersandAmpersandToken;
8066
+ if (and) {
8067
+ if (l2 === false || r2 === false) return { kind: "unknown", truthy: false };
8068
+ if (l2 === true) return r2 === null ? null : { kind: "unknown", truthy: r2 };
8069
+ if (r2 === true) return l2 === null ? null : { kind: "unknown", truthy: l2 };
8070
+ return null;
8071
+ }
8072
+ if (l2 === true || r2 === true) return { kind: "unknown", truthy: true };
8073
+ if (l2 === false) return r2 === null ? null : { kind: "unknown", truthy: r2 };
8074
+ if (r2 === false) return l2 === null ? null : { kind: "unknown", truthy: l2 };
8075
+ return null;
8076
+ }
8077
+ const l = constValue(expr.left);
8078
+ const r = constValue(expr.right);
8079
+ if (l === null || r === null || l.kind !== "value" || r.kind !== "value") return null;
8080
+ const a = l.value;
8081
+ const b = r.value;
8082
+ switch (op) {
8083
+ case ts.SyntaxKind.EqualsEqualsEqualsToken:
8084
+ return { kind: "value", value: a === b };
8085
+ case ts.SyntaxKind.ExclamationEqualsEqualsToken:
8086
+ return { kind: "value", value: a !== b };
8087
+ case ts.SyntaxKind.EqualsEqualsToken:
8088
+ return { kind: "value", value: a == b };
8089
+ case ts.SyntaxKind.ExclamationEqualsToken:
8090
+ return { kind: "value", value: a != b };
8091
+ default:
8092
+ break;
8093
+ }
8094
+ if (typeof a === "number" && typeof b === "number") {
8095
+ switch (op) {
8096
+ case ts.SyntaxKind.LessThanToken:
8097
+ return { kind: "value", value: a < b };
8098
+ case ts.SyntaxKind.LessThanEqualsToken:
8099
+ return { kind: "value", value: a <= b };
8100
+ case ts.SyntaxKind.GreaterThanToken:
8101
+ return { kind: "value", value: a > b };
8102
+ case ts.SyntaxKind.GreaterThanEqualsToken:
8103
+ return { kind: "value", value: a >= b };
8104
+ default:
8105
+ break;
8106
+ }
8107
+ }
8108
+ return null;
8109
+ }
8110
+ return null;
8111
+ }
8112
+ function isTerminal(stmt) {
8113
+ return ts.isReturnStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBreakStatement(stmt) || ts.isContinueStatement(stmt);
8114
+ }
8115
+ function namedFunctionBody(node) {
8116
+ if (ts.isFunctionDeclaration(node) && node.name && node.body) return { name: node.name.text, body: node.body, owner: node };
8117
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
8118
+ let init = node.initializer;
8119
+ while (ts.isParenthesizedExpression(init) || ts.isAsExpression(init) || ts.isSatisfiesExpression(init)) init = init.expression;
8120
+ if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) return { name: node.name.text, body: init.body, owner: node };
8121
+ }
8122
+ return null;
8123
+ }
8124
+ function isExportedDeclaration(node) {
8125
+ const stmt = ts.isVariableDeclaration(node) ? node.parent.parent : node;
8126
+ if (ts.isExportAssignment(stmt)) return true;
8127
+ return ts.canHaveModifiers(stmt) && (ts.getModifiers(stmt) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
8128
+ }
8129
+ function unreachableNodes(sf) {
8130
+ const dead = /* @__PURE__ */ new Set();
8131
+ const markBranches = (node) => {
8132
+ if (ts.isIfStatement(node)) {
8133
+ const t = constTruth(node.expression);
8134
+ if (t === false) dead.add(node.thenStatement);
8135
+ else if (t === true && node.elseStatement) dead.add(node.elseStatement);
8136
+ } else if (ts.isWhileStatement(node) || ts.isForStatement(node)) {
8137
+ const cond = ts.isWhileStatement(node) ? node.expression : node.condition;
8138
+ if (cond && constTruth(cond) === false) dead.add(node.statement);
8139
+ } else if (ts.isConditionalExpression(node)) {
8140
+ const t = constTruth(node.condition);
8141
+ if (t === false) dead.add(node.whenTrue);
8142
+ else if (t === true) dead.add(node.whenFalse);
8143
+ } else if (ts.isBinaryExpression(node)) {
8144
+ const op = node.operatorToken.kind;
8145
+ const l = constTruth(node.left);
8146
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken && l === false || op === ts.SyntaxKind.BarBarToken && l === true) {
8147
+ dead.add(node.right);
8148
+ }
8149
+ }
8150
+ if (ts.isSourceFile(node) || ts.isBlock(node) || ts.isModuleBlock(node) || ts.isCaseClause(node) || ts.isDefaultClause(node)) {
8151
+ let ended = false;
8152
+ for (const stmt of node.statements) {
8153
+ if (ended && !ts.isFunctionDeclaration(stmt)) dead.add(stmt);
8154
+ if (isTerminal(stmt)) ended = true;
8155
+ }
8156
+ }
8157
+ ts.forEachChild(node, markBranches);
8158
+ };
8159
+ markBranches(sf);
8160
+ const candidates = [];
8161
+ const collect2 = (node) => {
8162
+ const fn = namedFunctionBody(node);
8163
+ if (fn && !isExportedDeclaration(fn.owner)) candidates.push(fn);
8164
+ ts.forEachChild(node, collect2);
8165
+ };
8166
+ collect2(sf);
8167
+ if (candidates.length === 0) return dead;
8168
+ const inDead = (node) => {
8169
+ for (let n = node; n; n = n.parent) if (dead.has(n)) return true;
8170
+ return false;
8171
+ };
8172
+ const within2 = (node, owner) => node.pos >= owner.pos && node.end <= owner.end;
8173
+ for (let round = 0; round < MAX_ROUNDS; round++) {
8174
+ const live = /* @__PURE__ */ new Map();
8175
+ const gather = (node) => {
8176
+ if (dead.has(node)) return;
8177
+ if (ts.isIdentifier(node)) {
8178
+ const list3 = live.get(node.text);
8179
+ if (list3) list3.push(node);
8180
+ else live.set(node.text, [node]);
8181
+ }
8182
+ ts.forEachChild(node, gather);
8183
+ };
8184
+ gather(sf);
8185
+ let changed = false;
8186
+ for (const fn of candidates) {
8187
+ if (dead.has(fn.body) || inDead(fn.owner)) continue;
8188
+ const refs = (live.get(fn.name) ?? []).filter((id) => !within2(id, fn.owner));
8189
+ if (refs.length === 0) {
8190
+ dead.add(fn.body);
8191
+ changed = true;
8192
+ }
8193
+ }
8194
+ if (!changed) break;
8195
+ }
8196
+ return dead;
8197
+ }
8198
+ function literalInteriors(sf) {
8199
+ const out3 = [];
8200
+ const visit = (node) => {
8201
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
8202
+ out3.push([node.getStart(sf) + 1, Math.max(node.getStart(sf) + 1, node.getEnd() - 1)]);
8203
+ } else if (ts.isTemplateHead(node) || ts.isTemplateMiddle(node)) {
8204
+ out3.push([node.getStart(sf) + 1, Math.max(node.getStart(sf) + 1, node.getEnd() - 2)]);
8205
+ } else if (ts.isTemplateTail(node)) {
8206
+ out3.push([node.getStart(sf) + 1, Math.max(node.getStart(sf) + 1, node.getEnd() - 1)]);
8207
+ }
8208
+ ts.forEachChild(node, visit);
8209
+ };
8210
+ visit(sf);
8211
+ return out3;
8212
+ }
8213
+ function partitionJs(src) {
8214
+ const sf = parseSource("spec.ts", src);
8215
+ if (!sf) return null;
8216
+ const dead = unreachableNodes(sf);
8217
+ const deadRanges = [];
8218
+ for (const n of dead) deadRanges.push([n.getStart(sf), n.getEnd()]);
8219
+ const literals3 = literalInteriors(sf);
8220
+ const mask = new Uint8Array(src.length);
8221
+ for (const [s, e] of deadRanges) for (let i = s; i < e; i++) mask[i] = 1;
8222
+ for (const [s, e] of literals3) for (let i = s; i < e; i++) if (mask[i] === 0) mask[i] = 2;
8223
+ const live = src.split("");
8224
+ const hidden = src.split("");
8225
+ const inLiteral = new Uint8Array(src.length);
8226
+ for (let i = 0; i < src.length; i++) {
8227
+ if (mask[i] === 1) {
8228
+ if (live[i] !== "\n") live[i] = " ";
8229
+ } else if (mask[i] === 2) {
8230
+ inLiteral[i] = 1;
8231
+ } else if (hidden[i] !== "\n") hidden[i] = " ";
8232
+ }
8233
+ let lineStart = 0;
8234
+ for (let i = 0; i <= src.length; i++) {
8235
+ if (i === src.length || src[i] === "\n") {
8236
+ let all = i > lineStart;
8237
+ for (let j = lineStart; j < i && all; j++) if (mask[j] !== 2 && /\S/.test(src[j])) all = false;
8238
+ if (all) blankRange(live, lineStart, i);
8239
+ lineStart = i + 1;
8240
+ }
8241
+ }
8242
+ return { live: live.join(""), hidden: hidden.join(""), inLiteral };
8243
+ }
8244
+ function partitionPy(src) {
8245
+ const lines = src.split("\n");
8246
+ const dead = new Array(lines.length).fill(false);
8247
+ let killIndent = null;
8248
+ let suiteIndent = null;
8249
+ for (let i = 0; i < lines.length; i++) {
8250
+ const l = lines[i];
8251
+ if (!/\S/.test(l) || /^\s*#/.test(l)) continue;
8252
+ const indent = PY_INDENT(l);
8253
+ if (killIndent !== null && indent < killIndent) killIndent = null;
8254
+ if (suiteIndent !== null && indent <= suiteIndent) suiteIndent = null;
8255
+ if (killIndent !== null && indent >= killIndent || suiteIndent !== null && indent > suiteIndent) {
8256
+ dead[i] = true;
8257
+ continue;
8258
+ }
8259
+ if (PY_TERMINAL.test(l)) killIndent = indent;
8260
+ else if (PY_CONST_FALSE.test(l)) suiteIndent = indent;
8261
+ }
8262
+ const live = lines.map((l, i) => dead[i] ? "" : l).join("\n");
8263
+ const hidden = lines.map((l, i) => dead[i] ? l : "").join("\n");
8264
+ return { live, hidden, inLiteral: new Uint8Array(live.length) };
8265
+ }
8266
+ function partition(src, path) {
8267
+ const lang = langOf(path);
8268
+ if (lang === "js" || lang === null) {
8269
+ const p = partitionJs(src);
8270
+ if (p) return p;
8271
+ } else if (lang === "py") {
8272
+ return partitionPy(src);
8273
+ }
8274
+ return { live: src, hidden: "", inLiteral: new Uint8Array(src.length) };
8275
+ }
8276
+ function pytestCollectedDefs(src) {
8277
+ let n = 0;
8278
+ const stack = [];
8279
+ for (const line of src.split("\n")) {
8280
+ if (!/\S/.test(line) || /^\s*#/.test(line)) continue;
8281
+ const indent = PY_INDENT(line);
8282
+ while (stack.length && indent <= stack[stack.length - 1].indent) stack.pop();
8283
+ const cls = PY_CLASS.exec(line);
8284
+ if (cls) {
8285
+ stack.push({ indent, collected: PY_COLLECTED_CLASS(cls[1], cls[2] ?? "") });
8286
+ continue;
8287
+ }
8288
+ if (PY_TEST_DEF.test(line) && stack.every((c) => c.collected)) n++;
8289
+ }
8290
+ return n;
8291
+ }
8292
+ var MAX_ROUNDS, blankRange, PY_INDENT, PY_TERMINAL, PY_CONST_FALSE, PY_CLASS, PY_TEST_DEF, PY_COLLECTED_CLASS;
8293
+ var init_reachability = __esm({
8294
+ "src/detectors/reachability.ts"() {
8295
+ "use strict";
8296
+ init_ts_lazy();
8297
+ init_files();
8298
+ MAX_ROUNDS = 8;
8299
+ blankRange = (chars, start, end) => {
8300
+ for (let i = start; i < end && i < chars.length; i++) if (chars[i] !== "\n") chars[i] = " ";
8301
+ };
8302
+ PY_INDENT = (line) => line.length - line.trimStart().length;
8303
+ PY_TERMINAL = /^\s*(?:return\b|raise\b)/;
8304
+ PY_CONST_FALSE = /^\s*(?:if|while)\s+(?:False|0|None|not\s+True)\s*:/;
8305
+ PY_CLASS = /^\s*class\s+(\w+)\s*(?:\(([^)]*)\))?\s*:/;
8306
+ PY_TEST_DEF = /^\s*(?:async\s+)?def\s+test\w*\s*\(/;
8307
+ PY_COLLECTED_CLASS = (name, bases) => /^Test/.test(name) || /Test/.test(bases);
8308
+ }
8309
+ });
8310
+
7960
8311
  // src/detectors/test-deletion.ts
7961
8312
  function calleeName(expr) {
7962
8313
  if (ts.isIdentifier(expr)) return expr.text;
@@ -8006,6 +8357,7 @@ function eachRows(call) {
8006
8357
  }
8007
8358
  function countTests(src, path = "spec.ts", substantiveOnly = false) {
8008
8359
  const lang = langOf(path);
8360
+ if (lang === "py") return { min: pytestCollectedDefs(src), open: false };
8009
8361
  if (lang && lang !== "js") {
8010
8362
  const re = TEST_DEFS[lang];
8011
8363
  let n2 = 0;
@@ -8016,7 +8368,9 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
8016
8368
  if (!sf) return { min: 0, open: true };
8017
8369
  let n = 0;
8018
8370
  let open = false;
8371
+ const dead = unreachableNodes(sf);
8019
8372
  const visit = (node, mult) => {
8373
+ if (dead.has(node)) return;
8020
8374
  if (ts.isForOfStatement(node)) {
8021
8375
  const r = loopRows(node.expression);
8022
8376
  if (r.open) open = true;
@@ -8198,6 +8552,7 @@ var init_test_deletion = __esm({
8198
8552
  init_suite_config();
8199
8553
  init_invocation();
8200
8554
  init_narrow();
8555
+ init_reachability();
8201
8556
  init_runner_chain();
8202
8557
  RULE9 = "test-deletion";
8203
8558
  JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
@@ -8464,7 +8819,10 @@ function significantLinesOrdered(src, path) {
8464
8819
  }
8465
8820
  return out3;
8466
8821
  }
8467
- var RULE10, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, testContentRemoval;
8822
+ function liveSignificantLines(src, path) {
8823
+ return significantLinesOrdered(partition(src, path).live, path);
8824
+ }
8825
+ var RULE10, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, KeptPool, testContentRemoval;
8468
8826
  var init_test_content_removal = __esm({
8469
8827
  "src/detectors/test-content-removal.ts"() {
8470
8828
  "use strict";
@@ -8473,22 +8831,58 @@ var init_test_content_removal = __esm({
8473
8831
  init_finding();
8474
8832
  init_test_deletion();
8475
8833
  init_files();
8834
+ init_reachability();
8476
8835
  RULE10 = "test-content-removal";
8477
8836
  MIN_REMOVED_LINES = 3;
8478
8837
  MIN_REMOVED_ROWS = 2;
8479
8838
  ws = (s) => s.replace(/\s+/g, "");
8480
8839
  TEST_DIR_FILE = /(?:^|\/)(?:test|tests|__tests__|spec|specs)\//;
8840
+ KeptPool = class {
8841
+ text = "";
8842
+ inLiteral = [];
8843
+ addLive(src, path) {
8844
+ const p = partition(src, path);
8845
+ const lang = langOf(path);
8846
+ let lineStart = 0;
8847
+ for (let i = 0; i <= p.live.length; i++) {
8848
+ if (i < p.live.length && p.live[i] !== "\n") continue;
8849
+ const line = p.live.slice(lineStart, i);
8850
+ if (isSignificantLine(line.trim(), lang)) {
8851
+ for (let j = lineStart; j < i; j++) {
8852
+ if (/\s/.test(p.live[j])) continue;
8853
+ this.text += p.live[j];
8854
+ this.inLiteral.push(p.inLiteral[j]);
8855
+ }
8856
+ }
8857
+ lineStart = i + 1;
8858
+ }
8859
+ }
8860
+ /** Text added outside any spec (a case table moved to `test/fixtures/`): code. */
8861
+ addCode(ws2) {
8862
+ this.text += ws2;
8863
+ for (let i = 0; i < ws2.length; i++) this.inLiteral.push(0);
8864
+ }
8865
+ keeps(wsLine) {
8866
+ if (wsLine.length === 0) return true;
8867
+ let at = this.text.indexOf(wsLine);
8868
+ while (at !== -1) {
8869
+ if (this.inLiteral[at] === 0) return true;
8870
+ at = this.text.indexOf(wsLine, at + 1);
8871
+ }
8872
+ return false;
8873
+ }
8874
+ };
8481
8875
  testContentRemoval = {
8482
8876
  id: RULE10,
8483
8877
  surface: ["file"],
8484
8878
  certainty: "mechanical",
8485
8879
  run(changes, policy) {
8486
8880
  const out3 = [];
8487
- let keptPool = "";
8881
+ const keptPool = new KeptPool();
8488
8882
  let addedPool = "";
8489
8883
  for (const c of changes) {
8490
8884
  if (c.kind !== "file" || c.after == null) continue;
8491
- if (isProtected(c.path, policy, "tests")) keptPool += ws(significantLinesOrdered(c.after, c.path).join("\n"));
8885
+ if (isProtected(c.path, policy, "tests")) keptPool.addLive(c.after, c.path);
8492
8886
  let addedHere = "";
8493
8887
  if (c.op === "add") addedHere = ws(c.after);
8494
8888
  else if (c.before != null) {
@@ -8496,7 +8890,7 @@ var init_test_content_removal = __esm({
8496
8890
  addedHere = ws(c.after.split("\n").filter((l) => !had.has(l.trim())).join("\n"));
8497
8891
  }
8498
8892
  addedPool += addedHere;
8499
- if (!isProtected(c.path, policy, "tests") && TEST_DIR_FILE.test(c.path) && langOf(c.path) === "js") keptPool += addedHere;
8893
+ if (!isProtected(c.path, policy, "tests") && TEST_DIR_FILE.test(c.path) && langOf(c.path) === "js") keptPool.addCode(addedHere);
8500
8894
  }
8501
8895
  for (const c of changes) {
8502
8896
  if (c.kind !== "file" || c.op !== "modify" && c.op !== "rename" || c.before == null || c.after == null) continue;
@@ -8505,16 +8899,18 @@ var init_test_content_removal = __esm({
8505
8899
  const blocksBefore = countTests(c.before, c.path);
8506
8900
  const blocksAfter = countTests(c.after, c.path);
8507
8901
  if (blocksAfter.min < blocksBefore.min && !blocksAfter.open) continue;
8508
- const beforeSig = significantLinesOrdered(c.before, c.path);
8509
- const afterSig = significantLinesOrdered(c.after, c.path);
8902
+ const beforeSig = liveSignificantLines(c.before, c.path);
8903
+ const afterSig = liveSignificantLines(c.after, c.path);
8510
8904
  const netDrop = beforeSig.length - afterSig.length;
8511
8905
  const afterSet = new Set(afterSig);
8512
8906
  const gone = [];
8513
8907
  for (const line of new Set(beforeSig)) {
8514
8908
  if (afterSet.has(line)) continue;
8515
- if (keptPool.includes(ws(line))) continue;
8909
+ if (keptPool.keeps(ws(line))) continue;
8516
8910
  gone.push(line);
8517
8911
  }
8912
+ const hiddenPool = ws(partition(c.after, c.path).hidden);
8913
+ const hidden = gone.filter((line) => hiddenPool.includes(ws(line)));
8518
8914
  const table = eachTables(c.before, c.path);
8519
8915
  const tableAfter = eachTables(c.after, c.path);
8520
8916
  const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
@@ -8529,6 +8925,17 @@ var init_test_content_removal = __esm({
8529
8925
  );
8530
8926
  continue;
8531
8927
  }
8928
+ if (hidden.length >= MIN_REMOVED_LINES) {
8929
+ out3.push(
8930
+ makeFinding(RULE10, policy, {
8931
+ file: c.path,
8932
+ 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).`,
8933
+ evidence: hidden.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
8934
+ 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."
8935
+ })
8936
+ );
8937
+ continue;
8938
+ }
8532
8939
  if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
8533
8940
  out3.push(
8534
8941
  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.4",
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",