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.
Files changed (2) hide show
  1. package/dist/cli/index.js +159 -15
  2. 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") root = props.shift() ?? root;
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 (terminal && (terminal === "skip" || terminal === "only" || terminal === "todo") && props.slice(0, -1).every((p) => JS_CHAIN_MODIFIERS.has(p) || JS_RUNNERS.has(p))) {
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 (terminal === "each" && props.length === 1 && node.arguments.length > 0 && ts.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
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, JS_CHAIN_MODIFIERS, JS_DEFAULT_RUNNER_MODULES, JS_TEST_MODULES, RUNNER2, NON_RUNNER, UNKNOWN, withCause, MODULE_EXTENSIONS, RunnerResolver, ModuleBinder, NO_AST, testSkip;
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
- { re: /\b[tb]\.Skip(?:f|Now)?\(/, why: "a runtime t.Skip()/Skipf()/SkipNow() call" },
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, TABLE_METHOD, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
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
- TABLE_METHOD = /^(?:each|for)$/;
8079
- isEachOf = (expr) => ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
8080
- isDescribeEach = (expr) => ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
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
- return ts.isIdentifier(node.expression) ? node.expression.text : null;
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.1",
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",