tamperward 2.24.1 → 2.24.3
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 +159 -15
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -3713,8 +3713,101 @@ 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";
|
|
3795
|
+
function pyMarkAliases(source) {
|
|
3796
|
+
const out3 = /* @__PURE__ */ new Set();
|
|
3797
|
+
for (const m of source.matchAll(PY_MARK_ALIAS)) out3.add(m[1]);
|
|
3798
|
+
return out3;
|
|
3799
|
+
}
|
|
3800
|
+
function pyAliasDecoratorHits(c) {
|
|
3801
|
+
const added = addedLines(c);
|
|
3802
|
+
const aliases = pyMarkAliases(c.after ?? added.map((l) => l.content).join("\n"));
|
|
3803
|
+
if (aliases.size === 0) return [];
|
|
3804
|
+
const hits = [];
|
|
3805
|
+
for (const l of added) {
|
|
3806
|
+
const m = PY_DECORATOR.exec(l.content);
|
|
3807
|
+
if (m && aliases.has(m[1])) hits.push({ line: l.newLine ?? void 0, evidence: l.content.trim() });
|
|
3808
|
+
}
|
|
3809
|
+
return hits;
|
|
3810
|
+
}
|
|
3718
3811
|
function matchesOutsideString(p, content, lang) {
|
|
3719
3812
|
if (p.comment) return p.re.test(content);
|
|
3720
3813
|
const re = p.re.global ? p.re : new RegExp(p.re.source, p.re.flags + "g");
|
|
@@ -3870,12 +3963,14 @@ function staticPropertyName(expr, ctx, strings) {
|
|
|
3870
3963
|
}
|
|
3871
3964
|
function runnerChain(expr, ctx, binder, strings) {
|
|
3872
3965
|
const props = [];
|
|
3966
|
+
const propNodes = [];
|
|
3873
3967
|
const causes = [];
|
|
3874
3968
|
let cur = expr;
|
|
3875
3969
|
let terminalNode = expr;
|
|
3876
3970
|
while (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) {
|
|
3877
3971
|
if (ts.isPropertyAccessExpression(cur)) {
|
|
3878
3972
|
props.unshift(cur.name.text);
|
|
3973
|
+
propNodes.unshift(cur.name);
|
|
3879
3974
|
if (props.length === 1) terminalNode = cur.name;
|
|
3880
3975
|
cur = cur.expression;
|
|
3881
3976
|
continue;
|
|
@@ -3884,6 +3979,7 @@ function runnerChain(expr, ctx, binder, strings) {
|
|
|
3884
3979
|
const property = staticPropertyName(cur.argumentExpression, ctx, strings);
|
|
3885
3980
|
if (!property) return null;
|
|
3886
3981
|
props.unshift(property.name);
|
|
3982
|
+
propNodes.unshift(cur.argumentExpression);
|
|
3887
3983
|
causes.push(...property.causes);
|
|
3888
3984
|
if (props.length === 1) terminalNode = cur.argumentExpression;
|
|
3889
3985
|
cur = cur.expression;
|
|
@@ -3900,17 +3996,21 @@ function runnerChain(expr, ctx, binder, strings) {
|
|
|
3900
3996
|
}
|
|
3901
3997
|
} else {
|
|
3902
3998
|
namespace = requiredModule(cur);
|
|
3999
|
+
if (namespace == null && readTableCallee(cur)) return null;
|
|
3903
4000
|
verdict2 = namespace == null ? UNKNOWN : NON_RUNNER;
|
|
3904
4001
|
}
|
|
3905
4002
|
let root = ts.isIdentifier(cur) ? cur.text : "";
|
|
3906
4003
|
if (namespace != null && props.length > 0) {
|
|
3907
4004
|
verdict2 = withCause(binder.memberOf(namespace, props[0]), cur);
|
|
3908
|
-
if (verdict2.kind === "runner")
|
|
4005
|
+
if (verdict2.kind === "runner") {
|
|
4006
|
+
root = props.shift() ?? root;
|
|
4007
|
+
propNodes.shift();
|
|
4008
|
+
}
|
|
3909
4009
|
}
|
|
3910
4010
|
if (verdict2.kind === "unknown") return "unknown";
|
|
3911
4011
|
if (verdict2.kind === "non-runner") return null;
|
|
3912
4012
|
causes.push(...verdict2.causes);
|
|
3913
|
-
return { root, props, terminalNode, causes };
|
|
4013
|
+
return { root, props, propNodes, terminalNode, causes };
|
|
3914
4014
|
}
|
|
3915
4015
|
function optionDisables(value) {
|
|
3916
4016
|
return !(value.kind === ts.SyntaxKind.FalseKeyword || ts.isNumericLiteral(value) && Number(value.text) === 0);
|
|
@@ -3949,8 +4049,9 @@ function semanticSkipHits(ctx, binder) {
|
|
|
3949
4049
|
const last = lineOf(callee.getEnd());
|
|
3950
4050
|
for (let line = first; line <= last; line++) unowned.add(line);
|
|
3951
4051
|
} else if (chain) {
|
|
3952
|
-
const { props, terminalNode, causes } = chain;
|
|
4052
|
+
const { root, props, propNodes, terminalNode, causes } = chain;
|
|
3953
4053
|
const terminal = props.at(-1);
|
|
4054
|
+
const shape = readRunnerChain(root, props);
|
|
3954
4055
|
const push = (target, extraCauses, why) => {
|
|
3955
4056
|
hits.push({
|
|
3956
4057
|
semanticKey: `${callKey}\0${why}`,
|
|
@@ -3960,13 +4061,13 @@ function semanticSkipHits(ctx, binder) {
|
|
|
3960
4061
|
evidence: callText
|
|
3961
4062
|
});
|
|
3962
4063
|
};
|
|
3963
|
-
if (
|
|
3964
|
-
push(terminalNode, [], "a .skip/.only/.todo marker");
|
|
4064
|
+
if (shape?.skipMarker) {
|
|
4065
|
+
push(propNodes[shape.skipIndex] ?? terminalNode, [], "a .skip/.only/.todo marker");
|
|
3965
4066
|
} else if (terminal && ["skipIf", "runIf"].includes(terminal) && props.length === 1) {
|
|
3966
4067
|
push(terminalNode, [], "a .skipIf()/.runIf() condition (the test runs only when the condition allows)");
|
|
3967
4068
|
} else if (terminal && ["fails", "failing"].includes(terminal) && props.length === 1) {
|
|
3968
4069
|
push(terminalNode, [], "a .fails/.failing marker (the test now passes by failing)");
|
|
3969
|
-
} else if (
|
|
4070
|
+
} else if (shape?.table === "each" && node.arguments.length > 0 && ts.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
|
|
3970
4071
|
push(terminalNode, [], "an empty .each table (no case ever runs)");
|
|
3971
4072
|
}
|
|
3972
4073
|
if (props.length === 0) {
|
|
@@ -4046,11 +4147,12 @@ function astSkipHits(c, resolvers) {
|
|
|
4046
4147
|
}
|
|
4047
4148
|
return { hits, authoritative: true, unowned: analysis.unowned };
|
|
4048
4149
|
}
|
|
4049
|
-
var RULE4, acc, JS_RUNNER, JS_MOD, PATTERNS2, JS_RUNNERS,
|
|
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;
|
|
4050
4151
|
var init_test_skip = __esm({
|
|
4051
4152
|
"src/detectors/test-skip.ts"() {
|
|
4052
4153
|
"use strict";
|
|
4053
4154
|
init_ts_lazy();
|
|
4155
|
+
init_runner_chain();
|
|
4054
4156
|
init_select();
|
|
4055
4157
|
init_policy();
|
|
4056
4158
|
init_repo();
|
|
@@ -4060,6 +4162,21 @@ var init_test_skip = __esm({
|
|
|
4060
4162
|
acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
|
|
4061
4163
|
JS_RUNNER = "\\b(?:it|test|describe|suite)";
|
|
4062
4164
|
JS_MOD = "concurrent|sequential|shuffle|serial|parallel";
|
|
4165
|
+
GO_SKIP_PATTERNS = [
|
|
4166
|
+
{ re: /\b\w+\.Skip(?:f|Now)?\(/, why: "a runtime t.Skip()/Skipf()/SkipNow() call" },
|
|
4167
|
+
{ re: /\bT\(\)\.Skip(?:f|Now)?\(/, why: "a runtime T().Skip()/Skipf()/SkipNow() call (testify suite)" }
|
|
4168
|
+
];
|
|
4169
|
+
PY_MARK = "(?:\\w+\\.)?mark\\.(?:skip|skipif|xfail)\\b";
|
|
4170
|
+
PY_CONFTEST_PATTERNS = [
|
|
4171
|
+
{ re: /\bdef\s+pytest_collection_modifyitems\s*\(/, why: "a pytest_collection_modifyitems hook (collected tests are filtered or marked before they run)" },
|
|
4172
|
+
{ re: /\bdef\s+pytest_pycollect_makeitem\s*\(/, why: "a pytest_pycollect_makeitem hook (what counts as a test is decided here)" },
|
|
4173
|
+
{ re: /\bdef\s+pytest_runtest_makereport\s*\(/, why: "a pytest_runtest_makereport hook (the test report can be rewritten)" },
|
|
4174
|
+
{ re: new RegExp("\\.add_marker\\(\\s*" + PY_MARK), why: "an add_marker(mark.skip/skipif/xfail) call (the test is skipped programmatically)" },
|
|
4175
|
+
{ re: new RegExp("^\\s*\\w+\\s*=\\s*" + PY_MARK), why: "a skip/skipif/xfail marker bound to a name (an aliased decorator)" },
|
|
4176
|
+
{ re: /\b\w+\.outcome\s*=\s*['"](?:passed|skipped)['"]/, why: "a report outcome rewritten to passed/skipped" }
|
|
4177
|
+
];
|
|
4178
|
+
PY_MARK_ALIAS = new RegExp("^\\s*(\\w+)\\s*=\\s*" + PY_MARK, "gm");
|
|
4179
|
+
PY_DECORATOR = /^\s*@(\w+)\s*(?:\(|$)/;
|
|
4063
4180
|
PATTERNS2 = {
|
|
4064
4181
|
js: [
|
|
4065
4182
|
// The skip/only/todo family on a runner, by dot-or-bracket access, with one optional
|
|
@@ -4103,10 +4220,13 @@ var init_test_skip = __esm({
|
|
|
4103
4220
|
{ re: /@(?:unittest\.)?(?:skip|skipIf|skipUnless|expectedFailure)\b/, why: "a unittest skip/expectedFailure decorator" },
|
|
4104
4221
|
{ re: /\bself\.skipTest\(/, why: "a runtime self.skipTest() call" },
|
|
4105
4222
|
{ re: /\braise\s+(?:unittest\.)?SkipTest\b/, why: "a raised SkipTest" },
|
|
4106
|
-
{ re: /\b__test__\s*=\s*False\b/, why: "__test__ = False hides the test from collection" }
|
|
4223
|
+
{ re: /\b__test__\s*=\s*False\b/, why: "__test__ = False hides the test from collection" },
|
|
4224
|
+
...PY_CONFTEST_PATTERNS
|
|
4225
|
+
// #441
|
|
4107
4226
|
],
|
|
4108
4227
|
go: [
|
|
4109
|
-
|
|
4228
|
+
...GO_SKIP_PATTERNS,
|
|
4229
|
+
// #441: any receiver, and testify's T()
|
|
4110
4230
|
{ re: /\bif\s+testing\.Short\(\)/, why: "a testing.Short() guard (the body is skipped under -short)" },
|
|
4111
4231
|
{ re: /^\s*\/\/\s*(?:go:build|\+build)\s+ignore\b/, why: "a build-ignore constraint (the file is excluded from the test run)", comment: true }
|
|
4112
4232
|
],
|
|
@@ -4138,7 +4258,6 @@ var init_test_skip = __esm({
|
|
|
4138
4258
|
]
|
|
4139
4259
|
};
|
|
4140
4260
|
JS_RUNNERS = /* @__PURE__ */ new Set(["it", "test", "describe", "suite"]);
|
|
4141
|
-
JS_CHAIN_MODIFIERS = /* @__PURE__ */ new Set(["concurrent", "sequential", "shuffle", "serial", "parallel"]);
|
|
4142
4261
|
JS_DEFAULT_RUNNER_MODULES = /* @__PURE__ */ new Set(["node:test", "node:test/promises", "@playwright/test"]);
|
|
4143
4262
|
JS_TEST_MODULES = /* @__PURE__ */ new Set([
|
|
4144
4263
|
"vitest",
|
|
@@ -4440,6 +4559,20 @@ var init_test_skip = __esm({
|
|
|
4440
4559
|
);
|
|
4441
4560
|
}
|
|
4442
4561
|
}
|
|
4562
|
+
if (lang === "py") {
|
|
4563
|
+
for (const hit of pyAliasDecoratorHits(c)) {
|
|
4564
|
+
if (hit.line != null) astHitLines.add(hit.line);
|
|
4565
|
+
out3.push(
|
|
4566
|
+
makeFinding(RULE4, policy, {
|
|
4567
|
+
file: c.path,
|
|
4568
|
+
line: hit.line,
|
|
4569
|
+
message: "Test skipped or narrowed: a decorator aliasing a pytest skip/skipif/xfail marker.",
|
|
4570
|
+
evidence: hit.evidence,
|
|
4571
|
+
remediation: "Make the test pass rather than skipping it. If it is genuinely obsolete, a human must sign off."
|
|
4572
|
+
})
|
|
4573
|
+
);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4443
4576
|
for (const l of addedLines(c)) {
|
|
4444
4577
|
if (l.newLine != null && astHitLines.has(l.newLine)) continue;
|
|
4445
4578
|
const comment = isCommentLine(l.content.trim(), lang);
|
|
@@ -7830,6 +7963,8 @@ function calleeName(expr) {
|
|
|
7830
7963
|
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression)) {
|
|
7831
7964
|
return expr.expression.text;
|
|
7832
7965
|
}
|
|
7966
|
+
const chain = readCallee(expr);
|
|
7967
|
+
if (chain && !chain.table) return chain.runner;
|
|
7833
7968
|
return null;
|
|
7834
7969
|
}
|
|
7835
7970
|
function loopRows(iterable) {
|
|
@@ -8050,7 +8185,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
8050
8185
|
})
|
|
8051
8186
|
);
|
|
8052
8187
|
}
|
|
8053
|
-
var RULE9, JVM_TEST, TEST_DEFS,
|
|
8188
|
+
var RULE9, JVM_TEST, TEST_DEFS, isTableOf, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
|
|
8054
8189
|
var init_test_deletion = __esm({
|
|
8055
8190
|
"src/detectors/test-deletion.ts"() {
|
|
8056
8191
|
"use strict";
|
|
@@ -8063,6 +8198,7 @@ var init_test_deletion = __esm({
|
|
|
8063
8198
|
init_suite_config();
|
|
8064
8199
|
init_invocation();
|
|
8065
8200
|
init_narrow();
|
|
8201
|
+
init_runner_chain();
|
|
8066
8202
|
RULE9 = "test-deletion";
|
|
8067
8203
|
JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
|
|
8068
8204
|
TEST_DEFS = {
|
|
@@ -8075,9 +8211,12 @@ var init_test_deletion = __esm({
|
|
|
8075
8211
|
php: /^\s*(?:(?:public\s+)?function\s+test\w*\s*\(|#\[Test\]|\*\s*@test\b)/,
|
|
8076
8212
|
cs: /^\s*\[(?:Fact|Theory|Test|TestMethod|TestCase|DataTestMethod)\b/
|
|
8077
8213
|
};
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8214
|
+
isTableOf = (expr, runners) => {
|
|
8215
|
+
const chain = readCallee(expr);
|
|
8216
|
+
return chain != null && chain.table != null && runners.includes(chain.runner);
|
|
8217
|
+
};
|
|
8218
|
+
isEachOf = (expr) => isTableOf(expr, ["it", "test"]);
|
|
8219
|
+
isDescribeEach = (expr) => isTableOf(expr, ["describe"]);
|
|
8081
8220
|
OTHER_RUNNER_DIR = /(?:^|\/)(?:e2e|cypress|playwright|fixtures|__fixtures__|__mocks__)\//;
|
|
8082
8221
|
WHOLE_SUITE_SCRIPT = /^(?:test|test:ci|check|pretest)$/;
|
|
8083
8222
|
SLICE_SCRIPT = /^test:(?:unit|integration)$/;
|
|
@@ -8587,7 +8726,11 @@ function expectAssertion(node, sf) {
|
|
|
8587
8726
|
};
|
|
8588
8727
|
}
|
|
8589
8728
|
function simpleCallName(node) {
|
|
8590
|
-
|
|
8729
|
+
if (ts.isIdentifier(node.expression)) return node.expression.text;
|
|
8730
|
+
const chain = readCallee(node.expression) ?? readTableCallee(node.expression);
|
|
8731
|
+
if (!chain) return null;
|
|
8732
|
+
if (chain.table && !ts.isCallExpression(node.expression) && !ts.isTaggedTemplateExpression(node.expression)) return null;
|
|
8733
|
+
return chain.runner;
|
|
8591
8734
|
}
|
|
8592
8735
|
function callbackFor(node) {
|
|
8593
8736
|
const cb = node.arguments[1];
|
|
@@ -8707,6 +8850,7 @@ var init_assertion_weakening = __esm({
|
|
|
8707
8850
|
init_policy();
|
|
8708
8851
|
init_finding();
|
|
8709
8852
|
init_files();
|
|
8853
|
+
init_runner_chain();
|
|
8710
8854
|
RULE13 = "assertion-weakening";
|
|
8711
8855
|
TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
8712
8856
|
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.3",
|
|
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",
|