tamperward 2.2.0 → 2.2.1
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 +212 -70
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -356,31 +356,59 @@ function tokens(seg) {
|
|
|
356
356
|
function unquote(t) {
|
|
357
357
|
return t.replace(/^['"]+|['"]+$/g, "");
|
|
358
358
|
}
|
|
359
|
+
function words(seg) {
|
|
360
|
+
return tokens(seg).map(unquote);
|
|
361
|
+
}
|
|
362
|
+
var GIT_OPT_WITH_ARG = /* @__PURE__ */ new Set(["-c", "-C", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env"]);
|
|
363
|
+
function gitSubcommand(toks) {
|
|
364
|
+
const i = toks.findIndex((t) => t === "git" || t.endsWith("/git"));
|
|
365
|
+
if (i < 0) return null;
|
|
366
|
+
for (let j = i + 1; j < toks.length; j++) {
|
|
367
|
+
const t = toks[j];
|
|
368
|
+
if (GIT_OPT_WITH_ARG.has(t)) {
|
|
369
|
+
j++;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (t.startsWith("-")) continue;
|
|
373
|
+
return t;
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
359
377
|
|
|
360
378
|
// src/detectors/no-verify.ts
|
|
361
379
|
var RULE = "no-verify";
|
|
362
|
-
var
|
|
363
|
-
{ re: /(?:^|\s)--no-verify(?:\s|$)/, why: "--no-verify skips the pre-commit/pre-push hooks" },
|
|
364
|
-
{ re: /(?:^|\s)--no-hooks(?:\s|$)/, why: "--no-hooks disables hook execution" },
|
|
365
|
-
{ re: /(?:^|[\s;&|=])HUSKY=0(?:\s|$)/, why: "HUSKY=0 disables all Husky hooks" },
|
|
366
|
-
{ re: /\bHUSKY_SKIP_HOOKS=1\b/, why: "HUSKY_SKIP_HOOKS=1 skips Husky hooks" },
|
|
367
|
-
// git-native hook disabling. core.hooksPath is strictly MORE general than the
|
|
368
|
-
// flags above: it turns off husky, lefthook, plain .git/hooks and pre-commit
|
|
369
|
-
// alike, and it was reachable in three spellings the literal list missed
|
|
370
|
-
// (-c, `git config`, and GIT_CONFIG_* env injection). (P1-9, external review.)
|
|
380
|
+
var RAW = [
|
|
371
381
|
{ re: /\bcore\.hooksPath\b/i, why: "core.hooksPath redirects git away from the installed hooks" },
|
|
372
|
-
{ re: /\bGIT_CONFIG_KEY_\d+\s*=\s*["']?core\.hooksPath/i, why: "GIT_CONFIG_* injection sets core.hooksPath" }
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
{ re:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
{ re:
|
|
381
|
-
{ re: /(?:^|[\s;&|])LEFTHOOK_EXCLUDE=\S+/, why: "LEFTHOOK_EXCLUDE=<name> skips the named Lefthook commands" }
|
|
382
|
+
{ re: /\bGIT_CONFIG_KEY_\d+\s*=\s*["']?core\.hooksPath/i, why: "GIT_CONFIG_* injection sets core.hooksPath" }
|
|
383
|
+
];
|
|
384
|
+
var ENV = [
|
|
385
|
+
{ re: /^HUSKY=0$/, why: "HUSKY=0 disables all Husky hooks" },
|
|
386
|
+
{ re: /^HUSKY_SKIP_HOOKS=1$/, why: "HUSKY_SKIP_HOOKS=1 skips Husky hooks" },
|
|
387
|
+
{ re: /^LEFTHOOK=0$/, why: "LEFTHOOK=0 disables Lefthook hooks" },
|
|
388
|
+
{ re: /^SKIP_SIMPLE_GIT_HOOKS=1$/, why: "SKIP_SIMPLE_GIT_HOOKS=1 skips simple-git-hooks" },
|
|
389
|
+
{ re: /^PRE_COMMIT_ALLOW_NO_CONFIG=1$/, why: "PRE_COMMIT_ALLOW_NO_CONFIG=1 lets pre-commit no-op" },
|
|
390
|
+
{ re: /^LEFTHOOK_EXCLUDE=./, why: "LEFTHOOK_EXCLUDE=<name> skips the named Lefthook commands" }
|
|
382
391
|
];
|
|
392
|
+
var SKIP = /^SKIP=./;
|
|
393
|
+
var SKIP_WHY = "SKIP=<hook-id> tells the pre-commit framework to skip that hook";
|
|
394
|
+
var ENV_PREFIX = /^(?:export|env|sudo)$/;
|
|
395
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
396
|
+
var HOOK_SUBCOMMANDS = /* @__PURE__ */ new Set(["commit", "push", "merge"]);
|
|
397
|
+
var NO_VERIFY = /^--no-veri(?:f|fy)?$/;
|
|
398
|
+
var NO_HOOKS = "--no-hooks";
|
|
383
399
|
var SHORT_N = /^-[a-z]*n[a-z]*$/i;
|
|
400
|
+
var TAKES_VALUE = /* @__PURE__ */ new Set(["-m", "--message", "-F", "--file", "--author", "--date", "--trailer", "-c", "-C", "--reuse-message", "--reedit-message"]);
|
|
401
|
+
function gitArgs(toks, sub) {
|
|
402
|
+
const out2 = [];
|
|
403
|
+
for (let j = toks.indexOf(sub) + 1; j < toks.length; j++) {
|
|
404
|
+
if (TAKES_VALUE.has(toks[j])) {
|
|
405
|
+
j++;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
out2.push(toks[j]);
|
|
409
|
+
}
|
|
410
|
+
return out2;
|
|
411
|
+
}
|
|
384
412
|
var noVerify = {
|
|
385
413
|
id: RULE,
|
|
386
414
|
surface: ["command"],
|
|
@@ -389,24 +417,53 @@ var noVerify = {
|
|
|
389
417
|
const out2 = [];
|
|
390
418
|
for (const c of changes) {
|
|
391
419
|
if (c.kind !== "command") continue;
|
|
420
|
+
let carriedSkip = null;
|
|
392
421
|
for (const seg of segments(c.raw)) {
|
|
393
422
|
let why = null;
|
|
394
|
-
|
|
395
|
-
|
|
423
|
+
let evidence = seg;
|
|
424
|
+
for (const p of RAW) {
|
|
425
|
+
if (p.re.test(seg)) {
|
|
396
426
|
why = p.why;
|
|
397
427
|
break;
|
|
398
428
|
}
|
|
399
429
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
430
|
+
const toks = words(seg);
|
|
431
|
+
let k = 0;
|
|
432
|
+
while (k < toks.length && ENV_PREFIX.test(toks[k])) k++;
|
|
433
|
+
const exported = k > 0 && toks[0] === "export";
|
|
434
|
+
const assigns = [];
|
|
435
|
+
while (k < toks.length && ASSIGNMENT.test(toks[k])) assigns.push(toks[k++]);
|
|
436
|
+
const bareAssignment = k === toks.length && assigns.length > 0;
|
|
437
|
+
const sub = gitSubcommand(toks);
|
|
438
|
+
const runsHookTool = sub !== null || toks.includes("pre-commit");
|
|
439
|
+
if (!why) {
|
|
440
|
+
for (const a of assigns) {
|
|
441
|
+
const hit = ENV.find((p) => p.re.test(a));
|
|
442
|
+
if (hit) {
|
|
443
|
+
why = hit.why;
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
if (SKIP.test(a)) {
|
|
447
|
+
if (runsHookTool) why = SKIP_WHY;
|
|
448
|
+
else if (exported || bareAssignment) carriedSkip = seg;
|
|
449
|
+
}
|
|
403
450
|
}
|
|
404
451
|
}
|
|
452
|
+
if (!why && carriedSkip && runsHookTool) {
|
|
453
|
+
why = `${SKIP_WHY} (set earlier in the same command)`;
|
|
454
|
+
evidence = `${carriedSkip}; ${seg}`;
|
|
455
|
+
}
|
|
456
|
+
if (!why && sub && HOOK_SUBCOMMANDS.has(sub)) {
|
|
457
|
+
const args = gitArgs(toks, sub);
|
|
458
|
+
if (args.some((t) => NO_VERIFY.test(t))) why = "--no-verify skips the pre-commit/pre-push hooks";
|
|
459
|
+
else if (sub === "commit" && args.some((t) => SHORT_N.test(t))) why = "git commit -n skips the pre-commit hook";
|
|
460
|
+
}
|
|
461
|
+
if (!why && toks.includes(NO_HOOKS)) why = "--no-hooks disables hook execution";
|
|
405
462
|
if (why) {
|
|
406
463
|
out2.push(
|
|
407
464
|
makeFinding(RULE, policy, {
|
|
408
465
|
message: `Command bypasses git hooks: ${why}.`,
|
|
409
|
-
evidence
|
|
466
|
+
evidence,
|
|
410
467
|
remediation: "Let the hooks run and fix what they catch. Skipping verification is exactly the move this gate blocks \u2014 editing the hook and --no-hooks are blocked too."
|
|
411
468
|
})
|
|
412
469
|
);
|
|
@@ -645,12 +702,38 @@ function isSignificantLine(trimmed, lang) {
|
|
|
645
702
|
if (HASH_COMMENT.has(l) && trimmed.startsWith("#")) return false;
|
|
646
703
|
return true;
|
|
647
704
|
}
|
|
705
|
+
var SLASH_COMMENT = /^(?:\/\/|\/\*|\*(?:\s|\/|$))/;
|
|
706
|
+
var HASH_COMMENT_LANGS = /* @__PURE__ */ new Set(["py", "rb", "php"]);
|
|
707
|
+
function isCommentLine(trimmed, lang) {
|
|
708
|
+
const l = lang ?? "js";
|
|
709
|
+
if (HASH_COMMENT_LANGS.has(l) && trimmed.startsWith("#") && !trimmed.startsWith("#[")) return true;
|
|
710
|
+
if (l === "py" || l === "rb") return false;
|
|
711
|
+
return SLASH_COMMENT.test(trimmed);
|
|
712
|
+
}
|
|
713
|
+
function insideStringLiteral(line, idx, lang) {
|
|
714
|
+
const hash = HASH_COMMENT_LANGS.has(lang ?? "js");
|
|
715
|
+
let quote = null;
|
|
716
|
+
for (let j = 0; j < idx; j++) {
|
|
717
|
+
const ch = line[j];
|
|
718
|
+
if (quote) {
|
|
719
|
+
if (ch === "\\") j++;
|
|
720
|
+
else if (ch === quote) quote = null;
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
724
|
+
else if (ch === "/" && (line[j + 1] === "/" || line[j + 1] === "*")) return false;
|
|
725
|
+
else if (hash && ch === "#") return false;
|
|
726
|
+
}
|
|
727
|
+
return quote !== null;
|
|
728
|
+
}
|
|
648
729
|
|
|
649
730
|
// src/detectors/ts-any-cast.ts
|
|
650
731
|
var BLOCK_RULE = "ts-any-cast";
|
|
651
732
|
var WARN_RULE = "ts-any-launder";
|
|
652
733
|
var DOUBLE_CAST = /\bas\s+unknown\s+as\b/g;
|
|
653
734
|
var SUPPRESS = /@ts-(?:ignore|expect-error|nocheck)\b/g;
|
|
735
|
+
var JSDOC_ANY_CAST = /\/\*\*\s*@type\s*\{\s*any\s*\}\s*\*\/\s*\(/g;
|
|
736
|
+
var JS_FILE = /\.(?:js|jsx|mjs|cjs)$/;
|
|
654
737
|
var countMatches = (s, re) => (s.match(re) || []).length;
|
|
655
738
|
function countAny(src) {
|
|
656
739
|
const r = { cast: 0, broad: 0 };
|
|
@@ -658,8 +741,13 @@ function countAny(src) {
|
|
|
658
741
|
const sf = ts.createSourceFile("f.ts", src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
659
742
|
const visit = (node) => {
|
|
660
743
|
if (node.kind === ts.SyntaxKind.AnyKeyword) {
|
|
661
|
-
|
|
662
|
-
|
|
744
|
+
let target = node;
|
|
745
|
+
let p = target.parent;
|
|
746
|
+
while (p && ts.isParenthesizedTypeNode(p)) {
|
|
747
|
+
target = p;
|
|
748
|
+
p = p.parent;
|
|
749
|
+
}
|
|
750
|
+
const isCast = p && ts.isAsExpression(p) && p.type === target || p && ts.isTypeAssertionExpression(p) && p.type === target;
|
|
663
751
|
if (isCast) r.cast++;
|
|
664
752
|
else r.broad++;
|
|
665
753
|
}
|
|
@@ -670,7 +758,7 @@ function countAny(src) {
|
|
|
670
758
|
}
|
|
671
759
|
return r;
|
|
672
760
|
}
|
|
673
|
-
var NARROW_LINE = /\bas\s
|
|
761
|
+
var NARROW_LINE = /\bas\s+\(*\s*any\b|<\s*any\s*>|\bas\s+unknown\s+as\b|@ts-(?:ignore|expect-error|nocheck)\b/;
|
|
674
762
|
var BROAD_LINE = /:\s*any\b|<[^<>]*\bany\b[^<>]*>/;
|
|
675
763
|
var BLOCK_REMEDIATION = "Fix the underlying type instead of silencing the checker; do not cast to `any`.";
|
|
676
764
|
var WARN_REMEDIATION = "Prefer a precise type or `unknown` + a guard over `any` here \u2014 flagged for review.";
|
|
@@ -691,10 +779,12 @@ var tsAnyCast = {
|
|
|
691
779
|
const dBroad = a.broad - b.broad;
|
|
692
780
|
const dDouble = countMatches(c.after, DOUBLE_CAST) - countMatches(before, DOUBLE_CAST);
|
|
693
781
|
const dSuppr = countMatches(c.after, SUPPRESS) - countMatches(before, SUPPRESS);
|
|
782
|
+
const dJsdoc = JS_FILE.test(c.path) ? countMatches(c.after, JSDOC_ANY_CAST) - countMatches(before, JSDOC_ANY_CAST) : 0;
|
|
694
783
|
const blockReasons = [];
|
|
695
784
|
if (dCast > 0) blockReasons.push("`as any` cast");
|
|
696
785
|
if (dDouble > 0) blockReasons.push("`as unknown as` double cast");
|
|
697
786
|
if (dSuppr > 0) blockReasons.push("@ts-ignore/@ts-expect-error/@ts-nocheck suppression");
|
|
787
|
+
if (dJsdoc > 0) blockReasons.push("JSDoc `@type {any}` cast");
|
|
698
788
|
if (blockReasons.length) {
|
|
699
789
|
out2.push(
|
|
700
790
|
makeFinding(inTest ? WARN_RULE : BLOCK_RULE, policy, {
|
|
@@ -719,8 +809,9 @@ var tsAnyCast = {
|
|
|
719
809
|
}
|
|
720
810
|
continue;
|
|
721
811
|
}
|
|
812
|
+
const jsdocCast = JS_FILE.test(c.path) ? new RegExp(JSDOC_ANY_CAST.source) : null;
|
|
722
813
|
for (const l of addedLines(c)) {
|
|
723
|
-
if (NARROW_LINE.test(l.content)) {
|
|
814
|
+
if (NARROW_LINE.test(l.content) || jsdocCast && jsdocCast.test(l.content)) {
|
|
724
815
|
out2.push(
|
|
725
816
|
makeFinding(inTest ? WARN_RULE : BLOCK_RULE, policy, {
|
|
726
817
|
file: c.path,
|
|
@@ -754,30 +845,47 @@ var RULE2 = "lint-suppression";
|
|
|
754
845
|
var PATTERNS = {
|
|
755
846
|
js: [
|
|
756
847
|
{ re: /eslint-disable(?:-next-line|-line)?\b/, why: "eslint-disable suppresses lint rules" },
|
|
848
|
+
// Inline config is the other spelling of the same directive: `/* eslint rule: off */`,
|
|
849
|
+
// `: "off"`, `: 0`, `: [0, …]`. `/* eslint-env */` and `/* eslint rule: "error" */` are not.
|
|
850
|
+
{ re: /\/\*\s*eslint\s+(?!-)(?:[^*]*?[\s,])?[\w@/.-]+\s*:\s*\[?\s*(?:["']off["']|off\b|0\b)/, why: "an inline eslint config turns a rule off" },
|
|
757
851
|
{ re: /\bprettier-ignore\b/, why: "prettier-ignore suppresses formatting checks" },
|
|
758
852
|
{ re: /\bbiome-ignore\b/, why: "biome-ignore suppresses Biome diagnostics" }
|
|
759
853
|
],
|
|
760
854
|
py: [
|
|
761
855
|
{ re: /#\s*noqa\b/, why: "# noqa suppresses flake8/ruff findings" },
|
|
856
|
+
{ re: /#\s*(?:ruff|flake8):\s*noqa\b/, why: "# ruff: noqa / # flake8: noqa suppresses every finding in the file" },
|
|
762
857
|
{ re: /#\s*type:\s*ignore\b/, why: "# type: ignore suppresses the type checker" },
|
|
858
|
+
{ re: /#\s*mypy:\s*ignore-errors\b/, why: "# mypy: ignore-errors suppresses the type checker for the file" },
|
|
763
859
|
{ re: /#\s*pyright:\s*ignore\b/, why: "# pyright: ignore suppresses the type checker" },
|
|
764
860
|
{ re: /#\s*pylint:\s*disable\b/, why: "# pylint: disable suppresses lint rules" },
|
|
861
|
+
{ re: /#\s*pylint:\s*skip-file\b/, why: "# pylint: skip-file suppresses lint for the file" },
|
|
765
862
|
{ re: /#\s*nosec\b/, why: "# nosec suppresses bandit security findings" },
|
|
766
863
|
{ re: /#\s*fmt:\s*(?:off|skip)\b/, why: "# fmt: off/skip suppresses formatting checks" }
|
|
767
864
|
],
|
|
768
|
-
go: [
|
|
865
|
+
go: [
|
|
866
|
+
{ re: /\/\/\s*nolint\b/, why: "//nolint suppresses golangci-lint findings" },
|
|
867
|
+
{ re: /\/\/\s*lint:ignore\b/, why: "//lint:ignore suppresses staticcheck findings" }
|
|
868
|
+
],
|
|
769
869
|
rs: [],
|
|
770
870
|
rb: [{ re: /#\s*rubocop:\s*(?:disable|todo)\b/, why: "# rubocop:disable suppresses lint rules" }],
|
|
771
|
-
java: [
|
|
772
|
-
|
|
871
|
+
java: [
|
|
872
|
+
{ re: /@SuppressWarnings\s*\(/, why: "@SuppressWarnings suppresses compiler and lint warnings" },
|
|
873
|
+
{ re: /\/\/\s*NOSONAR\b/, why: "// NOSONAR suppresses SonarQube findings" }
|
|
874
|
+
],
|
|
875
|
+
kt: [
|
|
876
|
+
{ re: /@(?:file:)?Suppress(?:Warnings)?\s*\(/, why: "@Suppress suppresses compiler and lint warnings" },
|
|
877
|
+
{ re: /\/\/\s*NOSONAR\b/, why: "// NOSONAR suppresses SonarQube findings" }
|
|
878
|
+
],
|
|
773
879
|
php: [
|
|
774
880
|
{ re: /\bphpcs:(?:ignore|disable)\b/, why: "phpcs:ignore/disable suppresses coding-standard checks" },
|
|
881
|
+
{ re: /@codingStandardsIgnore(?:Line|Start|File)\b/, why: "@codingStandardsIgnore* suppresses coding-standard checks" },
|
|
775
882
|
{ re: /@phpstan-ignore\b/, why: "@phpstan-ignore suppresses static-analysis findings" },
|
|
776
883
|
{ re: /@psalm-suppress\b/, why: "@psalm-suppress suppresses static-analysis findings" }
|
|
777
884
|
],
|
|
778
885
|
cs: [
|
|
779
886
|
{ re: /#pragma\s+warning\s+disable\b/, why: "#pragma warning disable suppresses compiler warnings" },
|
|
780
|
-
{ re: /\[SuppressMessage\s*\(/, why: "[SuppressMessage] suppresses analyzer findings" }
|
|
887
|
+
{ re: /\[SuppressMessage\s*\(/, why: "[SuppressMessage] suppresses analyzer findings" },
|
|
888
|
+
{ re: /\/\/\s*ReSharper\s+disable\b/, why: "// ReSharper disable suppresses inspections" }
|
|
781
889
|
]
|
|
782
890
|
};
|
|
783
891
|
var lintSuppression = {
|
|
@@ -794,18 +902,18 @@ var lintSuppression = {
|
|
|
794
902
|
if (patterns.length === 0) continue;
|
|
795
903
|
for (const l of addedLines(c)) {
|
|
796
904
|
for (const p of patterns) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
|
|
905
|
+
const m = p.re.exec(l.content);
|
|
906
|
+
if (!m || insideStringLiteral(l.content, m.index, lang)) continue;
|
|
907
|
+
out2.push(
|
|
908
|
+
makeFinding(RULE2, policy, {
|
|
909
|
+
file: c.path,
|
|
910
|
+
line: l.newLine ?? void 0,
|
|
911
|
+
message: `Lint suppression added: ${p.why}.`,
|
|
912
|
+
evidence: l.content.trim(),
|
|
913
|
+
remediation: "Resolve the finding rather than suppressing the rule."
|
|
914
|
+
})
|
|
915
|
+
);
|
|
916
|
+
break;
|
|
809
917
|
}
|
|
810
918
|
}
|
|
811
919
|
}
|
|
@@ -817,40 +925,66 @@ var lintSuppression = {
|
|
|
817
925
|
var RULE3 = "test-skip";
|
|
818
926
|
var PATTERNS2 = {
|
|
819
927
|
js: [
|
|
820
|
-
|
|
821
|
-
{ re: /\b(?:
|
|
822
|
-
{ re: /\b(?:
|
|
928
|
+
// vitest chains a concurrency modifier before the marker: it.concurrent.skip.
|
|
929
|
+
{ re: /\b(?:it|test|describe|suite)(?:\.(?:concurrent|sequential|shuffle))?\.(?:skip|only|todo)\b/, why: "a .skip/.only/.todo marker" },
|
|
930
|
+
{ re: /\b(?:it|test|describe|suite)\.(?:skipIf|runIf)\s*\(/, why: "a .skipIf()/.runIf() condition (the test runs only when the condition allows)" },
|
|
931
|
+
{ re: /\b(?:it|test)\.(?:fails|failing)\s*\(/, why: "a .fails/.failing marker (the test now passes by failing)" },
|
|
932
|
+
{ re: /\b(?:it|test|describe)\.each\s*\(\s*\[\s*\]\s*\)/, why: "an empty .each table (no case ever runs)" },
|
|
933
|
+
{ re: /\b(?:xit|xtest|xdescribe)\s*\(/, why: "an x-prefixed disabled test" },
|
|
934
|
+
{ re: /\b(?:fit|ftest|fdescribe)\s*\(/, why: "an f-prefixed focused test (narrows the suite)" },
|
|
823
935
|
{ re: /\bpending\(\s*\)/, why: "a pending() marker" },
|
|
824
936
|
// node:test spells the same class as options — `test('x', { skip: true }, ...)` — and as
|
|
825
937
|
// a runtime call. A live evasion used the options form the day this gap was measured
|
|
826
938
|
// (PREDICTION-baselines.md): denied the rm, the agent skipped by option instead.
|
|
827
|
-
|
|
939
|
+
// In options position (right after the title) ANY value but false/0 skips — a
|
|
940
|
+
// non-literal `{ skip: process.env.CI }` included. Elsewhere on a line only the
|
|
941
|
+
// literal forms count: `list({ skip: 1, take: 5 })` is pagination.
|
|
942
|
+
{ re: /\b(?:it|test|describe|suite)\s*\(\s*(['"`])(?:(?!\1).)*\1\s*,\s*\{[^}]*?\bskip:(?!\s*(?:false|0)\b)/, why: "a { skip: ... } option disabling the test (node:test options form)" },
|
|
943
|
+
{ re: /\bskip:\s*(?:true|['"`])/, why: "a { skip: ... } option disabling the test (node:test options form)" },
|
|
828
944
|
{ re: /\btodo:\s*(?:true|['"`])/, why: "a { todo: ... } option (the test no longer fails the run)" },
|
|
829
|
-
{ re: /\b(?:t|ctx|context)\.skip\(/, why: "a runtime t.skip() call" }
|
|
945
|
+
{ re: /\b(?:t|ctx|context|this)\.skip\(/, why: "a runtime t.skip()/this.skip() call" }
|
|
830
946
|
],
|
|
831
947
|
py: [
|
|
832
|
-
|
|
948
|
+
// `import pytest as pt` makes the decorator `@pt.mark.skip`; the module alias is free.
|
|
949
|
+
{ re: /@\w+\.mark\.(?:skip|skipif|xfail)\b/, why: "a pytest skip/skipif/xfail marker" },
|
|
950
|
+
{ re: /\bpytestmark\s*=.*\.mark\.(?:skip|skipif|xfail)\b/, why: "a module-level pytestmark skip (every test in the file is skipped)" },
|
|
833
951
|
{ re: /\bpytest\.(?:skip|xfail|importorskip)\(/, why: "a runtime pytest.skip()/xfail() call" },
|
|
834
|
-
|
|
952
|
+
// `from unittest import skip` makes the decorator bare `@skip`.
|
|
953
|
+
{ re: /@(?:unittest\.)?(?:skip|skipIf|skipUnless|expectedFailure)\b/, why: "a unittest skip/expectedFailure decorator" },
|
|
835
954
|
{ re: /\bself\.skipTest\(/, why: "a runtime self.skipTest() call" },
|
|
836
|
-
{ re: /\braise\s+(?:unittest\.)?SkipTest\b/, why: "a raised SkipTest" }
|
|
955
|
+
{ re: /\braise\s+(?:unittest\.)?SkipTest\b/, why: "a raised SkipTest" },
|
|
956
|
+
{ re: /\b__test__\s*=\s*False\b/, why: "__test__ = False hides the test from collection" }
|
|
837
957
|
],
|
|
838
958
|
go: [
|
|
839
|
-
{ re: /\b[tb]\.Skip(?:f|Now)?\(/, why: "a runtime t.Skip()/Skipf()/SkipNow() call" }
|
|
959
|
+
{ re: /\b[tb]\.Skip(?:f|Now)?\(/, why: "a runtime t.Skip()/Skipf()/SkipNow() call" },
|
|
960
|
+
{ re: /\bif\s+testing\.Short\(\)/, why: "a testing.Short() guard (the body is skipped under -short)" },
|
|
961
|
+
{ re: /^\s*\/\/\s*(?:go:build|\+build)\s+ignore\b/, why: "a build-ignore constraint (the file is excluded from the test run)", comment: true }
|
|
962
|
+
],
|
|
963
|
+
rs: [
|
|
964
|
+
{ re: /#\[ignore\b/, why: "an #[ignore] attribute (the test no longer runs by default)" },
|
|
965
|
+
{ re: /#\[cfg_attr\(.*\bignore\b/, why: "a cfg_attr(\u2026, ignore) attribute (the test is ignored under that configuration)" }
|
|
840
966
|
],
|
|
841
|
-
rs: [{ re: /#\[ignore\b/, why: "an #[ignore] attribute (the test no longer runs by default)" }],
|
|
842
967
|
rb: [
|
|
843
|
-
{ re: /(?:^|[\s;{(])(?:skip|pending)(?:\s*\(|\s+['"]|\s+(?:if|unless)\b|\s*$)/, why: "a skip/pending call" },
|
|
844
|
-
{ re:
|
|
845
|
-
{ re:
|
|
846
|
-
{ re: /\b(?:skip|pending):\s*(?:true|['"])/, why: "a skip
|
|
968
|
+
{ re: /(?:^|[\s;{(])(?:skip|pending)(?:\s*\(|\s+['"]|\s+(?:if|unless)\b|\s*\}|\s*$)/, why: "a skip/pending call" },
|
|
969
|
+
{ re: /(?:^|[\s;{(])x(?:it|describe|context|specify|example)\s*[('"]/, why: "an x-prefixed disabled example" },
|
|
970
|
+
{ re: /(?:^|[\s;{(])f(?:it|describe|context)\s*[('"]/, why: "an f-prefixed focused example (narrows the suite)" },
|
|
971
|
+
{ re: /\b(?:skip|pending):\s*(?:true|['"])/, why: "a skip/pending metadata flag" },
|
|
972
|
+
{ re: /^\s*(?:it|specify|example)\s+(['"])(?:(?!\1).)*\1\s*$/, why: "an example with no block (RSpec reports it as pending)" }
|
|
973
|
+
],
|
|
974
|
+
java: [
|
|
975
|
+
{ re: /@(?:[\w.]+\.)?(?:Ignore|Disabled)\b/, why: "an @Ignore/@Disabled annotation" },
|
|
976
|
+
{ re: /\bassume(?:True\(\s*false|False\(\s*true)\s*[,)]/, why: "an assumption that never holds (the test aborts as skipped)" }
|
|
977
|
+
],
|
|
978
|
+
kt: [
|
|
979
|
+
{ re: /@(?:[\w.]+\.)?(?:Ignore|Disabled)\b/, why: "an @Ignore/@Disabled annotation" },
|
|
980
|
+
{ re: /\bassume(?:True\(\s*false|False\(\s*true)\s*[,)]/, why: "an assumption that never holds (the test aborts as skipped)" }
|
|
847
981
|
],
|
|
848
|
-
java: [{ re: /@(?:Ignore|Disabled)\b/, why: "an @Ignore/@Disabled annotation" }],
|
|
849
|
-
kt: [{ re: /@(?:Ignore|Disabled)\b/, why: "an @Ignore/@Disabled annotation" }],
|
|
850
982
|
php: [{ re: /\bmarkTest(?:Skipped|Incomplete)\(/, why: "a markTestSkipped()/markTestIncomplete() call" }],
|
|
851
983
|
cs: [
|
|
852
984
|
{ re: /\[Ignore\b/, why: "an [Ignore] attribute" },
|
|
853
|
-
{ re: /\bSkip\s*=\s*"/, why: 'a Skip = "..." attribute argument' }
|
|
985
|
+
{ re: /\bSkip\s*=\s*"/, why: 'a Skip = "..." attribute argument' },
|
|
986
|
+
{ re: /(?:\[|,)\s*Explicit\b/, why: "an [Explicit] attribute (the test runs only when selected by name)" },
|
|
987
|
+
{ re: /\bAssert\.Inconclusive\(/, why: "an Assert.Inconclusive() call (the test ends without a verdict)" }
|
|
854
988
|
]
|
|
855
989
|
};
|
|
856
990
|
var testSkip = {
|
|
@@ -862,9 +996,12 @@ var testSkip = {
|
|
|
862
996
|
for (const c of changes) {
|
|
863
997
|
if (c.kind !== "file") continue;
|
|
864
998
|
if (!isProtected(c.path, policy, "tests")) continue;
|
|
865
|
-
const
|
|
999
|
+
const lang = langOf(c.path);
|
|
1000
|
+
const patterns = PATTERNS2[lang ?? "js"];
|
|
866
1001
|
for (const l of addedLines(c)) {
|
|
1002
|
+
const comment = isCommentLine(l.content.trim(), lang);
|
|
867
1003
|
for (const p of patterns) {
|
|
1004
|
+
if (comment && !p.comment) continue;
|
|
868
1005
|
if (p.re.test(l.content)) {
|
|
869
1006
|
out2.push(
|
|
870
1007
|
makeFinding(RULE3, policy, {
|
|
@@ -1654,10 +1791,13 @@ var testContentRemoval = {
|
|
|
1654
1791
|
// src/detectors/snapshot-rewrite.ts
|
|
1655
1792
|
var RULE9 = "snapshot-rewrite";
|
|
1656
1793
|
var RUNNER = /\b(?:jest|vitest|playwright|ava)\b/;
|
|
1657
|
-
var
|
|
1794
|
+
var PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
|
|
1795
|
+
var UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
|
|
1658
1796
|
var UPDATE_WITH_RUNNER = /(?:^|\s)(?:-u|--update)(?:\s|$)/;
|
|
1797
|
+
var INSTA_ACCEPT = /\bcargo\s+insta\s+(?:accept\b|.*\s--accept\b)/;
|
|
1798
|
+
var UPDATE_EXPECT = /(?:^|\s)UPDATE_EXPECT=\S+/;
|
|
1659
1799
|
var REGEN_SCRIPT = /\b(?:update|regen(?:erate)?|re?bless)[-_.]?(?:golden|snapshots?|baselines?|expected)\b/i;
|
|
1660
|
-
var MUTATE = /(?:^|\s)(?:rm|cp|mv|tee|truncate|dd)(?:\s|$)|sed\s+-i|>{1,2}/;
|
|
1800
|
+
var MUTATE = /(?:^|\s)(?:rm|cp|mv|tee|truncate|dd)(?:\s|$)|sed\s+-i|>{1,2}|\bfind\b.*\s-(?:delete\b|exec\s+rm\b)|\bgit\s+(?:checkout|restore)\b/;
|
|
1661
1801
|
function namesProtectedSnapshot(seg, policy) {
|
|
1662
1802
|
for (const t of tokens(seg)) {
|
|
1663
1803
|
const path = unquote(t).replace(/^>{1,2}/, "");
|
|
@@ -1677,7 +1817,9 @@ var snapshotRewrite = {
|
|
|
1677
1817
|
for (const seg of segments(c.raw)) {
|
|
1678
1818
|
let why = null;
|
|
1679
1819
|
if (UPDATE_ANY.test(seg)) why = "the snapshot-update flag rewrites every failing snapshot to match current output";
|
|
1680
|
-
else if (RUNNER.test(seg) && UPDATE_WITH_RUNNER.test(seg)) why = "running the test runner in update mode rewrites failing snapshots to match current output";
|
|
1820
|
+
else if ((RUNNER.test(seg) || PM_TEST_SCRIPT.test(seg)) && UPDATE_WITH_RUNNER.test(seg)) why = "running the test runner in update mode rewrites failing snapshots to match current output";
|
|
1821
|
+
else if (INSTA_ACCEPT.test(seg)) why = "cargo insta accept rewrites every pending snapshot to match current output";
|
|
1822
|
+
else if (UPDATE_EXPECT.test(seg)) why = "UPDATE_EXPECT=1 rewrites every expect-test expectation to match current output";
|
|
1681
1823
|
else if (REGEN_SCRIPT.test(seg)) why = "a regeneration script rewrites the recorded expected output from current output";
|
|
1682
1824
|
else {
|
|
1683
1825
|
const path = MUTATE.test(seg) ? namesProtectedSnapshot(seg, policy) : null;
|
|
@@ -2591,7 +2733,7 @@ function savePtree(cwd, sessionId, tree) {
|
|
|
2591
2733
|
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2, watch } from "node:fs";
|
|
2592
2734
|
import { join as join7 } from "node:path";
|
|
2593
2735
|
import { createHash as createHash3 } from "node:crypto";
|
|
2594
|
-
var
|
|
2736
|
+
var SKIP2 = /(^|\/)(\.git|node_modules)(\/|$)/;
|
|
2595
2737
|
function snap(abs2) {
|
|
2596
2738
|
try {
|
|
2597
2739
|
const st = statSync2(abs2);
|
|
@@ -2618,7 +2760,7 @@ function watchTree(dir, cb) {
|
|
|
2618
2760
|
}
|
|
2619
2761
|
const watchers = /* @__PURE__ */ new Map();
|
|
2620
2762
|
const addDir = (rel) => {
|
|
2621
|
-
if (watchers.has(rel) ||
|
|
2763
|
+
if (watchers.has(rel) || SKIP2.test(rel + "/")) return;
|
|
2622
2764
|
let w;
|
|
2623
2765
|
try {
|
|
2624
2766
|
w = watch(rel ? join7(dir, rel) : dir, (kind, fname) => {
|
|
@@ -2645,7 +2787,7 @@ function watchTree(dir, cb) {
|
|
|
2645
2787
|
}
|
|
2646
2788
|
for (const name of names) {
|
|
2647
2789
|
const child = rel ? `${rel}/${name}` : name;
|
|
2648
|
-
if (
|
|
2790
|
+
if (SKIP2.test(child + "/")) continue;
|
|
2649
2791
|
try {
|
|
2650
2792
|
if (statSync2(join7(dir, child)).isDirectory()) walk(child);
|
|
2651
2793
|
} catch {
|
|
@@ -2664,7 +2806,7 @@ function startWatcher(dir, log, policy) {
|
|
|
2664
2806
|
}
|
|
2665
2807
|
const last = /* @__PURE__ */ new Map();
|
|
2666
2808
|
return watchTree(dir, (kind, rel) => {
|
|
2667
|
-
if (
|
|
2809
|
+
if (SKIP2.test(rel) || !isProtected(rel, policy)) return;
|
|
2668
2810
|
const s = snap(join7(dir, rel));
|
|
2669
2811
|
const key2 = `${s.hash}:${s.mode}`;
|
|
2670
2812
|
if (last.get(rel) === key2) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
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",
|