tamperward 2.25.2 → 2.27.0
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/README.md +17 -4
- package/dist/cli/index.js +2152 -1551
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -500,6 +500,12 @@ function defaultPolicy(version = 1) {
|
|
|
500
500
|
"**/tsconfig*.json",
|
|
501
501
|
"**/.eslintrc*",
|
|
502
502
|
"**/eslint.config.*",
|
|
503
|
+
// The lint's own blind-spot list and biome's config: read by
|
|
504
|
+
// config-weakening (#447) alongside tsconfig and the eslint configs, so a
|
|
505
|
+
// path added to `.eslintignore` is as visible as one added to `ignores`.
|
|
506
|
+
"**/.eslintignore",
|
|
507
|
+
"**/biome.json",
|
|
508
|
+
"**/biome.jsonc",
|
|
503
509
|
"**/package.json",
|
|
504
510
|
// Coverage gates outside the Jest/Vitest shapes. coverage-lowering could only
|
|
505
511
|
// see a threshold it was shown: a Python `fail_under`, an nyc `lines`, a
|
|
@@ -594,6 +600,11 @@ function defaultPolicy(version = 1) {
|
|
|
594
600
|
// harness/fp-study/CAST-GROWTH-CORPUS.md), so it is a review prompt, not a gate.
|
|
595
601
|
"ts-cast-growth": { severity: "warn" },
|
|
596
602
|
"lint-suppression": { severity: "block" },
|
|
603
|
+
// tsconfig strictness lowered, eslint/biome rules turned off or ignores grown,
|
|
604
|
+
// a runner setup module loaded from an unprotected file (#447). Ships warn
|
|
605
|
+
// against a committed corpus (harness/fp-study/CONFIG-WEAKENING-CORPUS.md);
|
|
606
|
+
// block is a separate decision with its own measurement.
|
|
607
|
+
"config-weakening": { severity: "warn" },
|
|
597
608
|
"coverage-lowering": { severity: "block" },
|
|
598
609
|
// the per-function form of coverage-lowering: an inline `istanbul ignore` /
|
|
599
610
|
// `c8 ignore` / `v8 ignore` / `node:coverage ignore` / `# pragma: no cover` /
|
|
@@ -1079,7 +1090,7 @@ var init_trusted = __esm({
|
|
|
1079
1090
|
|
|
1080
1091
|
// src/repo-context.ts
|
|
1081
1092
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1082
|
-
import { accessSync, constants as constants2, existsSync as existsSync2, statSync } from "node:fs";
|
|
1093
|
+
import { accessSync, constants as constants2, existsSync as existsSync2, realpathSync as realpathSync3, statSync } from "node:fs";
|
|
1083
1094
|
import { isAbsolute as isAbsolute3, resolve } from "node:path";
|
|
1084
1095
|
function rev(cwd, flag) {
|
|
1085
1096
|
try {
|
|
@@ -1112,6 +1123,27 @@ function repoContext(cwd) {
|
|
|
1112
1123
|
function repoRoot(cwd) {
|
|
1113
1124
|
return repoContext(cwd)?.root ?? cwd;
|
|
1114
1125
|
}
|
|
1126
|
+
function validateClaimAgainstRoot(claimedCwd, trustedRoot, base) {
|
|
1127
|
+
if (claimedCwd === void 0) return { ok: true, trustedRoot };
|
|
1128
|
+
if (typeof claimedCwd !== "string" || claimedCwd.trim() === "") return { ok: false, rejected: "malformed cwd claim" };
|
|
1129
|
+
const abs = isAbsolute3(claimedCwd) ? claimedCwd : resolve(base, claimedCwd);
|
|
1130
|
+
let realClaim;
|
|
1131
|
+
try {
|
|
1132
|
+
realClaim = realpathSync3(abs);
|
|
1133
|
+
} catch (e) {
|
|
1134
|
+
return { ok: false, rejected: `cwd claim path cannot be resolved (${abs}: ${e instanceof Error ? e.message : String(e)})` };
|
|
1135
|
+
}
|
|
1136
|
+
let realTrusted;
|
|
1137
|
+
try {
|
|
1138
|
+
realTrusted = realpathSync3(trustedRoot);
|
|
1139
|
+
} catch {
|
|
1140
|
+
realTrusted = trustedRoot;
|
|
1141
|
+
}
|
|
1142
|
+
const ctx = repoContext(realClaim);
|
|
1143
|
+
if (!ctx) return { ok: false, rejected: `cwd claim resolves to no repository (${realClaim})` };
|
|
1144
|
+
if (ctx.root !== realTrusted) return { ok: false, rejected: `cwd claim resolves to a different repository (${ctx.root} != trusted ${realTrusted})` };
|
|
1145
|
+
return { ok: true, trustedRoot: realTrusted };
|
|
1146
|
+
}
|
|
1115
1147
|
function outsideRepository(cwd) {
|
|
1116
1148
|
const key2 = resolve(cwd);
|
|
1117
1149
|
let isDir;
|
|
@@ -4200,24 +4232,24 @@ function topLevelStaticStrings(ctx) {
|
|
|
4200
4232
|
}
|
|
4201
4233
|
return resolved;
|
|
4202
4234
|
}
|
|
4203
|
-
function staticPropertyName(expr, ctx,
|
|
4235
|
+
function staticPropertyName(expr, ctx, strings2) {
|
|
4204
4236
|
if (ts.isStringLiteralLike(expr)) return { name: expr.text, causes: [] };
|
|
4205
|
-
if (ts.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx,
|
|
4237
|
+
if (ts.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx, strings2);
|
|
4206
4238
|
if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
4207
|
-
const left = staticPropertyName(expr.left, ctx,
|
|
4208
|
-
const right = staticPropertyName(expr.right, ctx,
|
|
4239
|
+
const left = staticPropertyName(expr.left, ctx, strings2);
|
|
4240
|
+
const right = staticPropertyName(expr.right, ctx, strings2);
|
|
4209
4241
|
if (!left || !right) return null;
|
|
4210
4242
|
return { name: left.name + right.name, causes: [...left.causes, ...right.causes] };
|
|
4211
4243
|
}
|
|
4212
4244
|
if (ts.isIdentifier(expr)) {
|
|
4213
4245
|
const sym = symbolAt(ctx, expr);
|
|
4214
4246
|
if (!sym) return null;
|
|
4215
|
-
const value =
|
|
4247
|
+
const value = strings2.get(sym);
|
|
4216
4248
|
return value ? { name: value.value, causes: value.causes } : null;
|
|
4217
4249
|
}
|
|
4218
4250
|
return null;
|
|
4219
4251
|
}
|
|
4220
|
-
function runnerChain(expr, ctx, binder,
|
|
4252
|
+
function runnerChain(expr, ctx, binder, strings2) {
|
|
4221
4253
|
const props = [];
|
|
4222
4254
|
const propNodes = [];
|
|
4223
4255
|
const causes = [];
|
|
@@ -4232,7 +4264,7 @@ function runnerChain(expr, ctx, binder, strings) {
|
|
|
4232
4264
|
continue;
|
|
4233
4265
|
}
|
|
4234
4266
|
if (!cur.argumentExpression) return null;
|
|
4235
|
-
const property = staticPropertyName(cur.argumentExpression, ctx,
|
|
4267
|
+
const property = staticPropertyName(cur.argumentExpression, ctx, strings2);
|
|
4236
4268
|
if (!property) return null;
|
|
4237
4269
|
props.unshift(property.name);
|
|
4238
4270
|
propNodes.unshift(cur.argumentExpression);
|
|
@@ -4286,7 +4318,7 @@ function structuralNodeKey(node, sf) {
|
|
|
4286
4318
|
return `${node.kind}(${children.map((child) => structuralNodeKey(child, sf)).join(",")})`;
|
|
4287
4319
|
}
|
|
4288
4320
|
function semanticSkipHits(ctx, binder) {
|
|
4289
|
-
const
|
|
4321
|
+
const strings2 = topLevelStaticStrings(ctx);
|
|
4290
4322
|
const hits = [];
|
|
4291
4323
|
const unowned = /* @__PURE__ */ new Set();
|
|
4292
4324
|
const callOrdinals = /* @__PURE__ */ new Map();
|
|
@@ -4298,7 +4330,7 @@ function semanticSkipHits(ctx, binder) {
|
|
|
4298
4330
|
const ordinal = (callOrdinals.get(callIdentity) ?? 0) + 1;
|
|
4299
4331
|
callOrdinals.set(callIdentity, ordinal);
|
|
4300
4332
|
const callKey = `${callIdentity}\0${ordinal}`;
|
|
4301
|
-
const chain = runnerChain(node.expression, ctx, binder,
|
|
4333
|
+
const chain = runnerChain(node.expression, ctx, binder, strings2);
|
|
4302
4334
|
if (chain === "unknown") {
|
|
4303
4335
|
const callee = node.expression;
|
|
4304
4336
|
const first = lineOf(callee.getStart(ctx.sf));
|
|
@@ -4336,7 +4368,7 @@ function semanticSkipHits(ctx, binder) {
|
|
|
4336
4368
|
if (ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name)) {
|
|
4337
4369
|
name = prop.name.text;
|
|
4338
4370
|
} else if (ts.isComputedPropertyName(prop.name)) {
|
|
4339
|
-
const resolved = staticPropertyName(prop.name.expression, ctx,
|
|
4371
|
+
const resolved = staticPropertyName(prop.name.expression, ctx, strings2);
|
|
4340
4372
|
name = resolved?.name ?? null;
|
|
4341
4373
|
propertyCauses = resolved?.causes ?? [];
|
|
4342
4374
|
}
|
|
@@ -5599,1123 +5631,980 @@ var init_coverage_lowering = __esm({
|
|
|
5599
5631
|
}
|
|
5600
5632
|
});
|
|
5601
5633
|
|
|
5602
|
-
// src/detectors/
|
|
5603
|
-
function
|
|
5604
|
-
const
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
continue;
|
|
5612
|
-
}
|
|
5613
|
-
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
5614
|
-
else if (opener && opener.test(line.slice(j, j + 2))) return true;
|
|
5634
|
+
// src/detectors/suite-config.ts
|
|
5635
|
+
function runnerSkips(path) {
|
|
5636
|
+
const parts = path.split("/");
|
|
5637
|
+
if (/\.rs$/.test(path)) {
|
|
5638
|
+
const i = parts.indexOf("tests");
|
|
5639
|
+
if (i < 0 || parts.slice(0, i).includes("src")) return null;
|
|
5640
|
+
const rest = parts.slice(i + 1);
|
|
5641
|
+
if (rest.length === 1 || rest.length === 2 && rest[1] === "main.rs") return null;
|
|
5642
|
+
return "cargo builds only tests/*.rs and tests/*/main.rs as test targets";
|
|
5615
5643
|
}
|
|
5616
|
-
|
|
5644
|
+
if (/\.go$/.test(path)) {
|
|
5645
|
+
const hit = parts.find((p) => p === "testdata" || p.startsWith("_") || p.startsWith("."));
|
|
5646
|
+
if (hit === void 0) return null;
|
|
5647
|
+
return `go test skips ${hit === "testdata" ? "testdata/" : hit.startsWith("_") ? "_*" : ".*"} paths (${JSON.stringify(hit)})`;
|
|
5648
|
+
}
|
|
5649
|
+
if (/\.py$/.test(path)) {
|
|
5650
|
+
const hit = PYTEST_DEFAULT_IGNORE.find((g) => globs2([g])(path));
|
|
5651
|
+
return hit === void 0 ? null : `pytest's default norecursedirs matches it (${JSON.stringify(hit)})`;
|
|
5652
|
+
}
|
|
5653
|
+
if (/\.[cm]?[jt]sx?$/.test(path)) {
|
|
5654
|
+
const hit = JS_UNWALKED.find((g) => globs2([g])(path));
|
|
5655
|
+
return hit === void 0 ? null : `the runner's default exclude matches it (${JSON.stringify(hit)})`;
|
|
5656
|
+
}
|
|
5657
|
+
return null;
|
|
5617
5658
|
}
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
{ re: /\/\*\s*node:coverage\s+(?:ignore|disable)\b/, why: "`node:coverage ignore/disable` takes the following code out of the coverage measurement" }
|
|
5633
|
-
],
|
|
5634
|
-
py: [{ re: /#\s*pragma[:\s]\s*no\s*cover\b/i, why: "`# pragma: no cover` takes the line or block out of the coverage measurement" }],
|
|
5635
|
-
rs: [{ re: /#\s*!?\[[^\]]*\bcoverage\s*\(\s*off\s*\)/, why: "`#[coverage(off)]` takes the item out of the coverage measurement" }],
|
|
5636
|
-
go: [
|
|
5637
|
-
{
|
|
5638
|
-
re: /^\s*\/\/\s*(?:go:build\b|\+build\b)/,
|
|
5639
|
-
why: "a build constraint added to an existing file takes it out of the default build, and out of `go test -cover`",
|
|
5640
|
-
existingOnly: true
|
|
5641
|
-
}
|
|
5642
|
-
]
|
|
5643
|
-
};
|
|
5644
|
-
OUT_OF_SCOPE_PATH = /(?:^|\/)(?:node_modules|vendor|third_party|dist|build|out|coverage|generated|__generated__|codegen|examples?|docs?|scripts?|fixtures?|__fixtures__|__mocks__)\/|\.d\.[cm]?ts$|\.(?:generated|gen|pb)\.[^/]*$/;
|
|
5645
|
-
LINE_COMMENT = { js: /^\/\//, go: /^\/\//, rs: /^\/\//, py: /^#/ };
|
|
5646
|
-
coverageExclusion = {
|
|
5647
|
-
id: RULE6,
|
|
5648
|
-
surface: ["file"],
|
|
5649
|
-
certainty: "mechanical",
|
|
5650
|
-
run(changes, policy) {
|
|
5651
|
-
const out3 = [];
|
|
5652
|
-
for (const c of changes) {
|
|
5653
|
-
if (c.kind !== "file" || c.op === "delete") continue;
|
|
5654
|
-
const lang = langOf(c.path);
|
|
5655
|
-
if (!lang) continue;
|
|
5656
|
-
const patterns = PATTERNS3[lang];
|
|
5657
|
-
if (!patterns) continue;
|
|
5658
|
-
const category = protectedCategory(c.path, policy);
|
|
5659
|
-
if (OUT_OF_SCOPE_PATH.test(c.path) || category === "tests" || category === "config") continue;
|
|
5660
|
-
const removed = new Set(removedLines(c).map((l) => l.content.trim()));
|
|
5661
|
-
const before = c.before ?? "";
|
|
5662
|
-
for (const l of addedLines(c)) {
|
|
5663
|
-
const trimmed = l.content.trim();
|
|
5664
|
-
if (removed.has(trimmed)) continue;
|
|
5665
|
-
for (const p of patterns) {
|
|
5666
|
-
const m = p.re.exec(l.content);
|
|
5667
|
-
if (!m || isText(l.content, m.index, lang)) continue;
|
|
5668
|
-
if (p.existingOnly && (c.op === "add" || c.before == null || before.split("\n").some((b) => p.re.test(b)))) continue;
|
|
5669
|
-
out3.push(
|
|
5670
|
-
makeFinding(RULE6, policy, {
|
|
5671
|
-
file: c.path,
|
|
5672
|
-
line: l.newLine ?? void 0,
|
|
5673
|
-
message: `Coverage exclusion added: ${p.why}.`,
|
|
5674
|
-
evidence: trimmed,
|
|
5675
|
-
remediation: "Cover the code with a test instead of excluding it from the measurement; if it is genuinely unreachable, say so in review.",
|
|
5676
|
-
defaultSeverity: "warn"
|
|
5677
|
-
})
|
|
5678
|
-
);
|
|
5679
|
-
break;
|
|
5680
|
-
}
|
|
5681
|
-
}
|
|
5682
|
-
}
|
|
5683
|
-
return out3;
|
|
5684
|
-
}
|
|
5685
|
-
};
|
|
5659
|
+
function keyName2(name) {
|
|
5660
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
|
|
5661
|
+
return null;
|
|
5662
|
+
}
|
|
5663
|
+
function literals2(e) {
|
|
5664
|
+
if (isStr(e)) return { items: [e.text], opaque: false };
|
|
5665
|
+
if (!ts.isArrayLiteralExpression(e)) return { items: [], opaque: true };
|
|
5666
|
+
const items = [];
|
|
5667
|
+
let opaque = false;
|
|
5668
|
+
for (const el of e.elements) {
|
|
5669
|
+
if (isStr(el)) items.push(el.text);
|
|
5670
|
+
else if (ts.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
|
|
5671
|
+
items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
5672
|
+
} else opaque = true;
|
|
5686
5673
|
}
|
|
5687
|
-
}
|
|
5688
|
-
|
|
5689
|
-
// src/detectors/gh-expression.ts
|
|
5690
|
-
function foldExpressions(s) {
|
|
5691
|
-
return s.replace(/\$\{\{([^}]*)\}\}/g, (_m, e) => {
|
|
5692
|
-
const v = foldConst(e.trim());
|
|
5693
|
-
if (v !== void 0 && v !== TRUTHY) return String(v);
|
|
5694
|
-
return /\b(?:matrix|strategy|inputs)\./.test(e) ? MATRIX_TOKEN : EXPR_TOKEN;
|
|
5695
|
-
});
|
|
5674
|
+
return { items, opaque };
|
|
5696
5675
|
}
|
|
5697
|
-
function
|
|
5698
|
-
const
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
const
|
|
5728
|
-
if (
|
|
5729
|
-
|
|
5730
|
-
|
|
5676
|
+
function ownerKey2(node) {
|
|
5677
|
+
const obj = node.parent;
|
|
5678
|
+
if (!obj || !ts.isObjectLiteralExpression(obj)) return null;
|
|
5679
|
+
let q = obj.parent;
|
|
5680
|
+
while (q && (ts.isParenthesizedExpression(q) || ts.isAsExpression(q) || q.kind === ts.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
5681
|
+
return q && ts.isPropertyAssignment(q) ? keyName2(q.name) : null;
|
|
5682
|
+
}
|
|
5683
|
+
function asExpression2(src) {
|
|
5684
|
+
const t = src.trim();
|
|
5685
|
+
return t.startsWith("{") ? `(${t})` : src;
|
|
5686
|
+
}
|
|
5687
|
+
function runnerOf(path, content) {
|
|
5688
|
+
const base = path.split("/").pop() ?? path;
|
|
5689
|
+
if (/^jest\.config\./.test(base) || base === "package.json") return "jest";
|
|
5690
|
+
if (/^vitest\.(?:config|workspace)\./.test(base)) return "vitest";
|
|
5691
|
+
if (/^vite\.config\./.test(base) && content != null && /(?:^|[\s,{])["']?test["']?\s*:/m.test(content)) return "vitest";
|
|
5692
|
+
if (base === "pytest.ini" || base === ".pytest.ini") return "pytest";
|
|
5693
|
+
if (content == null) return null;
|
|
5694
|
+
if (base === "tox.ini" && /^\s*\[pytest\]/m.test(content)) return "pytest";
|
|
5695
|
+
if (base === "setup.cfg" && /^\s*\[tool:pytest\]/m.test(content)) return "pytest";
|
|
5696
|
+
if (base === "pyproject.toml" && /^\s*\[tool\.pytest\.ini_options\]/m.test(content)) return "pytest";
|
|
5697
|
+
return null;
|
|
5698
|
+
}
|
|
5699
|
+
function claimsPytest(name, src) {
|
|
5700
|
+
if (name === "pytest.ini" || name === ".pytest.ini") return true;
|
|
5701
|
+
const section = name === "pyproject.toml" ? /^\s*\[tool\.pytest\.ini_options\]\s*$/m : name === "tox.ini" ? /^\s*\[pytest\]\s*$/m : name === "setup.cfg" ? /^\s*\[tool:pytest\]\s*$/m : null;
|
|
5702
|
+
return section !== null && section.test(src);
|
|
5703
|
+
}
|
|
5704
|
+
function effectivePytestFile(files) {
|
|
5705
|
+
for (const name of PYTEST_INI_ORDER) {
|
|
5706
|
+
const content = files.get(name);
|
|
5707
|
+
if (content == null) continue;
|
|
5708
|
+
if (claimsPytest(name, content)) return { path: name, content };
|
|
5709
|
+
}
|
|
5710
|
+
return null;
|
|
5711
|
+
}
|
|
5712
|
+
function effectivePytestConfig(path, ctx) {
|
|
5713
|
+
if (path.includes("/")) return false;
|
|
5714
|
+
const base = path;
|
|
5715
|
+
const rank = PYTEST_INI_ORDER.findIndex((name) => name === base);
|
|
5716
|
+
if (rank < 0) return false;
|
|
5717
|
+
const files = ctx?.trackedFiles;
|
|
5718
|
+
if (!files) return true;
|
|
5719
|
+
const rootNames = new Set(files.filter((f) => !f.includes("/")));
|
|
5720
|
+
return !PYTEST_INI_ORDER.slice(0, rank).some((n) => rootNames.has(n));
|
|
5721
|
+
}
|
|
5722
|
+
function pytestBlock(src) {
|
|
5723
|
+
const lines = src.split(/\r?\n/);
|
|
5724
|
+
const head = /^\s*\[(pytest|tool:pytest|tool\.pytest\.ini_options)\]\s*$/;
|
|
5725
|
+
let i = lines.findIndex((l) => head.test(l));
|
|
5726
|
+
if (i < 0) return null;
|
|
5727
|
+
const out3 = {};
|
|
5728
|
+
let key2 = null;
|
|
5729
|
+
for (i += 1; i < lines.length; i++) {
|
|
5730
|
+
const line = lines[i];
|
|
5731
|
+
if (/^\s*\[/.test(line)) break;
|
|
5732
|
+
if (/^\s*[#;]/.test(line) || !line.trim()) {
|
|
5731
5733
|
continue;
|
|
5732
5734
|
}
|
|
5733
|
-
const
|
|
5734
|
-
if (
|
|
5735
|
-
|
|
5736
|
-
|
|
5735
|
+
const m = line.match(/^(\w[\w.-]*)\s*[=:]\s*(.*)$/);
|
|
5736
|
+
if (m) {
|
|
5737
|
+
key2 = m[1];
|
|
5738
|
+
out3[key2] = m[2].trim();
|
|
5737
5739
|
continue;
|
|
5738
5740
|
}
|
|
5739
|
-
|
|
5741
|
+
if (key2 && /^\s+\S/.test(line)) out3[key2] += " " + line.trim();
|
|
5740
5742
|
}
|
|
5741
5743
|
return out3;
|
|
5742
5744
|
}
|
|
5743
|
-
function
|
|
5744
|
-
const
|
|
5745
|
-
if (
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
const
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5745
|
+
function pytestTokens(raw) {
|
|
5746
|
+
const v = raw.trim();
|
|
5747
|
+
if (v.startsWith("[")) {
|
|
5748
|
+
return [...v.matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? "");
|
|
5749
|
+
}
|
|
5750
|
+
const unq2 = v.replace(/^"([\s\S]*)"$/, "$1").replace(/^'([\s\S]*)'$/, "$1");
|
|
5751
|
+
return [...unq2.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "").filter(Boolean);
|
|
5752
|
+
}
|
|
5753
|
+
function pytestSelection(src) {
|
|
5754
|
+
const sel = empty();
|
|
5755
|
+
const blk = pytestBlock(src);
|
|
5756
|
+
if (!blk) return sel;
|
|
5757
|
+
sel.present = true;
|
|
5758
|
+
const ignore = blk.norecursedirs !== void 0 ? pytestTokens(blk.norecursedirs).flatMap((d) => pathGlobs(d).map((pattern) => ({ pattern, key: "norecursedirs" }))) : PYTEST_DEFAULT_IGNORE.map((pattern) => ({ pattern, key: "norecursedirs" }));
|
|
5759
|
+
if (blk.testpaths !== void 0) sel.roots = pytestTokens(blk.testpaths).map((t) => t.replace(/^\.\//, ""));
|
|
5760
|
+
if (blk.python_files !== void 0) sel.include = pytestTokens(blk.python_files).map((g) => g.includes("/") ? g : "**/" + g);
|
|
5761
|
+
for (const key2 of ["python_classes", "python_functions"]) {
|
|
5762
|
+
if (blk[key2] !== void 0) ignore.push({ pattern: "\0" + key2, key: key2 });
|
|
5763
|
+
}
|
|
5764
|
+
const opts = blk.addopts !== void 0 ? pytestTokens(blk.addopts) : [];
|
|
5765
|
+
for (let j = 0; j < opts.length; j++) {
|
|
5766
|
+
const o = opts[j];
|
|
5767
|
+
const val = (inline) => o.includes("=") ? o.slice(o.indexOf("=") + 1) : opts[++j] ?? inline;
|
|
5768
|
+
if (o === "--ignore" || o.startsWith("--ignore=")) for (const g of pathGlobs(val(""))) ignore.push({ pattern: g, key: "--ignore" });
|
|
5769
|
+
else if (o === "--ignore-glob" || o.startsWith("--ignore-glob=")) {
|
|
5770
|
+
const g = val("");
|
|
5771
|
+
ignore.push({ pattern: g, key: "--ignore-glob" });
|
|
5772
|
+
if (!g.startsWith("**/") && !g.startsWith("/")) ignore.push({ pattern: "**/" + g, key: "--ignore-glob" });
|
|
5773
|
+
} else if (o === "--deselect" || o.startsWith("--deselect=")) for (const g of pathGlobs(val(""))) ignore.push({ pattern: g, key: "--deselect" });
|
|
5774
|
+
else if (o === "-k" || o.startsWith("-k=")) {
|
|
5775
|
+
for (const w of (val("").match(/\bnot\s+([A-Za-z_][\w]*)/g) ?? []).map((t) => t.replace(/^not\s+/, "")))
|
|
5776
|
+
ignore.push({ pattern: `**/*${w}*`, key: "-k" });
|
|
5777
|
+
} else if (o === "-m" || o.startsWith("-m=")) {
|
|
5778
|
+
ignore.push({ pattern: "\0-m " + val(""), key: "-m" });
|
|
5779
|
+
} else if (o === "-p" || o.startsWith("-p=") || o.length > 2 && o.startsWith("-p")) {
|
|
5780
|
+
const v = o === "-p" ? opts[++j] ?? "" : o.startsWith("-p=") ? o.slice(3) : o.slice(2);
|
|
5781
|
+
if (v.startsWith("no:")) ignore.push({ pattern: "\0-p " + v, key: "-p" });
|
|
5782
|
+
} else if (o === "--collect-only" || o === "--co") ignore.push({ pattern: "**", key: o });
|
|
5783
|
+
}
|
|
5784
|
+
sel.ignore = ignore;
|
|
5785
|
+
return sel;
|
|
5786
|
+
}
|
|
5787
|
+
function dirValue(e) {
|
|
5788
|
+
if (!isStr(e)) return { dir: null, opaque: true };
|
|
5789
|
+
const d = stripRootDir(e.text).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
5790
|
+
if (d === "." || d === "" || d.split("/").includes("..")) return { dir: null, opaque: false };
|
|
5791
|
+
return { dir: d, opaque: false };
|
|
5792
|
+
}
|
|
5793
|
+
function collect(root, runner) {
|
|
5794
|
+
const sel = empty();
|
|
5795
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5796
|
+
const take = (key2, e) => {
|
|
5797
|
+
const { items, opaque } = literals2(e);
|
|
5798
|
+
if (opaque || seen.has(key2)) sel.opaque = true;
|
|
5799
|
+
seen.add(key2);
|
|
5800
|
+
sel.present = true;
|
|
5801
|
+
return items.map(stripRootDir);
|
|
5767
5802
|
};
|
|
5768
|
-
const
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
const r = cmp();
|
|
5772
|
-
if (l !== void 0 && !truthy(l)) continue;
|
|
5773
|
-
if (l !== void 0) l = r;
|
|
5774
|
-
else l = r !== void 0 && !truthy(r) ? false : void 0;
|
|
5775
|
-
}
|
|
5776
|
-
return l;
|
|
5803
|
+
const ignores = (key2, e) => {
|
|
5804
|
+
const entries = take(key2, e).map((pattern) => ({ pattern, key: key2 }));
|
|
5805
|
+
sel.ignore = [...sel.ignore ?? [], ...entries];
|
|
5777
5806
|
};
|
|
5778
|
-
const
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
const t = peek();
|
|
5782
|
-
if (!t || t.t !== "op" || !/^(?:==|!=|<|>|<=|>=)$/.test(t.v)) return l;
|
|
5783
|
-
p++;
|
|
5784
|
-
const r = unary();
|
|
5785
|
-
if (l === void 0 || r === void 0) {
|
|
5786
|
-
l = void 0;
|
|
5787
|
-
continue;
|
|
5788
|
-
}
|
|
5789
|
-
if (t.v === "==" || t.v === "!=") {
|
|
5790
|
-
const e = eq(l, r);
|
|
5791
|
-
l = e === void 0 ? void 0 : t.v === "==" ? e : !e;
|
|
5792
|
-
} else {
|
|
5793
|
-
const x = num(l);
|
|
5794
|
-
const y2 = num(r);
|
|
5795
|
-
if (Number.isNaN(x) || Number.isNaN(y2)) l = void 0;
|
|
5796
|
-
else l = t.v === "<" ? x < y2 : t.v === ">" ? x > y2 : t.v === "<=" ? x <= y2 : x >= y2;
|
|
5797
|
-
}
|
|
5798
|
-
}
|
|
5807
|
+
const namePattern = (e) => {
|
|
5808
|
+
sel.present = true;
|
|
5809
|
+
sel.wide.push({ pattern: "\0testNamePattern " + (isStr(e) ? e.text : e.getText()), key: "testNamePattern" });
|
|
5799
5810
|
};
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
}
|
|
5805
|
-
return primary();
|
|
5811
|
+
let projectList = null;
|
|
5812
|
+
const projects = (e) => {
|
|
5813
|
+
sel.present = true;
|
|
5814
|
+
projectList = e;
|
|
5806
5815
|
};
|
|
5807
|
-
const
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
const v2 = or();
|
|
5812
|
-
return eat(")") ? v2 : void 0;
|
|
5816
|
+
const visit = (node) => {
|
|
5817
|
+
if (ts.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
|
|
5818
|
+
sel.present = true;
|
|
5819
|
+
sel.opaque = true;
|
|
5813
5820
|
}
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
if (
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
if (
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
if (
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5821
|
+
if (ts.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
|
|
5822
|
+
sel.present = true;
|
|
5823
|
+
sel.opaque = true;
|
|
5824
|
+
}
|
|
5825
|
+
if (ts.isPropertyAssignment(node)) {
|
|
5826
|
+
const k = keyName2(node.name);
|
|
5827
|
+
if (runner === "jest") {
|
|
5828
|
+
if (k === "testMatch") sel.include = take(k, node.initializer);
|
|
5829
|
+
else if (k === "testRegex") sel.regex = take(k, node.initializer);
|
|
5830
|
+
else if (k === "testPathIgnorePatterns" || k === "modulePathIgnorePatterns") ignores(k, node.initializer);
|
|
5831
|
+
else if (k === "roots") sel.roots = take(k, node.initializer);
|
|
5832
|
+
else if (k === "rootDir") {
|
|
5833
|
+
const { dir, opaque } = dirValue(node.initializer);
|
|
5834
|
+
sel.present = true;
|
|
5835
|
+
if (opaque) sel.opaque = true;
|
|
5836
|
+
else sel.base = dir;
|
|
5837
|
+
} else if (k === "projects") {
|
|
5838
|
+
projects(node.initializer);
|
|
5839
|
+
return;
|
|
5840
|
+
} else if (k === "testNamePattern") namePattern(node.initializer);
|
|
5841
|
+
} else if (ownerKey2(node) === "test") {
|
|
5842
|
+
if (k === "include") sel.include = take(k, node.initializer);
|
|
5843
|
+
else if (k === "exclude") ignores(k, node.initializer);
|
|
5844
|
+
else if (k === "dir" || k === "root") {
|
|
5845
|
+
const { dir, opaque } = dirValue(node.initializer);
|
|
5846
|
+
sel.present = true;
|
|
5847
|
+
if (opaque) sel.opaque = true;
|
|
5848
|
+
else sel.base = dir;
|
|
5849
|
+
} else if (k === "projects") {
|
|
5850
|
+
projects(node.initializer);
|
|
5851
|
+
return;
|
|
5852
|
+
} else if (k === "testNamePattern") namePattern(node.initializer);
|
|
5842
5853
|
}
|
|
5843
|
-
return void 0;
|
|
5844
5854
|
}
|
|
5845
|
-
|
|
5855
|
+
ts.forEachChild(node, visit);
|
|
5846
5856
|
};
|
|
5847
|
-
|
|
5848
|
-
|
|
5857
|
+
visit(root);
|
|
5858
|
+
if (projectList) sel.projects = readProjects(projectList, sel, runner);
|
|
5859
|
+
if (sel.projects?.some((p) => p.opaque)) sel.opaque = true;
|
|
5860
|
+
return sel;
|
|
5849
5861
|
}
|
|
5850
|
-
function
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
vals.push(a);
|
|
5862
|
+
function unwrapConfigCall(e) {
|
|
5863
|
+
let x = e;
|
|
5864
|
+
while (ts.isParenthesizedExpression(x) || ts.isAsExpression(x) || ts.isSatisfiesExpression(x)) {
|
|
5865
|
+
x = x.expression;
|
|
5855
5866
|
}
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
case "join":
|
|
5869
|
-
return vals.length >= 1 && vals.length <= 2 ? s(0) : void 0;
|
|
5870
|
-
case "tojson":
|
|
5871
|
-
return vals.length === 1 ? JSON.stringify(vals[0]) : void 0;
|
|
5872
|
-
case "fromjson": {
|
|
5873
|
-
if (vals.length !== 1) return void 0;
|
|
5874
|
-
try {
|
|
5875
|
-
const v = JSON.parse(s(0));
|
|
5876
|
-
if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
|
|
5877
|
-
return TRUTHY;
|
|
5878
|
-
} catch {
|
|
5879
|
-
return void 0;
|
|
5880
|
-
}
|
|
5881
|
-
}
|
|
5882
|
-
default:
|
|
5883
|
-
return void 0;
|
|
5867
|
+
if (ts.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
|
|
5868
|
+
return x;
|
|
5869
|
+
}
|
|
5870
|
+
function spreadsIntoConfig(node, runner) {
|
|
5871
|
+
const obj = node.parent;
|
|
5872
|
+
if (!ts.isObjectLiteralExpression(obj)) return false;
|
|
5873
|
+
const keys = obj.properties.map((p) => ts.isPropertyAssignment(p) ? keyName2(p.name) : null);
|
|
5874
|
+
if (runner === "vitest") {
|
|
5875
|
+
if (keys.includes("test")) return true;
|
|
5876
|
+
let q = obj.parent;
|
|
5877
|
+
while (q && (ts.isParenthesizedExpression(q) || ts.isAsExpression(q) || q.kind === ts.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
5878
|
+
return !!q && ts.isPropertyAssignment(q) && keyName2(q.name) === "test";
|
|
5884
5879
|
}
|
|
5880
|
+
return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
|
|
5885
5881
|
}
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
TRUTHY = /* @__PURE__ */ Symbol("truthy");
|
|
5893
|
-
truthy = (v) => v === TRUTHY ? true : typeof v === "string" ? v !== "" : typeof v === "number" ? v !== 0 : v === true;
|
|
5882
|
+
function extendsOf(el) {
|
|
5883
|
+
for (const p of el.properties) {
|
|
5884
|
+
if (!ts.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
|
|
5885
|
+
if (p.initializer.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
5886
|
+
if (p.initializer.kind === ts.SyntaxKind.FalseKeyword) return null;
|
|
5887
|
+
return "path";
|
|
5894
5888
|
}
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
// src/detectors/invocation.ts
|
|
5898
|
-
function canonical(core) {
|
|
5899
|
-
let s = foldExpressions(core.replace(/^(['"])(.*)\1$/, "$2").trim()).replace(MATRIX_FLAG, " ").replace(/\s+/g, " ").trim();
|
|
5900
|
-
s = s.replace(/^(?:cd|pushd)\s+\S+\s*&&\s*/, "");
|
|
5901
|
-
s = s.replace(/^(?:npx|pnpm\s+(?:exec|dlx)|yarn\s+(?:exec|dlx)|bunx|bun\s+x)\s+/, "exec ");
|
|
5902
|
-
s = s.replace(/^(?:npm|pnpm|yarn|bun)\s+(?:run(?:-script)?\s+)?/, "exec ");
|
|
5903
|
-
s = s.replace(/^exec\s+(?:t|tst)(?=\s|$)/, "exec test");
|
|
5904
|
-
s = s.replace(/^exec\s+/, "");
|
|
5905
|
-
return s.replace(/\s+--\s*$/, "").trim();
|
|
5889
|
+
return null;
|
|
5906
5890
|
}
|
|
5907
|
-
function
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
return canonical(core).split(/\s*(?:&&|\|\||;|\|)\s*/).some((seg) => CHECK_HEAD.test(canonical(seg.replace(COMMAND_PREFIX, ""))));
|
|
5891
|
+
function readProject(el, root, runner) {
|
|
5892
|
+
const p = collect(el, runner);
|
|
5893
|
+
const ext = extendsOf(el);
|
|
5894
|
+
if (ext === "path") p.opaque = true;
|
|
5895
|
+
else if (ext === true && root) {
|
|
5896
|
+
p.include ??= root.include;
|
|
5897
|
+
p.regex ??= root.regex;
|
|
5898
|
+
p.ignore ??= root.ignore;
|
|
5899
|
+
p.roots ??= root.roots;
|
|
5900
|
+
p.base ??= root.base;
|
|
5901
|
+
if (root.opaque) p.opaque = true;
|
|
5902
|
+
}
|
|
5903
|
+
p.present = true;
|
|
5904
|
+
return p;
|
|
5922
5905
|
}
|
|
5923
|
-
function
|
|
5924
|
-
|
|
5906
|
+
function readProjects(e, root, runner) {
|
|
5907
|
+
if (!ts.isArrayLiteralExpression(e)) {
|
|
5908
|
+
root.opaque = true;
|
|
5909
|
+
return [];
|
|
5910
|
+
}
|
|
5911
|
+
const list3 = [];
|
|
5912
|
+
for (const el of e.elements) {
|
|
5913
|
+
const lit = unwrapConfigCall(el);
|
|
5914
|
+
if (ts.isObjectLiteralExpression(lit)) list3.push(readProject(lit, root, runner));
|
|
5915
|
+
else root.opaque = true;
|
|
5916
|
+
}
|
|
5917
|
+
return list3;
|
|
5925
5918
|
}
|
|
5926
|
-
function
|
|
5927
|
-
const
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
if (
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5919
|
+
function workspaceArray(sf) {
|
|
5920
|
+
for (const st of sf.statements) {
|
|
5921
|
+
let e;
|
|
5922
|
+
if (ts.isExportAssignment(st)) e = st.expression;
|
|
5923
|
+
else if (ts.isExpressionStatement(st) && ts.isBinaryExpression(st.expression) && /module\.exports/.test(st.expression.left.getText())) e = st.expression.right;
|
|
5924
|
+
if (!e) continue;
|
|
5925
|
+
while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e)) {
|
|
5926
|
+
e = e.expression;
|
|
5927
|
+
}
|
|
5928
|
+
if (ts.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
|
|
5929
|
+
return ts.isArrayLiteralExpression(e) ? e : null;
|
|
5935
5930
|
}
|
|
5936
5931
|
return null;
|
|
5937
5932
|
}
|
|
5938
|
-
function
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5933
|
+
function parseSelection(src, runner, path = "") {
|
|
5934
|
+
let sel = empty();
|
|
5935
|
+
if (runner === "pytest") return pytestSelection(src);
|
|
5936
|
+
try {
|
|
5937
|
+
const sf = parseSource("cfg.ts", asExpression2(src));
|
|
5938
|
+
if (!sf) return sel;
|
|
5939
|
+
if (runner === "vitest" && isWorkspaceFile(path)) {
|
|
5940
|
+
const arr = workspaceArray(sf);
|
|
5941
|
+
if (arr) {
|
|
5942
|
+
sel.projects = readProjects(arr, sel, runner);
|
|
5943
|
+
sel.present = true;
|
|
5944
|
+
if (sel.projects.some((p) => p.opaque)) sel.opaque = true;
|
|
5945
|
+
} else if (src.trim()) sel.opaque = true;
|
|
5946
|
+
} else sel = collect(sf, runner);
|
|
5947
|
+
} catch {
|
|
5950
5948
|
}
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
const
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
else return { state: "kept" };
|
|
5962
|
-
} else if (r.startsWith(a + " ") && invokesCheck(a)) {
|
|
5963
|
-
return { state: "kept" };
|
|
5964
|
-
} else {
|
|
5965
|
-
const w = a.match(TIMEOUT_WRAP);
|
|
5966
|
-
if (w && (canonical(w[1]) === r || canonical(w[1]).startsWith(r + " "))) neutralised ??= raw;
|
|
5967
|
-
}
|
|
5949
|
+
return sel;
|
|
5950
|
+
}
|
|
5951
|
+
function predicate(sel, runner) {
|
|
5952
|
+
if (sel.projects) {
|
|
5953
|
+
const ps = sel.projects.map((p) => predicate(p, runner));
|
|
5954
|
+
const wide = sel.wide[0];
|
|
5955
|
+
return {
|
|
5956
|
+
selects: (p) => wide === void 0 && ps.some((x) => x.selects(p)),
|
|
5957
|
+
why: (p) => wide !== void 0 ? wideWhy(wide) : ps.length ? `no project selects it (${ps[0].why(p)})` : "the projects list is empty"
|
|
5958
|
+
};
|
|
5968
5959
|
}
|
|
5969
|
-
const
|
|
5970
|
-
const
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5960
|
+
const includeList = sel.include ?? (runner === "jest" ? sel.regex ? null : JEST_DEFAULT_MATCH : runner === "pytest" ? PYTEST_DEFAULT_INCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
5961
|
+
const ignoreList = sel.ignore ?? (runner === "jest" ? JEST_DEFAULT_IGNORE : runner === "pytest" ? PYTEST_DEFAULT_IGNORE : VITEST_DEFAULT_EXCLUDE).map((pattern) => ({
|
|
5962
|
+
pattern,
|
|
5963
|
+
key: runner === "jest" ? "testPathIgnorePatterns" : runner === "pytest" ? "norecursedirs" : "test.exclude"
|
|
5964
|
+
}));
|
|
5965
|
+
const blockWide = [...ignoreList.filter((e) => e.pattern.startsWith("\0")), ...sel.wide];
|
|
5966
|
+
const included = includeList ? globList2(includeList) : sel.regex ? regexes(sel.regex) : () => true;
|
|
5967
|
+
const ignoredBy = (p) => blockWide[0] ?? ignoreList.filter((e) => !e.pattern.startsWith("\0")).find((e) => (runner === "jest" ? regexes([e.pattern]) : globs2([e.pattern]))(p));
|
|
5968
|
+
const roots = sel.roots?.map((r) => r.replace(/^\.\//, "").replace(/\/+$/, "")).map((r) => r === "." ? "" : r) ?? null;
|
|
5969
|
+
const rebase = (p) => sel.base === null ? p : underDir(p, sel.base) ? p.slice(sel.base.length + 1) : null;
|
|
5970
|
+
const inRoots = (p) => roots === null || roots.some((r) => underDir(p, r));
|
|
5971
|
+
return {
|
|
5972
|
+
selects: (p) => {
|
|
5973
|
+
const r = rebase(p);
|
|
5974
|
+
return r !== null && inRoots(r) && included(r) && !ignoredBy(r);
|
|
5975
|
+
},
|
|
5976
|
+
why: (p) => {
|
|
5977
|
+
const r = rebase(p);
|
|
5978
|
+
const baseKey = runner === "jest" ? "rootDir" : "test.dir";
|
|
5979
|
+
if (r === null) return `${baseKey} no longer covers it (${JSON.stringify(sel.base)})`;
|
|
5980
|
+
if (!inRoots(r)) return `roots no longer covers it (${JSON.stringify(sel.roots)})`;
|
|
5981
|
+
const hit = ignoredBy(r);
|
|
5982
|
+
if (hit) {
|
|
5983
|
+
if (hit.pattern.startsWith("\0")) return wideWhy(hit);
|
|
5984
|
+
return `${hit.key === "exclude" ? "test.exclude" : hit.key} now matches it (${JSON.stringify(hit.pattern)})`;
|
|
5985
|
+
}
|
|
5986
|
+
const key2 = runner === "jest" ? sel.include ? "testMatch" : sel.regex ? "testRegex" : "testMatch (default)" : sel.include ? "test.include" : "test.include (default)";
|
|
5987
|
+
return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? includeList ?? [])})`;
|
|
5983
5988
|
}
|
|
5984
|
-
|
|
5985
|
-
const mask = maskOf(args);
|
|
5986
|
-
const newMask = mask !== null && mask !== priorMask;
|
|
5987
|
-
if (a.includes(" ") && (newMask || narrowed(args) || runnerNarrowed(a, args))) neutralised ??= raw;
|
|
5988
|
-
else respelled = true;
|
|
5989
|
-
}
|
|
5990
|
-
if (respelled) return { state: "kept" };
|
|
5991
|
-
return neutralised ? { state: "neutralised", by: neutralised } : { state: "gone" };
|
|
5989
|
+
};
|
|
5992
5990
|
}
|
|
5993
|
-
function
|
|
5994
|
-
const
|
|
5995
|
-
if (
|
|
5996
|
-
const
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
5991
|
+
function suiteNarrowings(before, after, path, samples) {
|
|
5992
|
+
const runner = runnerOf(path, after) ?? runnerOf(path, before);
|
|
5993
|
+
if (!runner) return [];
|
|
5994
|
+
const b = parseSelection(before ?? "", runner, path);
|
|
5995
|
+
const a = parseSelection(after, runner, path);
|
|
5996
|
+
if (!a.present || a.opaque) return [];
|
|
5997
|
+
const pb = predicate(b, runner);
|
|
5998
|
+
const pa = predicate(a, runner);
|
|
5999
|
+
const out3 = [];
|
|
6000
|
+
for (const s of samples) {
|
|
6001
|
+
if (pb.selects(s) && !pa.selects(s)) out3.push({ path: s, reason: pa.why(s) });
|
|
6000
6002
|
}
|
|
6001
|
-
return
|
|
6002
|
-
}
|
|
6003
|
-
function scriptKind3(inner) {
|
|
6004
|
-
const kind = checkKind(inner);
|
|
6005
|
-
if (!kind) return null;
|
|
6006
|
-
const head = canonical(inner).match(/^(tests?|lint|type-?check|coverage)(?=[\s:]|$)/i)?.[1];
|
|
6007
|
-
if (!head) return kind;
|
|
6008
|
-
return /^type/i.test(head) ? "types" : /^lint/i.test(head) ? "lint" : "test";
|
|
6003
|
+
return out3;
|
|
6009
6004
|
}
|
|
6010
|
-
function
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6005
|
+
function requiredPath(e) {
|
|
6006
|
+
let x = e;
|
|
6007
|
+
while (ts.isParenthesizedExpression(x) || ts.isAsExpression(x) || ts.isSatisfiesExpression(x) || ts.isAwaitExpression(x)) x = x.expression;
|
|
6008
|
+
if (ts.isCallExpression(x) && x.arguments.length > 0 && isStr(x.arguments[0])) {
|
|
6009
|
+
const callee = x.expression;
|
|
6010
|
+
if (ts.isIdentifier(callee) && callee.text === "require" || callee.kind === ts.SyntaxKind.ImportKeyword) return x.arguments[0].text;
|
|
6011
|
+
}
|
|
6012
|
+
return null;
|
|
6015
6013
|
}
|
|
6016
|
-
function
|
|
6014
|
+
function delegationsIn(src, runner) {
|
|
6017
6015
|
const out3 = [];
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6016
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6017
|
+
const add = (target, how, local = true) => {
|
|
6018
|
+
if (local && !isLocalPath(target) || hasGlob(target) || seen.has(target)) return;
|
|
6019
|
+
seen.add(target);
|
|
6020
|
+
out3.push({ target, how });
|
|
6021
|
+
};
|
|
6022
|
+
let sf = null;
|
|
6023
|
+
try {
|
|
6024
|
+
sf = parseSource("cfg.ts", asExpression2(src));
|
|
6025
|
+
} catch {
|
|
6026
|
+
return out3;
|
|
6027
|
+
}
|
|
6028
|
+
if (!sf) return out3;
|
|
6029
|
+
const bound = /* @__PURE__ */ new Map();
|
|
6030
|
+
const bindNames = (name, spec2) => {
|
|
6031
|
+
if (ts.isIdentifier(name)) bound.set(name.text, spec2);
|
|
6032
|
+
else for (const el of name.elements) if (ts.isBindingElement(el)) bindNames(el.name, spec2);
|
|
6033
|
+
};
|
|
6034
|
+
for (const st of sf.statements) {
|
|
6035
|
+
if (ts.isImportDeclaration(st) && isStr(st.moduleSpecifier) && isLocalPath(st.moduleSpecifier.text) && st.importClause) {
|
|
6036
|
+
const spec2 = st.moduleSpecifier.text;
|
|
6037
|
+
if (st.importClause.name) bound.set(st.importClause.name.text, spec2);
|
|
6038
|
+
const nb = st.importClause.namedBindings;
|
|
6039
|
+
if (nb && ts.isNamespaceImport(nb)) bound.set(nb.name.text, spec2);
|
|
6040
|
+
else if (nb) for (const el of nb.elements) bound.set(el.name.text, spec2);
|
|
6041
|
+
} else if (ts.isVariableStatement(st)) {
|
|
6042
|
+
for (const d of st.declarationList.declarations) {
|
|
6043
|
+
const spec2 = d.initializer ? requiredPath(d.initializer) : null;
|
|
6044
|
+
if (spec2 !== null && isLocalPath(spec2)) bindNames(d.name, spec2);
|
|
6045
|
+
}
|
|
6030
6046
|
}
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6047
|
+
}
|
|
6048
|
+
const moduleOf = (e) => {
|
|
6049
|
+
let x = e;
|
|
6050
|
+
while (ts.isParenthesizedExpression(x) || ts.isAsExpression(x) || ts.isSatisfiesExpression(x) || ts.isAwaitExpression(x) || ts.isPropertyAccessExpression(x) || ts.isNonNullExpression(x))
|
|
6051
|
+
x = x.expression;
|
|
6052
|
+
if (ts.isIdentifier(x)) return bound.get(x.text) ?? null;
|
|
6053
|
+
return requiredPath(x);
|
|
6054
|
+
};
|
|
6055
|
+
const exported = (e, how) => {
|
|
6056
|
+
const m = moduleOf(unwrapConfigCall(e));
|
|
6057
|
+
if (m !== null) add(m, how);
|
|
6058
|
+
};
|
|
6059
|
+
const visit = (node) => {
|
|
6060
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && isStr(node.moduleSpecifier)) {
|
|
6061
|
+
const clause = node.exportClause;
|
|
6062
|
+
const whole = !clause || ts.isNamedExports(clause) && clause.elements.some((el) => el.name.text === "default");
|
|
6063
|
+
if (whole) add(node.moduleSpecifier.text, oneLine(node));
|
|
6064
|
+
} else if (ts.isExportAssignment(node)) {
|
|
6065
|
+
exported(node.expression, oneLine(node));
|
|
6066
|
+
} else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && /^(?:module\.)?exports(?:\.default)?$/.test(node.left.getText())) {
|
|
6067
|
+
exported(node.right, oneLine(node));
|
|
6068
|
+
} else if (ts.isSpreadAssignment(node)) {
|
|
6069
|
+
const m = moduleOf(node.expression);
|
|
6070
|
+
if (m !== null) add(m, `...${oneLine(node.expression)}`);
|
|
6071
|
+
} else if (ts.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0]) {
|
|
6072
|
+
const m = moduleOf(node.arguments[0]);
|
|
6073
|
+
if (m !== null) add(m, `${oneLine(node.expression)}(${oneLine(node.arguments[0])}, \u2026)`);
|
|
6074
|
+
} else if (ts.isPropertyAssignment(node)) {
|
|
6075
|
+
const k = keyName2(node.name);
|
|
6076
|
+
const v = node.initializer;
|
|
6077
|
+
if (runner === "vitest" && k === "extends" && isStr(v)) add(v.text, `extends: ${JSON.stringify(v.text)}`);
|
|
6078
|
+
else if (runner === "jest" && k === "preset" && isStr(v)) add(v.text, `preset: ${JSON.stringify(v.text)}`);
|
|
6079
|
+
else if (k === "projects" && ts.isArrayLiteralExpression(v) && (runner === "jest" || ownerKey2(node) === "test")) {
|
|
6080
|
+
for (const el of v.elements) if (isStr(el)) add(el.text, `projects: [${JSON.stringify(el.text)}]`, false);
|
|
6081
|
+
}
|
|
6034
6082
|
}
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6083
|
+
ts.forEachChild(node, visit);
|
|
6084
|
+
};
|
|
6085
|
+
visit(sf);
|
|
6086
|
+
return out3;
|
|
6087
|
+
}
|
|
6088
|
+
function suiteDelegations(before, after, path) {
|
|
6089
|
+
const runner = runnerOf(path, after) ?? runnerOf(path, before);
|
|
6090
|
+
if (!runner || runner === "pytest") return [];
|
|
6091
|
+
const was = new Set(delegationsIn(before ?? "", runner).map((d) => d.target));
|
|
6092
|
+
return delegationsIn(after, runner).filter((d) => !was.has(d.target));
|
|
6093
|
+
}
|
|
6094
|
+
function targetCandidates(configPath, target) {
|
|
6095
|
+
const dir = configPath.includes("/") ? configPath.slice(0, configPath.lastIndexOf("/")) : "";
|
|
6096
|
+
const rel = target.replace(/^<rootDir>\/?/, "./");
|
|
6097
|
+
const parts = rel.startsWith("/") || !dir ? [] : dir.split("/");
|
|
6098
|
+
for (const seg of rel.split("/")) {
|
|
6099
|
+
if (seg === "" || seg === ".") continue;
|
|
6100
|
+
if (seg === "..") parts.pop();
|
|
6101
|
+
else parts.push(seg);
|
|
6049
6102
|
}
|
|
6050
|
-
|
|
6051
|
-
|
|
6103
|
+
const base = parts.join("/");
|
|
6104
|
+
if (!base) return [];
|
|
6105
|
+
const exts = [".js", ".cjs", ".mjs", ".ts", ".cts", ".mts", ".json"];
|
|
6106
|
+
return [base, ...exts.map((e) => base + e), ...exts.map((e) => `${base}/index${e}`)];
|
|
6052
6107
|
}
|
|
6053
|
-
function
|
|
6108
|
+
function setupEntriesIn(src, runner, path) {
|
|
6054
6109
|
const out3 = [];
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
const following = stmts[i + 1]?.seg ?? "";
|
|
6067
|
-
if (op === "||" || op === "|" || op === "&" || op === ";" && !invokesCheck(unwrap(following))) {
|
|
6068
|
-
out3.push({ key: `op:${op}`, what: `${op} ${following.split(/\s+/).slice(0, 2).join(" ")}`.trim(), direction: "added" });
|
|
6069
|
-
}
|
|
6070
|
-
for (const re of MASKING_FLAGS) {
|
|
6071
|
-
const f = spelled(re, args);
|
|
6072
|
-
if (f) out3.push({ key: `flag:${f}`, what: f, direction: "added" });
|
|
6073
|
-
}
|
|
6074
|
-
for (const re of GATE_FLAGS) {
|
|
6075
|
-
const f = spelled(re, " " + seg);
|
|
6076
|
-
if (f) out3.push({ key: `gate:${f}`, what: f, direction: "removed" });
|
|
6110
|
+
let sf = null;
|
|
6111
|
+
try {
|
|
6112
|
+
sf = parseSource("cfg.ts", asExpression2(src));
|
|
6113
|
+
} catch {
|
|
6114
|
+
return out3;
|
|
6115
|
+
}
|
|
6116
|
+
if (!sf) return out3;
|
|
6117
|
+
const inPackageJson = (path.split("/").pop() ?? path) === "package.json";
|
|
6118
|
+
const underJestKey = (node) => {
|
|
6119
|
+
for (let q = node.parent; q; q = q.parent) {
|
|
6120
|
+
if (ts.isPropertyAssignment(q) && keyName2(q.name) === "jest") return true;
|
|
6077
6121
|
}
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6122
|
+
return false;
|
|
6123
|
+
};
|
|
6124
|
+
const values = (e) => {
|
|
6125
|
+
if (isStr(e)) return [e.text];
|
|
6126
|
+
if (!ts.isArrayLiteralExpression(e)) return [];
|
|
6127
|
+
const vs = [];
|
|
6128
|
+
for (const el of e.elements) {
|
|
6129
|
+
if (isStr(el)) vs.push(el.text);
|
|
6130
|
+
else if (ts.isArrayLiteralExpression(el) && el.elements[0] && isStr(el.elements[0])) vs.push(el.elements[0].text);
|
|
6131
|
+
}
|
|
6132
|
+
return vs;
|
|
6133
|
+
};
|
|
6134
|
+
const visit = (node) => {
|
|
6135
|
+
if (ts.isPropertyAssignment(node)) {
|
|
6136
|
+
const k = keyName2(node.name);
|
|
6137
|
+
const applies = k !== null && (runner === "jest" ? JEST_SETUP_KEYS.test(k) && (!inPackageJson || underJestKey(node)) : VITEST_SETUP_KEYS.test(k) && ownerKey2(node) === "test");
|
|
6138
|
+
if (applies && k !== null) {
|
|
6139
|
+
for (const v of values(node.initializer)) if (namesFile(v)) out3.push({ key: k, target: v });
|
|
6140
|
+
}
|
|
6083
6141
|
}
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
if (opts.fresh) continue;
|
|
6088
|
-
const cfg = args.match(CONFIG_FLAG);
|
|
6089
|
-
if (cfg && !opts.configReviewed?.(cfg[3])) out3.push({ key: `config:${cfg[3]}`, what: `${cfg[1]} ${cfg[3]}`, direction: "added" });
|
|
6090
|
-
const pos = positionalOf(args);
|
|
6091
|
-
if (pos) out3.push({ key: "positional", what: `a path positional (${pos})`, direction: "added" });
|
|
6092
|
-
}
|
|
6093
|
-
return out3;
|
|
6094
|
-
}
|
|
6095
|
-
function checkStatements(cmd) {
|
|
6096
|
-
const out3 = [];
|
|
6097
|
-
for (const seg of segments(cmd)) {
|
|
6098
|
-
const w = seg.match(TIMEOUT_WRAP);
|
|
6099
|
-
const inner = unwrap(w ? w[1] : seg);
|
|
6100
|
-
const kind = scriptKind3(inner);
|
|
6101
|
-
if (kind) out3.push({ kind, canon: canonical(inner) });
|
|
6102
|
-
}
|
|
6142
|
+
ts.forEachChild(node, visit);
|
|
6143
|
+
};
|
|
6144
|
+
visit(sf);
|
|
6103
6145
|
return out3;
|
|
6104
6146
|
}
|
|
6105
|
-
function
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
return last !== void 0 && FAILS_LOUDLY.test(last.trim());
|
|
6111
|
-
}
|
|
6112
|
-
function invocationWeakening(before, after, runner, opts = {}) {
|
|
6113
|
-
const was = checkKinds(before);
|
|
6114
|
-
const now = checkKinds(after);
|
|
6115
|
-
if (now.size === 0 && failsLoudly(after)) return { state: "kept" };
|
|
6116
|
-
for (const k of was) {
|
|
6117
|
-
if (now.has(k) || opts.relocated?.(k)) continue;
|
|
6118
|
-
const instead = [...now].filter((x) => !was.has(x));
|
|
6119
|
-
return { state: "removed", kind: k, what: instead.length ? `runs a ${instead.join("/")} check instead` : now.size ? "the check is gone" : "no check is run" };
|
|
6120
|
-
}
|
|
6121
|
-
const survived = new Set(checkStatements(before).map((s) => s.canon));
|
|
6122
|
-
const extended = new Set(checkStatements(after).filter((s) => survived.has(s.canon)).map((s) => s.kind));
|
|
6123
|
-
const read = { ...opts, fresh: was.size === 0, extended };
|
|
6124
|
-
const beforeDs = descriptors(before, runner, read);
|
|
6125
|
-
const afterDs = descriptors(after, runner, read);
|
|
6126
|
-
const seenBefore = new Set(beforeDs.map((d) => d.key));
|
|
6127
|
-
const added = afterDs.find((d) => d.direction === "added" && !seenBefore.has(d.key));
|
|
6128
|
-
if (added) return { state: "neutralised", what: added.what, direction: "added" };
|
|
6129
|
-
if (coverageFloorLowered(before, after)) return { state: "neutralised", what: "a lowered --cov-fail-under", direction: "added" };
|
|
6130
|
-
const seenAfter = new Set(afterDs.map((d) => d.key));
|
|
6131
|
-
const dropped = beforeDs.find((d) => d.direction === "removed" && !seenAfter.has(d.key));
|
|
6132
|
-
if (dropped) return { state: "neutralised", what: dropped.what, direction: "removed" };
|
|
6133
|
-
return { state: "kept" };
|
|
6147
|
+
function newSetupEntries(before, after, path) {
|
|
6148
|
+
const runner = runnerOf(path, after) ?? runnerOf(path, before);
|
|
6149
|
+
if (!runner || runner === "pytest") return [];
|
|
6150
|
+
const was = new Set(setupEntriesIn(before ?? "", runner, path).map((e) => `${e.key} ${e.target}`));
|
|
6151
|
+
return setupEntriesIn(after, runner, path).filter((e) => !was.has(`${e.key} ${e.target}`));
|
|
6134
6152
|
}
|
|
6135
|
-
var
|
|
6136
|
-
var
|
|
6137
|
-
"src/detectors/
|
|
6153
|
+
var JEST_DEFAULT_MATCH, JEST_DEFAULT_IGNORE, VITEST_DEFAULT_INCLUDE, PYTEST_DEFAULT_INCLUDE, PYTEST_DEFAULT_IGNORE, VITEST_DEFAULT_EXCLUDE, JS_UNWALKED, CANONICAL_SAMPLES, PYTEST_CANONICAL_SAMPLES, isStr, PYTEST_INI_ORDER, pathGlobs, isWorkspaceFile, empty, stripRootDir, JEST_SELECTION_KEYS, globs2, globList2, regexes, wideWhy, underDir, isLocalPath, hasGlob, oneLine, JEST_SETUP_KEYS, VITEST_SETUP_KEYS, namesFile;
|
|
6154
|
+
var init_suite_config = __esm({
|
|
6155
|
+
"src/detectors/suite-config.ts"() {
|
|
6138
6156
|
"use strict";
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
{
|
|
6153
|
-
// a package-manager script: `test --prefix dir`, `-w pkg`, `--workspace=pkg`,
|
|
6154
|
-
// `--if-present` (no script, no failure), pnpm's `--filter` / `-F`, npm's `-C`
|
|
6155
|
-
runner: /^(?:test|tests|coverage)(?::\S+)?(?=\s|$)/,
|
|
6156
|
-
flags: /\s--if-present(?=\s|$)|\s--prefix(?:=|\s)|\s-C(?:=|\s)|\s-w(?:=|\s)|\s--workspace(?:=|\s)|\s--filter(?:=|\s)|\s-F(?:=|\s)/
|
|
6157
|
-
},
|
|
6158
|
-
{
|
|
6159
|
-
runner: /^(?:jest|vitest)\b/,
|
|
6160
|
-
flags: /\s--shard(?:=|\s)|\s--root(?:Dir)?(?:=|\s)|\s--testMatch(?:=|\s)|\s--testRegex(?:=|\s)|\s--modulePathIgnorePatterns(?:=|\s)|\s--selectProjects(?:=|\s)|\s--grep(?:=|\s)|\s-g(?:=|\s)/
|
|
6161
|
-
},
|
|
6162
|
-
{ runner: /^playwright\b/, flags: /\s--shard(?:=|\s)|\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--grep-invert(?:=|\s)/ },
|
|
6163
|
-
{ runner: /^mocha\b/, flags: /\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--fgrep(?:=|\s)|\s-f(?:=|\s)/ },
|
|
6164
|
-
{ runner: /^ava\b/, flags: /\s--match(?:=|\s)|\s-m(?:=|\s)/ },
|
|
6165
|
-
{ runner: /^node --test\b/, flags: /\s--test-name-pattern(?:=|\s)|\s--test-skip-pattern(?:=|\s)|\s--test-only(?=\s|$)/ },
|
|
6166
|
-
{
|
|
6167
|
-
// `-p no:<plugin>` disables collection when the plugin is the collector
|
|
6168
|
-
// (`python`, `doctest`, `cov`); the cache, warning and output plugins select nothing
|
|
6169
|
-
runner: /^pytest\b/,
|
|
6170
|
-
flags: /\s-k(?:=|\s)|\s-m(?:=|\s)|\s--deselect(?:=|\s)|\s--ignore(?:-glob)?(?:=|\s)|\s-p\s*no:(?!(?:cacheprovider|warnings|faulthandler|randomly|logging|legacypath|pastebin|stepwise|tmpdir|junitxml|terminal|terminalreporter)(?=\s|$))|\s--collect-only(?=\s|$)|\s--co(?=\s|$)/
|
|
6171
|
-
},
|
|
6172
|
-
{ runner: /^cargo test\b/, flags: /\s--\s(?:\S+\s+)*--skip(?:=|\s)|\s--exclude(?:=|\s)/ },
|
|
6173
|
-
{ runner: /^go test\b/, flags: /\s-run(?:=|\s)|\s-skip(?:=|\s)/ }
|
|
6157
|
+
init_lazy_deps();
|
|
6158
|
+
init_ts_lazy();
|
|
6159
|
+
JEST_DEFAULT_MATCH = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"];
|
|
6160
|
+
JEST_DEFAULT_IGNORE = ["/node_modules/"];
|
|
6161
|
+
VITEST_DEFAULT_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
|
|
6162
|
+
PYTEST_DEFAULT_INCLUDE = ["**/test_*.py", "**/*_test.py"];
|
|
6163
|
+
PYTEST_DEFAULT_IGNORE = ["**/build/**", "**/dist/**", "**/node_modules/**", "**/venv/**", "**/.*/**"];
|
|
6164
|
+
VITEST_DEFAULT_EXCLUDE = [
|
|
6165
|
+
"**/node_modules/**",
|
|
6166
|
+
"**/dist/**",
|
|
6167
|
+
"**/cypress/**",
|
|
6168
|
+
"**/.{idea,git,cache,output,temp}/**",
|
|
6169
|
+
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*"
|
|
6174
6170
|
];
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
"
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6171
|
+
JS_UNWALKED = [...VITEST_DEFAULT_EXCLUDE, "**/.*/**"];
|
|
6172
|
+
CANONICAL_SAMPLES = [
|
|
6173
|
+
"src/a.test.ts",
|
|
6174
|
+
"src/a.spec.ts",
|
|
6175
|
+
"test/a.test.ts",
|
|
6176
|
+
"tests/a.test.js",
|
|
6177
|
+
"src/__tests__/a.ts",
|
|
6178
|
+
"a.test.tsx",
|
|
6179
|
+
"a.spec.js",
|
|
6180
|
+
"test/a.test.mjs"
|
|
6181
|
+
];
|
|
6182
|
+
PYTEST_CANONICAL_SAMPLES = [
|
|
6183
|
+
"tests/test_a.py",
|
|
6184
|
+
"tests/a_test.py",
|
|
6185
|
+
"test_a.py",
|
|
6186
|
+
"src/tests/test_a.py",
|
|
6187
|
+
"tests/conftest.py"
|
|
6188
|
+
];
|
|
6189
|
+
isStr = (e) => ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e);
|
|
6190
|
+
PYTEST_INI_ORDER = ["pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"];
|
|
6191
|
+
pathGlobs = (p) => {
|
|
6192
|
+
const clean = p.replace(/^\.\//, "").replace(/::.*$/, "").replace(/\/+$/, "");
|
|
6193
|
+
return clean ? [clean, clean + "/**"] : [];
|
|
6188
6194
|
};
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6195
|
+
isWorkspaceFile = (path) => /^vitest\.workspace\./.test(path.split("/").pop() ?? path);
|
|
6196
|
+
empty = () => ({ include: null, regex: null, ignore: null, roots: null, base: null, projects: null, wide: [], opaque: false, present: false });
|
|
6197
|
+
stripRootDir = (g) => g.replace(/^<rootDir>\/?/, "");
|
|
6198
|
+
JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
|
|
6199
|
+
globs2 = (patterns) => {
|
|
6200
|
+
const ms = patterns.map((g) => {
|
|
6201
|
+
try {
|
|
6202
|
+
return picomatch(g, { dot: true });
|
|
6203
|
+
} catch {
|
|
6204
|
+
return () => false;
|
|
6205
|
+
}
|
|
6206
|
+
});
|
|
6207
|
+
return (path) => ms.some((m) => m(path));
|
|
6201
6208
|
};
|
|
6202
|
-
|
|
6209
|
+
globList2 = (patterns) => {
|
|
6210
|
+
const pos = globs2(patterns.filter((g) => !g.startsWith("!")));
|
|
6211
|
+
const neg = globs2(patterns.filter((g) => g.startsWith("!")).map((g) => g.slice(1)));
|
|
6212
|
+
return (path) => pos(path) && !neg(path);
|
|
6213
|
+
};
|
|
6214
|
+
regexes = (patterns) => {
|
|
6215
|
+
const rs = patterns.flatMap((p) => {
|
|
6216
|
+
try {
|
|
6217
|
+
return [new RegExp(p.replace(/<rootDir>/g, ""))];
|
|
6218
|
+
} catch {
|
|
6219
|
+
return [];
|
|
6220
|
+
}
|
|
6221
|
+
});
|
|
6222
|
+
return (path) => rs.some((r) => r.test("/" + path));
|
|
6223
|
+
};
|
|
6224
|
+
wideWhy = (hit) => `${hit.key} narrows the whole run (${JSON.stringify(hit.pattern.slice(1).replace(/^testNamePattern /, ""))})`;
|
|
6225
|
+
underDir = (path, dir) => dir === "" || path.startsWith(dir + "/");
|
|
6226
|
+
isLocalPath = (s) => /^(?:\.\.?\/|\/|<rootDir>)/.test(s);
|
|
6227
|
+
hasGlob = (s) => /[*?[\]{}]/.test(s);
|
|
6228
|
+
oneLine = (node) => node.getText().replace(/\s+/g, " ").slice(0, 120);
|
|
6229
|
+
JEST_SETUP_KEYS = /^(?:setupFiles|setupFilesAfterEnv|setupFilesAfterEach|globalSetup|globalTeardown|runner|testRunner|testEnvironment|reporters|testResultsProcessor|testSequencer|snapshotResolver|resolver)$/;
|
|
6230
|
+
VITEST_SETUP_KEYS = /^(?:setupFiles|setupFilesAfterEach|globalSetup|environment|runner|reporters)$/;
|
|
6231
|
+
namesFile = (s) => isLocalPath(s) || /\.[cm]?[jt]sx?$/.test(s) && !s.startsWith("@");
|
|
6203
6232
|
}
|
|
6204
6233
|
});
|
|
6205
6234
|
|
|
6206
|
-
// src/detectors/
|
|
6207
|
-
function
|
|
6208
|
-
|
|
6235
|
+
// src/detectors/config-weakening.ts
|
|
6236
|
+
function parseJsonc(path, text) {
|
|
6237
|
+
try {
|
|
6238
|
+
const r = ts.parseConfigFileTextToJson(path, text);
|
|
6239
|
+
if (r.error) return null;
|
|
6240
|
+
const cfg = r.config;
|
|
6241
|
+
return cfg;
|
|
6242
|
+
} catch {
|
|
6243
|
+
return null;
|
|
6244
|
+
}
|
|
6209
6245
|
}
|
|
6210
|
-
function
|
|
6211
|
-
|
|
6246
|
+
function parseYaml(text) {
|
|
6247
|
+
try {
|
|
6248
|
+
const v = yaml.parse(text);
|
|
6249
|
+
return v;
|
|
6250
|
+
} catch {
|
|
6251
|
+
return null;
|
|
6252
|
+
}
|
|
6212
6253
|
}
|
|
6213
|
-
function
|
|
6214
|
-
|
|
6215
|
-
const m = v.match(/^\$\{\{\s*(.+?)\s*\}\}$/);
|
|
6216
|
-
return m ? m[1] : v;
|
|
6254
|
+
function corePattern(p) {
|
|
6255
|
+
return p.trim().replace(/^!/, "").replace(/^(?:\.\/|\/)+/, "").replace(/^(?:\*\*\/)+/, "").replace(/(?:\/\*\*?)+\/?$/, "").replace(/\/+$/, "");
|
|
6217
6256
|
}
|
|
6218
|
-
function
|
|
6219
|
-
const
|
|
6220
|
-
if (
|
|
6221
|
-
const
|
|
6222
|
-
return
|
|
6257
|
+
function housekeepingByName(p) {
|
|
6258
|
+
const core = corePattern(p);
|
|
6259
|
+
if (core === "" || core === "**" || core === "*") return false;
|
|
6260
|
+
const first = core.split("/")[0];
|
|
6261
|
+
return HOUSEKEEPING_DIR.test(first) || HOUSEKEEPING_FILE.test(core);
|
|
6223
6262
|
}
|
|
6224
|
-
function
|
|
6225
|
-
const
|
|
6226
|
-
if (
|
|
6227
|
-
const
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
return f !== void 0 && truthy(f);
|
|
6263
|
+
function ignoreGlobs(p) {
|
|
6264
|
+
const core = corePattern(p);
|
|
6265
|
+
if (!core) return [];
|
|
6266
|
+
const anchored = p.trim().replace(/^!/, "").startsWith("/") || core.includes("/");
|
|
6267
|
+
const heads = anchored ? [core] : [core, `**/${core}`];
|
|
6268
|
+
return heads.flatMap((h) => [h, `${h}/**`]);
|
|
6231
6269
|
}
|
|
6232
|
-
function
|
|
6233
|
-
const
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6270
|
+
function hidesSource(pattern, dir, ctx) {
|
|
6271
|
+
const files = trackedFiles(ctx);
|
|
6272
|
+
if (!files) return housekeepingByName(pattern) ? null : pattern;
|
|
6273
|
+
const m = matcher(ignoreGlobs(pattern));
|
|
6274
|
+
const hit = files.find((f) => {
|
|
6275
|
+
if (dir && !f.startsWith(dir)) return false;
|
|
6276
|
+
const rel = f.slice(dir.length);
|
|
6277
|
+
if (!CODE_FILE.test(rel) || HOUSEKEEPING_FILE.test(rel)) return false;
|
|
6278
|
+
if (rel.split("/").slice(0, -1).some((seg) => HOUSEKEEPING_DIR.test(seg))) return false;
|
|
6279
|
+
return m(rel);
|
|
6280
|
+
});
|
|
6281
|
+
return hit === void 0 ? null : hit;
|
|
6241
6282
|
}
|
|
6242
|
-
function
|
|
6243
|
-
const
|
|
6244
|
-
|
|
6245
|
-
while (h >= 0 && (lines[h].trim() === "" || indentOf(lines[h]) >= ind)) h--;
|
|
6246
|
-
if (h < 0 || !/^\s+[\w-]+:\s*(?:#.*)?$/.test(lines[h])) return null;
|
|
6247
|
-
let t = h - 1;
|
|
6248
|
-
while (t >= 0 && (lines[t].trim() === "" || indentOf(lines[t]) > 0)) t--;
|
|
6249
|
-
if (t < 0 || !/^jobs:/.test(lines[t])) return null;
|
|
6250
|
-
const hi = indentOf(lines[h]);
|
|
6251
|
-
let e = i + 1;
|
|
6252
|
-
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > hi)) e++;
|
|
6253
|
-
return [h, e];
|
|
6283
|
+
function grown(before, after) {
|
|
6284
|
+
const was = new Set(before.map(corePattern));
|
|
6285
|
+
return after.filter((e) => !was.has(corePattern(e)));
|
|
6254
6286
|
}
|
|
6255
|
-
function
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
}
|
|
6264
|
-
return out3;
|
|
6287
|
+
function tsconfigFacts(path, text) {
|
|
6288
|
+
if (text === null) return null;
|
|
6289
|
+
const cfg = parseJsonc(path, text);
|
|
6290
|
+
if (!isRecord(cfg)) return null;
|
|
6291
|
+
return {
|
|
6292
|
+
options: isRecord(cfg.compilerOptions) ? cfg.compilerOptions : {},
|
|
6293
|
+
exclude: strings(cfg.exclude),
|
|
6294
|
+
extends: strings(cfg.extends)
|
|
6295
|
+
};
|
|
6265
6296
|
}
|
|
6266
|
-
function
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6297
|
+
function effective(f, flag) {
|
|
6298
|
+
if (!f) return false;
|
|
6299
|
+
const v = f.options[flag];
|
|
6300
|
+
if (typeof v === "boolean") return v;
|
|
6301
|
+
return STRICT_FAMILY.has(flag) && f.options.strict === true;
|
|
6302
|
+
}
|
|
6303
|
+
function tsconfigWeakenings(c, all, policy, addedPaths, ctx, out3) {
|
|
6304
|
+
if (c.before === null || c.after === null) return;
|
|
6305
|
+
const b = tsconfigFacts(c.path, c.before);
|
|
6306
|
+
const a = tsconfigFacts(c.path, c.after);
|
|
6307
|
+
if (!b || !a) return;
|
|
6308
|
+
const others = all.filter((o) => o !== c && o.after !== null && isTsconfig(o.path)).map((o) => ({ b: tsconfigFacts(o.path, o.before), a: tsconfigFacts(o.path, o.after) }));
|
|
6309
|
+
const movedTo = (flag) => others.find((o) => effective(o.a, flag) && !effective(o.b, flag));
|
|
6310
|
+
const push = (message, evidence, remediation) => out3.push(makeFinding(RULE6, policy, { file: c.path, message, evidence, remediation, defaultSeverity: "warn" }));
|
|
6311
|
+
const strictLowered = effective(b, "strict") && !effective(a, "strict");
|
|
6312
|
+
for (const flag of TRUE_IS_STRICT) {
|
|
6313
|
+
if (!effective(b, flag) || effective(a, flag)) continue;
|
|
6314
|
+
if (flag !== "strict" && STRICT_FAMILY.has(flag) && strictLowered && a.options[flag] === void 0) continue;
|
|
6315
|
+
if (movedTo(flag)) continue;
|
|
6316
|
+
const was = typeof b.options[flag] === "boolean" ? String(b.options[flag]) : "implied by strict";
|
|
6317
|
+
const now = a.options[flag] === void 0 ? "removed" : String(a.options[flag]);
|
|
6318
|
+
push(
|
|
6319
|
+
`tsconfig strictness lowered: compilerOptions.${flag} ${was} \u2192 ${now}.`,
|
|
6320
|
+
`${c.path}: ${flag} ${was} \u2192 ${now}`,
|
|
6321
|
+
`Keep ${flag} on and fix the code it reports. Lowering the compiler's strictness hides the errors it was finding, in every file at once.`
|
|
6322
|
+
);
|
|
6283
6323
|
}
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
function parseTriggers(src) {
|
|
6292
|
-
const t = {
|
|
6293
|
-
events: /* @__PURE__ */ new Set(),
|
|
6294
|
-
branches: /* @__PURE__ */ new Map(),
|
|
6295
|
-
branchesIgnore: /* @__PURE__ */ new Map(),
|
|
6296
|
-
pathsIgnore: /* @__PURE__ */ new Map(),
|
|
6297
|
-
paths: /* @__PURE__ */ new Map(),
|
|
6298
|
-
types: /* @__PURE__ */ new Map(),
|
|
6299
|
-
tags: /* @__PURE__ */ new Map(),
|
|
6300
|
-
present: false
|
|
6301
|
-
};
|
|
6302
|
-
const lines = src.split("\n");
|
|
6303
|
-
const i = lines.findIndex((l) => /^(?:on|"on"|'on'|true):/.test(l));
|
|
6304
|
-
if (i < 0) return t;
|
|
6305
|
-
t.present = true;
|
|
6306
|
-
const head = lines[i].replace(/^(?:on|"on"|'on'|true):\s*/, "");
|
|
6307
|
-
const inline = uncommented(head);
|
|
6308
|
-
if (inline) {
|
|
6309
|
-
const list3 = flowList(inline);
|
|
6310
|
-
for (const e of list3 ?? [inline]) t.events.add(e);
|
|
6311
|
-
return t;
|
|
6324
|
+
for (const flag of TRUE_IS_WEAK) {
|
|
6325
|
+
if (b.options[flag] === true || a.options[flag] !== true) continue;
|
|
6326
|
+
push(
|
|
6327
|
+
`tsconfig check loosened: compilerOptions.${flag}: true added${flag === "skipLibCheck" ? " (declaration files are no longer type-checked)" : ""}.`,
|
|
6328
|
+
`${c.path}: ${flag}: true`,
|
|
6329
|
+
`Leave ${flag} off and fix what the compiler reports; a check switched off here is off for every build.`
|
|
6330
|
+
);
|
|
6312
6331
|
}
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
const item = body.match(/^\s*-\s+(.+)$/);
|
|
6323
|
-
if (item && listKey && event && ind > listInd) {
|
|
6324
|
-
const target = t[listKey].get(event) ?? [];
|
|
6325
|
-
target.push(item[1].replace(/^(['"])(.*)\1$/, "$2"));
|
|
6326
|
-
t[listKey].set(event, target);
|
|
6327
|
-
continue;
|
|
6328
|
-
}
|
|
6329
|
-
const kv = body.match(/^\s*([\w-]+):\s*(.*)$/);
|
|
6330
|
-
if (!kv) continue;
|
|
6331
|
-
if (ind === 2 || event === null && ind < 4) {
|
|
6332
|
-
event = kv[1];
|
|
6333
|
-
t.events.add(event);
|
|
6334
|
-
listKey = null;
|
|
6335
|
-
continue;
|
|
6336
|
-
}
|
|
6337
|
-
const key2 = LIST_KEYS[kv[1]];
|
|
6338
|
-
if (event && key2) {
|
|
6339
|
-
listKey = key2;
|
|
6340
|
-
listInd = ind;
|
|
6341
|
-
const list3 = flowList(kv[2]);
|
|
6342
|
-
t[listKey].set(event, list3 ?? []);
|
|
6343
|
-
} else listKey = null;
|
|
6332
|
+
const dir = dirOf2(c.path);
|
|
6333
|
+
for (const e of grown(b.exclude, a.exclude)) {
|
|
6334
|
+
const hit = hidesSource(e, dir, ctx);
|
|
6335
|
+
if (hit === null) continue;
|
|
6336
|
+
push(
|
|
6337
|
+
`tsconfig exclude grown: ${JSON.stringify(e)} now hides ${hit === e ? "source" : hit} from the compiler.`,
|
|
6338
|
+
`${c.path}: exclude += ${JSON.stringify(e)}`,
|
|
6339
|
+
"Keep the compiler pointed at the whole source. A file excluded from tsconfig is not type-checked anywhere."
|
|
6340
|
+
);
|
|
6344
6341
|
}
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
return (p) => r.test(p);
|
|
6352
|
-
} catch {
|
|
6353
|
-
return () => false;
|
|
6342
|
+
if (b.extends.length && a.extends.length === 0) {
|
|
6343
|
+
push(
|
|
6344
|
+
`tsconfig no longer extends ${b.extends.map((x) => JSON.stringify(x)).join(", ")}: the strictness the base carried is gone with it.`,
|
|
6345
|
+
`${c.path}: extends removed`,
|
|
6346
|
+
"Keep the base config, or carry its compiler options over explicitly so the check stays as strict as it was."
|
|
6347
|
+
);
|
|
6354
6348
|
}
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6349
|
+
for (const target of a.extends.filter((t) => !b.extends.includes(t) && isLocalTarget(t))) {
|
|
6350
|
+
const cands = targetCandidates(c.path, target);
|
|
6351
|
+
const added = cands.find((f) => addedPaths.has(f));
|
|
6352
|
+
if (added === void 0 && cands.some((f) => isProtected(f, policy, "config"))) continue;
|
|
6353
|
+
push(
|
|
6354
|
+
`tsconfig now extends ${JSON.stringify(target)}, ${added !== void 0 ? `a file this change adds (${added})` : "a file no protected glob covers"}.`,
|
|
6355
|
+
`${c.path}: extends ${JSON.stringify(target)}`,
|
|
6356
|
+
"Extend a protected config, or protect the base (protected.config in .tamperward.yml); the strictness now lives where the gate cannot read it."
|
|
6357
|
+
);
|
|
6361
6358
|
}
|
|
6362
|
-
return on;
|
|
6363
6359
|
}
|
|
6364
|
-
function
|
|
6365
|
-
const
|
|
6366
|
-
if (
|
|
6367
|
-
|
|
6368
|
-
return
|
|
6360
|
+
function levelOf(v) {
|
|
6361
|
+
const head = Array.isArray(v) ? v[0] : v;
|
|
6362
|
+
if (head === "off" || head === 0) return "off";
|
|
6363
|
+
if (head === "warn" || head === "error" || head === 1 || head === 2) return "on";
|
|
6364
|
+
return null;
|
|
6369
6365
|
}
|
|
6370
|
-
function
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
const candidates = opts.defaultBranch ? [opts.defaultBranch] : ["main", "master"];
|
|
6375
|
-
const namesDefault = (l) => candidates.some((b) => admits(l, b));
|
|
6376
|
-
for (const e of CODE_EVENTS) {
|
|
6377
|
-
if (before.events.has(e) && !after.events.has(e)) out3.push(`the ${e} trigger was removed \u2014 the workflow no longer runs on it`);
|
|
6366
|
+
function eslintObjectFacts(v, f, key2 = null) {
|
|
6367
|
+
if (Array.isArray(v)) {
|
|
6368
|
+
for (const el of v) eslintObjectFacts(el, f, null);
|
|
6369
|
+
return;
|
|
6378
6370
|
}
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6371
|
+
if (!isRecord(v)) return;
|
|
6372
|
+
if (key2 === "rules") {
|
|
6373
|
+
for (const [rule, level] of Object.entries(v)) {
|
|
6374
|
+
const l = levelOf(level);
|
|
6375
|
+
if (l === "off") f.off.set(rule, `${rule}: ${JSON.stringify(level)}`);
|
|
6376
|
+
else if (l === "on") f.on.add(rule);
|
|
6384
6377
|
}
|
|
6385
|
-
|
|
6386
|
-
if (had.length && ignoresEverySource(had, opts.sources)) continue;
|
|
6387
|
-
if (ignoresEverySource(list3, opts.sources)) out3.push(`on.${e}.paths-ignore now ignores every source file (now [${list3.join(", ")}]) \u2014 the workflow never runs on code`);
|
|
6388
|
-
}
|
|
6389
|
-
for (const [e, list3] of after.paths) {
|
|
6390
|
-
if (!CODE_EVENTS.includes(e) || !after.events.has(e)) continue;
|
|
6391
|
-
const had = before.paths.get(e);
|
|
6392
|
-
if (had && !pathsMatchSource(had, opts.sources)) continue;
|
|
6393
|
-
if (!pathsMatchSource(list3, opts.sources)) out3.push(`on.${e}.paths matches no source file (now [${list3.join(", ")}]) \u2014 the workflow never runs on code`);
|
|
6378
|
+
return;
|
|
6394
6379
|
}
|
|
6395
|
-
for (const [
|
|
6396
|
-
if (
|
|
6397
|
-
|
|
6398
|
-
if (had && !had.some((t) => PR_CODE_TYPES.test(t))) continue;
|
|
6399
|
-
if (!list3.some((t) => PR_CODE_TYPES.test(t))) out3.push(`on.${e}.types no longer includes opened/synchronize (now [${list3.join(", ")}]) \u2014 the workflow does not run on the pull request's code`);
|
|
6380
|
+
for (const [k, val] of Object.entries(v)) {
|
|
6381
|
+
if (k === "ignores" || k === "ignorePatterns") f.ignores.push(...strings(val));
|
|
6382
|
+
else eslintObjectFacts(val, f, k);
|
|
6400
6383
|
}
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6384
|
+
}
|
|
6385
|
+
function eslintAstFacts(text, f) {
|
|
6386
|
+
let sf = null;
|
|
6387
|
+
try {
|
|
6388
|
+
sf = parseSource("eslint.config.ts", text);
|
|
6389
|
+
} catch {
|
|
6390
|
+
return;
|
|
6404
6391
|
}
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
if (
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6392
|
+
if (!sf) return;
|
|
6393
|
+
const isStr2 = (e) => ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e);
|
|
6394
|
+
const keyText = (n) => ts.isIdentifier(n) || isStr2(n) || ts.isNumericLiteral(n) ? n.text : null;
|
|
6395
|
+
const literal2 = (e) => {
|
|
6396
|
+
if (isStr2(e)) return e.text;
|
|
6397
|
+
if (ts.isNumericLiteral(e)) return Number(e.text);
|
|
6398
|
+
if (ts.isArrayLiteralExpression(e)) return e.elements.map(literal2);
|
|
6399
|
+
return void 0;
|
|
6400
|
+
};
|
|
6401
|
+
const arrayStrings = (e) => ts.isArrayLiteralExpression(e) ? e.elements.filter(isStr2).map((s) => s.text) : isStr2(e) ? [e.text] : [];
|
|
6402
|
+
const visit = (node) => {
|
|
6403
|
+
if (ts.isPropertyAssignment(node)) {
|
|
6404
|
+
const k = keyText(node.name);
|
|
6405
|
+
if (k === "rules" && ts.isObjectLiteralExpression(node.initializer)) {
|
|
6406
|
+
for (const p of node.initializer.properties) {
|
|
6407
|
+
if (!ts.isPropertyAssignment(p)) continue;
|
|
6408
|
+
const rule = keyText(p.name);
|
|
6409
|
+
if (rule === null) continue;
|
|
6410
|
+
const l = levelOf(literal2(p.initializer));
|
|
6411
|
+
if (l === "off") f.off.set(rule, `${rule}: ${p.initializer.getText().replace(/\s+/g, " ").slice(0, 60)}`);
|
|
6412
|
+
else if (l === "on") f.on.add(rule);
|
|
6413
|
+
}
|
|
6414
|
+
} else if (k === "ignores" || k === "ignorePatterns") f.ignores.push(...arrayStrings(node.initializer));
|
|
6415
|
+
} else if (ts.isCallExpression(node) && /^(?:\w+\.)?globalIgnores$/.test(node.expression.getText()) && node.arguments[0]) {
|
|
6416
|
+
f.ignores.push(...arrayStrings(node.arguments[0]));
|
|
6414
6417
|
}
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
const replaced = !namesDefault(was) && !namesDefault(now) && was.length > 0 && !was.some((b) => now.includes(b));
|
|
6419
|
-
if (!lostDefault && !replaced) continue;
|
|
6420
|
-
const dropped = was.filter((b) => !now.includes(b) && !ALL(b));
|
|
6421
|
-
out3.push(
|
|
6422
|
-
dropped.length ? `on.${e}.branches no longer names ${dropped.join(", ")} (now [${now.join(", ")}])` : `on.${e}.branches now filters to [${now.join(", ")}], which does not admit the default branch`
|
|
6423
|
-
);
|
|
6424
|
-
}
|
|
6425
|
-
return out3;
|
|
6426
|
-
}
|
|
6427
|
-
function localUses(line) {
|
|
6428
|
-
const wf = line.match(REUSED_WORKFLOW)?.[1];
|
|
6429
|
-
if (wf) return [wf];
|
|
6430
|
-
const action = line.match(LOCAL_ACTION)?.[1];
|
|
6431
|
-
return action ? [`${action}/action.yml`, `${action}/action.yaml`] : [];
|
|
6432
|
-
}
|
|
6433
|
-
function literalValue(raw) {
|
|
6434
|
-
const v = foldExpressions(uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim());
|
|
6435
|
-
return v.includes(EXPR_TOKEN) || v.includes(MATRIX_TOKEN) ? null : v;
|
|
6418
|
+
ts.forEachChild(node, visit);
|
|
6419
|
+
};
|
|
6420
|
+
visit(sf);
|
|
6436
6421
|
}
|
|
6437
|
-
function
|
|
6438
|
-
const
|
|
6439
|
-
if (
|
|
6440
|
-
const
|
|
6441
|
-
if (
|
|
6442
|
-
|
|
6422
|
+
function eslintFacts(path, text) {
|
|
6423
|
+
const f = emptyFacts();
|
|
6424
|
+
if (text === null) return f;
|
|
6425
|
+
const base = basename2(path);
|
|
6426
|
+
if (isEslintIgnore(path)) {
|
|
6427
|
+
f.ignores.push(...text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("!")));
|
|
6428
|
+
return f;
|
|
6429
|
+
}
|
|
6430
|
+
if (/\.(?:[cm]?[jt]s)$/.test(base)) eslintAstFacts(text, f);
|
|
6431
|
+
else if (/\.ya?ml$/.test(base)) eslintObjectFacts(parseYaml(text), f);
|
|
6432
|
+
else if (base === "package.json") {
|
|
6433
|
+
const pkg = parseJsonc(path, text);
|
|
6434
|
+
if (isRecord(pkg)) eslintObjectFacts(pkg.eslintConfig, f);
|
|
6435
|
+
} else {
|
|
6436
|
+
const j = parseJsonc(path, text);
|
|
6437
|
+
eslintObjectFacts(j !== null ? j : base === ".eslintrc" ? parseYaml(text) : null, f);
|
|
6438
|
+
}
|
|
6439
|
+
return f;
|
|
6440
|
+
}
|
|
6441
|
+
function biomeFacts(path, text) {
|
|
6442
|
+
const f = emptyFacts();
|
|
6443
|
+
if (text === null) return f;
|
|
6444
|
+
const cfg = parseJsonc(path, text);
|
|
6445
|
+
if (!isRecord(cfg)) return f;
|
|
6446
|
+
const readLinter = (linter) => {
|
|
6447
|
+
if (!isRecord(linter)) return;
|
|
6448
|
+
if (linter.enabled === false) f.linterOff = true;
|
|
6449
|
+
f.ignores.push(...strings(linter.ignore));
|
|
6450
|
+
const rules = linter.rules;
|
|
6451
|
+
if (!isRecord(rules)) return;
|
|
6452
|
+
if (rules.recommended === false) f.off.set("recommended", "rules.recommended: false");
|
|
6453
|
+
for (const [group, body] of Object.entries(rules)) {
|
|
6454
|
+
if (!isRecord(body)) continue;
|
|
6455
|
+
if (body.recommended === false) f.off.set(`${group}/recommended`, `${group}.recommended: false`);
|
|
6456
|
+
for (const [rule, level] of Object.entries(body)) {
|
|
6457
|
+
const l = level === "off" || isRecord(level) && level.level === "off" ? "off" : typeof level === "string" || isRecord(level) ? "on" : null;
|
|
6458
|
+
if (l === "off") f.off.set(`${group}/${rule}`, `${group}.${rule}: off`);
|
|
6459
|
+
else if (l === "on") f.on.add(`${group}/${rule}`);
|
|
6460
|
+
}
|
|
6461
|
+
}
|
|
6462
|
+
};
|
|
6463
|
+
readLinter(cfg.linter);
|
|
6464
|
+
if (Array.isArray(cfg.overrides)) {
|
|
6465
|
+
for (const o of cfg.overrides) if (isRecord(o)) readLinter(o.linter);
|
|
6466
|
+
}
|
|
6467
|
+
if (isRecord(cfg.files)) {
|
|
6468
|
+
f.ignores.push(...strings(cfg.files.ignore));
|
|
6469
|
+
f.ignores.push(...strings(cfg.files.includes).filter((p) => p.startsWith("!")));
|
|
6470
|
+
}
|
|
6471
|
+
return f;
|
|
6443
6472
|
}
|
|
6444
|
-
function
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
return d !== null && /^\s*defaults:\s*$/.test(lines[d]) ? d : null;
|
|
6473
|
+
function replacedByVariant(rule, on) {
|
|
6474
|
+
if (rule.includes("/")) return false;
|
|
6475
|
+
for (const r of on) if (r.endsWith(`/${rule}`)) return true;
|
|
6476
|
+
return false;
|
|
6449
6477
|
}
|
|
6450
|
-
function
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6478
|
+
function lintWeakenings(c, facts, priorAll, policy, ctx, out3) {
|
|
6479
|
+
if (c.after === null) return;
|
|
6480
|
+
const a = facts(c.path, c.after);
|
|
6481
|
+
const tool = isBiome(c.path) ? "biome" : "eslint";
|
|
6482
|
+
const push = (message, evidence, remediation) => out3.push(makeFinding(RULE6, policy, { file: c.path, message, evidence, remediation, defaultSeverity: "warn" }));
|
|
6483
|
+
if (a.linterOff && !priorAll.linterOff) {
|
|
6484
|
+
push(`${tool} linter switched off: linter.enabled: false.`, `${c.path}: linter.enabled: false`, "Keep the linter on and fix what it reports.");
|
|
6485
|
+
}
|
|
6486
|
+
for (const [rule, how] of a.off) {
|
|
6487
|
+
if (priorAll.off.has(rule) || replacedByVariant(rule, a.on)) continue;
|
|
6488
|
+
push(
|
|
6489
|
+
`${tool} rule turned off: ${rule}.`,
|
|
6490
|
+
`${c.path}: ${how}`,
|
|
6491
|
+
`Keep ${rule} on and fix what it reports, or scope a justified exception with a reason. A rule turned off in the config is off for every file at once.`
|
|
6492
|
+
);
|
|
6493
|
+
}
|
|
6494
|
+
const dir = dirOf2(c.path);
|
|
6495
|
+
for (const e of grown(priorAll.ignores, a.ignores)) {
|
|
6496
|
+
const hit = hidesSource(e, dir, ctx);
|
|
6497
|
+
if (hit === null) continue;
|
|
6498
|
+
const key2 = isEslintIgnore(c.path) ? ".eslintignore" : tool === "biome" ? "files.ignore" : /\.eslintrc/.test(basename2(c.path)) ? "ignorePatterns" : "ignores";
|
|
6499
|
+
push(
|
|
6500
|
+
`${tool} ${key2} grown: ${JSON.stringify(e)} now hides ${hit === e ? "source" : hit} from the lint.`,
|
|
6501
|
+
`${c.path}: ${key2} += ${JSON.stringify(e)}`,
|
|
6502
|
+
"Keep the lint pointed at the whole source. A path ignored here is never linted, whatever the rules say."
|
|
6503
|
+
);
|
|
6458
6504
|
}
|
|
6459
|
-
return null;
|
|
6460
|
-
}
|
|
6461
|
-
function ancestorOf(lines, i) {
|
|
6462
|
-
const ind = indentOf(lines[i]);
|
|
6463
|
-
let h = i - 1;
|
|
6464
|
-
while (h >= 0 && (lines[h].trim() === "" || /^\s*#/.test(lines[h]) || indentOf(lines[h]) >= ind)) h--;
|
|
6465
|
-
return h >= 0 ? h : null;
|
|
6466
6505
|
}
|
|
6467
|
-
var
|
|
6468
|
-
var
|
|
6469
|
-
"src/detectors/
|
|
6506
|
+
var RULE6, basename2, isTsconfig, isEslintConfig, isEslintIgnore, isBiome, CODE_FILE, HOUSEKEEPING_DIR, HOUSEKEEPING_FILE, matcher, dirOf2, strings, STRICT_FAMILY, TRUE_IS_STRICT, TRUE_IS_WEAK, isLocalTarget, emptyFacts, configWeakening;
|
|
6507
|
+
var init_config_weakening = __esm({
|
|
6508
|
+
"src/detectors/config-weakening.ts"() {
|
|
6470
6509
|
"use strict";
|
|
6471
|
-
|
|
6510
|
+
init_lazy_deps();
|
|
6511
|
+
init_ts_lazy();
|
|
6472
6512
|
init_policy();
|
|
6513
|
+
init_narrow();
|
|
6473
6514
|
init_finding();
|
|
6474
|
-
init_files();
|
|
6475
|
-
init_hook_wiring();
|
|
6476
6515
|
init_repo();
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
jobHasCheck = (lines, range) => lines.slice(range[0], range[1]).some((l) => invokesCheck2(l) || usesCheck(l) || /^\s*uses:\s*\S/.test(l));
|
|
6497
|
-
NEUTRALISERS = [
|
|
6498
|
-
{ re: /^\s*set\s+\+e\b/, what: "set +e \u2014 a failing check no longer fails the step" },
|
|
6499
|
-
{ re: /^\s*exit\s+0\s*(?:$|[;#])/, what: "exit 0 \u2014 the step ends green whatever the check did" },
|
|
6500
|
-
{ re: /^\s*if\s+false\s*;?\s*then\b/, what: "if false; then \u2014 the check is never run" },
|
|
6501
|
-
{ re: /^\s*:\s*<<\s*['"]?\w+/, what: "a heredoc fed to `:` \u2014 the check is commented out" }
|
|
6502
|
-
];
|
|
6503
|
-
LIST_KEYS = {
|
|
6504
|
-
branches: "branches",
|
|
6505
|
-
"branches-ignore": "branchesIgnore",
|
|
6506
|
-
"paths-ignore": "pathsIgnore",
|
|
6507
|
-
paths: "paths",
|
|
6508
|
-
types: "types",
|
|
6509
|
-
tags: "tags"
|
|
6516
|
+
init_suite_config();
|
|
6517
|
+
RULE6 = "config-weakening";
|
|
6518
|
+
basename2 = (path) => path.split("/").pop() ?? path;
|
|
6519
|
+
isTsconfig = (path) => /^tsconfig.*\.json$/.test(basename2(path));
|
|
6520
|
+
isEslintConfig = (path) => /^(?:\.eslintrc(?:\..*)?|eslint\.config\..*)$/.test(basename2(path));
|
|
6521
|
+
isEslintIgnore = (path) => basename2(path) === ".eslintignore";
|
|
6522
|
+
isBiome = (path) => /^biome\.jsonc?$/.test(basename2(path));
|
|
6523
|
+
CODE_FILE = /\.(?:[cm]?[jt]sx?|vue|svelte)$/;
|
|
6524
|
+
HOUSEKEEPING_DIR = /^(?:node_modules|dist|build|out|output|coverage|lib|esm|cjs|umd|target|tmp|temp|vendor|generated|__generated__|public|static|storybook-static|\..+)$/;
|
|
6525
|
+
HOUSEKEEPING_FILE = /(?:\.(?:min\.[cm]?js|d\.[cm]?ts|snap|map|json|jsonc|md|mdx|lock|ya?ml|toml|html?|css|scss|svg|png|jpe?g|gif|txt)|(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|CHANGELOG\.md))$/i;
|
|
6526
|
+
matcher = (patterns) => {
|
|
6527
|
+
const ms = patterns.map((g) => {
|
|
6528
|
+
try {
|
|
6529
|
+
return picomatch(g, { dot: true });
|
|
6530
|
+
} catch {
|
|
6531
|
+
return () => false;
|
|
6532
|
+
}
|
|
6533
|
+
});
|
|
6534
|
+
return (path) => ms.some((m) => m(path));
|
|
6510
6535
|
};
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6536
|
+
dirOf2 = (path) => path.includes("/") ? path.slice(0, path.lastIndexOf("/") + 1) : "";
|
|
6537
|
+
strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : typeof v === "string" ? [v] : [];
|
|
6538
|
+
STRICT_FAMILY = /* @__PURE__ */ new Set([
|
|
6539
|
+
"noImplicitAny",
|
|
6540
|
+
"strictNullChecks",
|
|
6541
|
+
"strictFunctionTypes",
|
|
6542
|
+
"strictBindCallApply",
|
|
6543
|
+
"strictPropertyInitialization",
|
|
6544
|
+
"noImplicitThis",
|
|
6545
|
+
"alwaysStrict",
|
|
6546
|
+
"useUnknownInCatchVariables",
|
|
6547
|
+
"strictBuiltinIteratorReturn"
|
|
6548
|
+
]);
|
|
6549
|
+
TRUE_IS_STRICT = [
|
|
6550
|
+
"strict",
|
|
6551
|
+
...STRICT_FAMILY,
|
|
6552
|
+
"noUnusedLocals",
|
|
6553
|
+
"noUnusedParameters",
|
|
6554
|
+
"noImplicitReturns",
|
|
6555
|
+
"noFallthroughCasesInSwitch",
|
|
6556
|
+
"noUncheckedIndexedAccess",
|
|
6557
|
+
"noImplicitOverride",
|
|
6558
|
+
"noPropertyAccessFromIndexSignature",
|
|
6559
|
+
"exactOptionalPropertyTypes",
|
|
6560
|
+
"checkJs",
|
|
6561
|
+
"forceConsistentCasingInFileNames"
|
|
6562
|
+
];
|
|
6563
|
+
TRUE_IS_WEAK = ["skipLibCheck", "allowUnreachableCode", "allowUnusedLabels", "suppressImplicitAnyIndexErrors", "suppressExcessPropertyErrors", "noStrictGenericChecks"];
|
|
6564
|
+
isLocalTarget = (s) => /^(?:\.\.?\/|\/)/.test(s);
|
|
6565
|
+
emptyFacts = () => ({ off: /* @__PURE__ */ new Map(), on: /* @__PURE__ */ new Set(), ignores: [], linterOff: false });
|
|
6566
|
+
configWeakening = {
|
|
6567
|
+
id: RULE6,
|
|
6526
6568
|
surface: ["file"],
|
|
6527
6569
|
certainty: "mechanical",
|
|
6528
6570
|
run(changes, policy, _view, ctx) {
|
|
6529
6571
|
const out3 = [];
|
|
6530
|
-
const
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
);
|
|
6544
|
-
continue;
|
|
6545
|
-
}
|
|
6546
|
-
if (!isProtected(c.path, policy, "ci")) continue;
|
|
6547
|
-
const afterLines = c.after != null ? c.after.split("\n") : null;
|
|
6548
|
-
const addedAt = new Set(addedLines(c).map((l) => l.newLine).filter((n) => n != null));
|
|
6549
|
-
const github = isGithubFile(c.path);
|
|
6550
|
-
for (const l of github ? addedLines(c) : []) {
|
|
6551
|
-
const coe = l.content.match(/^\s*-?\s*continue-on-error:\s*(.+?)\s*$/);
|
|
6552
|
-
const cond = l.content.match(/^\s*-?\s*if:\s*(.+?)\s*$/);
|
|
6553
|
-
if (!coe && !cond) continue;
|
|
6554
|
-
const step = afterLines && l.newLine != null ? stepOf(afterLines, l.newLine - 1) : null;
|
|
6555
|
-
if (afterLines && step && !stepHasCheck(afterLines, step)) continue;
|
|
6556
|
-
if (afterLines && !step && l.newLine != null) {
|
|
6557
|
-
const job = jobOf(afterLines, l.newLine - 1);
|
|
6558
|
-
if (job && !jobHasCheck(afterLines, job)) continue;
|
|
6559
|
-
}
|
|
6560
|
-
if (coe && isTruthy(coe[1])) {
|
|
6561
|
-
out3.push(
|
|
6562
|
-
makeFinding(RULE7, policy, {
|
|
6563
|
-
file: c.path,
|
|
6564
|
-
line: l.newLine ?? void 0,
|
|
6565
|
-
message: "continue-on-error: true added \u2014 failures will no longer fail the job.",
|
|
6566
|
-
evidence: l.content.trim(),
|
|
6567
|
-
remediation: "Remove it. A check that cannot fail is not a check."
|
|
6568
|
-
})
|
|
6569
|
-
);
|
|
6570
|
-
} else if (cond && isAlwaysFalse(cond[1])) {
|
|
6571
|
-
out3.push(
|
|
6572
|
-
makeFinding(RULE7, policy, {
|
|
6573
|
-
file: c.path,
|
|
6574
|
-
line: l.newLine ?? void 0,
|
|
6575
|
-
message: `if: ${uncommented(cond[1])} added \u2014 this step can never run.`,
|
|
6576
|
-
evidence: l.content.trim(),
|
|
6577
|
-
remediation: "Re-enable the step instead of conditioning it off."
|
|
6578
|
-
})
|
|
6579
|
-
);
|
|
6580
|
-
}
|
|
6581
|
-
}
|
|
6582
|
-
if (afterLines && github) {
|
|
6583
|
-
for (const b of runBlocks(afterLines)) {
|
|
6584
|
-
if (!b.body.some(invokesCheck2)) continue;
|
|
6585
|
-
const propagates = b.body.some((l) => /\bexit\s+"?\$/.test(l));
|
|
6586
|
-
for (let k = 0; k < b.body.length; k++) {
|
|
6587
|
-
const idx = b.start + 1 + k;
|
|
6588
|
-
if (!addedAt.has(idx + 1)) continue;
|
|
6589
|
-
const n = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
|
|
6590
|
-
if (!n || propagates && /set\s\+e/.test(b.body[k])) continue;
|
|
6591
|
-
out3.push(
|
|
6592
|
-
makeFinding(RULE7, policy, {
|
|
6593
|
-
file: c.path,
|
|
6594
|
-
line: idx + 1,
|
|
6595
|
-
message: `A check's run block was neutralised: ${n.what}.`,
|
|
6596
|
-
evidence: b.body[k].trim(),
|
|
6597
|
-
remediation: "Let the check decide the step. Masking its exit status is the same tamper as removing it."
|
|
6598
|
-
})
|
|
6599
|
-
);
|
|
6600
|
-
}
|
|
6601
|
-
}
|
|
6602
|
-
for (const l of addedLines(c)) {
|
|
6603
|
-
if (!/^\s*-?\s*shell:\s*.*\{0\}/.test(l.content) || l.newLine == null) continue;
|
|
6604
|
-
if (SHELL_ERREXIT.test(uncommented(l.content.replace(/^\s*-?\s*shell:\s*/, "")))) continue;
|
|
6605
|
-
const step = stepOf(afterLines, l.newLine - 1);
|
|
6606
|
-
if (step && !stepHasCheck(afterLines, step)) continue;
|
|
6607
|
-
out3.push(
|
|
6608
|
-
makeFinding(RULE7, policy, {
|
|
6609
|
-
file: c.path,
|
|
6610
|
-
line: l.newLine,
|
|
6611
|
-
message: "shell: \u2026 {0} added on a check step \u2014 the shell no longer stops on the first failure.",
|
|
6612
|
-
evidence: l.content.trim(),
|
|
6613
|
-
remediation: "Keep the default shell (fail-fast) on steps that run checks."
|
|
6614
|
-
})
|
|
6615
|
-
);
|
|
6616
|
-
}
|
|
6617
|
-
for (const l of addedLines(c)) {
|
|
6618
|
-
if (l.newLine == null) continue;
|
|
6619
|
-
const i = l.newLine - 1;
|
|
6620
|
-
const wd = l.content.match(WORKDIR_LINE);
|
|
6621
|
-
if (wd) {
|
|
6622
|
-
const dir = literalValue(wd[1]);
|
|
6623
|
-
if (dir === null || !dirRunsNothing(dir, ctx)) continue;
|
|
6624
|
-
const step2 = stepOf(afterLines, i);
|
|
6625
|
-
if (step2) {
|
|
6626
|
-
if (!stepHasCheck(afterLines, step2)) continue;
|
|
6627
|
-
} else {
|
|
6628
|
-
const d = defaultsOf(afterLines, i);
|
|
6629
|
-
if (d === null) continue;
|
|
6630
|
-
const job = jobOf(afterLines, d);
|
|
6631
|
-
if (job ? !jobHasCheck(afterLines, job) : !afterLines.some((x) => invokesCheck2(x) || usesCheck(x))) continue;
|
|
6632
|
-
}
|
|
6633
|
-
out3.push(
|
|
6634
|
-
makeFinding(RULE7, policy, {
|
|
6635
|
-
file: c.path,
|
|
6636
|
-
line: l.newLine,
|
|
6637
|
-
message: `A check was pointed at ${dir} by working-directory \u2014 a directory holding no code or package the check could run.`,
|
|
6638
|
-
evidence: l.content.trim(),
|
|
6639
|
-
remediation: "Run the check where the code is. A suite pointed at an empty directory passes on nothing."
|
|
6640
|
-
})
|
|
6641
|
-
);
|
|
6642
|
-
continue;
|
|
6643
|
-
}
|
|
6644
|
-
const ref = l.content.match(REF_LINE);
|
|
6645
|
-
if (!ref) continue;
|
|
6646
|
-
const step = enclosingStep(afterLines, i);
|
|
6647
|
-
if (!step || !afterLines.slice(step[0], step[1]).some((x) => CHECKOUT.test(x))) continue;
|
|
6648
|
-
const target = literalValue(ref[1]);
|
|
6649
|
-
if (target === null) continue;
|
|
6650
|
-
out3.push(
|
|
6651
|
-
makeFinding(RULE7, policy, {
|
|
6652
|
-
file: c.path,
|
|
6653
|
-
line: l.newLine,
|
|
6654
|
-
message: `actions/checkout was pinned to ref: ${target} \u2014 the checks run against that ref, not against the change under review.`,
|
|
6655
|
-
evidence: l.content.trim(),
|
|
6656
|
-
remediation: "Let checkout take the event's ref (its default). A suite that checks out main proves nothing about the pull request."
|
|
6657
|
-
})
|
|
6658
|
-
);
|
|
6659
|
-
}
|
|
6660
|
-
if (c.before != null && c.after != null && !isCompositeAction(c.path)) {
|
|
6661
|
-
triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
|
|
6662
|
-
for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
|
|
6663
|
-
out3.push(
|
|
6664
|
-
makeFinding(RULE7, policy, {
|
|
6665
|
-
file: c.path,
|
|
6666
|
-
message: `The workflow's triggers were narrowed: ${reason}.`,
|
|
6667
|
-
evidence: reason,
|
|
6668
|
-
remediation: "Keep the workflow running on push and pull_request for the default branch and its source paths; a check that never runs is no check."
|
|
6669
|
-
})
|
|
6670
|
-
);
|
|
6671
|
-
}
|
|
6672
|
-
}
|
|
6673
|
-
}
|
|
6674
|
-
const afterCommands = afterLines ? commandLines(afterLines) : null;
|
|
6675
|
-
const afterCores = (afterCommands ?? addedLines(c).map((l) => l.content)).map(commandCore);
|
|
6676
|
-
const addedCores = addedLines(c).map((l) => commandCore(afterCommands && l.newLine != null ? afterCommands[l.newLine - 1] : l.content));
|
|
6677
|
-
for (const l of afterLines ?? []) {
|
|
6678
|
-
for (const reused of localUses(l)) {
|
|
6679
|
-
const content = afterByPath.get(reused);
|
|
6680
|
-
if (content == null || reused === c.path) continue;
|
|
6681
|
-
const cores = commandLines(content.split("\n")).map(commandCore);
|
|
6682
|
-
afterCores.push(...cores);
|
|
6683
|
-
addedCores.push(...cores);
|
|
6684
|
-
}
|
|
6685
|
-
}
|
|
6686
|
-
if (!github && c.after == null) {
|
|
6687
|
-
for (const o of changes) {
|
|
6688
|
-
if (o.kind !== "file" || o.op !== "add" || o.after == null || !isProtected(o.path, policy, "ci")) continue;
|
|
6689
|
-
const cores = commandLines(o.after.split("\n")).map(commandCore);
|
|
6690
|
-
afterCores.push(...cores);
|
|
6691
|
-
addedCores.push(...cores);
|
|
6692
|
-
}
|
|
6572
|
+
const files = changes.filter((c) => c.kind === "file");
|
|
6573
|
+
const addedPaths = new Set(files.filter((c) => c.op === "add").map((c) => c.path));
|
|
6574
|
+
const tsconfigs = files.filter((c) => isTsconfig(c.path) && isProtected(c.path, policy, "config"));
|
|
6575
|
+
const eslints = files.filter((c) => (isEslintConfig(c.path) || isEslintIgnore(c.path)) && isProtected(c.path, policy, "config"));
|
|
6576
|
+
const biomes = files.filter((c) => isBiome(c.path) && isProtected(c.path, policy, "config"));
|
|
6577
|
+
const priorOf = (list3, facts) => {
|
|
6578
|
+
const all = emptyFacts();
|
|
6579
|
+
for (const c of list3) {
|
|
6580
|
+
const f = facts(c.path, c.before);
|
|
6581
|
+
for (const [k, v] of f.off) all.off.set(k, v);
|
|
6582
|
+
for (const k of f.on) all.on.add(k);
|
|
6583
|
+
all.ignores.push(...f.ignores);
|
|
6584
|
+
if (f.linterOff) all.linterOff = true;
|
|
6693
6585
|
}
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
})
|
|
6710
|
-
);
|
|
6711
|
-
continue;
|
|
6712
|
-
}
|
|
6586
|
+
return all;
|
|
6587
|
+
};
|
|
6588
|
+
const eslintPrior = priorOf(eslints, eslintFacts);
|
|
6589
|
+
const biomePrior = priorOf(biomes, biomeFacts);
|
|
6590
|
+
for (const c of tsconfigs) tsconfigWeakenings(c, tsconfigs, policy, addedPaths, ctx, out3);
|
|
6591
|
+
for (const c of eslints) lintWeakenings(c, eslintFacts, eslintPrior, policy, ctx, out3);
|
|
6592
|
+
for (const c of biomes) lintWeakenings(c, biomeFacts, biomePrior, policy, ctx, out3);
|
|
6593
|
+
for (const c of files) {
|
|
6594
|
+
if (c.after === null || !isProtected(c.path, policy, "config")) continue;
|
|
6595
|
+
const rn = runnerOf(c.path, c.after) ?? runnerOf(c.path, c.before);
|
|
6596
|
+
if (rn !== "jest" && rn !== "vitest") continue;
|
|
6597
|
+
for (const e of newSetupEntries(c.before, c.after, c.path)) {
|
|
6598
|
+
const cands = targetCandidates(c.path, e.target);
|
|
6599
|
+
const added = cands.find((f) => addedPaths.has(f));
|
|
6600
|
+
if (added === void 0 && cands.some((f) => isProtected(f, policy))) continue;
|
|
6713
6601
|
out3.push(
|
|
6714
|
-
makeFinding(
|
|
6602
|
+
makeFinding(RULE6, policy, {
|
|
6715
6603
|
file: c.path,
|
|
6716
|
-
message:
|
|
6717
|
-
evidence:
|
|
6718
|
-
remediation: "
|
|
6604
|
+
message: `The runner now loads ${e.target} through ${e.key}, ${added !== void 0 ? `a file this change adds (${added})` : "a file no protected glob covers"}: it runs with the suite's globals in reach.`,
|
|
6605
|
+
evidence: `${c.path}: ${e.key} += ${JSON.stringify(e.target)}`,
|
|
6606
|
+
remediation: "Review the module the runner will execute around every test, and protect it (protected.config in .tamperward.yml) so later edits to it are gated too.",
|
|
6607
|
+
defaultSeverity: "warn"
|
|
6719
6608
|
})
|
|
6720
6609
|
);
|
|
6721
6610
|
}
|
|
@@ -6726,444 +6615,1130 @@ var init_ci_tampering = __esm({
|
|
|
6726
6615
|
}
|
|
6727
6616
|
});
|
|
6728
6617
|
|
|
6729
|
-
// src/detectors/
|
|
6730
|
-
function
|
|
6731
|
-
const
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6618
|
+
// src/detectors/coverage-exclusion.ts
|
|
6619
|
+
function isText(line, idx, lang) {
|
|
6620
|
+
const opener = LINE_COMMENT[lang];
|
|
6621
|
+
let quote = null;
|
|
6622
|
+
for (let j = 0; j < idx; j++) {
|
|
6623
|
+
const ch = line[j];
|
|
6624
|
+
if (quote) {
|
|
6625
|
+
if (ch === "\\") j++;
|
|
6626
|
+
else if (ch === quote) quote = null;
|
|
6627
|
+
continue;
|
|
6628
|
+
}
|
|
6629
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
6630
|
+
else if (opener && opener.test(line.slice(j, j + 2))) return true;
|
|
6738
6631
|
}
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6632
|
+
return quote !== null;
|
|
6633
|
+
}
|
|
6634
|
+
var RULE7, PATTERNS3, OUT_OF_SCOPE_PATH, LINE_COMMENT, coverageExclusion;
|
|
6635
|
+
var init_coverage_exclusion = __esm({
|
|
6636
|
+
"src/detectors/coverage-exclusion.ts"() {
|
|
6637
|
+
"use strict";
|
|
6638
|
+
init_select();
|
|
6639
|
+
init_files();
|
|
6640
|
+
init_policy();
|
|
6641
|
+
init_finding();
|
|
6642
|
+
RULE7 = "coverage-exclusion";
|
|
6643
|
+
PATTERNS3 = {
|
|
6644
|
+
js: [
|
|
6645
|
+
{ re: /\/\*\s*istanbul\s+ignore\b/, why: "`istanbul ignore` takes the next statement, branch or the whole file out of the coverage measurement" },
|
|
6646
|
+
{ re: /\/\*\s*c8\s+ignore\b(?!\s*stop\b)/, why: "`c8 ignore` takes the next statement or the marked range out of the coverage measurement" },
|
|
6647
|
+
{ re: /\/\*\s*v8\s+ignore\b(?!\s*stop\b)/, why: "`v8 ignore` takes the next statement or the marked range out of the coverage measurement" },
|
|
6648
|
+
{ re: /\/\*\s*node:coverage\s+(?:ignore|disable)\b/, why: "`node:coverage ignore/disable` takes the following code out of the coverage measurement" }
|
|
6649
|
+
],
|
|
6650
|
+
py: [{ re: /#\s*pragma[:\s]\s*no\s*cover\b/i, why: "`# pragma: no cover` takes the line or block out of the coverage measurement" }],
|
|
6651
|
+
rs: [{ re: /#\s*!?\[[^\]]*\bcoverage\s*\(\s*off\s*\)/, why: "`#[coverage(off)]` takes the item out of the coverage measurement" }],
|
|
6652
|
+
go: [
|
|
6653
|
+
{
|
|
6654
|
+
re: /^\s*\/\/\s*(?:go:build\b|\+build\b)/,
|
|
6655
|
+
why: "a build constraint added to an existing file takes it out of the default build, and out of `go test -cover`",
|
|
6656
|
+
existingOnly: true
|
|
6657
|
+
}
|
|
6658
|
+
]
|
|
6659
|
+
};
|
|
6660
|
+
OUT_OF_SCOPE_PATH = /(?:^|\/)(?:node_modules|vendor|third_party|dist|build|out|coverage|generated|__generated__|codegen|examples?|docs?|scripts?|fixtures?|__fixtures__|__mocks__)\/|\.d\.[cm]?ts$|\.(?:generated|gen|pb)\.[^/]*$/;
|
|
6661
|
+
LINE_COMMENT = { js: /^\/\//, go: /^\/\//, rs: /^\/\//, py: /^#/ };
|
|
6662
|
+
coverageExclusion = {
|
|
6663
|
+
id: RULE7,
|
|
6664
|
+
surface: ["file"],
|
|
6665
|
+
certainty: "mechanical",
|
|
6666
|
+
run(changes, policy) {
|
|
6667
|
+
const out3 = [];
|
|
6668
|
+
for (const c of changes) {
|
|
6669
|
+
if (c.kind !== "file" || c.op === "delete") continue;
|
|
6670
|
+
const lang = langOf(c.path);
|
|
6671
|
+
if (!lang) continue;
|
|
6672
|
+
const patterns = PATTERNS3[lang];
|
|
6673
|
+
if (!patterns) continue;
|
|
6674
|
+
const category = protectedCategory(c.path, policy);
|
|
6675
|
+
if (OUT_OF_SCOPE_PATH.test(c.path) || category === "tests" || category === "config") continue;
|
|
6676
|
+
const removed = new Set(removedLines(c).map((l) => l.content.trim()));
|
|
6677
|
+
const before = c.before ?? "";
|
|
6678
|
+
for (const l of addedLines(c)) {
|
|
6679
|
+
const trimmed = l.content.trim();
|
|
6680
|
+
if (removed.has(trimmed)) continue;
|
|
6681
|
+
for (const p of patterns) {
|
|
6682
|
+
const m = p.re.exec(l.content);
|
|
6683
|
+
if (!m || isText(l.content, m.index, lang)) continue;
|
|
6684
|
+
if (p.existingOnly && (c.op === "add" || c.before == null || before.split("\n").some((b) => p.re.test(b)))) continue;
|
|
6685
|
+
out3.push(
|
|
6686
|
+
makeFinding(RULE7, policy, {
|
|
6687
|
+
file: c.path,
|
|
6688
|
+
line: l.newLine ?? void 0,
|
|
6689
|
+
message: `Coverage exclusion added: ${p.why}.`,
|
|
6690
|
+
evidence: trimmed,
|
|
6691
|
+
remediation: "Cover the code with a test instead of excluding it from the measurement; if it is genuinely unreachable, say so in review.",
|
|
6692
|
+
defaultSeverity: "warn"
|
|
6693
|
+
})
|
|
6694
|
+
);
|
|
6695
|
+
break;
|
|
6696
|
+
}
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
return out3;
|
|
6700
|
+
}
|
|
6701
|
+
};
|
|
6743
6702
|
}
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6703
|
+
});
|
|
6704
|
+
|
|
6705
|
+
// src/detectors/gh-expression.ts
|
|
6706
|
+
function foldExpressions(s) {
|
|
6707
|
+
return s.replace(/\$\{\{([^}]*)\}\}/g, (_m, e) => {
|
|
6708
|
+
const v = foldConst(e.trim());
|
|
6709
|
+
if (v !== void 0 && v !== TRUTHY) return String(v);
|
|
6710
|
+
return /\b(?:matrix|strategy|inputs)\./.test(e) ? MATRIX_TOKEN : EXPR_TOKEN;
|
|
6711
|
+
});
|
|
6712
|
+
}
|
|
6713
|
+
function lex(src) {
|
|
6714
|
+
const out3 = [];
|
|
6715
|
+
let i = 0;
|
|
6716
|
+
while (i < src.length) {
|
|
6717
|
+
const ch = src[i];
|
|
6718
|
+
if (/\s/.test(ch)) {
|
|
6719
|
+
i++;
|
|
6720
|
+
continue;
|
|
6721
|
+
}
|
|
6722
|
+
if (ch === "'") {
|
|
6723
|
+
let j = i + 1;
|
|
6724
|
+
let s = "";
|
|
6725
|
+
while (j < src.length) {
|
|
6726
|
+
if (src[j] === "'" && src[j + 1] === "'") {
|
|
6727
|
+
s += "'";
|
|
6728
|
+
j += 2;
|
|
6729
|
+
} else if (src[j] === "'") break;
|
|
6730
|
+
else s += src[j++];
|
|
6731
|
+
}
|
|
6732
|
+
if (j >= src.length) return null;
|
|
6733
|
+
out3.push({ t: "str", v: s });
|
|
6734
|
+
i = j + 1;
|
|
6735
|
+
continue;
|
|
6736
|
+
}
|
|
6737
|
+
const num = src.slice(i).match(/^-?\d+(?:\.\d+)?/);
|
|
6738
|
+
if (num) {
|
|
6739
|
+
out3.push({ t: "num", v: num[0] });
|
|
6740
|
+
i += num[0].length;
|
|
6741
|
+
continue;
|
|
6742
|
+
}
|
|
6743
|
+
const op = src.slice(i).match(/^(?:==|!=|&&|\|\||<=|>=|[!()<>,[\]])/);
|
|
6744
|
+
if (op) {
|
|
6745
|
+
out3.push({ t: "op", v: op[0] });
|
|
6746
|
+
i += op[0].length;
|
|
6747
|
+
continue;
|
|
6748
|
+
}
|
|
6749
|
+
const id = src.slice(i).match(/^[A-Za-z_][\w.\-*]*/);
|
|
6750
|
+
if (id) {
|
|
6751
|
+
out3.push({ t: "id", v: id[0] });
|
|
6752
|
+
i += id[0].length;
|
|
6753
|
+
continue;
|
|
6754
|
+
}
|
|
6755
|
+
return null;
|
|
6756
|
+
}
|
|
6757
|
+
return out3;
|
|
6758
|
+
}
|
|
6759
|
+
function foldConst(src) {
|
|
6760
|
+
const toks = lex(src);
|
|
6761
|
+
if (!toks) return void 0;
|
|
6762
|
+
let p = 0;
|
|
6763
|
+
const peek = () => toks[p];
|
|
6764
|
+
const eat = (v2) => toks[p]?.t === "op" && toks[p].v === v2 ? (p++, true) : false;
|
|
6765
|
+
const num = (v2) => v2 === TRUTHY ? NaN : typeof v2 === "number" ? v2 : typeof v2 === "boolean" ? v2 ? 1 : 0 : v2 === null ? 0 : v2.trim() === "" ? 0 : Number(v2);
|
|
6766
|
+
const eq = (a, b) => {
|
|
6767
|
+
if (a === TRUTHY || b === TRUTHY) return void 0;
|
|
6768
|
+
if (typeof a === "string" && typeof b === "string") return a.toLowerCase() === b.toLowerCase();
|
|
6769
|
+
if (a === null && b === null) return true;
|
|
6770
|
+
const x = num(a);
|
|
6771
|
+
const y2 = num(b);
|
|
6772
|
+
return !Number.isNaN(x) && x === y2;
|
|
6773
|
+
};
|
|
6774
|
+
const or = () => {
|
|
6775
|
+
let l = and();
|
|
6776
|
+
while (eat("||")) {
|
|
6777
|
+
const r = and();
|
|
6778
|
+
if (l !== void 0 && truthy(l)) continue;
|
|
6779
|
+
if (l !== void 0) l = r;
|
|
6780
|
+
else l = r !== void 0 && truthy(r) ? TRUTHY : void 0;
|
|
6781
|
+
}
|
|
6782
|
+
return l;
|
|
6783
|
+
};
|
|
6784
|
+
const and = () => {
|
|
6785
|
+
let l = cmp();
|
|
6786
|
+
while (eat("&&")) {
|
|
6787
|
+
const r = cmp();
|
|
6788
|
+
if (l !== void 0 && !truthy(l)) continue;
|
|
6789
|
+
if (l !== void 0) l = r;
|
|
6790
|
+
else l = r !== void 0 && !truthy(r) ? false : void 0;
|
|
6791
|
+
}
|
|
6792
|
+
return l;
|
|
6793
|
+
};
|
|
6794
|
+
const cmp = () => {
|
|
6795
|
+
let l = unary();
|
|
6796
|
+
for (; ; ) {
|
|
6797
|
+
const t = peek();
|
|
6798
|
+
if (!t || t.t !== "op" || !/^(?:==|!=|<|>|<=|>=)$/.test(t.v)) return l;
|
|
6799
|
+
p++;
|
|
6800
|
+
const r = unary();
|
|
6801
|
+
if (l === void 0 || r === void 0) {
|
|
6802
|
+
l = void 0;
|
|
6803
|
+
continue;
|
|
6804
|
+
}
|
|
6805
|
+
if (t.v === "==" || t.v === "!=") {
|
|
6806
|
+
const e = eq(l, r);
|
|
6807
|
+
l = e === void 0 ? void 0 : t.v === "==" ? e : !e;
|
|
6808
|
+
} else {
|
|
6809
|
+
const x = num(l);
|
|
6810
|
+
const y2 = num(r);
|
|
6811
|
+
if (Number.isNaN(x) || Number.isNaN(y2)) l = void 0;
|
|
6812
|
+
else l = t.v === "<" ? x < y2 : t.v === ">" ? x > y2 : t.v === "<=" ? x <= y2 : x >= y2;
|
|
6813
|
+
}
|
|
6814
|
+
}
|
|
6815
|
+
};
|
|
6816
|
+
const unary = () => {
|
|
6817
|
+
if (eat("!")) {
|
|
6818
|
+
const v2 = unary();
|
|
6819
|
+
return v2 === void 0 ? void 0 : !truthy(v2);
|
|
6820
|
+
}
|
|
6821
|
+
return primary();
|
|
6822
|
+
};
|
|
6823
|
+
const primary = () => {
|
|
6824
|
+
const t = peek();
|
|
6825
|
+
if (!t) return void 0;
|
|
6826
|
+
if (eat("(")) {
|
|
6827
|
+
const v2 = or();
|
|
6828
|
+
return eat(")") ? v2 : void 0;
|
|
6829
|
+
}
|
|
6830
|
+
p++;
|
|
6831
|
+
if (t.t === "str") return t.v;
|
|
6832
|
+
if (t.t === "num") return Number(t.v);
|
|
6833
|
+
if (t.t === "id") {
|
|
6834
|
+
if (/^true$/i.test(t.v)) return true;
|
|
6835
|
+
if (/^false$/i.test(t.v)) return false;
|
|
6836
|
+
if (/^null$/i.test(t.v)) return null;
|
|
6837
|
+
if (eat("(")) {
|
|
6838
|
+
const args = [];
|
|
6839
|
+
if (!eat(")")) {
|
|
6840
|
+
for (; ; ) {
|
|
6841
|
+
args.push(or());
|
|
6842
|
+
if (eat(")")) break;
|
|
6843
|
+
if (!eat(",")) return void 0;
|
|
6844
|
+
}
|
|
6845
|
+
}
|
|
6846
|
+
let indexed = false;
|
|
6847
|
+
while (eat("[")) {
|
|
6848
|
+
or();
|
|
6849
|
+
indexed = true;
|
|
6850
|
+
if (!eat("]")) return void 0;
|
|
6851
|
+
}
|
|
6852
|
+
if (indexed) return void 0;
|
|
6853
|
+
return callConst(t.v, args);
|
|
6854
|
+
}
|
|
6855
|
+
while (eat("[")) {
|
|
6856
|
+
or();
|
|
6857
|
+
if (!eat("]")) return void 0;
|
|
6858
|
+
}
|
|
6859
|
+
return void 0;
|
|
6860
|
+
}
|
|
6861
|
+
return void 0;
|
|
6862
|
+
};
|
|
6863
|
+
const v = or();
|
|
6864
|
+
return p === toks.length ? v : void 0;
|
|
6865
|
+
}
|
|
6866
|
+
function callConst(name, args) {
|
|
6867
|
+
const vals = [];
|
|
6868
|
+
for (const a of args) {
|
|
6869
|
+
if (a === void 0 || a === TRUTHY) return void 0;
|
|
6870
|
+
vals.push(a);
|
|
6871
|
+
}
|
|
6872
|
+
const s = (i) => vals[i] == null ? "" : String(vals[i]);
|
|
6873
|
+
switch (name.toLowerCase()) {
|
|
6874
|
+
case "contains":
|
|
6875
|
+
return vals.length === 2 ? s(0).toLowerCase().includes(s(1).toLowerCase()) : void 0;
|
|
6876
|
+
case "startswith":
|
|
6877
|
+
return vals.length === 2 ? s(0).toLowerCase().startsWith(s(1).toLowerCase()) : void 0;
|
|
6878
|
+
case "endswith":
|
|
6879
|
+
return vals.length === 2 ? s(0).toLowerCase().endsWith(s(1).toLowerCase()) : void 0;
|
|
6880
|
+
case "format": {
|
|
6881
|
+
if (vals.length < 1) return void 0;
|
|
6882
|
+
return s(0).replace(/\{\{|\}\}|\{(\d+)\}/g, (m, i) => m === "{{" ? "{" : m === "}}" ? "}" : s(Number(i) + 1));
|
|
6883
|
+
}
|
|
6884
|
+
case "join":
|
|
6885
|
+
return vals.length >= 1 && vals.length <= 2 ? s(0) : void 0;
|
|
6886
|
+
case "tojson":
|
|
6887
|
+
return vals.length === 1 ? JSON.stringify(vals[0]) : void 0;
|
|
6888
|
+
case "fromjson": {
|
|
6889
|
+
if (vals.length !== 1) return void 0;
|
|
6890
|
+
try {
|
|
6891
|
+
const v = JSON.parse(s(0));
|
|
6892
|
+
if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
|
|
6893
|
+
return TRUTHY;
|
|
6894
|
+
} catch {
|
|
6895
|
+
return void 0;
|
|
6896
|
+
}
|
|
6897
|
+
}
|
|
6898
|
+
default:
|
|
6899
|
+
return void 0;
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
var EXPR_TOKEN, MATRIX_TOKEN, TRUTHY, truthy;
|
|
6903
|
+
var init_gh_expression = __esm({
|
|
6904
|
+
"src/detectors/gh-expression.ts"() {
|
|
6905
|
+
"use strict";
|
|
6906
|
+
EXPR_TOKEN = "__expr__";
|
|
6907
|
+
MATRIX_TOKEN = "__matrix__";
|
|
6908
|
+
TRUTHY = /* @__PURE__ */ Symbol("truthy");
|
|
6909
|
+
truthy = (v) => v === TRUTHY ? true : typeof v === "string" ? v !== "" : typeof v === "number" ? v !== 0 : v === true;
|
|
6910
|
+
}
|
|
6911
|
+
});
|
|
6912
|
+
|
|
6913
|
+
// src/detectors/invocation.ts
|
|
6914
|
+
function canonical(core) {
|
|
6915
|
+
let s = foldExpressions(core.replace(/^(['"])(.*)\1$/, "$2").trim()).replace(MATRIX_FLAG, " ").replace(/\s+/g, " ").trim();
|
|
6916
|
+
s = s.replace(/^(?:cd|pushd)\s+\S+\s*&&\s*/, "");
|
|
6917
|
+
s = s.replace(/^(?:npx|pnpm\s+(?:exec|dlx)|yarn\s+(?:exec|dlx)|bunx|bun\s+x)\s+/, "exec ");
|
|
6918
|
+
s = s.replace(/^(?:npm|pnpm|yarn|bun)\s+(?:run(?:-script)?\s+)?/, "exec ");
|
|
6919
|
+
s = s.replace(/^exec\s+(?:t|tst)(?=\s|$)/, "exec test");
|
|
6920
|
+
s = s.replace(/^exec\s+/, "");
|
|
6921
|
+
return s.replace(/\s+--\s*$/, "").trim();
|
|
6922
|
+
}
|
|
6923
|
+
function runnerNarrowed(cmd, args) {
|
|
6924
|
+
return runnerNarrowing(cmd, args) !== null;
|
|
6925
|
+
}
|
|
6926
|
+
function runnerNarrowing(cmd, args) {
|
|
6927
|
+
const t = RUNNER_NARROWINGS.find((x) => x.runner.test(cmd));
|
|
6928
|
+
const m = t ? (" " + args.trim()).match(t.flags) : null;
|
|
6929
|
+
return m ? m[0].trim().replace(/[=\s].*$/, "") : null;
|
|
6930
|
+
}
|
|
6931
|
+
function coverageFloorLowered(removed, added) {
|
|
6932
|
+
const was = removed.match(COVERAGE_FLOOR);
|
|
6933
|
+
const now = added.match(COVERAGE_FLOOR);
|
|
6934
|
+
return was !== null && now !== null && Number(now[1]) < Number(was[1]);
|
|
6935
|
+
}
|
|
6936
|
+
function inCommandPosition(core) {
|
|
6937
|
+
return canonical(core).split(/\s*(?:&&|\|\||;|\|)\s*/).some((seg) => CHECK_HEAD.test(canonical(seg.replace(COMMAND_PREFIX, ""))));
|
|
6938
|
+
}
|
|
6939
|
+
function narrowedByPositional(args) {
|
|
6940
|
+
return positionalOf(args) !== null;
|
|
6941
|
+
}
|
|
6942
|
+
function positionalOf(args) {
|
|
6943
|
+
const toks = args.trim().split(/\s+/).filter(Boolean);
|
|
6944
|
+
for (let i = 0; i < toks.length; i++) {
|
|
6945
|
+
const t = toks[i];
|
|
6946
|
+
if (t === "--" || t.startsWith("-")) continue;
|
|
6947
|
+
const prev = toks[i - 1];
|
|
6948
|
+
if (prev && (prev.startsWith("-") && prev !== "--" && !prev.includes("=") || /[<>]/.test(prev))) continue;
|
|
6949
|
+
if (/[<>|;&]/.test(t)) continue;
|
|
6950
|
+
if (PATH_SHAPED.test(t)) return t;
|
|
6951
|
+
}
|
|
6952
|
+
return null;
|
|
6953
|
+
}
|
|
6954
|
+
function checkKind(core) {
|
|
6955
|
+
if (!invokesCheck(core)) return null;
|
|
6956
|
+
const s = canonical(core);
|
|
6957
|
+
if (/\btamperward\b/.test(s)) return "gate";
|
|
6958
|
+
if (/\b(?:lint|eslint|biome|oxlint|golangci-lint|ruff)\b/i.test(s)) return "lint";
|
|
6959
|
+
if (/\b(?:typecheck|type-check|tsc|tsgo|mypy)\b|\bdeno\s+check\b/i.test(s)) return "types";
|
|
6960
|
+
return "test";
|
|
6961
|
+
}
|
|
6962
|
+
function survives(removedCore, isUses, afterCores, addedCores) {
|
|
6963
|
+
if (isUses) {
|
|
6964
|
+
const ref = usesRef(removedCore);
|
|
6965
|
+
return afterCores.some((a) => usesRef(a) === ref) ? { state: "kept" } : { state: "gone" };
|
|
6966
|
+
}
|
|
6967
|
+
const r = canonical(removedCore);
|
|
6968
|
+
const kind = checkKind(removedCore);
|
|
6969
|
+
const narrowed = (args) => kind === "test" && narrowedByPositional(args);
|
|
6970
|
+
let neutralised;
|
|
6971
|
+
for (const raw of afterCores) {
|
|
6972
|
+
const a = canonical(raw);
|
|
6973
|
+
if (a === r) return { state: "kept" };
|
|
6974
|
+
if (a.startsWith(r + " ")) {
|
|
6975
|
+
const rest = a.slice(r.length);
|
|
6976
|
+
if (NEUTRALISING_SUFFIX.test(rest) || narrowed(rest) || runnerNarrowed(a, rest)) neutralised ??= raw;
|
|
6977
|
+
else return { state: "kept" };
|
|
6978
|
+
} else if (r.startsWith(a + " ") && invokesCheck(a)) {
|
|
6979
|
+
return { state: "kept" };
|
|
6980
|
+
} else {
|
|
6981
|
+
const w = a.match(TIMEOUT_WRAP);
|
|
6982
|
+
if (w && (canonical(w[1]) === r || canonical(w[1]).startsWith(r + " "))) neutralised ??= raw;
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6985
|
+
const maskOf = (args) => (" " + args).match(NEUTRALISING_SUFFIX)?.[0]?.trim() ?? null;
|
|
6986
|
+
const priorMask = r.includes(" ") ? maskOf(r.slice(r.indexOf(" ") + 1)) : null;
|
|
6987
|
+
let respelled = false;
|
|
6988
|
+
for (const raw of addedCores) {
|
|
6989
|
+
if (!kind || checkKind(raw) !== kind) continue;
|
|
6990
|
+
const a = canonical(raw);
|
|
6991
|
+
if (a.match(TIMEOUT_WRAP)) {
|
|
6992
|
+
neutralised ??= raw;
|
|
6993
|
+
continue;
|
|
6994
|
+
}
|
|
6995
|
+
if (!inCommandPosition(raw)) continue;
|
|
6996
|
+
if (coverageFloorLowered(r, a)) {
|
|
6997
|
+
neutralised ??= raw;
|
|
6998
|
+
continue;
|
|
6999
|
+
}
|
|
7000
|
+
const args = a.slice(a.indexOf(" ") + 1 || a.length);
|
|
7001
|
+
const mask = maskOf(args);
|
|
7002
|
+
const newMask = mask !== null && mask !== priorMask;
|
|
7003
|
+
if (a.includes(" ") && (newMask || narrowed(args) || runnerNarrowed(a, args))) neutralised ??= raw;
|
|
7004
|
+
else respelled = true;
|
|
7005
|
+
}
|
|
7006
|
+
if (respelled) return { state: "kept" };
|
|
7007
|
+
return neutralised ? { state: "neutralised", by: neutralised } : { state: "gone" };
|
|
7008
|
+
}
|
|
7009
|
+
function unwrap(seg) {
|
|
7010
|
+
const s = seg.trim().replace(PYTHON_M, "").replace(NODE_BIN, "$1").replace(BIN_OF, "$1$2");
|
|
7011
|
+
if (invokesCheck(s) || PRINTER.test(s) || /^["']/.test(s)) return s;
|
|
7012
|
+
const toks = s.split(/\s+/);
|
|
7013
|
+
for (let i = 1; i < toks.length && i <= 16; i++) {
|
|
7014
|
+
const rest = toks.slice(i).join(" ");
|
|
7015
|
+
if (invokesCheck(rest)) return rest;
|
|
7016
|
+
}
|
|
7017
|
+
return s;
|
|
7018
|
+
}
|
|
7019
|
+
function scriptKind3(inner) {
|
|
7020
|
+
const kind = checkKind(inner);
|
|
7021
|
+
if (!kind) return null;
|
|
7022
|
+
const head = canonical(inner).match(/^(tests?|lint|type-?check|coverage)(?=[\s:]|$)/i)?.[1];
|
|
7023
|
+
if (!head) return kind;
|
|
7024
|
+
return /^type/i.test(head) ? "types" : /^lint/i.test(head) ? "lint" : "test";
|
|
7025
|
+
}
|
|
7026
|
+
function runnerOfSegment(canon) {
|
|
7027
|
+
const m = canon.match(RUNNER_NAME);
|
|
7028
|
+
if (!m) return null;
|
|
7029
|
+
const n = m[1];
|
|
7030
|
+
return n === "jest" || n === "vitest" || n === "mocha" || n === "pytest" || n === "go" || n === "cargo" ? n : "node";
|
|
7031
|
+
}
|
|
7032
|
+
function statements(raw) {
|
|
7033
|
+
const out3 = [];
|
|
7034
|
+
let single = false;
|
|
7035
|
+
let double = false;
|
|
7036
|
+
let last = 0;
|
|
7037
|
+
for (let i = 0; i < raw.length; i++) {
|
|
7038
|
+
const ch = raw[i];
|
|
7039
|
+
if (ch === "\\" && !single) {
|
|
7040
|
+
i++;
|
|
7041
|
+
continue;
|
|
7042
|
+
}
|
|
7043
|
+
if (ch === "'" && !double) {
|
|
7044
|
+
single = !single;
|
|
7045
|
+
continue;
|
|
7046
|
+
}
|
|
7047
|
+
if (ch === '"' && !single) {
|
|
7048
|
+
double = !double;
|
|
7049
|
+
continue;
|
|
7050
|
+
}
|
|
7051
|
+
if (single || double) continue;
|
|
7052
|
+
const prev = raw[i - 1];
|
|
7053
|
+
const next = raw[i + 1];
|
|
7054
|
+
let op = null;
|
|
7055
|
+
if (ch === "&" && next === "&") op = "&&";
|
|
7056
|
+
else if (ch === "|" && next === "|") op = "||";
|
|
7057
|
+
else if (ch === "|" && next === "&") op = "|&";
|
|
7058
|
+
else if (ch === ";" || ch === "\n") op = ";";
|
|
7059
|
+
else if (ch === "|" && prev !== ">") op = "|";
|
|
7060
|
+
else if (ch === "&" && prev !== ">" && next !== ">") op = "&";
|
|
7061
|
+
if (!op) continue;
|
|
7062
|
+
out3.push({ seg: raw.slice(last, i).trim(), op });
|
|
7063
|
+
i += op.length - 1;
|
|
7064
|
+
last = i + 1;
|
|
7065
|
+
}
|
|
7066
|
+
out3.push({ seg: raw.slice(last).trim(), op: null });
|
|
7067
|
+
return out3.filter((s) => s.seg !== "");
|
|
7068
|
+
}
|
|
7069
|
+
function descriptors(cmd, runner, opts) {
|
|
7070
|
+
const out3 = [];
|
|
7071
|
+
const stmts = statements(cmd);
|
|
7072
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
7073
|
+
const { seg, op } = stmts[i];
|
|
7074
|
+
const wrapped = seg.match(TIMEOUT_WRAP);
|
|
7075
|
+
const inner = unwrap(wrapped ? wrapped[1] : seg);
|
|
7076
|
+
if (!invokesCheck(inner)) continue;
|
|
7077
|
+
const kind = scriptKind3(inner);
|
|
7078
|
+
const canon = canonical(inner);
|
|
7079
|
+
const rn = runner ?? runnerOfSegment(canon);
|
|
7080
|
+
const args = canon.includes(" ") ? " " + canon.slice(canon.indexOf(" ") + 1) : " ";
|
|
7081
|
+
if (wrapped) out3.push({ key: "timeout", what: "a timeout wrapper", direction: "added" });
|
|
7082
|
+
const following = stmts[i + 1]?.seg ?? "";
|
|
7083
|
+
if (op === "||" || op === "|" || op === "&" || op === ";" && !invokesCheck(unwrap(following))) {
|
|
7084
|
+
out3.push({ key: `op:${op}`, what: `${op} ${following.split(/\s+/).slice(0, 2).join(" ")}`.trim(), direction: "added" });
|
|
7085
|
+
}
|
|
7086
|
+
for (const re of MASKING_FLAGS) {
|
|
7087
|
+
const f = spelled(re, args);
|
|
7088
|
+
if (f) out3.push({ key: `flag:${f}`, what: f, direction: "added" });
|
|
7089
|
+
}
|
|
7090
|
+
for (const re of GATE_FLAGS) {
|
|
7091
|
+
const f = spelled(re, " " + seg);
|
|
7092
|
+
if (f) out3.push({ key: `gate:${f}`, what: f, direction: "removed" });
|
|
7093
|
+
}
|
|
7094
|
+
if (kind !== "test" || opts.sliceByDesign || opts.extended?.has(kind)) continue;
|
|
7095
|
+
const flags = [SUITE_NARROWING_FLAGS, ...NARROWING_BY_RUNNER["*"], ...rn ? NARROWING_BY_RUNNER[rn] : []];
|
|
7096
|
+
for (const re of flags) {
|
|
7097
|
+
const f = spelled(re, args);
|
|
7098
|
+
if (f && !opts.excuseFlag?.(f, canon)) out3.push({ key: `flag:${f}`, what: f, direction: "added" });
|
|
7099
|
+
}
|
|
7100
|
+
if (rn === "cargo" && CARGO_SKIP.test(args)) out3.push({ key: "flag:-- --skip", what: "-- --skip", direction: "added" });
|
|
7101
|
+
const shared = runnerNarrowing(canon, args);
|
|
7102
|
+
if (shared && !opts.excuseFlag?.(shared, canon)) out3.push({ key: `flag:${shared}`, what: shared, direction: "added" });
|
|
7103
|
+
if (opts.fresh) continue;
|
|
7104
|
+
const cfg = args.match(CONFIG_FLAG);
|
|
7105
|
+
if (cfg && !opts.configReviewed?.(cfg[3])) out3.push({ key: `config:${cfg[3]}`, what: `${cfg[1]} ${cfg[3]}`, direction: "added" });
|
|
7106
|
+
const pos = positionalOf(args);
|
|
7107
|
+
if (pos) out3.push({ key: "positional", what: `a path positional (${pos})`, direction: "added" });
|
|
6747
7108
|
}
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
7109
|
+
return out3;
|
|
7110
|
+
}
|
|
7111
|
+
function checkStatements(cmd) {
|
|
7112
|
+
const out3 = [];
|
|
7113
|
+
for (const seg of segments(cmd)) {
|
|
7114
|
+
const w = seg.match(TIMEOUT_WRAP);
|
|
7115
|
+
const inner = unwrap(w ? w[1] : seg);
|
|
7116
|
+
const kind = scriptKind3(inner);
|
|
7117
|
+
if (kind) out3.push({ kind, canon: canonical(inner) });
|
|
6751
7118
|
}
|
|
6752
|
-
return
|
|
7119
|
+
return out3;
|
|
6753
7120
|
}
|
|
6754
|
-
function
|
|
6755
|
-
|
|
6756
|
-
return null;
|
|
7121
|
+
function checkKinds(cmd) {
|
|
7122
|
+
return new Set(checkStatements(cmd).map((s) => s.kind));
|
|
6757
7123
|
}
|
|
6758
|
-
function
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
7124
|
+
function failsLoudly(cmd) {
|
|
7125
|
+
const last = segments(cmd).pop();
|
|
7126
|
+
return last !== void 0 && FAILS_LOUDLY.test(last.trim());
|
|
7127
|
+
}
|
|
7128
|
+
function invocationWeakening(before, after, runner, opts = {}) {
|
|
7129
|
+
const was = checkKinds(before);
|
|
7130
|
+
const now = checkKinds(after);
|
|
7131
|
+
if (now.size === 0 && failsLoudly(after)) return { state: "kept" };
|
|
7132
|
+
for (const k of was) {
|
|
7133
|
+
if (now.has(k) || opts.relocated?.(k)) continue;
|
|
7134
|
+
const instead = [...now].filter((x) => !was.has(x));
|
|
7135
|
+
return { state: "removed", kind: k, what: instead.length ? `runs a ${instead.join("/")} check instead` : now.size ? "the check is gone" : "no check is run" };
|
|
6768
7136
|
}
|
|
6769
|
-
|
|
7137
|
+
const survived = new Set(checkStatements(before).map((s) => s.canon));
|
|
7138
|
+
const extended = new Set(checkStatements(after).filter((s) => survived.has(s.canon)).map((s) => s.kind));
|
|
7139
|
+
const read = { ...opts, fresh: was.size === 0, extended };
|
|
7140
|
+
const beforeDs = descriptors(before, runner, read);
|
|
7141
|
+
const afterDs = descriptors(after, runner, read);
|
|
7142
|
+
const seenBefore = new Set(beforeDs.map((d) => d.key));
|
|
7143
|
+
const added = afterDs.find((d) => d.direction === "added" && !seenBefore.has(d.key));
|
|
7144
|
+
if (added) return { state: "neutralised", what: added.what, direction: "added" };
|
|
7145
|
+
if (coverageFloorLowered(before, after)) return { state: "neutralised", what: "a lowered --cov-fail-under", direction: "added" };
|
|
7146
|
+
const seenAfter = new Set(afterDs.map((d) => d.key));
|
|
7147
|
+
const dropped = beforeDs.find((d) => d.direction === "removed" && !seenAfter.has(d.key));
|
|
7148
|
+
if (dropped) return { state: "neutralised", what: dropped.what, direction: "removed" };
|
|
7149
|
+
return { state: "kept" };
|
|
6770
7150
|
}
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
7151
|
+
var INVOKES_TOOL, INVOKES_SCRIPT, invokesCheck, MATRIX_FLAG, usesRef, SUITE_NARROWING_FLAGS, NEUTRALISING_SUFFIX, TIMEOUT_WRAP, RUNNER_NARROWINGS, COVERAGE_FLOOR, COMMAND_PREFIX, CHECK_HEAD, PATH_SHAPED, NARROWING_BY_RUNNER, CARGO_SKIP, MASKING_FLAGS, GATE_FLAGS, CONFIG_FLAG, PRINTER, PYTHON_M, NODE_BIN, BIN_OF, RUNNER_NAME, spelled, FAILS_LOUDLY;
|
|
7152
|
+
var init_invocation = __esm({
|
|
7153
|
+
"src/detectors/invocation.ts"() {
|
|
7154
|
+
"use strict";
|
|
7155
|
+
init_gh_expression();
|
|
7156
|
+
init_command();
|
|
7157
|
+
INVOKES_TOOL = /(?:^\s*|[;&|`]\s*|\$\(\s*|\b(?:npx|yarn|pnpm|bunx?)\s+)(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward|mocha|ava|oxlint|tsgo|mypy|golangci-lint|node\s+--test|biome\s+(?:ci|check|lint)|deno\s+(?:test|lint|check)|ruff\s+check)\b/;
|
|
7158
|
+
INVOKES_SCRIPT = /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|tests|lint|typecheck|type-check|coverage)\b|\b(?:make|cargo|go)\s+test\b|\bgradle\w*\s+(?:test|check)\b/;
|
|
7159
|
+
invokesCheck = (line) => INVOKES_TOOL.test(line) || INVOKES_SCRIPT.test(line);
|
|
7160
|
+
MATRIX_FLAG = new RegExp(`(?:^|\\s)-{1,2}[\\w-]+(?:=|\\s+)${MATRIX_TOKEN}(?:/\\S*)?(?=\\s|$)`, "g");
|
|
7161
|
+
usesRef = (core) => core.replace(/@.*$/, "");
|
|
7162
|
+
SUITE_NARROWING_FLAGS = /--testPathIgnorePatterns\b|--testPathPattern\b|--testNamePattern\b|(?:^|\s)-t\s|--onlyChanged\b|--changed\b|--findRelatedTests\b|--exclude\b|--dir\b|--project\b/;
|
|
7163
|
+
NEUTRALISING_SUFFIX = new RegExp(
|
|
7164
|
+
`\\|\\||;|(?<!&)&(?!&)|\\|(?!\\|)|--passWithNoTests\\b|--coverage=false\\b|--coverageThreshold\\b|${SUITE_NARROWING_FLAGS.source}`
|
|
7165
|
+
);
|
|
7166
|
+
TIMEOUT_WRAP = /^timeout\s+(?:-{1,2}\S+\s+)*\d\S*\s+(.+)$/;
|
|
7167
|
+
RUNNER_NARROWINGS = [
|
|
7168
|
+
{
|
|
7169
|
+
// a package-manager script: `test --prefix dir`, `-w pkg`, `--workspace=pkg`,
|
|
7170
|
+
// `--if-present` (no script, no failure), pnpm's `--filter` / `-F`, npm's `-C`
|
|
7171
|
+
runner: /^(?:test|tests|coverage)(?::\S+)?(?=\s|$)/,
|
|
7172
|
+
flags: /\s--if-present(?=\s|$)|\s--prefix(?:=|\s)|\s-C(?:=|\s)|\s-w(?:=|\s)|\s--workspace(?:=|\s)|\s--filter(?:=|\s)|\s-F(?:=|\s)/
|
|
7173
|
+
},
|
|
7174
|
+
{
|
|
7175
|
+
runner: /^(?:jest|vitest)\b/,
|
|
7176
|
+
flags: /\s--shard(?:=|\s)|\s--root(?:Dir)?(?:=|\s)|\s--testMatch(?:=|\s)|\s--testRegex(?:=|\s)|\s--modulePathIgnorePatterns(?:=|\s)|\s--selectProjects(?:=|\s)|\s--grep(?:=|\s)|\s-g(?:=|\s)/
|
|
7177
|
+
},
|
|
7178
|
+
{ runner: /^playwright\b/, flags: /\s--shard(?:=|\s)|\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--grep-invert(?:=|\s)/ },
|
|
7179
|
+
{ runner: /^mocha\b/, flags: /\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--fgrep(?:=|\s)|\s-f(?:=|\s)/ },
|
|
7180
|
+
{ runner: /^ava\b/, flags: /\s--match(?:=|\s)|\s-m(?:=|\s)/ },
|
|
7181
|
+
{ runner: /^node --test\b/, flags: /\s--test-name-pattern(?:=|\s)|\s--test-skip-pattern(?:=|\s)|\s--test-only(?=\s|$)/ },
|
|
7182
|
+
{
|
|
7183
|
+
// `-p no:<plugin>` disables collection when the plugin is the collector
|
|
7184
|
+
// (`python`, `doctest`, `cov`); the cache, warning and output plugins select nothing
|
|
7185
|
+
runner: /^pytest\b/,
|
|
7186
|
+
flags: /\s-k(?:=|\s)|\s-m(?:=|\s)|\s--deselect(?:=|\s)|\s--ignore(?:-glob)?(?:=|\s)|\s-p\s*no:(?!(?:cacheprovider|warnings|faulthandler|randomly|logging|legacypath|pastebin|stepwise|tmpdir|junitxml|terminal|terminalreporter)(?=\s|$))|\s--collect-only(?=\s|$)|\s--co(?=\s|$)/
|
|
7187
|
+
},
|
|
7188
|
+
{ runner: /^cargo test\b/, flags: /\s--\s(?:\S+\s+)*--skip(?:=|\s)|\s--exclude(?:=|\s)/ },
|
|
7189
|
+
{ runner: /^go test\b/, flags: /\s-run(?:=|\s)|\s-skip(?:=|\s)/ }
|
|
7190
|
+
];
|
|
7191
|
+
COVERAGE_FLOOR = /--cov-fail-under(?:=|\s+)(\d+(?:\.\d+)?)/;
|
|
7192
|
+
COMMAND_PREFIX = /^(?:(?:[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S*)|sudo|env|nice|time|exec|command|nohup|xvfb-run|dbus-launch|cross-env|dotenv|poetry\s+run|pipenv\s+run|uv\s+run|hatch\s+run|pdm\s+run|bundle\s+exec)\s+(?:-{1,2}\S+\s+)*)*/;
|
|
7193
|
+
CHECK_HEAD = /^(?:(?:test|tests|lint|typecheck|type-check|coverage)(?::\S+)?(?=\s|$)|(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward|mocha|ava|oxlint|tsgo|mypy|golangci-lint)\b|node --test\b|biome (?:ci|check|lint)\b|deno (?:test|lint|check)\b|ruff check\b|(?:make|cargo|go) test\b|gradle\w* (?:test|check)\b)/;
|
|
7194
|
+
PATH_SHAPED = /\/|\.(?:test|spec)\./;
|
|
7195
|
+
NARROWING_BY_RUNNER = {
|
|
7196
|
+
"*": [/--shard\b/],
|
|
7197
|
+
jest: [/--root\b/, /--rootDir\b/, /--testMatch\b/, /--testRegex\b/, /--modulePathIgnorePatterns\b/, /--selectProjects\b/, /--ignoreProjects\b/, /--filter\b/],
|
|
7198
|
+
vitest: [/--grep\b/],
|
|
7199
|
+
mocha: [/--grep\b/, /(?:^|\s)-g(?:\s|=)/, /--ignore\b/, /(?:^|\s)-f(?:\s|=)/, /--fgrep\b/],
|
|
7200
|
+
pytest: [/(?:^|\s)-k(?:\s|=)/, /(?:^|\s)-m(?:\s|=)/, /--deselect\b/, /--ignore\b/, /--ignore-glob\b/, /--last-failed\b/, /(?:^|\s)--lf(?:\s|$)/, /--collect-only\b/, /(?:^|\s)--co(?:\s|$)/],
|
|
7201
|
+
go: [/(?:^|\s)-run(?:\s|=)/, /(?:^|\s)-skip(?:\s|=)/],
|
|
7202
|
+
cargo: [],
|
|
7203
|
+
node: [/--test-name-pattern\b/, /--test-skip-pattern\b/]
|
|
7204
|
+
};
|
|
7205
|
+
CARGO_SKIP = /\s--\s+(?:\S+\s+)*--skip\b/;
|
|
7206
|
+
MASKING_FLAGS = [/--passWithNoTests\b/, /--coverage=false\b/, /--coverageThreshold\b/, /--if-present\b/];
|
|
7207
|
+
GATE_FLAGS = [/--check-coverage\b/];
|
|
7208
|
+
CONFIG_FLAG = /(?:^|\s)(--config|-c)(?:=|\s+)(['"]?)([^\s'"]+)\2(?=\s|$)/;
|
|
7209
|
+
PRINTER = /^(?:echo|printf|true|false|:|cat|exit|return|sleep|test|\[|node|git|curl|wget)(?=\s|$)/;
|
|
7210
|
+
PYTHON_M = /^python\d?(?:\.\d+)?\s+-m\s+(?=pytest\b)/;
|
|
7211
|
+
NODE_BIN = /^node(?:\s+-\S+)*\s+\S*node_modules\/(?:\.bin\/)?(jest|vitest|mocha|ava)\b\S*/;
|
|
7212
|
+
BIN_OF = /\$\((?:yarn|npm|pnpm)\s+bin\s+(\w+)\)|\$\((?:yarn|npm|pnpm)\s+bin\)\/(\w+)/g;
|
|
7213
|
+
RUNNER_NAME = /^(jest|vitest|mocha|pytest|go|cargo|node)\b/;
|
|
7214
|
+
spelled = (re, s) => {
|
|
7215
|
+
const m = s.match(re);
|
|
7216
|
+
return m ? m[0].trim().replace(/[=\s].*$/, "") : null;
|
|
7217
|
+
};
|
|
7218
|
+
FAILS_LOUDLY = /^(?:exit\s+(?:[1-9]\d*)|false)\s*$/;
|
|
7219
|
+
}
|
|
7220
|
+
});
|
|
7221
|
+
|
|
7222
|
+
// src/detectors/ci-tampering.ts
|
|
7223
|
+
function isActiveWorkflow(path) {
|
|
7224
|
+
return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(path);
|
|
6777
7225
|
}
|
|
6778
|
-
function
|
|
6779
|
-
|
|
6780
|
-
return t.startsWith("{") ? `(${t})` : src;
|
|
7226
|
+
function commandCore(line) {
|
|
7227
|
+
return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses|script|command|before_script|after_script|sh|bash|pwsh|powershell):\s*/, "").replace(/\s+/g, " ").trim();
|
|
6781
7228
|
}
|
|
6782
|
-
function
|
|
6783
|
-
const
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
if (/^vite\.config\./.test(base) && content != null && /(?:^|[\s,{])["']?test["']?\s*:/m.test(content)) return "vitest";
|
|
6787
|
-
if (base === "pytest.ini" || base === ".pytest.ini") return "pytest";
|
|
6788
|
-
if (content == null) return null;
|
|
6789
|
-
if (base === "tox.ini" && /^\s*\[pytest\]/m.test(content)) return "pytest";
|
|
6790
|
-
if (base === "setup.cfg" && /^\s*\[tool:pytest\]/m.test(content)) return "pytest";
|
|
6791
|
-
if (base === "pyproject.toml" && /^\s*\[tool\.pytest\.ini_options\]/m.test(content)) return "pytest";
|
|
6792
|
-
return null;
|
|
7229
|
+
function expressionOf(raw) {
|
|
7230
|
+
const v = uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
7231
|
+
const m = v.match(/^\$\{\{\s*(.+?)\s*\}\}$/);
|
|
7232
|
+
return m ? m[1] : v;
|
|
6793
7233
|
}
|
|
6794
|
-
function
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
7234
|
+
function isAlwaysFalse(raw) {
|
|
7235
|
+
const e = expressionOf(raw);
|
|
7236
|
+
if (e === "") return false;
|
|
7237
|
+
const v = foldConst(e);
|
|
7238
|
+
return v !== void 0 && !truthy(v);
|
|
6798
7239
|
}
|
|
6799
|
-
function
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
return
|
|
7240
|
+
function isTruthy(raw) {
|
|
7241
|
+
const v = uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
7242
|
+
if (/^(?:true|yes|on|1)$/i.test(v)) return true;
|
|
7243
|
+
const m = v.match(/^\$\{\{\s*(.+?)\s*\}\}$/);
|
|
7244
|
+
if (!m) return false;
|
|
7245
|
+
const f = foldConst(m[1]);
|
|
7246
|
+
return f !== void 0 && truthy(f);
|
|
6806
7247
|
}
|
|
6807
|
-
function
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
if (
|
|
6812
|
-
const
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
return
|
|
7248
|
+
function stepOf(lines, i) {
|
|
7249
|
+
const ind = indentOf(lines[i]);
|
|
7250
|
+
let s = i - 1;
|
|
7251
|
+
while (s >= 0 && (lines[s].trim() === "" || indentOf(lines[s]) >= ind)) s--;
|
|
7252
|
+
if (s < 0 || !STEP_START.test(lines[s])) return null;
|
|
7253
|
+
const stepInd = indentOf(lines[s]);
|
|
7254
|
+
let e = i + 1;
|
|
7255
|
+
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > stepInd)) e++;
|
|
7256
|
+
return [s, e];
|
|
6816
7257
|
}
|
|
6817
|
-
function
|
|
6818
|
-
const
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
if (
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
7258
|
+
function jobOf(lines, i) {
|
|
7259
|
+
const ind = indentOf(lines[i]);
|
|
7260
|
+
let h = i - 1;
|
|
7261
|
+
while (h >= 0 && (lines[h].trim() === "" || indentOf(lines[h]) >= ind)) h--;
|
|
7262
|
+
if (h < 0 || !/^\s+[\w-]+:\s*(?:#.*)?$/.test(lines[h])) return null;
|
|
7263
|
+
let t = h - 1;
|
|
7264
|
+
while (t >= 0 && (lines[t].trim() === "" || indentOf(lines[t]) > 0)) t--;
|
|
7265
|
+
if (t < 0 || !/^jobs:/.test(lines[t])) return null;
|
|
7266
|
+
const hi = indentOf(lines[h]);
|
|
7267
|
+
let e = i + 1;
|
|
7268
|
+
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > hi)) e++;
|
|
7269
|
+
return [h, e];
|
|
7270
|
+
}
|
|
7271
|
+
function runBlocks(lines) {
|
|
7272
|
+
const out3 = [];
|
|
7273
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7274
|
+
if (!/^\s*-?\s*run:\s*[|>][-+]?\s*$/.test(lines[i])) continue;
|
|
7275
|
+
const ind = indentOf(lines[i].replace(/^(\s*)-\s+/, "$1 "));
|
|
7276
|
+
let e = i + 1;
|
|
7277
|
+
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > ind)) e++;
|
|
7278
|
+
out3.push({ start: i, end: e, body: lines.slice(i + 1, e) });
|
|
7279
|
+
}
|
|
7280
|
+
return out3;
|
|
7281
|
+
}
|
|
7282
|
+
function commandLines(lines) {
|
|
7283
|
+
const out3 = lines.slice();
|
|
7284
|
+
for (const b of runBlocks(lines)) {
|
|
7285
|
+
if (/^\s*-?\s*run:\s*>/.test(lines[b.start])) {
|
|
7286
|
+
out3[b.start + 1] = b.body.map((l) => l.trim()).filter(Boolean).join(" ");
|
|
7287
|
+
for (let k = b.start + 2; k < b.end; k++) out3[k] = "";
|
|
6828
7288
|
continue;
|
|
6829
7289
|
}
|
|
6830
|
-
const
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
7290
|
+
const indents = b.body.filter((l) => l.trim() !== "").map(indentOf);
|
|
7291
|
+
const ind = indents.length ? Math.min(...indents) : 0;
|
|
7292
|
+
const dedented = b.body.map((l) => l.slice(Math.min(ind, indentOf(l))));
|
|
7293
|
+
const kept = stripHeredocs(dedented);
|
|
7294
|
+
let j = 0;
|
|
7295
|
+
for (let k = 0; k < dedented.length; k++) {
|
|
7296
|
+
if (j < kept.length && kept[j] === dedented[k]) j++;
|
|
7297
|
+
else out3[b.start + 1 + k] = "";
|
|
6835
7298
|
}
|
|
6836
|
-
if (key2 && /^\s+\S/.test(line)) out3[key2] += " " + line.trim();
|
|
6837
7299
|
}
|
|
6838
7300
|
return out3;
|
|
6839
7301
|
}
|
|
6840
|
-
function
|
|
6841
|
-
const
|
|
6842
|
-
if (
|
|
6843
|
-
|
|
6844
|
-
}
|
|
6845
|
-
const unq2 = v.replace(/^"([\s\S]*)"$/, "$1").replace(/^'([\s\S]*)'$/, "$1");
|
|
6846
|
-
return [...unq2.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "").filter(Boolean);
|
|
6847
|
-
}
|
|
6848
|
-
function pytestSelection(src) {
|
|
6849
|
-
const sel = empty();
|
|
6850
|
-
const blk = pytestBlock(src);
|
|
6851
|
-
if (!blk) return sel;
|
|
6852
|
-
sel.present = true;
|
|
6853
|
-
const ignore = blk.norecursedirs !== void 0 ? pytestTokens(blk.norecursedirs).flatMap((d) => pathGlobs(d).map((pattern) => ({ pattern, key: "norecursedirs" }))) : PYTEST_DEFAULT_IGNORE.map((pattern) => ({ pattern, key: "norecursedirs" }));
|
|
6854
|
-
if (blk.testpaths !== void 0) sel.roots = pytestTokens(blk.testpaths).map((t) => t.replace(/^\.\//, ""));
|
|
6855
|
-
if (blk.python_files !== void 0) sel.include = pytestTokens(blk.python_files).map((g) => g.includes("/") ? g : "**/" + g);
|
|
6856
|
-
for (const key2 of ["python_classes", "python_functions"]) {
|
|
6857
|
-
if (blk[key2] !== void 0) ignore.push({ pattern: "\0" + key2, key: key2 });
|
|
6858
|
-
}
|
|
6859
|
-
const opts = blk.addopts !== void 0 ? pytestTokens(blk.addopts) : [];
|
|
6860
|
-
for (let j = 0; j < opts.length; j++) {
|
|
6861
|
-
const o = opts[j];
|
|
6862
|
-
const val = (inline) => o.includes("=") ? o.slice(o.indexOf("=") + 1) : opts[++j] ?? inline;
|
|
6863
|
-
if (o === "--ignore" || o.startsWith("--ignore=")) for (const g of pathGlobs(val(""))) ignore.push({ pattern: g, key: "--ignore" });
|
|
6864
|
-
else if (o === "--ignore-glob" || o.startsWith("--ignore-glob=")) {
|
|
6865
|
-
const g = val("");
|
|
6866
|
-
ignore.push({ pattern: g, key: "--ignore-glob" });
|
|
6867
|
-
if (!g.startsWith("**/") && !g.startsWith("/")) ignore.push({ pattern: "**/" + g, key: "--ignore-glob" });
|
|
6868
|
-
} else if (o === "--deselect" || o.startsWith("--deselect=")) for (const g of pathGlobs(val(""))) ignore.push({ pattern: g, key: "--deselect" });
|
|
6869
|
-
else if (o === "-k" || o.startsWith("-k=")) {
|
|
6870
|
-
for (const w of (val("").match(/\bnot\s+([A-Za-z_][\w]*)/g) ?? []).map((t) => t.replace(/^not\s+/, "")))
|
|
6871
|
-
ignore.push({ pattern: `**/*${w}*`, key: "-k" });
|
|
6872
|
-
} else if (o === "-m" || o.startsWith("-m=")) {
|
|
6873
|
-
ignore.push({ pattern: "\0-m " + val(""), key: "-m" });
|
|
6874
|
-
} else if (o === "-p" || o.startsWith("-p=") || o.length > 2 && o.startsWith("-p")) {
|
|
6875
|
-
const v = o === "-p" ? opts[++j] ?? "" : o.startsWith("-p=") ? o.slice(3) : o.slice(2);
|
|
6876
|
-
if (v.startsWith("no:")) ignore.push({ pattern: "\0-p " + v, key: "-p" });
|
|
6877
|
-
} else if (o === "--collect-only" || o === "--co") ignore.push({ pattern: "**", key: o });
|
|
6878
|
-
}
|
|
6879
|
-
sel.ignore = ignore;
|
|
6880
|
-
return sel;
|
|
6881
|
-
}
|
|
6882
|
-
function dirValue(e) {
|
|
6883
|
-
if (!isStr(e)) return { dir: null, opaque: true };
|
|
6884
|
-
const d = stripRootDir(e.text).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
6885
|
-
if (d === "." || d === "" || d.split("/").includes("..")) return { dir: null, opaque: false };
|
|
6886
|
-
return { dir: d, opaque: false };
|
|
7302
|
+
function flowList(v) {
|
|
7303
|
+
const m = v.trim().match(/^\[(.*)\]$/);
|
|
7304
|
+
if (!m) return null;
|
|
7305
|
+
return m[1].split(",").map((s) => s.trim().replace(/^(['"])(.*)\1$/, "$2")).filter(Boolean);
|
|
6887
7306
|
}
|
|
6888
|
-
function
|
|
6889
|
-
const
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
const ignores = (key2, e) => {
|
|
6899
|
-
const entries = take(key2, e).map((pattern) => ({ pattern, key: key2 }));
|
|
6900
|
-
sel.ignore = [...sel.ignore ?? [], ...entries];
|
|
6901
|
-
};
|
|
6902
|
-
let projectList = null;
|
|
6903
|
-
const projects = (e) => {
|
|
6904
|
-
sel.present = true;
|
|
6905
|
-
projectList = e;
|
|
7307
|
+
function parseTriggers(src) {
|
|
7308
|
+
const t = {
|
|
7309
|
+
events: /* @__PURE__ */ new Set(),
|
|
7310
|
+
branches: /* @__PURE__ */ new Map(),
|
|
7311
|
+
branchesIgnore: /* @__PURE__ */ new Map(),
|
|
7312
|
+
pathsIgnore: /* @__PURE__ */ new Map(),
|
|
7313
|
+
paths: /* @__PURE__ */ new Map(),
|
|
7314
|
+
types: /* @__PURE__ */ new Map(),
|
|
7315
|
+
tags: /* @__PURE__ */ new Map(),
|
|
7316
|
+
present: false
|
|
6906
7317
|
};
|
|
6907
|
-
const
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
7318
|
+
const lines = src.split("\n");
|
|
7319
|
+
const i = lines.findIndex((l) => /^(?:on|"on"|'on'|true):/.test(l));
|
|
7320
|
+
if (i < 0) return t;
|
|
7321
|
+
t.present = true;
|
|
7322
|
+
const head = lines[i].replace(/^(?:on|"on"|'on'|true):\s*/, "");
|
|
7323
|
+
const inline = uncommented(head);
|
|
7324
|
+
if (inline) {
|
|
7325
|
+
const list3 = flowList(inline);
|
|
7326
|
+
for (const e of list3 ?? [inline]) t.events.add(e);
|
|
7327
|
+
return t;
|
|
7328
|
+
}
|
|
7329
|
+
let event = null;
|
|
7330
|
+
let listKey = null;
|
|
7331
|
+
let listInd = -1;
|
|
7332
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
7333
|
+
const l = lines[j];
|
|
7334
|
+
if (l.trim() === "" || /^\s*#/.test(l)) continue;
|
|
7335
|
+
const ind = indentOf(l);
|
|
7336
|
+
if (ind === 0) break;
|
|
7337
|
+
const body = uncommented(l);
|
|
7338
|
+
const item = body.match(/^\s*-\s+(.+)$/);
|
|
7339
|
+
if (item && listKey && event && ind > listInd) {
|
|
7340
|
+
const target = t[listKey].get(event) ?? [];
|
|
7341
|
+
target.push(item[1].replace(/^(['"])(.*)\1$/, "$2"));
|
|
7342
|
+
t[listKey].set(event, target);
|
|
7343
|
+
continue;
|
|
6915
7344
|
}
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
else if (k === "rootDir") {
|
|
6924
|
-
const { dir, opaque } = dirValue(node.initializer);
|
|
6925
|
-
sel.present = true;
|
|
6926
|
-
if (opaque) sel.opaque = true;
|
|
6927
|
-
else sel.base = dir;
|
|
6928
|
-
} else if (k === "projects") {
|
|
6929
|
-
projects(node.initializer);
|
|
6930
|
-
return;
|
|
6931
|
-
}
|
|
6932
|
-
} else if (ownerKey2(node) === "test") {
|
|
6933
|
-
if (k === "include") sel.include = take(k, node.initializer);
|
|
6934
|
-
else if (k === "exclude") ignores(k, node.initializer);
|
|
6935
|
-
else if (k === "dir" || k === "root") {
|
|
6936
|
-
const { dir, opaque } = dirValue(node.initializer);
|
|
6937
|
-
sel.present = true;
|
|
6938
|
-
if (opaque) sel.opaque = true;
|
|
6939
|
-
else sel.base = dir;
|
|
6940
|
-
} else if (k === "projects") {
|
|
6941
|
-
projects(node.initializer);
|
|
6942
|
-
return;
|
|
6943
|
-
}
|
|
6944
|
-
}
|
|
7345
|
+
const kv = body.match(/^\s*([\w-]+):\s*(.*)$/);
|
|
7346
|
+
if (!kv) continue;
|
|
7347
|
+
if (ind === 2 || event === null && ind < 4) {
|
|
7348
|
+
event = kv[1];
|
|
7349
|
+
t.events.add(event);
|
|
7350
|
+
listKey = null;
|
|
7351
|
+
continue;
|
|
6945
7352
|
}
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
}
|
|
6953
|
-
function unwrapConfigCall(e) {
|
|
6954
|
-
let x = e;
|
|
6955
|
-
while (ts.isParenthesizedExpression(x) || ts.isAsExpression(x) || ts.isSatisfiesExpression(x)) {
|
|
6956
|
-
x = x.expression;
|
|
7353
|
+
const key2 = LIST_KEYS[kv[1]];
|
|
7354
|
+
if (event && key2) {
|
|
7355
|
+
listKey = key2;
|
|
7356
|
+
listInd = ind;
|
|
7357
|
+
const list3 = flowList(kv[2]);
|
|
7358
|
+
t[listKey].set(event, list3 ?? []);
|
|
7359
|
+
} else listKey = null;
|
|
6957
7360
|
}
|
|
6958
|
-
|
|
6959
|
-
return x;
|
|
7361
|
+
return t;
|
|
6960
7362
|
}
|
|
6961
|
-
function
|
|
6962
|
-
const
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
while (q && (ts.isParenthesizedExpression(q) || ts.isAsExpression(q) || q.kind === ts.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
6969
|
-
return !!q && ts.isPropertyAssignment(q) && keyName2(q.name) === "test";
|
|
7363
|
+
function ghGlob(g) {
|
|
7364
|
+
const re = g.split("**").map((part) => part.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")).join(".*");
|
|
7365
|
+
try {
|
|
7366
|
+
const r = new RegExp(`^${re}$`);
|
|
7367
|
+
return (p) => r.test(p);
|
|
7368
|
+
} catch {
|
|
7369
|
+
return () => false;
|
|
6970
7370
|
}
|
|
6971
|
-
return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
|
|
6972
7371
|
}
|
|
6973
|
-
function
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
if (
|
|
6978
|
-
return "path";
|
|
7372
|
+
function admits(globs4, name) {
|
|
7373
|
+
let on = false;
|
|
7374
|
+
for (const g of globs4) {
|
|
7375
|
+
const neg = g.startsWith("!");
|
|
7376
|
+
if (ghGlob(neg ? g.slice(1) : g)(name)) on = !neg;
|
|
6979
7377
|
}
|
|
6980
|
-
return
|
|
7378
|
+
return on;
|
|
6981
7379
|
}
|
|
6982
|
-
function
|
|
6983
|
-
const
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
p.include ??= root.include;
|
|
6988
|
-
p.regex ??= root.regex;
|
|
6989
|
-
p.ignore ??= root.ignore;
|
|
6990
|
-
p.roots ??= root.roots;
|
|
6991
|
-
p.base ??= root.base;
|
|
6992
|
-
if (root.opaque) p.opaque = true;
|
|
6993
|
-
}
|
|
6994
|
-
p.present = true;
|
|
6995
|
-
return p;
|
|
7380
|
+
function sourceProbes(ctx) {
|
|
7381
|
+
const files = trackedFiles(ctx);
|
|
7382
|
+
if (!files) return CODE_PROBES;
|
|
7383
|
+
const code2 = files.filter((f) => langOf(f) !== null);
|
|
7384
|
+
return code2.length ? code2 : CODE_PROBES;
|
|
6996
7385
|
}
|
|
6997
|
-
function
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7386
|
+
function triggerNarrowings(before, after, opts) {
|
|
7387
|
+
const out3 = [];
|
|
7388
|
+
if (!before.present || !after.present) return out3;
|
|
7389
|
+
const isDefault = (b) => opts.defaultBranch ? b === opts.defaultBranch : DEFAULT_BRANCH.test(b);
|
|
7390
|
+
const candidates = opts.defaultBranch ? [opts.defaultBranch] : ["main", "master"];
|
|
7391
|
+
const namesDefault = (l) => candidates.some((b) => admits(l, b));
|
|
7392
|
+
for (const e of CODE_EVENTS) {
|
|
7393
|
+
if (before.events.has(e) && !after.events.has(e)) out3.push(`the ${e} trigger was removed \u2014 the workflow no longer runs on it`);
|
|
7394
|
+
}
|
|
7395
|
+
for (const [e, list3] of after.pathsIgnore) {
|
|
7396
|
+
const had = before.pathsIgnore.get(e) ?? [];
|
|
7397
|
+
if (list3.some((g) => ALL(g) && !had.includes(g))) {
|
|
7398
|
+
out3.push(`on.${e}.paths-ignore now ignores every path \u2014 the workflow never runs on ${e}`);
|
|
7399
|
+
continue;
|
|
7400
|
+
}
|
|
7401
|
+
if (!CODE_EVENTS.includes(e) || !after.events.has(e)) continue;
|
|
7402
|
+
if (had.length && ignoresEverySource(had, opts.sources)) continue;
|
|
7403
|
+
if (ignoresEverySource(list3, opts.sources)) out3.push(`on.${e}.paths-ignore now ignores every source file (now [${list3.join(", ")}]) \u2014 the workflow never runs on code`);
|
|
7404
|
+
}
|
|
7405
|
+
for (const [e, list3] of after.paths) {
|
|
7406
|
+
if (!CODE_EVENTS.includes(e) || !after.events.has(e)) continue;
|
|
7407
|
+
const had = before.paths.get(e);
|
|
7408
|
+
if (had && !pathsMatchSource(had, opts.sources)) continue;
|
|
7409
|
+
if (!pathsMatchSource(list3, opts.sources)) out3.push(`on.${e}.paths matches no source file (now [${list3.join(", ")}]) \u2014 the workflow never runs on code`);
|
|
7410
|
+
}
|
|
7411
|
+
for (const [e, list3] of after.types) {
|
|
7412
|
+
if (!/^pull_request(?:_target)?$/.test(e) || !after.events.has(e)) continue;
|
|
7413
|
+
const had = before.types.get(e);
|
|
7414
|
+
if (had && !had.some((t) => PR_CODE_TYPES.test(t))) continue;
|
|
7415
|
+
if (!list3.some((t) => PR_CODE_TYPES.test(t))) out3.push(`on.${e}.types no longer includes opened/synchronize (now [${list3.join(", ")}]) \u2014 the workflow does not run on the pull request's code`);
|
|
7001
7416
|
}
|
|
7002
|
-
const list3
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
if (ts.isObjectLiteralExpression(lit)) list3.push(readProject(lit, root, runner));
|
|
7006
|
-
else root.opaque = true;
|
|
7417
|
+
for (const [e, list3] of after.branchesIgnore) {
|
|
7418
|
+
const had = before.branchesIgnore.get(e) ?? [];
|
|
7419
|
+
if (list3.some((g) => (ALL(g) || isDefault(g)) && !had.includes(g))) out3.push(`on.${e}.branches-ignore now covers the default branch`);
|
|
7007
7420
|
}
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
if (
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
e = e.expression;
|
|
7421
|
+
const events = /* @__PURE__ */ new Set([...before.branches.keys(), ...CODE_EVENTS.filter((e) => before.events.has(e))]);
|
|
7422
|
+
for (const e of events) {
|
|
7423
|
+
const was = before.branches.get(e) ?? (before.tags.has(e) ? null : ["**"]);
|
|
7424
|
+
if (!was || !after.events.has(e)) continue;
|
|
7425
|
+
const now = after.branches.get(e);
|
|
7426
|
+
if (!now) {
|
|
7427
|
+
const tags = after.tags.get(e);
|
|
7428
|
+
if (tags && namesDefault(was)) out3.push(`on.${e} now filters on tags only (tags: [${tags.join(", ")}]) \u2014 no push to the default branch runs the workflow`);
|
|
7429
|
+
continue;
|
|
7018
7430
|
}
|
|
7019
|
-
|
|
7020
|
-
|
|
7431
|
+
const renamed = (b) => DEFAULT_BRANCH.test(b) && now.includes(b) && admits(now, b) && opts.hasBranch(b) !== false;
|
|
7432
|
+
const lost = candidates.filter((b) => admits(was, b) && !admits(now, b));
|
|
7433
|
+
const lostDefault = lost.length > 0 && !["main", "master"].some(renamed);
|
|
7434
|
+
const replaced = !namesDefault(was) && !namesDefault(now) && was.length > 0 && !was.some((b) => now.includes(b));
|
|
7435
|
+
if (!lostDefault && !replaced) continue;
|
|
7436
|
+
const dropped = was.filter((b) => !now.includes(b) && !ALL(b));
|
|
7437
|
+
out3.push(
|
|
7438
|
+
dropped.length ? `on.${e}.branches no longer names ${dropped.join(", ")} (now [${now.join(", ")}])` : `on.${e}.branches now filters to [${now.join(", ")}], which does not admit the default branch`
|
|
7439
|
+
);
|
|
7021
7440
|
}
|
|
7022
|
-
return
|
|
7441
|
+
return out3;
|
|
7023
7442
|
}
|
|
7024
|
-
function
|
|
7025
|
-
|
|
7026
|
-
if (
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
if (!sf) return sel;
|
|
7030
|
-
if (runner === "vitest" && isWorkspaceFile(path)) {
|
|
7031
|
-
const arr = workspaceArray(sf);
|
|
7032
|
-
if (arr) {
|
|
7033
|
-
sel.projects = readProjects(arr, sel, runner);
|
|
7034
|
-
sel.present = true;
|
|
7035
|
-
if (sel.projects.some((p) => p.opaque)) sel.opaque = true;
|
|
7036
|
-
} else if (src.trim()) sel.opaque = true;
|
|
7037
|
-
} else sel = collect(sf, runner);
|
|
7038
|
-
} catch {
|
|
7039
|
-
}
|
|
7040
|
-
return sel;
|
|
7443
|
+
function localUses(line) {
|
|
7444
|
+
const wf = line.match(REUSED_WORKFLOW)?.[1];
|
|
7445
|
+
if (wf) return [wf];
|
|
7446
|
+
const action = line.match(LOCAL_ACTION)?.[1];
|
|
7447
|
+
return action ? [`${action}/action.yml`, `${action}/action.yaml`] : [];
|
|
7041
7448
|
}
|
|
7042
|
-
function
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
return {
|
|
7046
|
-
selects: (p) => ps.some((x) => x.selects(p)),
|
|
7047
|
-
why: (p) => ps.length ? `no project selects it (${ps[0].why(p)})` : "the projects list is empty"
|
|
7048
|
-
};
|
|
7049
|
-
}
|
|
7050
|
-
const includeList = sel.include ?? (runner === "jest" ? sel.regex ? null : JEST_DEFAULT_MATCH : runner === "pytest" ? PYTEST_DEFAULT_INCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
7051
|
-
const ignoreList = sel.ignore ?? (runner === "jest" ? JEST_DEFAULT_IGNORE : runner === "pytest" ? PYTEST_DEFAULT_IGNORE : VITEST_DEFAULT_EXCLUDE).map((pattern) => ({
|
|
7052
|
-
pattern,
|
|
7053
|
-
key: runner === "jest" ? "testPathIgnorePatterns" : runner === "pytest" ? "norecursedirs" : "test.exclude"
|
|
7054
|
-
}));
|
|
7055
|
-
const blockWide = ignoreList.filter((e) => e.pattern.startsWith("\0"));
|
|
7056
|
-
const included = includeList ? globList2(includeList) : sel.regex ? regexes(sel.regex) : () => true;
|
|
7057
|
-
const ignoredBy = (p) => blockWide[0] ?? ignoreList.filter((e) => !e.pattern.startsWith("\0")).find((e) => (runner === "jest" ? regexes([e.pattern]) : globs2([e.pattern]))(p));
|
|
7058
|
-
const roots = sel.roots?.map((r) => r.replace(/^\.\//, "").replace(/\/+$/, "")).map((r) => r === "." ? "" : r) ?? null;
|
|
7059
|
-
const rebase = (p) => sel.base === null ? p : underDir(p, sel.base) ? p.slice(sel.base.length + 1) : null;
|
|
7060
|
-
const inRoots = (p) => roots === null || roots.some((r) => underDir(p, r));
|
|
7061
|
-
return {
|
|
7062
|
-
selects: (p) => {
|
|
7063
|
-
const r = rebase(p);
|
|
7064
|
-
return r !== null && inRoots(r) && included(r) && !ignoredBy(r);
|
|
7065
|
-
},
|
|
7066
|
-
why: (p) => {
|
|
7067
|
-
const r = rebase(p);
|
|
7068
|
-
const baseKey = runner === "jest" ? "rootDir" : "test.dir";
|
|
7069
|
-
if (r === null) return `${baseKey} no longer covers it (${JSON.stringify(sel.base)})`;
|
|
7070
|
-
if (!inRoots(r)) return `roots no longer covers it (${JSON.stringify(sel.roots)})`;
|
|
7071
|
-
const hit = ignoredBy(r);
|
|
7072
|
-
if (hit) {
|
|
7073
|
-
if (hit.pattern.startsWith("\0")) return `${hit.key} narrows the whole run (${JSON.stringify(hit.pattern.slice(1))})`;
|
|
7074
|
-
return `${hit.key === "exclude" ? "test.exclude" : hit.key} now matches it (${JSON.stringify(hit.pattern)})`;
|
|
7075
|
-
}
|
|
7076
|
-
const key2 = runner === "jest" ? sel.include ? "testMatch" : sel.regex ? "testRegex" : "testMatch (default)" : sel.include ? "test.include" : "test.include (default)";
|
|
7077
|
-
return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? includeList ?? [])})`;
|
|
7078
|
-
}
|
|
7079
|
-
};
|
|
7449
|
+
function literalValue(raw) {
|
|
7450
|
+
const v = foldExpressions(uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim());
|
|
7451
|
+
return v.includes(EXPR_TOKEN) || v.includes(MATRIX_TOKEN) ? null : v;
|
|
7080
7452
|
}
|
|
7081
|
-
function
|
|
7082
|
-
const
|
|
7083
|
-
if (!
|
|
7084
|
-
const
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
const
|
|
7090
|
-
|
|
7091
|
-
|
|
7453
|
+
function dirRunsNothing(dir, ctx) {
|
|
7454
|
+
const files = trackedFiles(ctx);
|
|
7455
|
+
if (!files) return true;
|
|
7456
|
+
const d = dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
7457
|
+
if (d === "" || d === ".") return false;
|
|
7458
|
+
return !files.some((f) => f.startsWith(d + "/") && (langOf(f) !== null || MANIFEST.test(f)));
|
|
7459
|
+
}
|
|
7460
|
+
function defaultsOf(lines, i) {
|
|
7461
|
+
const run = ancestorOf(lines, i);
|
|
7462
|
+
if (run === null || !/^\s*run:\s*$/.test(lines[run])) return null;
|
|
7463
|
+
const d = ancestorOf(lines, run);
|
|
7464
|
+
return d !== null && /^\s*defaults:\s*$/.test(lines[d]) ? d : null;
|
|
7465
|
+
}
|
|
7466
|
+
function enclosingStep(lines, i) {
|
|
7467
|
+
let h = i;
|
|
7468
|
+
while ((h = ancestorOf(lines, h)) !== null) {
|
|
7469
|
+
if (!STEP_START.test(lines[h])) continue;
|
|
7470
|
+
const ind = indentOf(lines[h]);
|
|
7471
|
+
let e = i + 1;
|
|
7472
|
+
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > ind)) e++;
|
|
7473
|
+
return [h, e];
|
|
7092
7474
|
}
|
|
7093
|
-
return
|
|
7475
|
+
return null;
|
|
7094
7476
|
}
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7477
|
+
function ancestorOf(lines, i) {
|
|
7478
|
+
const ind = indentOf(lines[i]);
|
|
7479
|
+
let h = i - 1;
|
|
7480
|
+
while (h >= 0 && (lines[h].trim() === "" || /^\s*#/.test(lines[h]) || indentOf(lines[h]) >= ind)) h--;
|
|
7481
|
+
return h >= 0 ? h : null;
|
|
7482
|
+
}
|
|
7483
|
+
var RULE8, USES, CHECK, YAML_KEY, isCompositeAction, isGithubFile, GH_RUN_KEY, GENERIC_RUN_KEY, invokesCheck2, uncommented, SHELL_ERREXIT, indentOf, STEP_START, REPORTER_ACTION, usesCheck, stepHasCheck, jobHasCheck, NEUTRALISERS, LIST_KEYS, DEFAULT_BRANCH, ALL, CODE_EVENTS, PR_CODE_TYPES, pathsMatchSource, ignoresEverySource, CODE_PROBES, REUSED_WORKFLOW, LOCAL_ACTION, MANIFEST, WORKDIR_LINE, REF_LINE, CHECKOUT, ciTampering;
|
|
7484
|
+
var init_ci_tampering = __esm({
|
|
7485
|
+
"src/detectors/ci-tampering.ts"() {
|
|
7098
7486
|
"use strict";
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
];
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
"
|
|
7127
|
-
"
|
|
7128
|
-
"
|
|
7129
|
-
"
|
|
7487
|
+
init_select();
|
|
7488
|
+
init_policy();
|
|
7489
|
+
init_finding();
|
|
7490
|
+
init_files();
|
|
7491
|
+
init_hook_wiring();
|
|
7492
|
+
init_repo();
|
|
7493
|
+
init_gh_expression();
|
|
7494
|
+
init_invocation();
|
|
7495
|
+
init_invocation();
|
|
7496
|
+
RULE8 = "ci-tampering";
|
|
7497
|
+
USES = /^\s*-\s*uses:/;
|
|
7498
|
+
CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
|
|
7499
|
+
YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
|
|
7500
|
+
isCompositeAction = (path) => /(^|\/)\.github\/actions\/.+\/action\.ya?ml$/.test(path);
|
|
7501
|
+
isGithubFile = (path) => /(^|\/)\.github\//.test(path);
|
|
7502
|
+
GH_RUN_KEY = /^\s*-?\s*run:/;
|
|
7503
|
+
GENERIC_RUN_KEY = /^\s*-?\s*(?:run|script|command|before_script|after_script|sh|bash|pwsh|powershell):/;
|
|
7504
|
+
invokesCheck2 = (line) => invokesCheck(line) || INVOKES_TOOL.test(commandCore(line));
|
|
7505
|
+
uncommented = (v) => v.replace(/\s+#.*$/, "").trim();
|
|
7506
|
+
SHELL_ERREXIT = /(?:^|\s)-[a-zA-Z]*e[a-zA-Z]*(?:\s|$)|-o\s+errexit\b/;
|
|
7507
|
+
indentOf = (l) => l.length - l.trimStart().length;
|
|
7508
|
+
STEP_START = /^\s*-\s+(?:name|uses|run|if|id|with|env|shell|continue-on-error|working-directory|timeout-minutes):/;
|
|
7509
|
+
REPORTER_ACTION = /^\s*-?\s*uses:\s*\S*?(?:[\w-]*-reporter|[\w-]*-results?[\w-]*|upload-[\w-]*|codecov[\w-]*|coveralls[\w-]*)(?:@|\s|$)/i;
|
|
7510
|
+
usesCheck = (l) => USES.test(l) && CHECK.test(l) && !REPORTER_ACTION.test(l);
|
|
7511
|
+
stepHasCheck = (lines, range) => range === null || lines.slice(range[0], range[1]).some((l) => invokesCheck2(l) || usesCheck(l));
|
|
7512
|
+
jobHasCheck = (lines, range) => lines.slice(range[0], range[1]).some((l) => invokesCheck2(l) || usesCheck(l) || /^\s*uses:\s*\S/.test(l));
|
|
7513
|
+
NEUTRALISERS = [
|
|
7514
|
+
{ re: /^\s*set\s+\+e\b/, what: "set +e \u2014 a failing check no longer fails the step" },
|
|
7515
|
+
{ re: /^\s*exit\s+0\s*(?:$|[;#])/, what: "exit 0 \u2014 the step ends green whatever the check did" },
|
|
7516
|
+
{ re: /^\s*if\s+false\s*;?\s*then\b/, what: "if false; then \u2014 the check is never run" },
|
|
7517
|
+
{ re: /^\s*:\s*<<\s*['"]?\w+/, what: "a heredoc fed to `:` \u2014 the check is commented out" }
|
|
7130
7518
|
];
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
empty = () => ({ include: null, regex: null, ignore: null, roots: null, base: null, projects: null, opaque: false, present: false });
|
|
7139
|
-
stripRootDir = (g) => g.replace(/^<rootDir>\/?/, "");
|
|
7140
|
-
JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
|
|
7141
|
-
globs2 = (patterns) => {
|
|
7142
|
-
const ms = patterns.map((g) => {
|
|
7143
|
-
try {
|
|
7144
|
-
return picomatch(g, { dot: true });
|
|
7145
|
-
} catch {
|
|
7146
|
-
return () => false;
|
|
7147
|
-
}
|
|
7148
|
-
});
|
|
7149
|
-
return (path) => ms.some((m) => m(path));
|
|
7150
|
-
};
|
|
7151
|
-
globList2 = (patterns) => {
|
|
7152
|
-
const pos = globs2(patterns.filter((g) => !g.startsWith("!")));
|
|
7153
|
-
const neg = globs2(patterns.filter((g) => g.startsWith("!")).map((g) => g.slice(1)));
|
|
7154
|
-
return (path) => pos(path) && !neg(path);
|
|
7519
|
+
LIST_KEYS = {
|
|
7520
|
+
branches: "branches",
|
|
7521
|
+
"branches-ignore": "branchesIgnore",
|
|
7522
|
+
"paths-ignore": "pathsIgnore",
|
|
7523
|
+
paths: "paths",
|
|
7524
|
+
types: "types",
|
|
7525
|
+
tags: "tags"
|
|
7155
7526
|
};
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7527
|
+
DEFAULT_BRANCH = /^(?:main|master)$/;
|
|
7528
|
+
ALL = (g) => /^\*\*?$|^\*\*\/\*$/.test(g);
|
|
7529
|
+
CODE_EVENTS = ["push", "pull_request", "pull_request_target"];
|
|
7530
|
+
PR_CODE_TYPES = /^(?:opened|synchronize|reopened)$/;
|
|
7531
|
+
pathsMatchSource = (globs4, sources) => sources.some((s) => admits(globs4, s));
|
|
7532
|
+
ignoresEverySource = (globs4, sources) => sources.length > 0 && sources.every((s) => admits(globs4, s));
|
|
7533
|
+
CODE_PROBES = ["src/index.ts", "src/index.js", "lib/index.js", "index.ts", "main.go", "src/main.py", "src/lib.rs", "packages/a/src/index.ts", "test/a.test.ts"];
|
|
7534
|
+
REUSED_WORKFLOW = /^\s*-?\s*uses:\s*\.\/(\.github\/workflows\/[^\s@]+)/;
|
|
7535
|
+
LOCAL_ACTION = /^\s*-?\s*uses:\s*\.\/(\.github\/actions\/[^\s@]+?)\/?\s*(?:#.*)?$/;
|
|
7536
|
+
MANIFEST = /(?:^|\/)(?:package\.json|pyproject\.toml|setup\.py|setup\.cfg|Cargo\.toml|go\.mod|Makefile|build\.gradle(?:\.kts)?|pom\.xml|Gemfile|deno\.jsonc?)$/;
|
|
7537
|
+
WORKDIR_LINE = /^\s*-?\s*working-directory:\s*(.+?)\s*$/;
|
|
7538
|
+
REF_LINE = /^\s*ref:\s*(.+?)\s*$/;
|
|
7539
|
+
CHECKOUT = /^\s*-?\s*uses:\s*actions\/checkout(?:@|\s|$)/;
|
|
7540
|
+
ciTampering = {
|
|
7541
|
+
id: RULE8,
|
|
7542
|
+
surface: ["file"],
|
|
7543
|
+
certainty: "mechanical",
|
|
7544
|
+
run(changes, policy, _view, ctx) {
|
|
7545
|
+
const out3 = [];
|
|
7546
|
+
const afterByPath = /* @__PURE__ */ new Map();
|
|
7547
|
+
for (const c of changes) if (c.kind === "file" && c.after != null) afterByPath.set(c.path, c.after);
|
|
7548
|
+
let triggerOpts = null;
|
|
7549
|
+
for (const c of changes) {
|
|
7550
|
+
if (c.kind !== "file") continue;
|
|
7551
|
+
if (c.op === "rename" && c.oldPath && isActiveWorkflow(c.oldPath) && !isActiveWorkflow(c.path)) {
|
|
7552
|
+
out3.push(
|
|
7553
|
+
makeFinding(RULE8, policy, {
|
|
7554
|
+
file: c.path,
|
|
7555
|
+
message: `A CI workflow was renamed so it no longer runs (${c.oldPath} \u2192 ${c.path}).`,
|
|
7556
|
+
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
7557
|
+
remediation: "Restore the path and extension. GitHub runs only *.yml/*.yaml directly under .github/workflows \u2014 renaming outside that disables the checks as surely as deleting them."
|
|
7558
|
+
})
|
|
7559
|
+
);
|
|
7560
|
+
continue;
|
|
7561
|
+
}
|
|
7562
|
+
if (!isProtected(c.path, policy, "ci")) continue;
|
|
7563
|
+
const afterLines = c.after != null ? c.after.split("\n") : null;
|
|
7564
|
+
const addedAt = new Set(addedLines(c).map((l) => l.newLine).filter((n) => n != null));
|
|
7565
|
+
const github = isGithubFile(c.path);
|
|
7566
|
+
for (const l of github ? addedLines(c) : []) {
|
|
7567
|
+
const coe = l.content.match(/^\s*-?\s*continue-on-error:\s*(.+?)\s*$/);
|
|
7568
|
+
const cond = l.content.match(/^\s*-?\s*if:\s*(.+?)\s*$/);
|
|
7569
|
+
if (!coe && !cond) continue;
|
|
7570
|
+
const step = afterLines && l.newLine != null ? stepOf(afterLines, l.newLine - 1) : null;
|
|
7571
|
+
if (afterLines && step && !stepHasCheck(afterLines, step)) continue;
|
|
7572
|
+
if (afterLines && !step && l.newLine != null) {
|
|
7573
|
+
const job = jobOf(afterLines, l.newLine - 1);
|
|
7574
|
+
if (job && !jobHasCheck(afterLines, job)) continue;
|
|
7575
|
+
}
|
|
7576
|
+
if (coe && isTruthy(coe[1])) {
|
|
7577
|
+
out3.push(
|
|
7578
|
+
makeFinding(RULE8, policy, {
|
|
7579
|
+
file: c.path,
|
|
7580
|
+
line: l.newLine ?? void 0,
|
|
7581
|
+
message: "continue-on-error: true added \u2014 failures will no longer fail the job.",
|
|
7582
|
+
evidence: l.content.trim(),
|
|
7583
|
+
remediation: "Remove it. A check that cannot fail is not a check."
|
|
7584
|
+
})
|
|
7585
|
+
);
|
|
7586
|
+
} else if (cond && isAlwaysFalse(cond[1])) {
|
|
7587
|
+
out3.push(
|
|
7588
|
+
makeFinding(RULE8, policy, {
|
|
7589
|
+
file: c.path,
|
|
7590
|
+
line: l.newLine ?? void 0,
|
|
7591
|
+
message: `if: ${uncommented(cond[1])} added \u2014 this step can never run.`,
|
|
7592
|
+
evidence: l.content.trim(),
|
|
7593
|
+
remediation: "Re-enable the step instead of conditioning it off."
|
|
7594
|
+
})
|
|
7595
|
+
);
|
|
7596
|
+
}
|
|
7597
|
+
}
|
|
7598
|
+
if (afterLines && github) {
|
|
7599
|
+
for (const b of runBlocks(afterLines)) {
|
|
7600
|
+
if (!b.body.some(invokesCheck2)) continue;
|
|
7601
|
+
const propagates = b.body.some((l) => /\bexit\s+"?\$/.test(l));
|
|
7602
|
+
for (let k = 0; k < b.body.length; k++) {
|
|
7603
|
+
const idx = b.start + 1 + k;
|
|
7604
|
+
if (!addedAt.has(idx + 1)) continue;
|
|
7605
|
+
const n = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
|
|
7606
|
+
if (!n || propagates && /set\s\+e/.test(b.body[k])) continue;
|
|
7607
|
+
out3.push(
|
|
7608
|
+
makeFinding(RULE8, policy, {
|
|
7609
|
+
file: c.path,
|
|
7610
|
+
line: idx + 1,
|
|
7611
|
+
message: `A check's run block was neutralised: ${n.what}.`,
|
|
7612
|
+
evidence: b.body[k].trim(),
|
|
7613
|
+
remediation: "Let the check decide the step. Masking its exit status is the same tamper as removing it."
|
|
7614
|
+
})
|
|
7615
|
+
);
|
|
7616
|
+
}
|
|
7617
|
+
}
|
|
7618
|
+
for (const l of addedLines(c)) {
|
|
7619
|
+
if (!/^\s*-?\s*shell:\s*.*\{0\}/.test(l.content) || l.newLine == null) continue;
|
|
7620
|
+
if (SHELL_ERREXIT.test(uncommented(l.content.replace(/^\s*-?\s*shell:\s*/, "")))) continue;
|
|
7621
|
+
const step = stepOf(afterLines, l.newLine - 1);
|
|
7622
|
+
if (step && !stepHasCheck(afterLines, step)) continue;
|
|
7623
|
+
out3.push(
|
|
7624
|
+
makeFinding(RULE8, policy, {
|
|
7625
|
+
file: c.path,
|
|
7626
|
+
line: l.newLine,
|
|
7627
|
+
message: "shell: \u2026 {0} added on a check step \u2014 the shell no longer stops on the first failure.",
|
|
7628
|
+
evidence: l.content.trim(),
|
|
7629
|
+
remediation: "Keep the default shell (fail-fast) on steps that run checks."
|
|
7630
|
+
})
|
|
7631
|
+
);
|
|
7632
|
+
}
|
|
7633
|
+
for (const l of addedLines(c)) {
|
|
7634
|
+
if (l.newLine == null) continue;
|
|
7635
|
+
const i = l.newLine - 1;
|
|
7636
|
+
const wd = l.content.match(WORKDIR_LINE);
|
|
7637
|
+
if (wd) {
|
|
7638
|
+
const dir = literalValue(wd[1]);
|
|
7639
|
+
if (dir === null || !dirRunsNothing(dir, ctx)) continue;
|
|
7640
|
+
const step2 = stepOf(afterLines, i);
|
|
7641
|
+
if (step2) {
|
|
7642
|
+
if (!stepHasCheck(afterLines, step2)) continue;
|
|
7643
|
+
} else {
|
|
7644
|
+
const d = defaultsOf(afterLines, i);
|
|
7645
|
+
if (d === null) continue;
|
|
7646
|
+
const job = jobOf(afterLines, d);
|
|
7647
|
+
if (job ? !jobHasCheck(afterLines, job) : !afterLines.some((x) => invokesCheck2(x) || usesCheck(x))) continue;
|
|
7648
|
+
}
|
|
7649
|
+
out3.push(
|
|
7650
|
+
makeFinding(RULE8, policy, {
|
|
7651
|
+
file: c.path,
|
|
7652
|
+
line: l.newLine,
|
|
7653
|
+
message: `A check was pointed at ${dir} by working-directory \u2014 a directory holding no code or package the check could run.`,
|
|
7654
|
+
evidence: l.content.trim(),
|
|
7655
|
+
remediation: "Run the check where the code is. A suite pointed at an empty directory passes on nothing."
|
|
7656
|
+
})
|
|
7657
|
+
);
|
|
7658
|
+
continue;
|
|
7659
|
+
}
|
|
7660
|
+
const ref = l.content.match(REF_LINE);
|
|
7661
|
+
if (!ref) continue;
|
|
7662
|
+
const step = enclosingStep(afterLines, i);
|
|
7663
|
+
if (!step || !afterLines.slice(step[0], step[1]).some((x) => CHECKOUT.test(x))) continue;
|
|
7664
|
+
const target = literalValue(ref[1]);
|
|
7665
|
+
if (target === null) continue;
|
|
7666
|
+
out3.push(
|
|
7667
|
+
makeFinding(RULE8, policy, {
|
|
7668
|
+
file: c.path,
|
|
7669
|
+
line: l.newLine,
|
|
7670
|
+
message: `actions/checkout was pinned to ref: ${target} \u2014 the checks run against that ref, not against the change under review.`,
|
|
7671
|
+
evidence: l.content.trim(),
|
|
7672
|
+
remediation: "Let checkout take the event's ref (its default). A suite that checks out main proves nothing about the pull request."
|
|
7673
|
+
})
|
|
7674
|
+
);
|
|
7675
|
+
}
|
|
7676
|
+
if (c.before != null && c.after != null && !isCompositeAction(c.path)) {
|
|
7677
|
+
triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
|
|
7678
|
+
for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
|
|
7679
|
+
out3.push(
|
|
7680
|
+
makeFinding(RULE8, policy, {
|
|
7681
|
+
file: c.path,
|
|
7682
|
+
message: `The workflow's triggers were narrowed: ${reason}.`,
|
|
7683
|
+
evidence: reason,
|
|
7684
|
+
remediation: "Keep the workflow running on push and pull_request for the default branch and its source paths; a check that never runs is no check."
|
|
7685
|
+
})
|
|
7686
|
+
);
|
|
7687
|
+
}
|
|
7688
|
+
}
|
|
7689
|
+
}
|
|
7690
|
+
const afterCommands = afterLines ? commandLines(afterLines) : null;
|
|
7691
|
+
const afterCores = (afterCommands ?? addedLines(c).map((l) => l.content)).map(commandCore);
|
|
7692
|
+
const addedCores = addedLines(c).map((l) => commandCore(afterCommands && l.newLine != null ? afterCommands[l.newLine - 1] : l.content));
|
|
7693
|
+
for (const l of afterLines ?? []) {
|
|
7694
|
+
for (const reused of localUses(l)) {
|
|
7695
|
+
const content = afterByPath.get(reused);
|
|
7696
|
+
if (content == null || reused === c.path) continue;
|
|
7697
|
+
const cores = commandLines(content.split("\n")).map(commandCore);
|
|
7698
|
+
afterCores.push(...cores);
|
|
7699
|
+
addedCores.push(...cores);
|
|
7700
|
+
}
|
|
7701
|
+
}
|
|
7702
|
+
if (!github && c.after == null) {
|
|
7703
|
+
for (const o of changes) {
|
|
7704
|
+
if (o.kind !== "file" || o.op !== "add" || o.after == null || !isProtected(o.path, policy, "ci")) continue;
|
|
7705
|
+
const cores = commandLines(o.after.split("\n")).map(commandCore);
|
|
7706
|
+
afterCores.push(...cores);
|
|
7707
|
+
addedCores.push(...cores);
|
|
7708
|
+
}
|
|
7709
|
+
}
|
|
7710
|
+
const runKey = github ? GH_RUN_KEY : GENERIC_RUN_KEY;
|
|
7711
|
+
for (const l of removedLines(c)) {
|
|
7712
|
+
if (/^\s*#/.test(l.content)) continue;
|
|
7713
|
+
const isUsesLine = usesCheck(l.content);
|
|
7714
|
+
const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? runKey.test(l.content) : true) && invokesCheck2(l.content);
|
|
7715
|
+
if (!isUsesLine && !isCheckCommand) continue;
|
|
7716
|
+
const s = survives(commandCore(l.content), isUsesLine, afterCores, addedCores);
|
|
7717
|
+
if (s.state === "kept") continue;
|
|
7718
|
+
if (s.state === "neutralised") {
|
|
7719
|
+
out3.push(
|
|
7720
|
+
makeFinding(RULE8, policy, {
|
|
7721
|
+
file: c.path,
|
|
7722
|
+
message: "A CI check command was neutralised in place: it still runs, but its result no longer decides (or its suite was narrowed).",
|
|
7723
|
+
evidence: `${l.content.trim()} \u2192 ${s.by}`,
|
|
7724
|
+
remediation: "Let the check decide the step; do not mask its status, cut it short, or narrow what it runs."
|
|
7725
|
+
})
|
|
7726
|
+
);
|
|
7727
|
+
continue;
|
|
7728
|
+
}
|
|
7729
|
+
out3.push(
|
|
7730
|
+
makeFinding(RULE8, policy, {
|
|
7731
|
+
file: c.path,
|
|
7732
|
+
message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
|
|
7733
|
+
evidence: l.content.trim(),
|
|
7734
|
+
remediation: "Restore the step. Removing the check that protects main is itself the tamper."
|
|
7735
|
+
})
|
|
7736
|
+
);
|
|
7737
|
+
}
|
|
7162
7738
|
}
|
|
7163
|
-
|
|
7164
|
-
|
|
7739
|
+
return out3;
|
|
7740
|
+
}
|
|
7165
7741
|
};
|
|
7166
|
-
underDir = (path, dir) => dir === "" || path.startsWith(dir + "/");
|
|
7167
7742
|
}
|
|
7168
7743
|
});
|
|
7169
7744
|
|
|
@@ -7191,7 +7766,7 @@ function safeParse(src) {
|
|
|
7191
7766
|
return null;
|
|
7192
7767
|
}
|
|
7193
7768
|
}
|
|
7194
|
-
function
|
|
7769
|
+
function effective2(raw) {
|
|
7195
7770
|
const v = raw.version;
|
|
7196
7771
|
const version = typeof v === "number" && Number.isInteger(v) && v >= 1 ? v : 1;
|
|
7197
7772
|
const base = defaultPolicy(version);
|
|
@@ -7256,8 +7831,8 @@ function policyWeakening(before, after, probe) {
|
|
|
7256
7831
|
if (b === null) return null;
|
|
7257
7832
|
const a = safeParse(after);
|
|
7258
7833
|
if (a === null) return ["the policy no longer parses as a valid policy (emptied or corrupted)"];
|
|
7259
|
-
const be =
|
|
7260
|
-
const ae =
|
|
7834
|
+
const be = effective2(b);
|
|
7835
|
+
const ae = effective2(a);
|
|
7261
7836
|
const reasons = [];
|
|
7262
7837
|
if (ae.version < be.version) {
|
|
7263
7838
|
reasons.push(
|
|
@@ -7520,7 +8095,7 @@ function entryShape(h) {
|
|
|
7520
8095
|
}
|
|
7521
8096
|
return { problems, runs };
|
|
7522
8097
|
}
|
|
7523
|
-
function judgeGateEntry(event,
|
|
8098
|
+
function judgeGateEntry(event, matcher2, h) {
|
|
7524
8099
|
const cmd = typeof h.command === "string" ? h.command : "";
|
|
7525
8100
|
const a = analyseGateCommand(cmd, event);
|
|
7526
8101
|
const shape = entryShape(h);
|
|
@@ -7528,13 +8103,13 @@ function judgeGateEntry(event, matcher, h) {
|
|
|
7528
8103
|
let runs = a.runs && shape.runs;
|
|
7529
8104
|
let tools = null;
|
|
7530
8105
|
if (event === "PreToolUse") {
|
|
7531
|
-
const sel = matcherSelection(
|
|
8106
|
+
const sel = matcherSelection(matcher2);
|
|
7532
8107
|
tools = sel.tools;
|
|
7533
8108
|
if (sel.problem) {
|
|
7534
8109
|
problems.push(sel.problem);
|
|
7535
8110
|
}
|
|
7536
|
-
} else if (
|
|
7537
|
-
problems.push(`a \`matcher\` (${JSON.stringify(
|
|
8111
|
+
} else if (matcher2 !== void 0 && !(typeof matcher2 === "string" && (matcher2 === "" || matcher2 === "*"))) {
|
|
8112
|
+
problems.push(`a \`matcher\` (${JSON.stringify(matcher2)}) on the Stop entry`);
|
|
7538
8113
|
runs = false;
|
|
7539
8114
|
}
|
|
7540
8115
|
return { runs, problems, pin: a.pin, tools };
|
|
@@ -7741,7 +8316,7 @@ function claudeSettingsFindings(c, policy) {
|
|
|
7741
8316
|
if (c.after == null) return null;
|
|
7742
8317
|
const after = parseSettings(c.after);
|
|
7743
8318
|
const before = c.before != null ? parseSettings(c.before) : null;
|
|
7744
|
-
const finding = (reason) => makeFinding(
|
|
8319
|
+
const finding = (reason) => makeFinding(RULE9, policy, {
|
|
7745
8320
|
file: c.path,
|
|
7746
8321
|
message: `The Claude Code hook wiring was weakened: ${reason}.`,
|
|
7747
8322
|
evidence: reason,
|
|
@@ -7766,7 +8341,7 @@ function policyFindings(c, policy, ctx) {
|
|
|
7766
8341
|
const reasons = c.before == null ? policyAddWeakening(c.after, probe) : policyWeakening(c.before, c.after, probe);
|
|
7767
8342
|
if (reasons === null) return null;
|
|
7768
8343
|
return reasons.map(
|
|
7769
|
-
(reason) => makeFinding(
|
|
8344
|
+
(reason) => makeFinding(RULE9, policy, {
|
|
7770
8345
|
file: c.path,
|
|
7771
8346
|
message: added ? `A policy file was added that weakens the baseline: ${reason}.` : `The policy was weakened: ${reason}.`,
|
|
7772
8347
|
evidence: reason,
|
|
@@ -7876,7 +8451,7 @@ function configFindings(c, changes, policy, ctx) {
|
|
|
7876
8451
|
reasons = after === null ? ["the file no longer parses as YAML \u2014 the tool that reads it runs nothing"] : isLefthook(c.path) ? lefthookWeakening(before, after) : preCommitWeakening(before, after);
|
|
7877
8452
|
}
|
|
7878
8453
|
return reasons.map(
|
|
7879
|
-
(reason) => makeFinding(
|
|
8454
|
+
(reason) => makeFinding(RULE9, policy, {
|
|
7880
8455
|
file: c.path,
|
|
7881
8456
|
message: `The hook configuration was weakened: ${reason}.`,
|
|
7882
8457
|
evidence: reason,
|
|
@@ -7927,7 +8502,7 @@ function huskyRuntimeWrite(path, content, ctx) {
|
|
|
7927
8502
|
(i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
|
|
7928
8503
|
);
|
|
7929
8504
|
}
|
|
7930
|
-
var
|
|
8505
|
+
var RULE9, ownerExec2, EVENTS, isObj, declares, GATE_KEYS, HOOK_TYPES, SIBLING_KEYS, MENTIONS_GATE, FORGERY, ENV_MECHANISM, envMechanism, mentionsGate, carriesGate, CANONICAL_GATE, COMMIT_STAGES, namesGate, liveOrCanonical, HUSKY_SHIM, HUSKY_SHIM_NAMES, HUSKY_H_KNOWN, HUSKY_DEPRECATION, stripTrailingNl, isHuskyScript, isHookNote, hookTampering;
|
|
7931
8506
|
var init_hook_tampering = __esm({
|
|
7932
8507
|
"src/detectors/hook-tampering.ts"() {
|
|
7933
8508
|
"use strict";
|
|
@@ -7940,7 +8515,7 @@ var init_hook_tampering = __esm({
|
|
|
7940
8515
|
init_command();
|
|
7941
8516
|
init_repo();
|
|
7942
8517
|
init_hook_wiring();
|
|
7943
|
-
|
|
8518
|
+
RULE9 = "hook-tampering";
|
|
7944
8519
|
ownerExec2 = (mode) => (parseInt(mode.slice(-3), 8) & 64) !== 0;
|
|
7945
8520
|
EVENTS = ["PreToolUse", "Stop"];
|
|
7946
8521
|
isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
@@ -8008,7 +8583,7 @@ They WILL FAIL in v10.0.0
|
|
|
8008
8583
|
isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
|
|
8009
8584
|
isHookNote = (path) => !/(?:^|\/)\.husky\/_\//.test(path) && /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
|
|
8010
8585
|
hookTampering = {
|
|
8011
|
-
id:
|
|
8586
|
+
id: RULE9,
|
|
8012
8587
|
surface: ["file", "command"],
|
|
8013
8588
|
certainty: "mechanical",
|
|
8014
8589
|
run(changes, policy, _view, ctx) {
|
|
@@ -8021,7 +8596,7 @@ They WILL FAIL in v10.0.0
|
|
|
8021
8596
|
if (isPackageJson(c.path) && c.before != null && c.after != null) {
|
|
8022
8597
|
for (const reason of packageJsonWeakening(c.before, c.after)) {
|
|
8023
8598
|
out3.push(
|
|
8024
|
-
makeFinding(
|
|
8599
|
+
makeFinding(RULE9, policy, {
|
|
8025
8600
|
file: c.path,
|
|
8026
8601
|
message: `The hook installation was weakened: ${reason}.`,
|
|
8027
8602
|
evidence: reason,
|
|
@@ -8034,7 +8609,7 @@ They WILL FAIL in v10.0.0
|
|
|
8034
8609
|
}
|
|
8035
8610
|
if (c.op !== "delete" && c.oldMode && c.newMode && ownerExec2(c.oldMode) && !ownerExec2(c.newMode)) {
|
|
8036
8611
|
out3.push(
|
|
8037
|
-
makeFinding(
|
|
8612
|
+
makeFinding(RULE9, policy, {
|
|
8038
8613
|
file: c.path,
|
|
8039
8614
|
message: `Execute permission was removed from a protected hook (${c.oldMode} \u2192 ${c.newMode}) \u2014 git will skip it.`,
|
|
8040
8615
|
evidence: `${c.path}: mode ${c.oldMode} \u2192 ${c.newMode}`,
|
|
@@ -8044,7 +8619,7 @@ They WILL FAIL in v10.0.0
|
|
|
8044
8619
|
}
|
|
8045
8620
|
if (c.op === "delete") {
|
|
8046
8621
|
out3.push(
|
|
8047
|
-
makeFinding(
|
|
8622
|
+
makeFinding(RULE9, policy, {
|
|
8048
8623
|
file: c.path,
|
|
8049
8624
|
message: "A protected hook/policy file was deleted.",
|
|
8050
8625
|
evidence: c.path,
|
|
@@ -8053,7 +8628,7 @@ They WILL FAIL in v10.0.0
|
|
|
8053
8628
|
);
|
|
8054
8629
|
} else if (c.op === "rename" && !renameKept(c)) {
|
|
8055
8630
|
out3.push(
|
|
8056
|
-
makeFinding(
|
|
8631
|
+
makeFinding(RULE9, policy, {
|
|
8057
8632
|
file: c.path,
|
|
8058
8633
|
message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
|
|
8059
8634
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -8067,7 +8642,7 @@ They WILL FAIL in v10.0.0
|
|
|
8067
8642
|
if (c.before != null && c.after != null) {
|
|
8068
8643
|
for (const reason of codeownersWeakening(c.before, c.after, [...repoGateFiles(ctx), c.path])) {
|
|
8069
8644
|
out3.push(
|
|
8070
|
-
makeFinding(
|
|
8645
|
+
makeFinding(RULE9, policy, {
|
|
8071
8646
|
file: c.path,
|
|
8072
8647
|
message: `CODEOWNERS was weakened: ${reason}.`,
|
|
8073
8648
|
evidence: reason,
|
|
@@ -8089,7 +8664,7 @@ They WILL FAIL in v10.0.0
|
|
|
8089
8664
|
const kept = afterPin !== null && (beforePin === "" ? afterPin === "" || PLAIN_SEMVER.test(afterPin) : pinNotBelow(afterPin, beforePin));
|
|
8090
8665
|
if (!kept) {
|
|
8091
8666
|
out3.push(
|
|
8092
|
-
makeFinding(
|
|
8667
|
+
makeFinding(RULE9, policy, {
|
|
8093
8668
|
file: c.path,
|
|
8094
8669
|
message: "A protected hook script no longer matches the shape `tamperward init` writes.",
|
|
8095
8670
|
evidence: "the gate script no longer matches the shape init wrote; re-run init or sign off",
|
|
@@ -8112,7 +8687,7 @@ They WILL FAIL in v10.0.0
|
|
|
8112
8687
|
if (shebang || !live) {
|
|
8113
8688
|
const why = shebang ?? (gate.length ? `its \`${gate[0].identity}\` is ${gate[0].state}${gate[0].why ? ` (${gate[0].why})` : ""}` : "it runs no `tamperward check`");
|
|
8114
8689
|
out3.push(
|
|
8115
|
-
makeFinding(
|
|
8690
|
+
makeFinding(RULE9, policy, {
|
|
8116
8691
|
file: c.path,
|
|
8117
8692
|
message: `A protected hook script was added that does not run the gate live: ${why}. A new gate script needs a sign-off unless the gate is live in it.`,
|
|
8118
8693
|
evidence: `a hook script was added; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or make the gate live in it`,
|
|
@@ -8121,7 +8696,7 @@ They WILL FAIL in v10.0.0
|
|
|
8121
8696
|
);
|
|
8122
8697
|
} else if (earlyExit) {
|
|
8123
8698
|
out3.push(
|
|
8124
|
-
makeFinding(
|
|
8699
|
+
makeFinding(RULE9, policy, {
|
|
8125
8700
|
file: c.path,
|
|
8126
8701
|
line: earlyExit.newLine ?? void 0,
|
|
8127
8702
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -8143,7 +8718,7 @@ They WILL FAIL in v10.0.0
|
|
|
8143
8718
|
deadGuard2 ? `a guard that can never be true was inserted (\`${deadGuard2.content.trim()}\`)` : null
|
|
8144
8719
|
].filter((d) => d !== null);
|
|
8145
8720
|
out3.push(
|
|
8146
|
-
makeFinding(
|
|
8721
|
+
makeFinding(RULE9, policy, {
|
|
8147
8722
|
file: c.path,
|
|
8148
8723
|
message: `A hand-written protected hook script was changed: ${detail.length ? detail.join("; ") : "an edit other than a pin raise"}. Every edit to a gate script other than raising its pin needs a sign-off.`,
|
|
8149
8724
|
evidence: `the gate script changed; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or restore it`,
|
|
@@ -8155,7 +8730,7 @@ They WILL FAIL in v10.0.0
|
|
|
8155
8730
|
}
|
|
8156
8731
|
if (shebangChanged && c.after != null) {
|
|
8157
8732
|
out3.push(
|
|
8158
|
-
makeFinding(
|
|
8733
|
+
makeFinding(RULE9, policy, {
|
|
8159
8734
|
file: c.path,
|
|
8160
8735
|
line: 1,
|
|
8161
8736
|
message: `A protected hook's interpreter was changed: ${shebang}.`,
|
|
@@ -8166,7 +8741,7 @@ They WILL FAIL in v10.0.0
|
|
|
8166
8741
|
}
|
|
8167
8742
|
if (earlyExit) {
|
|
8168
8743
|
out3.push(
|
|
8169
|
-
makeFinding(
|
|
8744
|
+
makeFinding(RULE9, policy, {
|
|
8170
8745
|
file: c.path,
|
|
8171
8746
|
line: earlyExit.newLine ?? void 0,
|
|
8172
8747
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -8178,7 +8753,7 @@ They WILL FAIL in v10.0.0
|
|
|
8178
8753
|
const deadGuard = c.before == null || c.after == null ? added.find((l) => insertsDeadGuard(l.content)) : void 0;
|
|
8179
8754
|
if (deadGuard) {
|
|
8180
8755
|
out3.push(
|
|
8181
|
-
makeFinding(
|
|
8756
|
+
makeFinding(RULE9, policy, {
|
|
8182
8757
|
file: c.path,
|
|
8183
8758
|
line: deadGuard.newLine ?? void 0,
|
|
8184
8759
|
message: "A guard that can never be true was inserted into a protected hook \u2014 what it wraps no longer runs.",
|
|
@@ -8190,7 +8765,7 @@ They WILL FAIL in v10.0.0
|
|
|
8190
8765
|
const lost = scriptWeakening(beforeLines, afterLines, opts);
|
|
8191
8766
|
if (lost.length) {
|
|
8192
8767
|
out3.push(
|
|
8193
|
-
makeFinding(
|
|
8768
|
+
makeFinding(RULE9, policy, {
|
|
8194
8769
|
file: c.path,
|
|
8195
8770
|
message: `A check invocation was removed from a protected hook: ${lost[0].reason}.`,
|
|
8196
8771
|
evidence: lost[0].evidence,
|
|
@@ -8209,7 +8784,7 @@ They WILL FAIL in v10.0.0
|
|
|
8209
8784
|
const m = l.content.match(INLINE);
|
|
8210
8785
|
if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
|
|
8211
8786
|
out3.push(
|
|
8212
|
-
makeFinding(
|
|
8787
|
+
makeFinding(RULE9, policy, {
|
|
8213
8788
|
file: c.path,
|
|
8214
8789
|
line: l.newLine ?? void 0,
|
|
8215
8790
|
message: `Rule "${m[1]}" was lowered from block to warn in the policy.`,
|
|
@@ -8219,7 +8794,7 @@ They WILL FAIL in v10.0.0
|
|
|
8219
8794
|
);
|
|
8220
8795
|
} else if (/enabled:\s*false\b/.test(l.content)) {
|
|
8221
8796
|
out3.push(
|
|
8222
|
-
makeFinding(
|
|
8797
|
+
makeFinding(RULE9, policy, {
|
|
8223
8798
|
file: c.path,
|
|
8224
8799
|
line: l.newLine ?? void 0,
|
|
8225
8800
|
message: "A rule was disabled (enabled: false) in the policy.",
|
|
@@ -8237,7 +8812,7 @@ They WILL FAIL in v10.0.0
|
|
|
8237
8812
|
const hiding = updateIndexHiding(toks, policy);
|
|
8238
8813
|
if (hiding) {
|
|
8239
8814
|
out3.push(
|
|
8240
|
-
makeFinding(
|
|
8815
|
+
makeFinding(RULE9, policy, {
|
|
8241
8816
|
file: repoRelative(hiding.file, ctx),
|
|
8242
8817
|
message: `Hook tampering via shell: ${hiding.reason}.`,
|
|
8243
8818
|
evidence: seg,
|
|
@@ -8253,7 +8828,7 @@ They WILL FAIL in v10.0.0
|
|
|
8253
8828
|
const file = target ? repoRelative(target, ctx) : void 0;
|
|
8254
8829
|
const outside = file !== void 0 && (isAbsolute6(file) || /^[A-Za-z]:\//.test(file)) && isClaudeSettings(file) ? ` \u2014 ${file} is a Claude Code settings file the runtime reads hooks from` : "";
|
|
8255
8830
|
out3.push(
|
|
8256
|
-
makeFinding(
|
|
8831
|
+
makeFinding(RULE9, policy, {
|
|
8257
8832
|
...file !== void 0 ? { file } : {},
|
|
8258
8833
|
message: `Hook tampering via shell: ${why}${outside}.`,
|
|
8259
8834
|
evidence: seg,
|
|
@@ -8801,7 +9376,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
8801
9376
|
if (!was || !now || was.path === now.path) return [];
|
|
8802
9377
|
const samples = runnerSamples(now.path, policy, ctx, "pytest");
|
|
8803
9378
|
return suiteNarrowings(was.content, now.content, now.path, samples).map(
|
|
8804
|
-
(n) => makeFinding(
|
|
9379
|
+
(n) => makeFinding(RULE10, policy, {
|
|
8805
9380
|
file: now.path,
|
|
8806
9381
|
message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
|
|
8807
9382
|
evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
|
|
@@ -8809,7 +9384,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
8809
9384
|
})
|
|
8810
9385
|
);
|
|
8811
9386
|
}
|
|
8812
|
-
var
|
|
9387
|
+
var RULE10, JVM_TEST, TEST_DEFS, isTableOf, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
|
|
8813
9388
|
var init_test_deletion = __esm({
|
|
8814
9389
|
"src/detectors/test-deletion.ts"() {
|
|
8815
9390
|
"use strict";
|
|
@@ -8826,7 +9401,7 @@ var init_test_deletion = __esm({
|
|
|
8826
9401
|
init_narrow();
|
|
8827
9402
|
init_reachability();
|
|
8828
9403
|
init_runner_chain();
|
|
8829
|
-
|
|
9404
|
+
RULE10 = "test-deletion";
|
|
8830
9405
|
JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
|
|
8831
9406
|
TEST_DEFS = {
|
|
8832
9407
|
py: /^\s*(?:async\s+)?def\s+test\w*\s*\(/,
|
|
@@ -8849,7 +9424,7 @@ var init_test_deletion = __esm({
|
|
|
8849
9424
|
SLICE_SCRIPT = /^test:(?:unit|integration)$/;
|
|
8850
9425
|
SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
|
|
8851
9426
|
testDeletion = {
|
|
8852
|
-
id:
|
|
9427
|
+
id: RULE10,
|
|
8853
9428
|
surface: ["file", "command"],
|
|
8854
9429
|
certainty: "mechanical",
|
|
8855
9430
|
run(changes, policy, _view, ctx) {
|
|
@@ -8881,7 +9456,7 @@ var init_test_deletion = __esm({
|
|
|
8881
9456
|
if (c.op === "delete" && isTest) {
|
|
8882
9457
|
if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
|
|
8883
9458
|
out3.push(
|
|
8884
|
-
makeFinding(
|
|
9459
|
+
makeFinding(RULE10, policy, {
|
|
8885
9460
|
file: c.path,
|
|
8886
9461
|
message: "A test file was deleted.",
|
|
8887
9462
|
evidence: c.path,
|
|
@@ -8890,7 +9465,7 @@ var init_test_deletion = __esm({
|
|
|
8890
9465
|
);
|
|
8891
9466
|
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && isSpecShaped(c.oldPath, c.before, c.after) && !isSpec(c.path)) {
|
|
8892
9467
|
out3.push(
|
|
8893
|
-
makeFinding(
|
|
9468
|
+
makeFinding(RULE10, policy, {
|
|
8894
9469
|
file: c.path,
|
|
8895
9470
|
message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
|
|
8896
9471
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -8899,7 +9474,7 @@ var init_test_deletion = __esm({
|
|
|
8899
9474
|
);
|
|
8900
9475
|
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && isTest && leavesWalk(c.oldPath, c.path)) {
|
|
8901
9476
|
out3.push(
|
|
8902
|
-
makeFinding(
|
|
9477
|
+
makeFinding(RULE10, policy, {
|
|
8903
9478
|
file: c.path,
|
|
8904
9479
|
message: `A test file was renamed into a path the runner does not walk (${c.oldPath} \u2192 ${c.path}): ${leavesWalk(c.oldPath, c.path)}.`,
|
|
8905
9480
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -8914,7 +9489,7 @@ var init_test_deletion = __esm({
|
|
|
8914
9489
|
const removed = new Set([...significantLines(c.before, c.path)].filter((l) => !afterLines.has(l)));
|
|
8915
9490
|
if (isRelocation(removed, before.min - after.min)) continue;
|
|
8916
9491
|
out3.push(
|
|
8917
|
-
makeFinding(
|
|
9492
|
+
makeFinding(RULE10, policy, {
|
|
8918
9493
|
file: c.path,
|
|
8919
9494
|
message: `Test blocks removed: ${before.min} \u2192 ${after.min} it()/test() in this spec (each-table rows count as tests).`,
|
|
8920
9495
|
evidence: `${before.min - after.min} test block(s) removed from ${c.path}`,
|
|
@@ -8925,9 +9500,22 @@ var init_test_deletion = __esm({
|
|
|
8925
9500
|
} else if (c.op !== "delete" && c.after != null && !isSpec(c.path) && isProtected(c.path, policy, "config") && runnerOf(c.path, c.after)) {
|
|
8926
9501
|
const rn = runnerOf(c.path, c.after);
|
|
8927
9502
|
const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
|
|
9503
|
+
for (const d of suiteDelegations(c.before, c.after, c.path)) {
|
|
9504
|
+
const cands = targetCandidates(c.path, d.target);
|
|
9505
|
+
const added = cands.find((f) => addedPaths.has(f));
|
|
9506
|
+
if (added === void 0 && cands.some((f) => isProtected(f, policy, "config"))) continue;
|
|
9507
|
+
out3.push(
|
|
9508
|
+
makeFinding(RULE10, policy, {
|
|
9509
|
+
file: c.path,
|
|
9510
|
+
message: `The runner config now delegates its selection to ${d.target}, ${added !== void 0 ? `a file this change adds (${added})` : "a file no protected glob covers"}: ${d.how}.`,
|
|
9511
|
+
evidence: `${c.path}: ${d.how}`,
|
|
9512
|
+
remediation: "Keep the selection in the protected config, or protect the file it delegates to (protected.config in .tamperward.yml). A config the gate cannot read can select nothing."
|
|
9513
|
+
})
|
|
9514
|
+
);
|
|
9515
|
+
}
|
|
8928
9516
|
for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
|
|
8929
9517
|
out3.push(
|
|
8930
|
-
makeFinding(
|
|
9518
|
+
makeFinding(RULE10, policy, {
|
|
8931
9519
|
file: c.path,
|
|
8932
9520
|
message: `The runner no longer selects ${n.path}: ${n.reason}.`,
|
|
8933
9521
|
evidence: `${n.path} dropped from the suite by ${c.path}`,
|
|
@@ -8939,12 +9527,12 @@ var init_test_deletion = __esm({
|
|
|
8939
9527
|
const w = n.weakening;
|
|
8940
9528
|
const evidence = `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${n.after === "" ? "deleted" : JSON.stringify(n.after)}`;
|
|
8941
9529
|
out3.push(
|
|
8942
|
-
w.state === "removed" ? makeFinding(
|
|
9530
|
+
w.state === "removed" ? makeFinding(RULE10, policy, {
|
|
8943
9531
|
file: c.path,
|
|
8944
9532
|
message: `The test script no longer runs the ${w.kind} check: scripts.${n.script} ${w.what}.`,
|
|
8945
9533
|
evidence,
|
|
8946
9534
|
remediation: "Keep the script running its check. Replacing the runner in package.json removes every test from every `npm test` while the workflow line stays untouched."
|
|
8947
|
-
}) : makeFinding(
|
|
9535
|
+
}) : makeFinding(RULE10, policy, {
|
|
8948
9536
|
file: c.path,
|
|
8949
9537
|
message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${n.script}.`,
|
|
8950
9538
|
evidence,
|
|
@@ -9079,7 +9667,7 @@ var init_test_deletion = __esm({
|
|
|
9079
9667
|
}
|
|
9080
9668
|
if (why) {
|
|
9081
9669
|
out3.push(
|
|
9082
|
-
makeFinding(
|
|
9670
|
+
makeFinding(RULE10, policy, {
|
|
9083
9671
|
message: `Test removed via shell: ${why}.`,
|
|
9084
9672
|
evidence,
|
|
9085
9673
|
remediation: "Fix the code under test. Mutating a spec from the shell is still deleting the test \u2014 and is blocked the same way."
|
|
@@ -9138,7 +9726,7 @@ function significantLinesOrdered(src, path) {
|
|
|
9138
9726
|
function liveSignificantLines(src, path) {
|
|
9139
9727
|
return significantLinesOrdered(partition(src, path).live, path);
|
|
9140
9728
|
}
|
|
9141
|
-
var
|
|
9729
|
+
var RULE11, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, KeptPool, testContentRemoval;
|
|
9142
9730
|
var init_test_content_removal = __esm({
|
|
9143
9731
|
"src/detectors/test-content-removal.ts"() {
|
|
9144
9732
|
"use strict";
|
|
@@ -9149,7 +9737,7 @@ var init_test_content_removal = __esm({
|
|
|
9149
9737
|
init_files();
|
|
9150
9738
|
init_spec_shape();
|
|
9151
9739
|
init_reachability();
|
|
9152
|
-
|
|
9740
|
+
RULE11 = "test-content-removal";
|
|
9153
9741
|
MIN_REMOVED_LINES = 3;
|
|
9154
9742
|
MIN_REMOVED_ROWS = 2;
|
|
9155
9743
|
ws = (s) => s.replace(/\s+/g, "");
|
|
@@ -9190,7 +9778,7 @@ var init_test_content_removal = __esm({
|
|
|
9190
9778
|
}
|
|
9191
9779
|
};
|
|
9192
9780
|
testContentRemoval = {
|
|
9193
|
-
id:
|
|
9781
|
+
id: RULE11,
|
|
9194
9782
|
surface: ["file"],
|
|
9195
9783
|
certainty: "mechanical",
|
|
9196
9784
|
run(changes, policy) {
|
|
@@ -9234,7 +9822,7 @@ var init_test_content_removal = __esm({
|
|
|
9234
9822
|
const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
|
|
9235
9823
|
if (goneRows.length >= MIN_REMOVED_ROWS) {
|
|
9236
9824
|
out3.push(
|
|
9237
|
-
makeFinding(
|
|
9825
|
+
makeFinding(RULE11, policy, {
|
|
9238
9826
|
file: c.path,
|
|
9239
9827
|
message: `Rows removed from a test table: ${goneRows.length} case(s) gone from an each-table that spreads from elsewhere, and they reappear nowhere in the change.`,
|
|
9240
9828
|
evidence: goneRows.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9245,7 +9833,7 @@ var init_test_content_removal = __esm({
|
|
|
9245
9833
|
}
|
|
9246
9834
|
if (hidden.length >= MIN_REMOVED_LINES) {
|
|
9247
9835
|
out3.push(
|
|
9248
|
-
makeFinding(
|
|
9836
|
+
makeFinding(RULE11, policy, {
|
|
9249
9837
|
file: c.path,
|
|
9250
9838
|
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).`,
|
|
9251
9839
|
evidence: hidden.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9256,7 +9844,7 @@ var init_test_content_removal = __esm({
|
|
|
9256
9844
|
}
|
|
9257
9845
|
if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
|
|
9258
9846
|
out3.push(
|
|
9259
|
-
makeFinding(
|
|
9847
|
+
makeFinding(RULE11, policy, {
|
|
9260
9848
|
file: c.path,
|
|
9261
9849
|
message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
|
|
9262
9850
|
evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9279,14 +9867,14 @@ function namesProtectedSnapshot(seg, policy) {
|
|
|
9279
9867
|
}
|
|
9280
9868
|
return null;
|
|
9281
9869
|
}
|
|
9282
|
-
var
|
|
9870
|
+
var RULE12, RUNNER3, PM_TEST_SCRIPT, UPDATE_ANY, UPDATE_WITH_RUNNER, INSTA_ACCEPT, UPDATE_EXPECT, REGEN_SCRIPT, MUTATE, snapshotRewrite;
|
|
9283
9871
|
var init_snapshot_rewrite = __esm({
|
|
9284
9872
|
"src/detectors/snapshot-rewrite.ts"() {
|
|
9285
9873
|
"use strict";
|
|
9286
9874
|
init_policy();
|
|
9287
9875
|
init_finding();
|
|
9288
9876
|
init_command();
|
|
9289
|
-
|
|
9877
|
+
RULE12 = "snapshot-rewrite";
|
|
9290
9878
|
RUNNER3 = /\b(?:jest|vitest|playwright|ava)\b/;
|
|
9291
9879
|
PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
|
|
9292
9880
|
UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
|
|
@@ -9296,12 +9884,12 @@ var init_snapshot_rewrite = __esm({
|
|
|
9296
9884
|
REGEN_SCRIPT = /\b(?:update|regen(?:erate)?|re?bless)[-_.]?(?:golden|snapshots?|baselines?|expected)\b/i;
|
|
9297
9885
|
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/;
|
|
9298
9886
|
snapshotRewrite = {
|
|
9299
|
-
id:
|
|
9887
|
+
id: RULE12,
|
|
9300
9888
|
surface: ["command", "file"],
|
|
9301
9889
|
certainty: "mechanical",
|
|
9302
9890
|
run(changes, policy) {
|
|
9303
9891
|
const out3 = [];
|
|
9304
|
-
const warn = (input) => out3.push(makeFinding(
|
|
9892
|
+
const warn = (input) => out3.push(makeFinding(RULE12, policy, { ...input, defaultSeverity: "warn" }));
|
|
9305
9893
|
for (const c of changes) {
|
|
9306
9894
|
if (c.kind === "command") {
|
|
9307
9895
|
for (const seg of segments(c.raw)) {
|
|
@@ -9349,16 +9937,16 @@ function isSnapshotChange(c, policy) {
|
|
|
9349
9937
|
if (isProtected(c.path, policy, "snapshots")) return true;
|
|
9350
9938
|
return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
|
|
9351
9939
|
}
|
|
9352
|
-
var
|
|
9940
|
+
var RULE13, COMMIT_VIEWS, snapshotOnlyRewrite;
|
|
9353
9941
|
var init_snapshot_only = __esm({
|
|
9354
9942
|
"src/detectors/snapshot-only.ts"() {
|
|
9355
9943
|
"use strict";
|
|
9356
9944
|
init_policy();
|
|
9357
9945
|
init_finding();
|
|
9358
|
-
|
|
9946
|
+
RULE13 = "snapshot-only-rewrite";
|
|
9359
9947
|
COMMIT_VIEWS = ["staged", "range"];
|
|
9360
9948
|
snapshotOnlyRewrite = {
|
|
9361
|
-
id:
|
|
9949
|
+
id: RULE13,
|
|
9362
9950
|
surface: ["file"],
|
|
9363
9951
|
certainty: "mechanical",
|
|
9364
9952
|
run(changes, policy, view) {
|
|
@@ -9371,7 +9959,7 @@ var init_snapshot_only = __esm({
|
|
|
9371
9959
|
if (!first) return [];
|
|
9372
9960
|
const names = snaps.map((c) => c.path).slice(0, 3).join(", ");
|
|
9373
9961
|
return [
|
|
9374
|
-
makeFinding(
|
|
9962
|
+
makeFinding(RULE13, policy, {
|
|
9375
9963
|
file: first.path,
|
|
9376
9964
|
message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
|
|
9377
9965
|
evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
|
|
@@ -9428,7 +10016,7 @@ function staticFacts(node) {
|
|
|
9428
10016
|
}
|
|
9429
10017
|
function expectAssertion(node, sf) {
|
|
9430
10018
|
if (!ts.isPropertyAccessExpression(node.expression)) return null;
|
|
9431
|
-
const
|
|
10019
|
+
const matcher2 = node.expression.name.text;
|
|
9432
10020
|
let receiver = node.expression.expression;
|
|
9433
10021
|
let polarity = "";
|
|
9434
10022
|
if (ts.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
|
|
@@ -9443,10 +10031,10 @@ function expectAssertion(node, sf) {
|
|
|
9443
10031
|
return {
|
|
9444
10032
|
subject,
|
|
9445
10033
|
polarity,
|
|
9446
|
-
matcher,
|
|
10034
|
+
matcher: matcher2,
|
|
9447
10035
|
args,
|
|
9448
10036
|
firstArgFacts: staticFacts(node.arguments[0]),
|
|
9449
|
-
canonical: [subject, polarity,
|
|
10037
|
+
canonical: [subject, polarity, matcher2, ...args].join("|"),
|
|
9450
10038
|
text: node.getText(sf).replace(/\s+/g, " ")
|
|
9451
10039
|
};
|
|
9452
10040
|
}
|
|
@@ -9549,7 +10137,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
9549
10137
|
const weakened2 = directionalWeakening(before.assertions, after.assertions);
|
|
9550
10138
|
if (weakened2) {
|
|
9551
10139
|
const message = weakened2.kind === "throw-specificity" ? `Assertion weakened in test "${before.identity}": exception-message specificity was removed from toThrow().` : `Assertion weakened in test "${before.identity}": ${weakened2.before.matcher} was replaced by weaker ${weakened2.after.matcher} on the same subject.`;
|
|
9552
|
-
return makeFinding(
|
|
10140
|
+
return makeFinding(RULE14, policy, {
|
|
9553
10141
|
file: path,
|
|
9554
10142
|
message,
|
|
9555
10143
|
evidence: `${weakened2.before.text} -> ${weakened2.after.text}`,
|
|
@@ -9558,7 +10146,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
9558
10146
|
}
|
|
9559
10147
|
const removed = pureRemoval(before.assertions, after.assertions);
|
|
9560
10148
|
if (removed) {
|
|
9561
|
-
return makeFinding(
|
|
10149
|
+
return makeFinding(RULE14, policy, {
|
|
9562
10150
|
file: path,
|
|
9563
10151
|
message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
|
|
9564
10152
|
evidence: removed.text,
|
|
@@ -9567,7 +10155,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
9567
10155
|
}
|
|
9568
10156
|
return null;
|
|
9569
10157
|
}
|
|
9570
|
-
var
|
|
10158
|
+
var RULE14, TEST_CALLS, SUITE_CALLS, EXACT_MATCHERS, WEAK_MATCHERS, compact, assertionWeakening;
|
|
9571
10159
|
var init_assertion_weakening = __esm({
|
|
9572
10160
|
"src/detectors/assertion-weakening.ts"() {
|
|
9573
10161
|
"use strict";
|
|
@@ -9576,14 +10164,14 @@ var init_assertion_weakening = __esm({
|
|
|
9576
10164
|
init_finding();
|
|
9577
10165
|
init_files();
|
|
9578
10166
|
init_runner_chain();
|
|
9579
|
-
|
|
10167
|
+
RULE14 = "assertion-weakening";
|
|
9580
10168
|
TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
9581
10169
|
SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
|
|
9582
10170
|
EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
|
|
9583
10171
|
WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
|
|
9584
10172
|
compact = (s) => s.replace(/\s+/g, "");
|
|
9585
10173
|
assertionWeakening = {
|
|
9586
|
-
id:
|
|
10174
|
+
id: RULE14,
|
|
9587
10175
|
surface: ["file"],
|
|
9588
10176
|
certainty: "heuristic",
|
|
9589
10177
|
run(changes, policy) {
|
|
@@ -9617,7 +10205,7 @@ function significant(src, path) {
|
|
|
9617
10205
|
}
|
|
9618
10206
|
return out3;
|
|
9619
10207
|
}
|
|
9620
|
-
var
|
|
10208
|
+
var RULE15, MIN_REMOVED_LINES2, ws2, testSupport;
|
|
9621
10209
|
var init_test_support = __esm({
|
|
9622
10210
|
"src/detectors/test-support.ts"() {
|
|
9623
10211
|
"use strict";
|
|
@@ -9625,11 +10213,11 @@ var init_test_support = __esm({
|
|
|
9625
10213
|
init_finding();
|
|
9626
10214
|
init_spec_shape();
|
|
9627
10215
|
init_files();
|
|
9628
|
-
|
|
10216
|
+
RULE15 = "test-support";
|
|
9629
10217
|
MIN_REMOVED_LINES2 = 3;
|
|
9630
10218
|
ws2 = (s) => s.replace(/\s+/g, "");
|
|
9631
10219
|
testSupport = {
|
|
9632
|
-
id:
|
|
10220
|
+
id: RULE15,
|
|
9633
10221
|
surface: ["file"],
|
|
9634
10222
|
certainty: "mechanical",
|
|
9635
10223
|
run(changes, policy) {
|
|
@@ -9644,7 +10232,7 @@ var init_test_support = __esm({
|
|
|
9644
10232
|
if (c.op === "delete") {
|
|
9645
10233
|
if (!isSupport(c.path, c.before, null)) continue;
|
|
9646
10234
|
out3.push(
|
|
9647
|
-
makeFinding(
|
|
10235
|
+
makeFinding(RULE15, policy, {
|
|
9648
10236
|
file: c.path,
|
|
9649
10237
|
message: "A test-support file was deleted (a helper, setup module or fixture under the test globs; it defines no test).",
|
|
9650
10238
|
evidence: c.path,
|
|
@@ -9654,7 +10242,7 @@ var init_test_support = __esm({
|
|
|
9654
10242
|
);
|
|
9655
10243
|
} else if (c.op === "rename" && c.oldPath && isSupport(c.oldPath, c.before, c.after) && !isProtected(c.path, policy, "tests")) {
|
|
9656
10244
|
out3.push(
|
|
9657
|
-
makeFinding(
|
|
10245
|
+
makeFinding(RULE15, policy, {
|
|
9658
10246
|
file: c.path,
|
|
9659
10247
|
message: `A test-support file was renamed out of the test globs (${c.oldPath} \u2192 ${c.path}).`,
|
|
9660
10248
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -9672,7 +10260,7 @@ var init_test_support = __esm({
|
|
|
9672
10260
|
const gone = [...new Set(beforeSig)].filter((l) => !afterSet.has(l) && !kept.includes(ws2(l)));
|
|
9673
10261
|
if (gone.length < MIN_REMOVED_LINES2) continue;
|
|
9674
10262
|
out3.push(
|
|
9675
|
-
makeFinding(
|
|
10263
|
+
makeFinding(RULE15, policy, {
|
|
9676
10264
|
file: c.path,
|
|
9677
10265
|
message: `Test-support content removed: ${gone.length} significant line(s) gone from a helper, setup module or fixture under the test globs.`,
|
|
9678
10266
|
evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9699,6 +10287,7 @@ var init_detectors = __esm({
|
|
|
9699
10287
|
init_lint_suppression();
|
|
9700
10288
|
init_test_skip();
|
|
9701
10289
|
init_coverage_lowering();
|
|
10290
|
+
init_config_weakening();
|
|
9702
10291
|
init_coverage_exclusion();
|
|
9703
10292
|
init_ci_tampering();
|
|
9704
10293
|
init_hook_tampering();
|
|
@@ -9715,6 +10304,7 @@ var init_detectors = __esm({
|
|
|
9715
10304
|
lintSuppression,
|
|
9716
10305
|
testSkip,
|
|
9717
10306
|
coverageLowering,
|
|
10307
|
+
configWeakening,
|
|
9718
10308
|
coverageExclusion,
|
|
9719
10309
|
ciTampering,
|
|
9720
10310
|
hookTampering,
|
|
@@ -11188,7 +11778,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
|
|
|
11188
11778
|
};
|
|
11189
11779
|
}
|
|
11190
11780
|
function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
11191
|
-
if (!isEnabled(
|
|
11781
|
+
if (!isEnabled(RULE16, policy)) return [];
|
|
11192
11782
|
const byPath = /* @__PURE__ */ new Map();
|
|
11193
11783
|
for (const e of events) {
|
|
11194
11784
|
if (!isProtected(e.path, policy)) continue;
|
|
@@ -11208,7 +11798,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
11208
11798
|
const modeChurn = modes.size > 1;
|
|
11209
11799
|
if (!contentChurn && !modeChurn) continue;
|
|
11210
11800
|
const what = contentChurn ? `content was modified and restored within the turn (${hashes.size} distinct states observed)` : `file mode changed during the turn (${[...modes].map((m) => (m & 4095).toString(8)).join(" \u2192 ")})`;
|
|
11211
|
-
const f = makeFinding(
|
|
11801
|
+
const f = makeFinding(RULE16, policy, {
|
|
11212
11802
|
file: path,
|
|
11213
11803
|
message: `Transient protected mutation: ${what}, leaving no net diff.`,
|
|
11214
11804
|
evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
|
|
@@ -11223,14 +11813,14 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
11223
11813
|
}
|
|
11224
11814
|
return out3;
|
|
11225
11815
|
}
|
|
11226
|
-
var
|
|
11816
|
+
var RULE16, MAX_EVENT_READ_BYTES, MAX_EVENT_SWEEP_BYTES, DEFAULT_EVENT_READ_IO;
|
|
11227
11817
|
var init_fs_events = __esm({
|
|
11228
11818
|
"src/detectors/fs-events.ts"() {
|
|
11229
11819
|
"use strict";
|
|
11230
11820
|
init_policy();
|
|
11231
11821
|
init_finding();
|
|
11232
11822
|
init_narrow();
|
|
11233
|
-
|
|
11823
|
+
RULE16 = "transient-protected-mutation";
|
|
11234
11824
|
MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
|
|
11235
11825
|
MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
|
|
11236
11826
|
DEFAULT_EVENT_READ_IO = {
|
|
@@ -11294,10 +11884,7 @@ function failClosed(kind, detail) {
|
|
|
11294
11884
|
};
|
|
11295
11885
|
return verdict([f], kind);
|
|
11296
11886
|
}
|
|
11297
|
-
function
|
|
11298
|
-
if (blocks2.length === 0) return { exitCode: 0, stdout: "" };
|
|
11299
|
-
recordDenylog(blocks2);
|
|
11300
|
-
const reason = formatDenial(blocks2);
|
|
11887
|
+
function denyWire(reason, kind) {
|
|
11301
11888
|
const payload = kind === "PreToolUse" ? {
|
|
11302
11889
|
hookSpecificOutput: {
|
|
11303
11890
|
hookEventName: "PreToolUse",
|
|
@@ -11305,7 +11892,13 @@ function verdict(blocks2, kind) {
|
|
|
11305
11892
|
permissionDecisionReason: reason
|
|
11306
11893
|
}
|
|
11307
11894
|
} : { decision: "block", reason };
|
|
11308
|
-
return
|
|
11895
|
+
return JSON.stringify(payload) + "\n";
|
|
11896
|
+
}
|
|
11897
|
+
function verdict(blocks2, kind) {
|
|
11898
|
+
if (blocks2.length === 0) return { exitCode: 0, stdout: "" };
|
|
11899
|
+
recordDenylog(blocks2);
|
|
11900
|
+
const reason = formatDenial(blocks2);
|
|
11901
|
+
return { exitCode: 0, stdout: denyWire(reason, kind) };
|
|
11309
11902
|
}
|
|
11310
11903
|
function fileChanges(changes) {
|
|
11311
11904
|
return changes.filter((c) => c.kind === "file");
|
|
@@ -11483,9 +12076,13 @@ function sanctionPredictedWrites(cwd, sessionId, policy, changes) {
|
|
|
11483
12076
|
}
|
|
11484
12077
|
if (dirty) savePtree(cwd, sessionId, tree);
|
|
11485
12078
|
}
|
|
11486
|
-
function preToolUseVerdict(input, defaultCwd) {
|
|
12079
|
+
function preToolUseVerdict(input, defaultCwd, trustedRoot) {
|
|
11487
12080
|
try {
|
|
11488
12081
|
const sessionCwd = input.cwd ?? defaultCwd ?? process.cwd();
|
|
12082
|
+
if (trustedRoot !== void 0) {
|
|
12083
|
+
const v = validateClaimAgainstRoot(input.cwd, trustedRoot, defaultCwd ?? process.cwd());
|
|
12084
|
+
if (!v.ok) return failClosed("PreToolUse", `repository identity claim rejected: ${v.rejected}`);
|
|
12085
|
+
}
|
|
11489
12086
|
const cwd = repoRoot(sessionCwd);
|
|
11490
12087
|
turnBaseline(cwd, input.session_id);
|
|
11491
12088
|
const policy = loadPolicy(cwd);
|
|
@@ -11592,9 +12189,13 @@ function recordObserverHealth(state, reason) {
|
|
|
11592
12189
|
} catch {
|
|
11593
12190
|
}
|
|
11594
12191
|
}
|
|
11595
|
-
function stopVerdict(input, defaultCwd) {
|
|
12192
|
+
function stopVerdict(input, defaultCwd, trustedRoot) {
|
|
11596
12193
|
if (input.stop_hook_active) return { exitCode: 0, stdout: "" };
|
|
11597
12194
|
const sessionCwd = input.cwd ?? defaultCwd ?? process.cwd();
|
|
12195
|
+
if (trustedRoot !== void 0) {
|
|
12196
|
+
const v = validateClaimAgainstRoot(input.cwd, trustedRoot, defaultCwd ?? process.cwd());
|
|
12197
|
+
if (!v.ok) return failClosed("Stop", `repository identity claim rejected: ${v.rejected}`);
|
|
12198
|
+
}
|
|
11598
12199
|
const ctx = repoContext(sessionCwd);
|
|
11599
12200
|
if (!ctx) {
|
|
11600
12201
|
const why = outsideRepository(sessionCwd);
|
|
@@ -11637,16 +12238,16 @@ function emit(r) {
|
|
|
11637
12238
|
if (r.stdout) process.stdout.write(r.stdout);
|
|
11638
12239
|
return r.exitCode;
|
|
11639
12240
|
}
|
|
11640
|
-
function preToolUseFromRaw(raw, defaultCwd) {
|
|
12241
|
+
function preToolUseFromRaw(raw, defaultCwd, trustedRoot) {
|
|
11641
12242
|
try {
|
|
11642
|
-
return preToolUseVerdict(parseInput(raw), defaultCwd);
|
|
12243
|
+
return preToolUseVerdict(parseInput(raw), defaultCwd, trustedRoot);
|
|
11643
12244
|
} catch (e) {
|
|
11644
12245
|
return failClosed("PreToolUse", errText2(e));
|
|
11645
12246
|
}
|
|
11646
12247
|
}
|
|
11647
|
-
function stopFromRaw(raw, defaultCwd) {
|
|
12248
|
+
function stopFromRaw(raw, defaultCwd, trustedRoot) {
|
|
11648
12249
|
try {
|
|
11649
|
-
return stopVerdict(parseInput(raw), defaultCwd);
|
|
12250
|
+
return stopVerdict(parseInput(raw), defaultCwd, trustedRoot);
|
|
11650
12251
|
} catch (e) {
|
|
11651
12252
|
return failClosed("Stop", errText2(e));
|
|
11652
12253
|
}
|
|
@@ -11792,7 +12393,7 @@ var init_ptree_cache = __esm({
|
|
|
11792
12393
|
// src/cli/hook-service.ts
|
|
11793
12394
|
import { chmodSync, existsSync as existsSync10, lstatSync as lstatSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync11, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
11794
12395
|
import { createConnection as createConnection2, createServer } from "node:net";
|
|
11795
|
-
import { realpathSync as
|
|
12396
|
+
import { realpathSync as realpathSync4 } from "node:fs";
|
|
11796
12397
|
import { sep } from "node:path";
|
|
11797
12398
|
function readServiceState(paths) {
|
|
11798
12399
|
try {
|
|
@@ -11830,7 +12431,7 @@ function listening(socket) {
|
|
|
11830
12431
|
function within(root, cwd) {
|
|
11831
12432
|
let real;
|
|
11832
12433
|
try {
|
|
11833
|
-
real =
|
|
12434
|
+
real = realpathSync4(cwd);
|
|
11834
12435
|
} catch {
|
|
11835
12436
|
return false;
|
|
11836
12437
|
}
|
|
@@ -11895,7 +12496,7 @@ async function startHookService(opts) {
|
|
|
11895
12496
|
if (uid === void 0) throw new Error("no uid on this platform; refusing to start");
|
|
11896
12497
|
let root;
|
|
11897
12498
|
try {
|
|
11898
|
-
root =
|
|
12499
|
+
root = realpathSync4(repoRoot(realpathSync4(opts.root)));
|
|
11899
12500
|
} catch {
|
|
11900
12501
|
throw new Error(`cannot resolve ${opts.root}`);
|
|
11901
12502
|
}
|
|
@@ -12262,8 +12863,8 @@ var init_safe_write = __esm({
|
|
|
12262
12863
|
// src/cli/init.ts
|
|
12263
12864
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
12264
12865
|
import { createHash as createHash6 } from "node:crypto";
|
|
12265
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as
|
|
12266
|
-
import { basename as
|
|
12866
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync5 } from "node:fs";
|
|
12867
|
+
import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute8, join as join14, relative as relative3, resolve as resolve4 } from "node:path";
|
|
12267
12868
|
function gitConfig(cwd, key2) {
|
|
12268
12869
|
try {
|
|
12269
12870
|
return execFileSync6("git", ["config", "--get", key2], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -12322,13 +12923,13 @@ function readWorkflowMark(src) {
|
|
|
12322
12923
|
if (!m) return null;
|
|
12323
12924
|
return { version: m[1], hash: m[2], body: src.replace(WORKFLOW_MARK_RE, "") };
|
|
12324
12925
|
}
|
|
12325
|
-
function toolSet(
|
|
12326
|
-
const m = String(
|
|
12926
|
+
function toolSet(matcher2) {
|
|
12927
|
+
const m = String(matcher2 ?? "").trim();
|
|
12327
12928
|
if (m === "" || m === "*") return null;
|
|
12328
12929
|
return new Set(m.split(/[|,]/).map((t) => t.trim()).filter(Boolean));
|
|
12329
12930
|
}
|
|
12330
|
-
function missingTools(
|
|
12331
|
-
const have = toolSet(
|
|
12931
|
+
function missingTools(matcher2) {
|
|
12932
|
+
const have = toolSet(matcher2);
|
|
12332
12933
|
if (have === null) return [];
|
|
12333
12934
|
return PRE_MATCHER.split("|").filter((t) => !have.has(t));
|
|
12334
12935
|
}
|
|
@@ -12643,7 +13244,7 @@ function planPreCommit(cwd) {
|
|
|
12643
13244
|
}
|
|
12644
13245
|
const dir = hooksDir(cwd);
|
|
12645
13246
|
if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
|
|
12646
|
-
if (
|
|
13247
|
+
if (basename3(dir) === "_" && basename3(dirname4(dir)) === ".husky") {
|
|
12647
13248
|
path = join14(dirname4(dir), "pre-commit");
|
|
12648
13249
|
note = "husky";
|
|
12649
13250
|
} else {
|
|
@@ -12840,7 +13441,7 @@ function planned(item, path, plan) {
|
|
|
12840
13441
|
}
|
|
12841
13442
|
function sameDir(a, b) {
|
|
12842
13443
|
try {
|
|
12843
|
-
return
|
|
13444
|
+
return realpathSync5(a) === realpathSync5(b);
|
|
12844
13445
|
} catch {
|
|
12845
13446
|
return false;
|
|
12846
13447
|
}
|
|
@@ -13096,11 +13697,11 @@ import {
|
|
|
13096
13697
|
readdirSync as readdirSync3,
|
|
13097
13698
|
readFileSync as readFileSync13,
|
|
13098
13699
|
readlinkSync as readlinkSync2,
|
|
13099
|
-
realpathSync as
|
|
13700
|
+
realpathSync as realpathSync6,
|
|
13100
13701
|
statSync as statSync3
|
|
13101
13702
|
} from "node:fs";
|
|
13102
13703
|
import {
|
|
13103
|
-
basename as
|
|
13704
|
+
basename as basename4,
|
|
13104
13705
|
delimiter,
|
|
13105
13706
|
dirname as dirname5,
|
|
13106
13707
|
isAbsolute as isAbsolute9,
|
|
@@ -13128,7 +13729,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
13128
13729
|
let currentReal;
|
|
13129
13730
|
try {
|
|
13130
13731
|
rootStat = lstatSync8(root.path);
|
|
13131
|
-
currentReal =
|
|
13732
|
+
currentReal = realpathSync6(root.path);
|
|
13132
13733
|
} catch (e) {
|
|
13133
13734
|
throw new Unattestable(
|
|
13134
13735
|
root.kind + " root " + JSON.stringify(root.path) + " can no longer be resolved (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
@@ -13144,7 +13745,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
13144
13745
|
}
|
|
13145
13746
|
h.update("root\0" + root.kind + "\0" + root.path + "\0" + root.realPath + "\0" + String(rootStat.mode) + "\0");
|
|
13146
13747
|
if (rootStat.isSymbolicLink()) h.update(readlinkSync2(root.path) + "\0");
|
|
13147
|
-
const cwdReal =
|
|
13748
|
+
const cwdReal = realpathSync6(cwd);
|
|
13148
13749
|
const visited = /* @__PURE__ */ new Set();
|
|
13149
13750
|
const hashFile = (path, label) => {
|
|
13150
13751
|
let st;
|
|
@@ -13161,7 +13762,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
13161
13762
|
}
|
|
13162
13763
|
};
|
|
13163
13764
|
const walk2 = (dir, namespace) => {
|
|
13164
|
-
const realDir =
|
|
13765
|
+
const realDir = realpathSync6(dir);
|
|
13165
13766
|
if (visited.has(realDir)) return;
|
|
13166
13767
|
visited.add(realDir);
|
|
13167
13768
|
let entries;
|
|
@@ -13211,7 +13812,7 @@ function fingerprintRoot(root, cwd, h) {
|
|
|
13211
13812
|
h.update(target + "\0");
|
|
13212
13813
|
let resolved;
|
|
13213
13814
|
try {
|
|
13214
|
-
resolved =
|
|
13815
|
+
resolved = realpathSync6(path);
|
|
13215
13816
|
} catch {
|
|
13216
13817
|
continue;
|
|
13217
13818
|
}
|
|
@@ -13266,7 +13867,7 @@ function freezeRoot(kind, path) {
|
|
|
13266
13867
|
if (!statSync3(path).isDirectory()) {
|
|
13267
13868
|
throw new Error("not a directory");
|
|
13268
13869
|
}
|
|
13269
|
-
return { kind, path: resolve5(path), realPath:
|
|
13870
|
+
return { kind, path: resolve5(path), realPath: realpathSync6(path) };
|
|
13270
13871
|
} catch (e) {
|
|
13271
13872
|
throw new Unattestable(
|
|
13272
13873
|
kind + " root " + JSON.stringify(path) + " is not resolvable (" + (e instanceof Error ? e.message : String(e)) + ")"
|
|
@@ -13304,7 +13905,7 @@ function resolveExecutable(token, cwd, env) {
|
|
|
13304
13905
|
if (token.includes("/") || token.includes("\\")) {
|
|
13305
13906
|
const path = isAbsolute9(token) ? token : resolve5(cwd, token);
|
|
13306
13907
|
try {
|
|
13307
|
-
return existsSync12(path) ?
|
|
13908
|
+
return existsSync12(path) ? realpathSync6(path) : null;
|
|
13308
13909
|
} catch {
|
|
13309
13910
|
return null;
|
|
13310
13911
|
}
|
|
@@ -13312,7 +13913,7 @@ function resolveExecutable(token, cwd, env) {
|
|
|
13312
13913
|
for (const entry of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
13313
13914
|
const candidate = resolve5(entry, token);
|
|
13314
13915
|
try {
|
|
13315
|
-
if (existsSync12(candidate) && statSync3(candidate).isFile()) return
|
|
13916
|
+
if (existsSync12(candidate) && statSync3(candidate).isFile()) return realpathSync6(candidate);
|
|
13316
13917
|
} catch {
|
|
13317
13918
|
}
|
|
13318
13919
|
}
|
|
@@ -13322,23 +13923,23 @@ function pythonName(name) {
|
|
|
13322
13923
|
return /^(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)$/.test(name.toLowerCase());
|
|
13323
13924
|
}
|
|
13324
13925
|
function looksPython(command, executable, token) {
|
|
13325
|
-
const exe = executable ?
|
|
13326
|
-
const raw = token ?
|
|
13926
|
+
const exe = executable ? basename4(executable) : "";
|
|
13927
|
+
const raw = token ? basename4(token) : "";
|
|
13327
13928
|
if (pythonName(exe) || pythonName(raw)) return true;
|
|
13328
13929
|
return /(?:^|[\s;&|()])(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)(?=$|[\s;&|()])/.test(command);
|
|
13329
13930
|
}
|
|
13330
13931
|
function inferredVenvFromToken(token, cwd) {
|
|
13331
13932
|
if (!token || !token.includes("/") && !token.includes("\\")) return null;
|
|
13332
13933
|
const path = isAbsolute9(token) ? token : resolve5(cwd, token);
|
|
13333
|
-
if (!pythonName(
|
|
13934
|
+
if (!pythonName(basename4(path))) return null;
|
|
13334
13935
|
const parent = dirname5(path);
|
|
13335
|
-
const parentName =
|
|
13936
|
+
const parentName = basename4(parent).toLowerCase();
|
|
13336
13937
|
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
13337
13938
|
const root = dirname5(parent);
|
|
13338
13939
|
return existsSync12(join15(root, "pyvenv.cfg")) ? root : null;
|
|
13339
13940
|
}
|
|
13340
13941
|
function unsupportedEcosystem(command, token) {
|
|
13341
|
-
const name = token ?
|
|
13942
|
+
const name = token ? basename4(token).toLowerCase() : "";
|
|
13342
13943
|
const raw = command.toLowerCase();
|
|
13343
13944
|
if (["ruby", "bundle", "bundler", "rspec", "rake"].includes(name) || /(?:^|[\s;&|()])(?:bundle\s+exec|ruby|rspec|rake)(?=$|[\s;&|()])/.test(raw)) return "Ruby dependency environment discovery is not yet attestable";
|
|
13344
13945
|
if (["mvn", "mvnw", "gradle", "gradlew", "java"].includes(name) || /(?:^|[\s;&|()])(?:mvn|mvnw|gradle|gradlew|java)(?=$|[\s;&|()])/.test(raw)) return "JVM dependency environment discovery is not yet attestable";
|
|
@@ -13348,7 +13949,7 @@ function unsupportedEcosystem(command, token) {
|
|
|
13348
13949
|
function inferredVenv(executable) {
|
|
13349
13950
|
if (!executable) return null;
|
|
13350
13951
|
const parent = dirname5(executable);
|
|
13351
|
-
const parentName =
|
|
13952
|
+
const parentName = basename4(parent).toLowerCase();
|
|
13352
13953
|
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
13353
13954
|
const root = dirname5(parent);
|
|
13354
13955
|
return existsSync12(join15(root, "pyvenv.cfg")) ? root : null;
|
|
@@ -13818,7 +14419,7 @@ if (child) {
|
|
|
13818
14419
|
// src/verifier-backend.ts
|
|
13819
14420
|
import { createHash as createHash9, randomUUID } from "node:crypto";
|
|
13820
14421
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
13821
|
-
import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync15, realpathSync as
|
|
14422
|
+
import { accessSync as accessSync2, constants as constants3, readFileSync as readFileSync15, realpathSync as realpathSync7, statSync as statSync4 } from "node:fs";
|
|
13822
14423
|
import { delimiter as delimiter2, dirname as dirname6, join as join17, resolve as resolve6 } from "node:path";
|
|
13823
14424
|
function identity() {
|
|
13824
14425
|
const uid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
@@ -13856,7 +14457,7 @@ function findExecutable(name) {
|
|
|
13856
14457
|
const candidate = join17(raw, name);
|
|
13857
14458
|
try {
|
|
13858
14459
|
accessSync2(candidate, constants3.X_OK);
|
|
13859
|
-
return
|
|
14460
|
+
return realpathSync7(candidate);
|
|
13860
14461
|
} catch {
|
|
13861
14462
|
}
|
|
13862
14463
|
}
|
|
@@ -13931,7 +14532,7 @@ function inspectImageMetadata(enginePath, host, image) {
|
|
|
13931
14532
|
function engineStable(backend) {
|
|
13932
14533
|
if (!backend.enginePath || !backend.engineSha256) return false;
|
|
13933
14534
|
try {
|
|
13934
|
-
if (
|
|
14535
|
+
if (realpathSync7(backend.enginePath) !== backend.enginePath) return false;
|
|
13935
14536
|
if (replaceableByCaller(backend.enginePath)) return false;
|
|
13936
14537
|
return fileSha256(backend.enginePath) === backend.engineSha256;
|
|
13937
14538
|
} catch {
|
|
@@ -15172,7 +15773,7 @@ var init_verify = __esm({
|
|
|
15172
15773
|
// src/cli/run.ts
|
|
15173
15774
|
import { execFileSync as execFileSync9, spawn, spawnSync as spawnSync4 } from "node:child_process";
|
|
15174
15775
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
15175
|
-
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as
|
|
15776
|
+
import { accessSync as accessSync3, constants as fsConstants, mkdtempSync as mkdtempSync4, readdirSync as readdirSync4, readFileSync as readFileSync17, readlinkSync as readlinkSync4, realpathSync as realpathSync9, rmSync as rmSync8, statSync as statSync5, writeFileSync as writeFileSync10 } from "node:fs";
|
|
15176
15777
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
15177
15778
|
import { dirname as dirname8, join as join19, resolve as resolve8 } from "node:path";
|
|
15178
15779
|
function authoritativeRunLifecyclePlatform(platform = process.platform) {
|
|
@@ -15227,7 +15828,7 @@ function trustedLinuxPython(candidates = DEFAULT_TRUSTED_PYTHON_CANDIDATES) {
|
|
|
15227
15828
|
}
|
|
15228
15829
|
for (const candidate of candidates) {
|
|
15229
15830
|
try {
|
|
15230
|
-
const real =
|
|
15831
|
+
const real = realpathSync9(candidate);
|
|
15231
15832
|
const st = statSync5(real);
|
|
15232
15833
|
if (!st.isFile() || writableByCaller2(real)) continue;
|
|
15233
15834
|
const probe = spawnSync4(
|
|
@@ -15501,7 +16102,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
15501
16102
|
const out3 = [];
|
|
15502
16103
|
let real;
|
|
15503
16104
|
try {
|
|
15504
|
-
real =
|
|
16105
|
+
real = realpathSync9(cwd);
|
|
15505
16106
|
} catch {
|
|
15506
16107
|
return out3;
|
|
15507
16108
|
}
|
|
@@ -16723,7 +17324,7 @@ import {
|
|
|
16723
17324
|
mkdtempSync as mkdtempSync5,
|
|
16724
17325
|
readFileSync as readFileSync19,
|
|
16725
17326
|
readdirSync as readdirSync6,
|
|
16726
|
-
realpathSync as
|
|
17327
|
+
realpathSync as realpathSync10,
|
|
16727
17328
|
rmSync as rmSync9,
|
|
16728
17329
|
symlinkSync as symlinkSync2
|
|
16729
17330
|
} from "node:fs";
|
|
@@ -16871,7 +17472,7 @@ function rewriteTraceRoot(accesses, actualRoot) {
|
|
|
16871
17472
|
if (!inside3(actualRoot, abs)) return { ...item, path: abs };
|
|
16872
17473
|
const rel = relative6(actualRoot, abs).split(sep4).join("/");
|
|
16873
17474
|
try {
|
|
16874
|
-
const real = resolve10(
|
|
17475
|
+
const real = resolve10(realpathSync10(abs));
|
|
16875
17476
|
if (!inside3(actualRoot, real)) return { ...item, path: real };
|
|
16876
17477
|
} catch {
|
|
16877
17478
|
}
|
|
@@ -17107,7 +17708,7 @@ var init_trace_verify = __esm({
|
|
|
17107
17708
|
|
|
17108
17709
|
// src/cli/onboard.ts
|
|
17109
17710
|
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
17110
|
-
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync20, realpathSync as
|
|
17711
|
+
import { lstatSync as lstatSync10, mkdtempSync as mkdtempSync6, readFileSync as readFileSync20, realpathSync as realpathSync11, rmSync as rmSync10 } from "node:fs";
|
|
17111
17712
|
import { tmpdir as tmpdir7 } from "node:os";
|
|
17112
17713
|
import { join as join22, relative as relative7, resolve as resolve11, sep as sep5 } from "node:path";
|
|
17113
17714
|
import { createInterface } from "node:readline";
|
|
@@ -17249,7 +17850,7 @@ async function runOnboard(opts, io = {}) {
|
|
|
17249
17850
|
const cwd = resolve11(rootText);
|
|
17250
17851
|
let sameRoot = cwd === requestedCwd;
|
|
17251
17852
|
try {
|
|
17252
|
-
sameRoot =
|
|
17853
|
+
sameRoot = realpathSync11(cwd) === realpathSync11(requestedCwd);
|
|
17253
17854
|
} catch {
|
|
17254
17855
|
}
|
|
17255
17856
|
if (!sameRoot) {
|
|
@@ -17587,8 +18188,8 @@ function runDemo(cwd, head, runners, out3) {
|
|
|
17587
18188
|
let worktreeRoot;
|
|
17588
18189
|
let realTarget;
|
|
17589
18190
|
try {
|
|
17590
|
-
worktreeRoot =
|
|
17591
|
-
realTarget =
|
|
18191
|
+
worktreeRoot = realpathSync11(wt);
|
|
18192
|
+
realTarget = realpathSync11(targetPath);
|
|
17592
18193
|
} catch {
|
|
17593
18194
|
out3("Demo skipped: the selected test could not be resolved safely inside the disposable worktree.");
|
|
17594
18195
|
return;
|