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.
- package/dist/cli/index.js +417 -10
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
8509
|
-
const afterSig =
|
|
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.
|
|
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
|
+
"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",
|