tamperward 2.24.2 → 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 +525 -22
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -3713,6 +3713,83 @@ var init_lint_suppression = __esm({
|
|
|
3713
3713
|
}
|
|
3714
3714
|
});
|
|
3715
3715
|
|
|
3716
|
+
// src/detectors/runner-chain.ts
|
|
3717
|
+
function readRunnerChain(root, props) {
|
|
3718
|
+
let runner = root;
|
|
3719
|
+
const modifiers = [];
|
|
3720
|
+
let table = null;
|
|
3721
|
+
let skipMarker = null;
|
|
3722
|
+
let skipIndex = -1;
|
|
3723
|
+
for (let i = 0; i < props.length; i++) {
|
|
3724
|
+
const p = props[i];
|
|
3725
|
+
if (table) return null;
|
|
3726
|
+
if (RUNNER_NAMES.has(p) && modifiers.length === 0) {
|
|
3727
|
+
runner = p;
|
|
3728
|
+
continue;
|
|
3729
|
+
}
|
|
3730
|
+
if (CHAIN_MODIFIERS.has(p)) {
|
|
3731
|
+
modifiers.push(p);
|
|
3732
|
+
const marker = skipMarkerOf(p);
|
|
3733
|
+
if (marker && !skipMarker) {
|
|
3734
|
+
skipMarker = marker;
|
|
3735
|
+
skipIndex = i;
|
|
3736
|
+
}
|
|
3737
|
+
continue;
|
|
3738
|
+
}
|
|
3739
|
+
const t = tableOf(p);
|
|
3740
|
+
if (t) {
|
|
3741
|
+
table = t;
|
|
3742
|
+
continue;
|
|
3743
|
+
}
|
|
3744
|
+
return null;
|
|
3745
|
+
}
|
|
3746
|
+
return { runner, modifiers, table, skipMarker, skipIndex };
|
|
3747
|
+
}
|
|
3748
|
+
function readCallee(expr) {
|
|
3749
|
+
const props = [];
|
|
3750
|
+
let cur = expr;
|
|
3751
|
+
while (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) {
|
|
3752
|
+
if (ts.isPropertyAccessExpression(cur)) {
|
|
3753
|
+
props.unshift(cur.name.text);
|
|
3754
|
+
} else {
|
|
3755
|
+
const arg = cur.argumentExpression;
|
|
3756
|
+
if (!ts.isStringLiteralLike(arg)) return null;
|
|
3757
|
+
props.unshift(arg.text);
|
|
3758
|
+
}
|
|
3759
|
+
cur = cur.expression;
|
|
3760
|
+
}
|
|
3761
|
+
if (!ts.isIdentifier(cur)) return null;
|
|
3762
|
+
return readRunnerChain(cur.text, props);
|
|
3763
|
+
}
|
|
3764
|
+
function readTableCallee(expr) {
|
|
3765
|
+
const head = ts.isCallExpression(expr) ? expr.expression : ts.isTaggedTemplateExpression(expr) ? expr.tag : null;
|
|
3766
|
+
if (!head) return null;
|
|
3767
|
+
const chain = readCallee(head);
|
|
3768
|
+
return chain?.table ? chain : null;
|
|
3769
|
+
}
|
|
3770
|
+
var RUNNER_NAMES, CHAIN_MODIFIERS, skipMarkerOf, tableOf;
|
|
3771
|
+
var init_runner_chain = __esm({
|
|
3772
|
+
"src/detectors/runner-chain.ts"() {
|
|
3773
|
+
"use strict";
|
|
3774
|
+
init_ts_lazy();
|
|
3775
|
+
RUNNER_NAMES = /* @__PURE__ */ new Set(["it", "test", "describe", "suite"]);
|
|
3776
|
+
CHAIN_MODIFIERS = /* @__PURE__ */ new Set([
|
|
3777
|
+
"concurrent",
|
|
3778
|
+
"sequential",
|
|
3779
|
+
"shuffle",
|
|
3780
|
+
"serial",
|
|
3781
|
+
"parallel",
|
|
3782
|
+
"skip",
|
|
3783
|
+
"only",
|
|
3784
|
+
"todo",
|
|
3785
|
+
"fails",
|
|
3786
|
+
"failing"
|
|
3787
|
+
]);
|
|
3788
|
+
skipMarkerOf = (name) => name === "skip" || name === "only" || name === "todo" ? name : null;
|
|
3789
|
+
tableOf = (name) => name === "each" || name === "for" ? name : null;
|
|
3790
|
+
}
|
|
3791
|
+
});
|
|
3792
|
+
|
|
3716
3793
|
// src/detectors/test-skip.ts
|
|
3717
3794
|
import { posix as posix2 } from "node:path";
|
|
3718
3795
|
function pyMarkAliases(source) {
|
|
@@ -3742,6 +3819,47 @@ function matchesOutsideString(p, content, lang) {
|
|
|
3742
3819
|
}
|
|
3743
3820
|
return false;
|
|
3744
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
|
+
}
|
|
3745
3863
|
function scriptKind2(path) {
|
|
3746
3864
|
if (/\.tsx$/i.test(path)) return ts.ScriptKind.TSX;
|
|
3747
3865
|
if (/\.jsx$/i.test(path)) return ts.ScriptKind.JSX;
|
|
@@ -3886,12 +4004,14 @@ function staticPropertyName(expr, ctx, strings) {
|
|
|
3886
4004
|
}
|
|
3887
4005
|
function runnerChain(expr, ctx, binder, strings) {
|
|
3888
4006
|
const props = [];
|
|
4007
|
+
const propNodes = [];
|
|
3889
4008
|
const causes = [];
|
|
3890
4009
|
let cur = expr;
|
|
3891
4010
|
let terminalNode = expr;
|
|
3892
4011
|
while (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) {
|
|
3893
4012
|
if (ts.isPropertyAccessExpression(cur)) {
|
|
3894
4013
|
props.unshift(cur.name.text);
|
|
4014
|
+
propNodes.unshift(cur.name);
|
|
3895
4015
|
if (props.length === 1) terminalNode = cur.name;
|
|
3896
4016
|
cur = cur.expression;
|
|
3897
4017
|
continue;
|
|
@@ -3900,6 +4020,7 @@ function runnerChain(expr, ctx, binder, strings) {
|
|
|
3900
4020
|
const property = staticPropertyName(cur.argumentExpression, ctx, strings);
|
|
3901
4021
|
if (!property) return null;
|
|
3902
4022
|
props.unshift(property.name);
|
|
4023
|
+
propNodes.unshift(cur.argumentExpression);
|
|
3903
4024
|
causes.push(...property.causes);
|
|
3904
4025
|
if (props.length === 1) terminalNode = cur.argumentExpression;
|
|
3905
4026
|
cur = cur.expression;
|
|
@@ -3916,17 +4037,21 @@ function runnerChain(expr, ctx, binder, strings) {
|
|
|
3916
4037
|
}
|
|
3917
4038
|
} else {
|
|
3918
4039
|
namespace = requiredModule(cur);
|
|
4040
|
+
if (namespace == null && readTableCallee(cur)) return null;
|
|
3919
4041
|
verdict2 = namespace == null ? UNKNOWN : NON_RUNNER;
|
|
3920
4042
|
}
|
|
3921
4043
|
let root = ts.isIdentifier(cur) ? cur.text : "";
|
|
3922
4044
|
if (namespace != null && props.length > 0) {
|
|
3923
4045
|
verdict2 = withCause(binder.memberOf(namespace, props[0]), cur);
|
|
3924
|
-
if (verdict2.kind === "runner")
|
|
4046
|
+
if (verdict2.kind === "runner") {
|
|
4047
|
+
root = props.shift() ?? root;
|
|
4048
|
+
propNodes.shift();
|
|
4049
|
+
}
|
|
3925
4050
|
}
|
|
3926
4051
|
if (verdict2.kind === "unknown") return "unknown";
|
|
3927
4052
|
if (verdict2.kind === "non-runner") return null;
|
|
3928
4053
|
causes.push(...verdict2.causes);
|
|
3929
|
-
return { root, props, terminalNode, causes };
|
|
4054
|
+
return { root, props, propNodes, terminalNode, causes };
|
|
3930
4055
|
}
|
|
3931
4056
|
function optionDisables(value) {
|
|
3932
4057
|
return !(value.kind === ts.SyntaxKind.FalseKeyword || ts.isNumericLiteral(value) && Number(value.text) === 0);
|
|
@@ -3965,8 +4090,9 @@ function semanticSkipHits(ctx, binder) {
|
|
|
3965
4090
|
const last = lineOf(callee.getEnd());
|
|
3966
4091
|
for (let line = first; line <= last; line++) unowned.add(line);
|
|
3967
4092
|
} else if (chain) {
|
|
3968
|
-
const { props, terminalNode, causes } = chain;
|
|
4093
|
+
const { root, props, propNodes, terminalNode, causes } = chain;
|
|
3969
4094
|
const terminal = props.at(-1);
|
|
4095
|
+
const shape = readRunnerChain(root, props);
|
|
3970
4096
|
const push = (target, extraCauses, why) => {
|
|
3971
4097
|
hits.push({
|
|
3972
4098
|
semanticKey: `${callKey}\0${why}`,
|
|
@@ -3976,13 +4102,13 @@ function semanticSkipHits(ctx, binder) {
|
|
|
3976
4102
|
evidence: callText
|
|
3977
4103
|
});
|
|
3978
4104
|
};
|
|
3979
|
-
if (
|
|
3980
|
-
push(terminalNode, [], "a .skip/.only/.todo marker");
|
|
4105
|
+
if (shape?.skipMarker) {
|
|
4106
|
+
push(propNodes[shape.skipIndex] ?? terminalNode, [], "a .skip/.only/.todo marker");
|
|
3981
4107
|
} else if (terminal && ["skipIf", "runIf"].includes(terminal) && props.length === 1) {
|
|
3982
4108
|
push(terminalNode, [], "a .skipIf()/.runIf() condition (the test runs only when the condition allows)");
|
|
3983
4109
|
} else if (terminal && ["fails", "failing"].includes(terminal) && props.length === 1) {
|
|
3984
4110
|
push(terminalNode, [], "a .fails/.failing marker (the test now passes by failing)");
|
|
3985
|
-
} else if (
|
|
4111
|
+
} else if (shape?.table === "each" && node.arguments.length > 0 && ts.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
|
|
3986
4112
|
push(terminalNode, [], "an empty .each table (no case ever runs)");
|
|
3987
4113
|
}
|
|
3988
4114
|
if (props.length === 0) {
|
|
@@ -4062,11 +4188,12 @@ function astSkipHits(c, resolvers) {
|
|
|
4062
4188
|
}
|
|
4063
4189
|
return { hits, authoritative: true, unowned: analysis.unowned };
|
|
4064
4190
|
}
|
|
4065
|
-
var RULE4, acc, JS_RUNNER, JS_MOD, GO_SKIP_PATTERNS, PY_MARK, PY_CONFTEST_PATTERNS, PY_MARK_ALIAS, PY_DECORATOR, PATTERNS2, JS_RUNNERS,
|
|
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;
|
|
4066
4192
|
var init_test_skip = __esm({
|
|
4067
4193
|
"src/detectors/test-skip.ts"() {
|
|
4068
4194
|
"use strict";
|
|
4069
4195
|
init_ts_lazy();
|
|
4196
|
+
init_runner_chain();
|
|
4070
4197
|
init_select();
|
|
4071
4198
|
init_policy();
|
|
4072
4199
|
init_repo();
|
|
@@ -4076,6 +4203,7 @@ var init_test_skip = __esm({
|
|
|
4076
4203
|
acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
|
|
4077
4204
|
JS_RUNNER = "\\b(?:it|test|describe|suite)";
|
|
4078
4205
|
JS_MOD = "concurrent|sequential|shuffle|serial|parallel";
|
|
4206
|
+
JVM_CONDITIONAL = /@(?:[\w.]+\.)?(?:EnabledIf|DisabledIf)\w*\b/;
|
|
4079
4207
|
GO_SKIP_PATTERNS = [
|
|
4080
4208
|
{ re: /\b\w+\.Skip(?:f|Now)?\(/, why: "a runtime t.Skip()/Skipf()/SkipNow() call" },
|
|
4081
4209
|
{ re: /\bT\(\)\.Skip(?:f|Now)?\(/, why: "a runtime T().Skip()/Skipf()/SkipNow() call (testify suite)" }
|
|
@@ -4142,7 +4270,10 @@ var init_test_skip = __esm({
|
|
|
4142
4270
|
...GO_SKIP_PATTERNS,
|
|
4143
4271
|
// #441: any receiver, and testify's T()
|
|
4144
4272
|
{ re: /\bif\s+testing\.Short\(\)/, why: "a testing.Short() guard (the body is skipped under -short)" },
|
|
4145
|
-
|
|
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 }
|
|
4146
4277
|
],
|
|
4147
4278
|
rs: [
|
|
4148
4279
|
{ re: /#\[ignore\b/, why: "an #[ignore] attribute (the test no longer runs by default)" },
|
|
@@ -4153,14 +4284,19 @@ var init_test_skip = __esm({
|
|
|
4153
4284
|
{ re: /(?:^|[\s;{(])x(?:it|describe|context|specify|example)\s*[('"]/, why: "an x-prefixed disabled example" },
|
|
4154
4285
|
{ re: /(?:^|[\s;{(])f(?:it|describe|context)\s*[('"]/, why: "an f-prefixed focused example (narrows the suite)" },
|
|
4155
4286
|
{ re: /\b(?:skip|pending):\s*(?:true|['"])/, why: "a skip/pending metadata flag" },
|
|
4156
|
-
{ 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)" }
|
|
4157
4291
|
],
|
|
4158
4292
|
java: [
|
|
4159
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)" },
|
|
4160
4295
|
{ re: /\bassume(?:True\(\s*false|False\(\s*true)\s*[,)]/, why: "an assumption that never holds (the test aborts as skipped)" }
|
|
4161
4296
|
],
|
|
4162
4297
|
kt: [
|
|
4163
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)" },
|
|
4164
4300
|
{ re: /\bassume(?:True\(\s*false|False\(\s*true)\s*[,)]/, why: "an assumption that never holds (the test aborts as skipped)" }
|
|
4165
4301
|
],
|
|
4166
4302
|
php: [{ re: /\bmarkTest(?:Skipped|Incomplete)\(/, why: "a markTestSkipped()/markTestIncomplete() call" }],
|
|
@@ -4172,7 +4308,6 @@ var init_test_skip = __esm({
|
|
|
4172
4308
|
]
|
|
4173
4309
|
};
|
|
4174
4310
|
JS_RUNNERS = /* @__PURE__ */ new Set(["it", "test", "describe", "suite"]);
|
|
4175
|
-
JS_CHAIN_MODIFIERS = /* @__PURE__ */ new Set(["concurrent", "sequential", "shuffle", "serial", "parallel"]);
|
|
4176
4311
|
JS_DEFAULT_RUNNER_MODULES = /* @__PURE__ */ new Set(["node:test", "node:test/promises", "@playwright/test"]);
|
|
4177
4312
|
JS_TEST_MODULES = /* @__PURE__ */ new Set([
|
|
4178
4313
|
"vitest",
|
|
@@ -4183,6 +4318,12 @@ var init_test_skip = __esm({
|
|
|
4183
4318
|
"bun:test",
|
|
4184
4319
|
"@playwright/test"
|
|
4185
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+/;
|
|
4186
4327
|
RUNNER2 = { kind: "runner", causes: [] };
|
|
4187
4328
|
NON_RUNNER = { kind: "non-runner" };
|
|
4188
4329
|
UNKNOWN = { kind: "unknown" };
|
|
@@ -4474,6 +4615,18 @@ var init_test_skip = __esm({
|
|
|
4474
4615
|
);
|
|
4475
4616
|
}
|
|
4476
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
|
+
}
|
|
4477
4630
|
if (lang === "py") {
|
|
4478
4631
|
for (const hit of pyAliasDecoratorHits(c)) {
|
|
4479
4632
|
if (hit.line != null) astHitLines.add(hit.line);
|
|
@@ -7872,12 +8025,297 @@ They WILL FAIL in v10.0.0
|
|
|
7872
8025
|
}
|
|
7873
8026
|
});
|
|
7874
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
|
+
|
|
7875
8311
|
// src/detectors/test-deletion.ts
|
|
7876
8312
|
function calleeName(expr) {
|
|
7877
8313
|
if (ts.isIdentifier(expr)) return expr.text;
|
|
7878
8314
|
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression)) {
|
|
7879
8315
|
return expr.expression.text;
|
|
7880
8316
|
}
|
|
8317
|
+
const chain = readCallee(expr);
|
|
8318
|
+
if (chain && !chain.table) return chain.runner;
|
|
7881
8319
|
return null;
|
|
7882
8320
|
}
|
|
7883
8321
|
function loopRows(iterable) {
|
|
@@ -7919,6 +8357,7 @@ function eachRows(call) {
|
|
|
7919
8357
|
}
|
|
7920
8358
|
function countTests(src, path = "spec.ts", substantiveOnly = false) {
|
|
7921
8359
|
const lang = langOf(path);
|
|
8360
|
+
if (lang === "py") return { min: pytestCollectedDefs(src), open: false };
|
|
7922
8361
|
if (lang && lang !== "js") {
|
|
7923
8362
|
const re = TEST_DEFS[lang];
|
|
7924
8363
|
let n2 = 0;
|
|
@@ -7929,7 +8368,9 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
|
|
|
7929
8368
|
if (!sf) return { min: 0, open: true };
|
|
7930
8369
|
let n = 0;
|
|
7931
8370
|
let open = false;
|
|
8371
|
+
const dead = unreachableNodes(sf);
|
|
7932
8372
|
const visit = (node, mult) => {
|
|
8373
|
+
if (dead.has(node)) return;
|
|
7933
8374
|
if (ts.isForOfStatement(node)) {
|
|
7934
8375
|
const r = loopRows(node.expression);
|
|
7935
8376
|
if (r.open) open = true;
|
|
@@ -8098,7 +8539,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
8098
8539
|
})
|
|
8099
8540
|
);
|
|
8100
8541
|
}
|
|
8101
|
-
var RULE9, JVM_TEST, TEST_DEFS,
|
|
8542
|
+
var RULE9, JVM_TEST, TEST_DEFS, isTableOf, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
|
|
8102
8543
|
var init_test_deletion = __esm({
|
|
8103
8544
|
"src/detectors/test-deletion.ts"() {
|
|
8104
8545
|
"use strict";
|
|
@@ -8111,6 +8552,8 @@ var init_test_deletion = __esm({
|
|
|
8111
8552
|
init_suite_config();
|
|
8112
8553
|
init_invocation();
|
|
8113
8554
|
init_narrow();
|
|
8555
|
+
init_reachability();
|
|
8556
|
+
init_runner_chain();
|
|
8114
8557
|
RULE9 = "test-deletion";
|
|
8115
8558
|
JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
|
|
8116
8559
|
TEST_DEFS = {
|
|
@@ -8123,9 +8566,12 @@ var init_test_deletion = __esm({
|
|
|
8123
8566
|
php: /^\s*(?:(?:public\s+)?function\s+test\w*\s*\(|#\[Test\]|\*\s*@test\b)/,
|
|
8124
8567
|
cs: /^\s*\[(?:Fact|Theory|Test|TestMethod|TestCase|DataTestMethod)\b/
|
|
8125
8568
|
};
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8569
|
+
isTableOf = (expr, runners) => {
|
|
8570
|
+
const chain = readCallee(expr);
|
|
8571
|
+
return chain != null && chain.table != null && runners.includes(chain.runner);
|
|
8572
|
+
};
|
|
8573
|
+
isEachOf = (expr) => isTableOf(expr, ["it", "test"]);
|
|
8574
|
+
isDescribeEach = (expr) => isTableOf(expr, ["describe"]);
|
|
8129
8575
|
OTHER_RUNNER_DIR = /(?:^|\/)(?:e2e|cypress|playwright|fixtures|__fixtures__|__mocks__)\//;
|
|
8130
8576
|
WHOLE_SUITE_SCRIPT = /^(?:test|test:ci|check|pretest)$/;
|
|
8131
8577
|
SLICE_SCRIPT = /^test:(?:unit|integration)$/;
|
|
@@ -8373,7 +8819,10 @@ function significantLinesOrdered(src, path) {
|
|
|
8373
8819
|
}
|
|
8374
8820
|
return out3;
|
|
8375
8821
|
}
|
|
8376
|
-
|
|
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;
|
|
8377
8826
|
var init_test_content_removal = __esm({
|
|
8378
8827
|
"src/detectors/test-content-removal.ts"() {
|
|
8379
8828
|
"use strict";
|
|
@@ -8382,22 +8831,58 @@ var init_test_content_removal = __esm({
|
|
|
8382
8831
|
init_finding();
|
|
8383
8832
|
init_test_deletion();
|
|
8384
8833
|
init_files();
|
|
8834
|
+
init_reachability();
|
|
8385
8835
|
RULE10 = "test-content-removal";
|
|
8386
8836
|
MIN_REMOVED_LINES = 3;
|
|
8387
8837
|
MIN_REMOVED_ROWS = 2;
|
|
8388
8838
|
ws = (s) => s.replace(/\s+/g, "");
|
|
8389
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
|
+
};
|
|
8390
8875
|
testContentRemoval = {
|
|
8391
8876
|
id: RULE10,
|
|
8392
8877
|
surface: ["file"],
|
|
8393
8878
|
certainty: "mechanical",
|
|
8394
8879
|
run(changes, policy) {
|
|
8395
8880
|
const out3 = [];
|
|
8396
|
-
|
|
8881
|
+
const keptPool = new KeptPool();
|
|
8397
8882
|
let addedPool = "";
|
|
8398
8883
|
for (const c of changes) {
|
|
8399
8884
|
if (c.kind !== "file" || c.after == null) continue;
|
|
8400
|
-
if (isProtected(c.path, policy, "tests")) keptPool
|
|
8885
|
+
if (isProtected(c.path, policy, "tests")) keptPool.addLive(c.after, c.path);
|
|
8401
8886
|
let addedHere = "";
|
|
8402
8887
|
if (c.op === "add") addedHere = ws(c.after);
|
|
8403
8888
|
else if (c.before != null) {
|
|
@@ -8405,7 +8890,7 @@ var init_test_content_removal = __esm({
|
|
|
8405
8890
|
addedHere = ws(c.after.split("\n").filter((l) => !had.has(l.trim())).join("\n"));
|
|
8406
8891
|
}
|
|
8407
8892
|
addedPool += addedHere;
|
|
8408
|
-
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);
|
|
8409
8894
|
}
|
|
8410
8895
|
for (const c of changes) {
|
|
8411
8896
|
if (c.kind !== "file" || c.op !== "modify" && c.op !== "rename" || c.before == null || c.after == null) continue;
|
|
@@ -8414,16 +8899,18 @@ var init_test_content_removal = __esm({
|
|
|
8414
8899
|
const blocksBefore = countTests(c.before, c.path);
|
|
8415
8900
|
const blocksAfter = countTests(c.after, c.path);
|
|
8416
8901
|
if (blocksAfter.min < blocksBefore.min && !blocksAfter.open) continue;
|
|
8417
|
-
const beforeSig =
|
|
8418
|
-
const afterSig =
|
|
8902
|
+
const beforeSig = liveSignificantLines(c.before, c.path);
|
|
8903
|
+
const afterSig = liveSignificantLines(c.after, c.path);
|
|
8419
8904
|
const netDrop = beforeSig.length - afterSig.length;
|
|
8420
8905
|
const afterSet = new Set(afterSig);
|
|
8421
8906
|
const gone = [];
|
|
8422
8907
|
for (const line of new Set(beforeSig)) {
|
|
8423
8908
|
if (afterSet.has(line)) continue;
|
|
8424
|
-
if (keptPool.
|
|
8909
|
+
if (keptPool.keeps(ws(line))) continue;
|
|
8425
8910
|
gone.push(line);
|
|
8426
8911
|
}
|
|
8912
|
+
const hiddenPool = ws(partition(c.after, c.path).hidden);
|
|
8913
|
+
const hidden = gone.filter((line) => hiddenPool.includes(ws(line)));
|
|
8427
8914
|
const table = eachTables(c.before, c.path);
|
|
8428
8915
|
const tableAfter = eachTables(c.after, c.path);
|
|
8429
8916
|
const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
|
|
@@ -8438,6 +8925,17 @@ var init_test_content_removal = __esm({
|
|
|
8438
8925
|
);
|
|
8439
8926
|
continue;
|
|
8440
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
|
+
}
|
|
8441
8939
|
if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
|
|
8442
8940
|
out3.push(
|
|
8443
8941
|
makeFinding(RULE10, policy, {
|
|
@@ -8635,7 +9133,11 @@ function expectAssertion(node, sf) {
|
|
|
8635
9133
|
};
|
|
8636
9134
|
}
|
|
8637
9135
|
function simpleCallName(node) {
|
|
8638
|
-
|
|
9136
|
+
if (ts.isIdentifier(node.expression)) return node.expression.text;
|
|
9137
|
+
const chain = readCallee(node.expression) ?? readTableCallee(node.expression);
|
|
9138
|
+
if (!chain) return null;
|
|
9139
|
+
if (chain.table && !ts.isCallExpression(node.expression) && !ts.isTaggedTemplateExpression(node.expression)) return null;
|
|
9140
|
+
return chain.runner;
|
|
8639
9141
|
}
|
|
8640
9142
|
function callbackFor(node) {
|
|
8641
9143
|
const cb = node.arguments[1];
|
|
@@ -8755,6 +9257,7 @@ var init_assertion_weakening = __esm({
|
|
|
8755
9257
|
init_policy();
|
|
8756
9258
|
init_finding();
|
|
8757
9259
|
init_files();
|
|
9260
|
+
init_runner_chain();
|
|
8758
9261
|
RULE13 = "assertion-weakening";
|
|
8759
9262
|
TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
8760
9263
|
SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
|
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",
|