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.
- package/dist/cli/index.js +572 -42
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
8351
|
-
if (t.startsWith("-")
|
|
8352
|
-
const
|
|
8353
|
-
if (!
|
|
8354
|
-
if (root === null) return
|
|
8355
|
-
const dir =
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
8371
|
-
const rev2 = src ?? (revIdx >= 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 &&
|
|
8838
|
+
return revIdx >= 0 && cmdToks.some((t, i) => i > revIdx && named.includes(t));
|
|
8375
8839
|
})();
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
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 (
|
|
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
|
|
8389
|
-
why =
|
|
8390
|
-
} else if (cmd === "tee" && !
|
|
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 (
|
|
8397
|
-
const dest =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
8509
|
-
const afterSig =
|
|
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.
|
|
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
|
+
"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",
|