tamperward 2.25.2 → 2.26.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 +16 -3
- package/dist/cli/index.js +2085 -1516
- 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` /
|
|
@@ -4200,24 +4211,24 @@ function topLevelStaticStrings(ctx) {
|
|
|
4200
4211
|
}
|
|
4201
4212
|
return resolved;
|
|
4202
4213
|
}
|
|
4203
|
-
function staticPropertyName(expr, ctx,
|
|
4214
|
+
function staticPropertyName(expr, ctx, strings2) {
|
|
4204
4215
|
if (ts.isStringLiteralLike(expr)) return { name: expr.text, causes: [] };
|
|
4205
|
-
if (ts.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx,
|
|
4216
|
+
if (ts.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx, strings2);
|
|
4206
4217
|
if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
4207
|
-
const left = staticPropertyName(expr.left, ctx,
|
|
4208
|
-
const right = staticPropertyName(expr.right, ctx,
|
|
4218
|
+
const left = staticPropertyName(expr.left, ctx, strings2);
|
|
4219
|
+
const right = staticPropertyName(expr.right, ctx, strings2);
|
|
4209
4220
|
if (!left || !right) return null;
|
|
4210
4221
|
return { name: left.name + right.name, causes: [...left.causes, ...right.causes] };
|
|
4211
4222
|
}
|
|
4212
4223
|
if (ts.isIdentifier(expr)) {
|
|
4213
4224
|
const sym = symbolAt(ctx, expr);
|
|
4214
4225
|
if (!sym) return null;
|
|
4215
|
-
const value =
|
|
4226
|
+
const value = strings2.get(sym);
|
|
4216
4227
|
return value ? { name: value.value, causes: value.causes } : null;
|
|
4217
4228
|
}
|
|
4218
4229
|
return null;
|
|
4219
4230
|
}
|
|
4220
|
-
function runnerChain(expr, ctx, binder,
|
|
4231
|
+
function runnerChain(expr, ctx, binder, strings2) {
|
|
4221
4232
|
const props = [];
|
|
4222
4233
|
const propNodes = [];
|
|
4223
4234
|
const causes = [];
|
|
@@ -4232,7 +4243,7 @@ function runnerChain(expr, ctx, binder, strings) {
|
|
|
4232
4243
|
continue;
|
|
4233
4244
|
}
|
|
4234
4245
|
if (!cur.argumentExpression) return null;
|
|
4235
|
-
const property = staticPropertyName(cur.argumentExpression, ctx,
|
|
4246
|
+
const property = staticPropertyName(cur.argumentExpression, ctx, strings2);
|
|
4236
4247
|
if (!property) return null;
|
|
4237
4248
|
props.unshift(property.name);
|
|
4238
4249
|
propNodes.unshift(cur.argumentExpression);
|
|
@@ -4286,7 +4297,7 @@ function structuralNodeKey(node, sf) {
|
|
|
4286
4297
|
return `${node.kind}(${children.map((child) => structuralNodeKey(child, sf)).join(",")})`;
|
|
4287
4298
|
}
|
|
4288
4299
|
function semanticSkipHits(ctx, binder) {
|
|
4289
|
-
const
|
|
4300
|
+
const strings2 = topLevelStaticStrings(ctx);
|
|
4290
4301
|
const hits = [];
|
|
4291
4302
|
const unowned = /* @__PURE__ */ new Set();
|
|
4292
4303
|
const callOrdinals = /* @__PURE__ */ new Map();
|
|
@@ -4298,7 +4309,7 @@ function semanticSkipHits(ctx, binder) {
|
|
|
4298
4309
|
const ordinal = (callOrdinals.get(callIdentity) ?? 0) + 1;
|
|
4299
4310
|
callOrdinals.set(callIdentity, ordinal);
|
|
4300
4311
|
const callKey = `${callIdentity}\0${ordinal}`;
|
|
4301
|
-
const chain = runnerChain(node.expression, ctx, binder,
|
|
4312
|
+
const chain = runnerChain(node.expression, ctx, binder, strings2);
|
|
4302
4313
|
if (chain === "unknown") {
|
|
4303
4314
|
const callee = node.expression;
|
|
4304
4315
|
const first = lineOf(callee.getStart(ctx.sf));
|
|
@@ -4336,7 +4347,7 @@ function semanticSkipHits(ctx, binder) {
|
|
|
4336
4347
|
if (ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name)) {
|
|
4337
4348
|
name = prop.name.text;
|
|
4338
4349
|
} else if (ts.isComputedPropertyName(prop.name)) {
|
|
4339
|
-
const resolved = staticPropertyName(prop.name.expression, ctx,
|
|
4350
|
+
const resolved = staticPropertyName(prop.name.expression, ctx, strings2);
|
|
4340
4351
|
name = resolved?.name ?? null;
|
|
4341
4352
|
propertyCauses = resolved?.causes ?? [];
|
|
4342
4353
|
}
|
|
@@ -5599,1123 +5610,980 @@ var init_coverage_lowering = __esm({
|
|
|
5599
5610
|
}
|
|
5600
5611
|
});
|
|
5601
5612
|
|
|
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;
|
|
5613
|
+
// src/detectors/suite-config.ts
|
|
5614
|
+
function runnerSkips(path) {
|
|
5615
|
+
const parts = path.split("/");
|
|
5616
|
+
if (/\.rs$/.test(path)) {
|
|
5617
|
+
const i = parts.indexOf("tests");
|
|
5618
|
+
if (i < 0 || parts.slice(0, i).includes("src")) return null;
|
|
5619
|
+
const rest = parts.slice(i + 1);
|
|
5620
|
+
if (rest.length === 1 || rest.length === 2 && rest[1] === "main.rs") return null;
|
|
5621
|
+
return "cargo builds only tests/*.rs and tests/*/main.rs as test targets";
|
|
5615
5622
|
}
|
|
5616
|
-
|
|
5623
|
+
if (/\.go$/.test(path)) {
|
|
5624
|
+
const hit = parts.find((p) => p === "testdata" || p.startsWith("_") || p.startsWith("."));
|
|
5625
|
+
if (hit === void 0) return null;
|
|
5626
|
+
return `go test skips ${hit === "testdata" ? "testdata/" : hit.startsWith("_") ? "_*" : ".*"} paths (${JSON.stringify(hit)})`;
|
|
5627
|
+
}
|
|
5628
|
+
if (/\.py$/.test(path)) {
|
|
5629
|
+
const hit = PYTEST_DEFAULT_IGNORE.find((g) => globs2([g])(path));
|
|
5630
|
+
return hit === void 0 ? null : `pytest's default norecursedirs matches it (${JSON.stringify(hit)})`;
|
|
5631
|
+
}
|
|
5632
|
+
if (/\.[cm]?[jt]sx?$/.test(path)) {
|
|
5633
|
+
const hit = JS_UNWALKED.find((g) => globs2([g])(path));
|
|
5634
|
+
return hit === void 0 ? null : `the runner's default exclude matches it (${JSON.stringify(hit)})`;
|
|
5635
|
+
}
|
|
5636
|
+
return null;
|
|
5617
5637
|
}
|
|
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
|
-
};
|
|
5638
|
+
function keyName2(name) {
|
|
5639
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
|
|
5640
|
+
return null;
|
|
5641
|
+
}
|
|
5642
|
+
function literals2(e) {
|
|
5643
|
+
if (isStr(e)) return { items: [e.text], opaque: false };
|
|
5644
|
+
if (!ts.isArrayLiteralExpression(e)) return { items: [], opaque: true };
|
|
5645
|
+
const items = [];
|
|
5646
|
+
let opaque = false;
|
|
5647
|
+
for (const el of e.elements) {
|
|
5648
|
+
if (isStr(el)) items.push(el.text);
|
|
5649
|
+
else if (ts.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
|
|
5650
|
+
items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
5651
|
+
} else opaque = true;
|
|
5686
5652
|
}
|
|
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
|
-
});
|
|
5653
|
+
return { items, opaque };
|
|
5696
5654
|
}
|
|
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
|
-
|
|
5655
|
+
function ownerKey2(node) {
|
|
5656
|
+
const obj = node.parent;
|
|
5657
|
+
if (!obj || !ts.isObjectLiteralExpression(obj)) return null;
|
|
5658
|
+
let q = obj.parent;
|
|
5659
|
+
while (q && (ts.isParenthesizedExpression(q) || ts.isAsExpression(q) || q.kind === ts.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
5660
|
+
return q && ts.isPropertyAssignment(q) ? keyName2(q.name) : null;
|
|
5661
|
+
}
|
|
5662
|
+
function asExpression2(src) {
|
|
5663
|
+
const t = src.trim();
|
|
5664
|
+
return t.startsWith("{") ? `(${t})` : src;
|
|
5665
|
+
}
|
|
5666
|
+
function runnerOf(path, content) {
|
|
5667
|
+
const base = path.split("/").pop() ?? path;
|
|
5668
|
+
if (/^jest\.config\./.test(base) || base === "package.json") return "jest";
|
|
5669
|
+
if (/^vitest\.(?:config|workspace)\./.test(base)) return "vitest";
|
|
5670
|
+
if (/^vite\.config\./.test(base) && content != null && /(?:^|[\s,{])["']?test["']?\s*:/m.test(content)) return "vitest";
|
|
5671
|
+
if (base === "pytest.ini" || base === ".pytest.ini") return "pytest";
|
|
5672
|
+
if (content == null) return null;
|
|
5673
|
+
if (base === "tox.ini" && /^\s*\[pytest\]/m.test(content)) return "pytest";
|
|
5674
|
+
if (base === "setup.cfg" && /^\s*\[tool:pytest\]/m.test(content)) return "pytest";
|
|
5675
|
+
if (base === "pyproject.toml" && /^\s*\[tool\.pytest\.ini_options\]/m.test(content)) return "pytest";
|
|
5676
|
+
return null;
|
|
5677
|
+
}
|
|
5678
|
+
function claimsPytest(name, src) {
|
|
5679
|
+
if (name === "pytest.ini" || name === ".pytest.ini") return true;
|
|
5680
|
+
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;
|
|
5681
|
+
return section !== null && section.test(src);
|
|
5682
|
+
}
|
|
5683
|
+
function effectivePytestFile(files) {
|
|
5684
|
+
for (const name of PYTEST_INI_ORDER) {
|
|
5685
|
+
const content = files.get(name);
|
|
5686
|
+
if (content == null) continue;
|
|
5687
|
+
if (claimsPytest(name, content)) return { path: name, content };
|
|
5688
|
+
}
|
|
5689
|
+
return null;
|
|
5690
|
+
}
|
|
5691
|
+
function effectivePytestConfig(path, ctx) {
|
|
5692
|
+
if (path.includes("/")) return false;
|
|
5693
|
+
const base = path;
|
|
5694
|
+
const rank = PYTEST_INI_ORDER.findIndex((name) => name === base);
|
|
5695
|
+
if (rank < 0) return false;
|
|
5696
|
+
const files = ctx?.trackedFiles;
|
|
5697
|
+
if (!files) return true;
|
|
5698
|
+
const rootNames = new Set(files.filter((f) => !f.includes("/")));
|
|
5699
|
+
return !PYTEST_INI_ORDER.slice(0, rank).some((n) => rootNames.has(n));
|
|
5700
|
+
}
|
|
5701
|
+
function pytestBlock(src) {
|
|
5702
|
+
const lines = src.split(/\r?\n/);
|
|
5703
|
+
const head = /^\s*\[(pytest|tool:pytest|tool\.pytest\.ini_options)\]\s*$/;
|
|
5704
|
+
let i = lines.findIndex((l) => head.test(l));
|
|
5705
|
+
if (i < 0) return null;
|
|
5706
|
+
const out3 = {};
|
|
5707
|
+
let key2 = null;
|
|
5708
|
+
for (i += 1; i < lines.length; i++) {
|
|
5709
|
+
const line = lines[i];
|
|
5710
|
+
if (/^\s*\[/.test(line)) break;
|
|
5711
|
+
if (/^\s*[#;]/.test(line) || !line.trim()) {
|
|
5731
5712
|
continue;
|
|
5732
5713
|
}
|
|
5733
|
-
const
|
|
5734
|
-
if (
|
|
5735
|
-
|
|
5736
|
-
|
|
5714
|
+
const m = line.match(/^(\w[\w.-]*)\s*[=:]\s*(.*)$/);
|
|
5715
|
+
if (m) {
|
|
5716
|
+
key2 = m[1];
|
|
5717
|
+
out3[key2] = m[2].trim();
|
|
5737
5718
|
continue;
|
|
5738
5719
|
}
|
|
5739
|
-
|
|
5720
|
+
if (key2 && /^\s+\S/.test(line)) out3[key2] += " " + line.trim();
|
|
5740
5721
|
}
|
|
5741
5722
|
return out3;
|
|
5742
5723
|
}
|
|
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
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
}
|
|
5776
|
-
|
|
5724
|
+
function pytestTokens(raw) {
|
|
5725
|
+
const v = raw.trim();
|
|
5726
|
+
if (v.startsWith("[")) {
|
|
5727
|
+
return [...v.matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? "");
|
|
5728
|
+
}
|
|
5729
|
+
const unq2 = v.replace(/^"([\s\S]*)"$/, "$1").replace(/^'([\s\S]*)'$/, "$1");
|
|
5730
|
+
return [...unq2.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "").filter(Boolean);
|
|
5731
|
+
}
|
|
5732
|
+
function pytestSelection(src) {
|
|
5733
|
+
const sel = empty();
|
|
5734
|
+
const blk = pytestBlock(src);
|
|
5735
|
+
if (!blk) return sel;
|
|
5736
|
+
sel.present = true;
|
|
5737
|
+
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" }));
|
|
5738
|
+
if (blk.testpaths !== void 0) sel.roots = pytestTokens(blk.testpaths).map((t) => t.replace(/^\.\//, ""));
|
|
5739
|
+
if (blk.python_files !== void 0) sel.include = pytestTokens(blk.python_files).map((g) => g.includes("/") ? g : "**/" + g);
|
|
5740
|
+
for (const key2 of ["python_classes", "python_functions"]) {
|
|
5741
|
+
if (blk[key2] !== void 0) ignore.push({ pattern: "\0" + key2, key: key2 });
|
|
5742
|
+
}
|
|
5743
|
+
const opts = blk.addopts !== void 0 ? pytestTokens(blk.addopts) : [];
|
|
5744
|
+
for (let j = 0; j < opts.length; j++) {
|
|
5745
|
+
const o = opts[j];
|
|
5746
|
+
const val = (inline) => o.includes("=") ? o.slice(o.indexOf("=") + 1) : opts[++j] ?? inline;
|
|
5747
|
+
if (o === "--ignore" || o.startsWith("--ignore=")) for (const g of pathGlobs(val(""))) ignore.push({ pattern: g, key: "--ignore" });
|
|
5748
|
+
else if (o === "--ignore-glob" || o.startsWith("--ignore-glob=")) {
|
|
5749
|
+
const g = val("");
|
|
5750
|
+
ignore.push({ pattern: g, key: "--ignore-glob" });
|
|
5751
|
+
if (!g.startsWith("**/") && !g.startsWith("/")) ignore.push({ pattern: "**/" + g, key: "--ignore-glob" });
|
|
5752
|
+
} else if (o === "--deselect" || o.startsWith("--deselect=")) for (const g of pathGlobs(val(""))) ignore.push({ pattern: g, key: "--deselect" });
|
|
5753
|
+
else if (o === "-k" || o.startsWith("-k=")) {
|
|
5754
|
+
for (const w of (val("").match(/\bnot\s+([A-Za-z_][\w]*)/g) ?? []).map((t) => t.replace(/^not\s+/, "")))
|
|
5755
|
+
ignore.push({ pattern: `**/*${w}*`, key: "-k" });
|
|
5756
|
+
} else if (o === "-m" || o.startsWith("-m=")) {
|
|
5757
|
+
ignore.push({ pattern: "\0-m " + val(""), key: "-m" });
|
|
5758
|
+
} else if (o === "-p" || o.startsWith("-p=") || o.length > 2 && o.startsWith("-p")) {
|
|
5759
|
+
const v = o === "-p" ? opts[++j] ?? "" : o.startsWith("-p=") ? o.slice(3) : o.slice(2);
|
|
5760
|
+
if (v.startsWith("no:")) ignore.push({ pattern: "\0-p " + v, key: "-p" });
|
|
5761
|
+
} else if (o === "--collect-only" || o === "--co") ignore.push({ pattern: "**", key: o });
|
|
5762
|
+
}
|
|
5763
|
+
sel.ignore = ignore;
|
|
5764
|
+
return sel;
|
|
5765
|
+
}
|
|
5766
|
+
function dirValue(e) {
|
|
5767
|
+
if (!isStr(e)) return { dir: null, opaque: true };
|
|
5768
|
+
const d = stripRootDir(e.text).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
5769
|
+
if (d === "." || d === "" || d.split("/").includes("..")) return { dir: null, opaque: false };
|
|
5770
|
+
return { dir: d, opaque: false };
|
|
5771
|
+
}
|
|
5772
|
+
function collect(root, runner) {
|
|
5773
|
+
const sel = empty();
|
|
5774
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5775
|
+
const take = (key2, e) => {
|
|
5776
|
+
const { items, opaque } = literals2(e);
|
|
5777
|
+
if (opaque || seen.has(key2)) sel.opaque = true;
|
|
5778
|
+
seen.add(key2);
|
|
5779
|
+
sel.present = true;
|
|
5780
|
+
return items.map(stripRootDir);
|
|
5777
5781
|
};
|
|
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
|
-
}
|
|
5782
|
+
const ignores = (key2, e) => {
|
|
5783
|
+
const entries = take(key2, e).map((pattern) => ({ pattern, key: key2 }));
|
|
5784
|
+
sel.ignore = [...sel.ignore ?? [], ...entries];
|
|
5799
5785
|
};
|
|
5800
|
-
const
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
return v2 === void 0 ? void 0 : !truthy(v2);
|
|
5804
|
-
}
|
|
5805
|
-
return primary();
|
|
5786
|
+
const namePattern = (e) => {
|
|
5787
|
+
sel.present = true;
|
|
5788
|
+
sel.wide.push({ pattern: "\0testNamePattern " + (isStr(e) ? e.text : e.getText()), key: "testNamePattern" });
|
|
5806
5789
|
};
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5790
|
+
let projectList = null;
|
|
5791
|
+
const projects = (e) => {
|
|
5792
|
+
sel.present = true;
|
|
5793
|
+
projectList = e;
|
|
5794
|
+
};
|
|
5795
|
+
const visit = (node) => {
|
|
5796
|
+
if (ts.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
|
|
5797
|
+
sel.present = true;
|
|
5798
|
+
sel.opaque = true;
|
|
5813
5799
|
}
|
|
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
|
-
|
|
5800
|
+
if (ts.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
|
|
5801
|
+
sel.present = true;
|
|
5802
|
+
sel.opaque = true;
|
|
5803
|
+
}
|
|
5804
|
+
if (ts.isPropertyAssignment(node)) {
|
|
5805
|
+
const k = keyName2(node.name);
|
|
5806
|
+
if (runner === "jest") {
|
|
5807
|
+
if (k === "testMatch") sel.include = take(k, node.initializer);
|
|
5808
|
+
else if (k === "testRegex") sel.regex = take(k, node.initializer);
|
|
5809
|
+
else if (k === "testPathIgnorePatterns" || k === "modulePathIgnorePatterns") ignores(k, node.initializer);
|
|
5810
|
+
else if (k === "roots") sel.roots = take(k, node.initializer);
|
|
5811
|
+
else if (k === "rootDir") {
|
|
5812
|
+
const { dir, opaque } = dirValue(node.initializer);
|
|
5813
|
+
sel.present = true;
|
|
5814
|
+
if (opaque) sel.opaque = true;
|
|
5815
|
+
else sel.base = dir;
|
|
5816
|
+
} else if (k === "projects") {
|
|
5817
|
+
projects(node.initializer);
|
|
5818
|
+
return;
|
|
5819
|
+
} else if (k === "testNamePattern") namePattern(node.initializer);
|
|
5820
|
+
} else if (ownerKey2(node) === "test") {
|
|
5821
|
+
if (k === "include") sel.include = take(k, node.initializer);
|
|
5822
|
+
else if (k === "exclude") ignores(k, node.initializer);
|
|
5823
|
+
else if (k === "dir" || k === "root") {
|
|
5824
|
+
const { dir, opaque } = dirValue(node.initializer);
|
|
5825
|
+
sel.present = true;
|
|
5826
|
+
if (opaque) sel.opaque = true;
|
|
5827
|
+
else sel.base = dir;
|
|
5828
|
+
} else if (k === "projects") {
|
|
5829
|
+
projects(node.initializer);
|
|
5830
|
+
return;
|
|
5831
|
+
} else if (k === "testNamePattern") namePattern(node.initializer);
|
|
5842
5832
|
}
|
|
5843
|
-
return void 0;
|
|
5844
5833
|
}
|
|
5845
|
-
|
|
5834
|
+
ts.forEachChild(node, visit);
|
|
5846
5835
|
};
|
|
5847
|
-
|
|
5848
|
-
|
|
5836
|
+
visit(root);
|
|
5837
|
+
if (projectList) sel.projects = readProjects(projectList, sel, runner);
|
|
5838
|
+
if (sel.projects?.some((p) => p.opaque)) sel.opaque = true;
|
|
5839
|
+
return sel;
|
|
5849
5840
|
}
|
|
5850
|
-
function
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
vals.push(a);
|
|
5855
|
-
}
|
|
5856
|
-
const s = (i) => vals[i] == null ? "" : String(vals[i]);
|
|
5857
|
-
switch (name.toLowerCase()) {
|
|
5858
|
-
case "contains":
|
|
5859
|
-
return vals.length === 2 ? s(0).toLowerCase().includes(s(1).toLowerCase()) : void 0;
|
|
5860
|
-
case "startswith":
|
|
5861
|
-
return vals.length === 2 ? s(0).toLowerCase().startsWith(s(1).toLowerCase()) : void 0;
|
|
5862
|
-
case "endswith":
|
|
5863
|
-
return vals.length === 2 ? s(0).toLowerCase().endsWith(s(1).toLowerCase()) : void 0;
|
|
5864
|
-
case "format": {
|
|
5865
|
-
if (vals.length < 1) return void 0;
|
|
5866
|
-
return s(0).replace(/\{\{|\}\}|\{(\d+)\}/g, (m, i) => m === "{{" ? "{" : m === "}}" ? "}" : s(Number(i) + 1));
|
|
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;
|
|
5841
|
+
function unwrapConfigCall(e) {
|
|
5842
|
+
let x = e;
|
|
5843
|
+
while (ts.isParenthesizedExpression(x) || ts.isAsExpression(x) || ts.isSatisfiesExpression(x)) {
|
|
5844
|
+
x = x.expression;
|
|
5884
5845
|
}
|
|
5846
|
+
if (ts.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
|
|
5847
|
+
return x;
|
|
5885
5848
|
}
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5849
|
+
function spreadsIntoConfig(node, runner) {
|
|
5850
|
+
const obj = node.parent;
|
|
5851
|
+
if (!ts.isObjectLiteralExpression(obj)) return false;
|
|
5852
|
+
const keys = obj.properties.map((p) => ts.isPropertyAssignment(p) ? keyName2(p.name) : null);
|
|
5853
|
+
if (runner === "vitest") {
|
|
5854
|
+
if (keys.includes("test")) return true;
|
|
5855
|
+
let q = obj.parent;
|
|
5856
|
+
while (q && (ts.isParenthesizedExpression(q) || ts.isAsExpression(q) || q.kind === ts.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
5857
|
+
return !!q && ts.isPropertyAssignment(q) && keyName2(q.name) === "test";
|
|
5894
5858
|
}
|
|
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();
|
|
5859
|
+
return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
|
|
5906
5860
|
}
|
|
5907
|
-
function
|
|
5908
|
-
|
|
5861
|
+
function extendsOf(el) {
|
|
5862
|
+
for (const p of el.properties) {
|
|
5863
|
+
if (!ts.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
|
|
5864
|
+
if (p.initializer.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
5865
|
+
if (p.initializer.kind === ts.SyntaxKind.FalseKeyword) return null;
|
|
5866
|
+
return "path";
|
|
5867
|
+
}
|
|
5868
|
+
return null;
|
|
5909
5869
|
}
|
|
5910
|
-
function
|
|
5911
|
-
const
|
|
5912
|
-
const
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5870
|
+
function readProject(el, root, runner) {
|
|
5871
|
+
const p = collect(el, runner);
|
|
5872
|
+
const ext = extendsOf(el);
|
|
5873
|
+
if (ext === "path") p.opaque = true;
|
|
5874
|
+
else if (ext === true && root) {
|
|
5875
|
+
p.include ??= root.include;
|
|
5876
|
+
p.regex ??= root.regex;
|
|
5877
|
+
p.ignore ??= root.ignore;
|
|
5878
|
+
p.roots ??= root.roots;
|
|
5879
|
+
p.base ??= root.base;
|
|
5880
|
+
if (root.opaque) p.opaque = true;
|
|
5881
|
+
}
|
|
5882
|
+
p.present = true;
|
|
5883
|
+
return p;
|
|
5922
5884
|
}
|
|
5923
|
-
function
|
|
5924
|
-
|
|
5885
|
+
function readProjects(e, root, runner) {
|
|
5886
|
+
if (!ts.isArrayLiteralExpression(e)) {
|
|
5887
|
+
root.opaque = true;
|
|
5888
|
+
return [];
|
|
5889
|
+
}
|
|
5890
|
+
const list3 = [];
|
|
5891
|
+
for (const el of e.elements) {
|
|
5892
|
+
const lit = unwrapConfigCall(el);
|
|
5893
|
+
if (ts.isObjectLiteralExpression(lit)) list3.push(readProject(lit, root, runner));
|
|
5894
|
+
else root.opaque = true;
|
|
5895
|
+
}
|
|
5896
|
+
return list3;
|
|
5925
5897
|
}
|
|
5926
|
-
function
|
|
5927
|
-
const
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
if (
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5898
|
+
function workspaceArray(sf) {
|
|
5899
|
+
for (const st of sf.statements) {
|
|
5900
|
+
let e;
|
|
5901
|
+
if (ts.isExportAssignment(st)) e = st.expression;
|
|
5902
|
+
else if (ts.isExpressionStatement(st) && ts.isBinaryExpression(st.expression) && /module\.exports/.test(st.expression.left.getText())) e = st.expression.right;
|
|
5903
|
+
if (!e) continue;
|
|
5904
|
+
while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e)) {
|
|
5905
|
+
e = e.expression;
|
|
5906
|
+
}
|
|
5907
|
+
if (ts.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
|
|
5908
|
+
return ts.isArrayLiteralExpression(e) ? e : null;
|
|
5935
5909
|
}
|
|
5936
5910
|
return null;
|
|
5937
5911
|
}
|
|
5938
|
-
function
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5912
|
+
function parseSelection(src, runner, path = "") {
|
|
5913
|
+
let sel = empty();
|
|
5914
|
+
if (runner === "pytest") return pytestSelection(src);
|
|
5915
|
+
try {
|
|
5916
|
+
const sf = parseSource("cfg.ts", asExpression2(src));
|
|
5917
|
+
if (!sf) return sel;
|
|
5918
|
+
if (runner === "vitest" && isWorkspaceFile(path)) {
|
|
5919
|
+
const arr = workspaceArray(sf);
|
|
5920
|
+
if (arr) {
|
|
5921
|
+
sel.projects = readProjects(arr, sel, runner);
|
|
5922
|
+
sel.present = true;
|
|
5923
|
+
if (sel.projects.some((p) => p.opaque)) sel.opaque = true;
|
|
5924
|
+
} else if (src.trim()) sel.opaque = true;
|
|
5925
|
+
} else sel = collect(sf, runner);
|
|
5926
|
+
} catch {
|
|
5950
5927
|
}
|
|
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
|
-
}
|
|
5928
|
+
return sel;
|
|
5929
|
+
}
|
|
5930
|
+
function predicate(sel, runner) {
|
|
5931
|
+
if (sel.projects) {
|
|
5932
|
+
const ps = sel.projects.map((p) => predicate(p, runner));
|
|
5933
|
+
const wide = sel.wide[0];
|
|
5934
|
+
return {
|
|
5935
|
+
selects: (p) => wide === void 0 && ps.some((x) => x.selects(p)),
|
|
5936
|
+
why: (p) => wide !== void 0 ? wideWhy(wide) : ps.length ? `no project selects it (${ps[0].why(p)})` : "the projects list is empty"
|
|
5937
|
+
};
|
|
5968
5938
|
}
|
|
5969
|
-
const
|
|
5970
|
-
const
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5939
|
+
const includeList = sel.include ?? (runner === "jest" ? sel.regex ? null : JEST_DEFAULT_MATCH : runner === "pytest" ? PYTEST_DEFAULT_INCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
5940
|
+
const ignoreList = sel.ignore ?? (runner === "jest" ? JEST_DEFAULT_IGNORE : runner === "pytest" ? PYTEST_DEFAULT_IGNORE : VITEST_DEFAULT_EXCLUDE).map((pattern) => ({
|
|
5941
|
+
pattern,
|
|
5942
|
+
key: runner === "jest" ? "testPathIgnorePatterns" : runner === "pytest" ? "norecursedirs" : "test.exclude"
|
|
5943
|
+
}));
|
|
5944
|
+
const blockWide = [...ignoreList.filter((e) => e.pattern.startsWith("\0")), ...sel.wide];
|
|
5945
|
+
const included = includeList ? globList2(includeList) : sel.regex ? regexes(sel.regex) : () => true;
|
|
5946
|
+
const ignoredBy = (p) => blockWide[0] ?? ignoreList.filter((e) => !e.pattern.startsWith("\0")).find((e) => (runner === "jest" ? regexes([e.pattern]) : globs2([e.pattern]))(p));
|
|
5947
|
+
const roots = sel.roots?.map((r) => r.replace(/^\.\//, "").replace(/\/+$/, "")).map((r) => r === "." ? "" : r) ?? null;
|
|
5948
|
+
const rebase = (p) => sel.base === null ? p : underDir(p, sel.base) ? p.slice(sel.base.length + 1) : null;
|
|
5949
|
+
const inRoots = (p) => roots === null || roots.some((r) => underDir(p, r));
|
|
5950
|
+
return {
|
|
5951
|
+
selects: (p) => {
|
|
5952
|
+
const r = rebase(p);
|
|
5953
|
+
return r !== null && inRoots(r) && included(r) && !ignoredBy(r);
|
|
5954
|
+
},
|
|
5955
|
+
why: (p) => {
|
|
5956
|
+
const r = rebase(p);
|
|
5957
|
+
const baseKey = runner === "jest" ? "rootDir" : "test.dir";
|
|
5958
|
+
if (r === null) return `${baseKey} no longer covers it (${JSON.stringify(sel.base)})`;
|
|
5959
|
+
if (!inRoots(r)) return `roots no longer covers it (${JSON.stringify(sel.roots)})`;
|
|
5960
|
+
const hit = ignoredBy(r);
|
|
5961
|
+
if (hit) {
|
|
5962
|
+
if (hit.pattern.startsWith("\0")) return wideWhy(hit);
|
|
5963
|
+
return `${hit.key === "exclude" ? "test.exclude" : hit.key} now matches it (${JSON.stringify(hit.pattern)})`;
|
|
5964
|
+
}
|
|
5965
|
+
const key2 = runner === "jest" ? sel.include ? "testMatch" : sel.regex ? "testRegex" : "testMatch (default)" : sel.include ? "test.include" : "test.include (default)";
|
|
5966
|
+
return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? includeList ?? [])})`;
|
|
5983
5967
|
}
|
|
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" };
|
|
5968
|
+
};
|
|
5992
5969
|
}
|
|
5993
|
-
function
|
|
5994
|
-
const
|
|
5995
|
-
if (
|
|
5996
|
-
const
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
5970
|
+
function suiteNarrowings(before, after, path, samples) {
|
|
5971
|
+
const runner = runnerOf(path, after) ?? runnerOf(path, before);
|
|
5972
|
+
if (!runner) return [];
|
|
5973
|
+
const b = parseSelection(before ?? "", runner, path);
|
|
5974
|
+
const a = parseSelection(after, runner, path);
|
|
5975
|
+
if (!a.present || a.opaque) return [];
|
|
5976
|
+
const pb = predicate(b, runner);
|
|
5977
|
+
const pa = predicate(a, runner);
|
|
5978
|
+
const out3 = [];
|
|
5979
|
+
for (const s of samples) {
|
|
5980
|
+
if (pb.selects(s) && !pa.selects(s)) out3.push({ path: s, reason: pa.why(s) });
|
|
6000
5981
|
}
|
|
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";
|
|
5982
|
+
return out3;
|
|
6009
5983
|
}
|
|
6010
|
-
function
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
5984
|
+
function requiredPath(e) {
|
|
5985
|
+
let x = e;
|
|
5986
|
+
while (ts.isParenthesizedExpression(x) || ts.isAsExpression(x) || ts.isSatisfiesExpression(x) || ts.isAwaitExpression(x)) x = x.expression;
|
|
5987
|
+
if (ts.isCallExpression(x) && x.arguments.length > 0 && isStr(x.arguments[0])) {
|
|
5988
|
+
const callee = x.expression;
|
|
5989
|
+
if (ts.isIdentifier(callee) && callee.text === "require" || callee.kind === ts.SyntaxKind.ImportKeyword) return x.arguments[0].text;
|
|
5990
|
+
}
|
|
5991
|
+
return null;
|
|
6015
5992
|
}
|
|
6016
|
-
function
|
|
5993
|
+
function delegationsIn(src, runner) {
|
|
6017
5994
|
const out3 = [];
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
5995
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5996
|
+
const add = (target, how, local = true) => {
|
|
5997
|
+
if (local && !isLocalPath(target) || hasGlob(target) || seen.has(target)) return;
|
|
5998
|
+
seen.add(target);
|
|
5999
|
+
out3.push({ target, how });
|
|
6000
|
+
};
|
|
6001
|
+
let sf = null;
|
|
6002
|
+
try {
|
|
6003
|
+
sf = parseSource("cfg.ts", asExpression2(src));
|
|
6004
|
+
} catch {
|
|
6005
|
+
return out3;
|
|
6006
|
+
}
|
|
6007
|
+
if (!sf) return out3;
|
|
6008
|
+
const bound = /* @__PURE__ */ new Map();
|
|
6009
|
+
const bindNames = (name, spec2) => {
|
|
6010
|
+
if (ts.isIdentifier(name)) bound.set(name.text, spec2);
|
|
6011
|
+
else for (const el of name.elements) if (ts.isBindingElement(el)) bindNames(el.name, spec2);
|
|
6012
|
+
};
|
|
6013
|
+
for (const st of sf.statements) {
|
|
6014
|
+
if (ts.isImportDeclaration(st) && isStr(st.moduleSpecifier) && isLocalPath(st.moduleSpecifier.text) && st.importClause) {
|
|
6015
|
+
const spec2 = st.moduleSpecifier.text;
|
|
6016
|
+
if (st.importClause.name) bound.set(st.importClause.name.text, spec2);
|
|
6017
|
+
const nb = st.importClause.namedBindings;
|
|
6018
|
+
if (nb && ts.isNamespaceImport(nb)) bound.set(nb.name.text, spec2);
|
|
6019
|
+
else if (nb) for (const el of nb.elements) bound.set(el.name.text, spec2);
|
|
6020
|
+
} else if (ts.isVariableStatement(st)) {
|
|
6021
|
+
for (const d of st.declarationList.declarations) {
|
|
6022
|
+
const spec2 = d.initializer ? requiredPath(d.initializer) : null;
|
|
6023
|
+
if (spec2 !== null && isLocalPath(spec2)) bindNames(d.name, spec2);
|
|
6024
|
+
}
|
|
6030
6025
|
}
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6026
|
+
}
|
|
6027
|
+
const moduleOf = (e) => {
|
|
6028
|
+
let x = e;
|
|
6029
|
+
while (ts.isParenthesizedExpression(x) || ts.isAsExpression(x) || ts.isSatisfiesExpression(x) || ts.isAwaitExpression(x) || ts.isPropertyAccessExpression(x) || ts.isNonNullExpression(x))
|
|
6030
|
+
x = x.expression;
|
|
6031
|
+
if (ts.isIdentifier(x)) return bound.get(x.text) ?? null;
|
|
6032
|
+
return requiredPath(x);
|
|
6033
|
+
};
|
|
6034
|
+
const exported = (e, how) => {
|
|
6035
|
+
const m = moduleOf(unwrapConfigCall(e));
|
|
6036
|
+
if (m !== null) add(m, how);
|
|
6037
|
+
};
|
|
6038
|
+
const visit = (node) => {
|
|
6039
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && isStr(node.moduleSpecifier)) {
|
|
6040
|
+
const clause = node.exportClause;
|
|
6041
|
+
const whole = !clause || ts.isNamedExports(clause) && clause.elements.some((el) => el.name.text === "default");
|
|
6042
|
+
if (whole) add(node.moduleSpecifier.text, oneLine(node));
|
|
6043
|
+
} else if (ts.isExportAssignment(node)) {
|
|
6044
|
+
exported(node.expression, oneLine(node));
|
|
6045
|
+
} else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && /^(?:module\.)?exports(?:\.default)?$/.test(node.left.getText())) {
|
|
6046
|
+
exported(node.right, oneLine(node));
|
|
6047
|
+
} else if (ts.isSpreadAssignment(node)) {
|
|
6048
|
+
const m = moduleOf(node.expression);
|
|
6049
|
+
if (m !== null) add(m, `...${oneLine(node.expression)}`);
|
|
6050
|
+
} else if (ts.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0]) {
|
|
6051
|
+
const m = moduleOf(node.arguments[0]);
|
|
6052
|
+
if (m !== null) add(m, `${oneLine(node.expression)}(${oneLine(node.arguments[0])}, \u2026)`);
|
|
6053
|
+
} else if (ts.isPropertyAssignment(node)) {
|
|
6054
|
+
const k = keyName2(node.name);
|
|
6055
|
+
const v = node.initializer;
|
|
6056
|
+
if (runner === "vitest" && k === "extends" && isStr(v)) add(v.text, `extends: ${JSON.stringify(v.text)}`);
|
|
6057
|
+
else if (runner === "jest" && k === "preset" && isStr(v)) add(v.text, `preset: ${JSON.stringify(v.text)}`);
|
|
6058
|
+
else if (k === "projects" && ts.isArrayLiteralExpression(v) && (runner === "jest" || ownerKey2(node) === "test")) {
|
|
6059
|
+
for (const el of v.elements) if (isStr(el)) add(el.text, `projects: [${JSON.stringify(el.text)}]`, false);
|
|
6060
|
+
}
|
|
6034
6061
|
}
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6062
|
+
ts.forEachChild(node, visit);
|
|
6063
|
+
};
|
|
6064
|
+
visit(sf);
|
|
6065
|
+
return out3;
|
|
6066
|
+
}
|
|
6067
|
+
function suiteDelegations(before, after, path) {
|
|
6068
|
+
const runner = runnerOf(path, after) ?? runnerOf(path, before);
|
|
6069
|
+
if (!runner || runner === "pytest") return [];
|
|
6070
|
+
const was = new Set(delegationsIn(before ?? "", runner).map((d) => d.target));
|
|
6071
|
+
return delegationsIn(after, runner).filter((d) => !was.has(d.target));
|
|
6072
|
+
}
|
|
6073
|
+
function targetCandidates(configPath, target) {
|
|
6074
|
+
const dir = configPath.includes("/") ? configPath.slice(0, configPath.lastIndexOf("/")) : "";
|
|
6075
|
+
const rel = target.replace(/^<rootDir>\/?/, "./");
|
|
6076
|
+
const parts = rel.startsWith("/") || !dir ? [] : dir.split("/");
|
|
6077
|
+
for (const seg of rel.split("/")) {
|
|
6078
|
+
if (seg === "" || seg === ".") continue;
|
|
6079
|
+
if (seg === "..") parts.pop();
|
|
6080
|
+
else parts.push(seg);
|
|
6049
6081
|
}
|
|
6050
|
-
|
|
6051
|
-
|
|
6082
|
+
const base = parts.join("/");
|
|
6083
|
+
if (!base) return [];
|
|
6084
|
+
const exts = [".js", ".cjs", ".mjs", ".ts", ".cts", ".mts", ".json"];
|
|
6085
|
+
return [base, ...exts.map((e) => base + e), ...exts.map((e) => `${base}/index${e}`)];
|
|
6052
6086
|
}
|
|
6053
|
-
function
|
|
6087
|
+
function setupEntriesIn(src, runner, path) {
|
|
6054
6088
|
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" });
|
|
6089
|
+
let sf = null;
|
|
6090
|
+
try {
|
|
6091
|
+
sf = parseSource("cfg.ts", asExpression2(src));
|
|
6092
|
+
} catch {
|
|
6093
|
+
return out3;
|
|
6094
|
+
}
|
|
6095
|
+
if (!sf) return out3;
|
|
6096
|
+
const inPackageJson = (path.split("/").pop() ?? path) === "package.json";
|
|
6097
|
+
const underJestKey = (node) => {
|
|
6098
|
+
for (let q = node.parent; q; q = q.parent) {
|
|
6099
|
+
if (ts.isPropertyAssignment(q) && keyName2(q.name) === "jest") return true;
|
|
6077
6100
|
}
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6101
|
+
return false;
|
|
6102
|
+
};
|
|
6103
|
+
const values = (e) => {
|
|
6104
|
+
if (isStr(e)) return [e.text];
|
|
6105
|
+
if (!ts.isArrayLiteralExpression(e)) return [];
|
|
6106
|
+
const vs = [];
|
|
6107
|
+
for (const el of e.elements) {
|
|
6108
|
+
if (isStr(el)) vs.push(el.text);
|
|
6109
|
+
else if (ts.isArrayLiteralExpression(el) && el.elements[0] && isStr(el.elements[0])) vs.push(el.elements[0].text);
|
|
6110
|
+
}
|
|
6111
|
+
return vs;
|
|
6112
|
+
};
|
|
6113
|
+
const visit = (node) => {
|
|
6114
|
+
if (ts.isPropertyAssignment(node)) {
|
|
6115
|
+
const k = keyName2(node.name);
|
|
6116
|
+
const applies = k !== null && (runner === "jest" ? JEST_SETUP_KEYS.test(k) && (!inPackageJson || underJestKey(node)) : VITEST_SETUP_KEYS.test(k) && ownerKey2(node) === "test");
|
|
6117
|
+
if (applies && k !== null) {
|
|
6118
|
+
for (const v of values(node.initializer)) if (namesFile(v)) out3.push({ key: k, target: v });
|
|
6119
|
+
}
|
|
6083
6120
|
}
|
|
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
|
-
}
|
|
6121
|
+
ts.forEachChild(node, visit);
|
|
6122
|
+
};
|
|
6123
|
+
visit(sf);
|
|
6103
6124
|
return out3;
|
|
6104
6125
|
}
|
|
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" };
|
|
6126
|
+
function newSetupEntries(before, after, path) {
|
|
6127
|
+
const runner = runnerOf(path, after) ?? runnerOf(path, before);
|
|
6128
|
+
if (!runner || runner === "pytest") return [];
|
|
6129
|
+
const was = new Set(setupEntriesIn(before ?? "", runner, path).map((e) => `${e.key} ${e.target}`));
|
|
6130
|
+
return setupEntriesIn(after, runner, path).filter((e) => !was.has(`${e.key} ${e.target}`));
|
|
6134
6131
|
}
|
|
6135
|
-
var
|
|
6136
|
-
var
|
|
6137
|
-
"src/detectors/
|
|
6132
|
+
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;
|
|
6133
|
+
var init_suite_config = __esm({
|
|
6134
|
+
"src/detectors/suite-config.ts"() {
|
|
6138
6135
|
"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)/ }
|
|
6136
|
+
init_lazy_deps();
|
|
6137
|
+
init_ts_lazy();
|
|
6138
|
+
JEST_DEFAULT_MATCH = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"];
|
|
6139
|
+
JEST_DEFAULT_IGNORE = ["/node_modules/"];
|
|
6140
|
+
VITEST_DEFAULT_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
|
|
6141
|
+
PYTEST_DEFAULT_INCLUDE = ["**/test_*.py", "**/*_test.py"];
|
|
6142
|
+
PYTEST_DEFAULT_IGNORE = ["**/build/**", "**/dist/**", "**/node_modules/**", "**/venv/**", "**/.*/**"];
|
|
6143
|
+
VITEST_DEFAULT_EXCLUDE = [
|
|
6144
|
+
"**/node_modules/**",
|
|
6145
|
+
"**/dist/**",
|
|
6146
|
+
"**/cypress/**",
|
|
6147
|
+
"**/.{idea,git,cache,output,temp}/**",
|
|
6148
|
+
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*"
|
|
6174
6149
|
];
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
"
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6150
|
+
JS_UNWALKED = [...VITEST_DEFAULT_EXCLUDE, "**/.*/**"];
|
|
6151
|
+
CANONICAL_SAMPLES = [
|
|
6152
|
+
"src/a.test.ts",
|
|
6153
|
+
"src/a.spec.ts",
|
|
6154
|
+
"test/a.test.ts",
|
|
6155
|
+
"tests/a.test.js",
|
|
6156
|
+
"src/__tests__/a.ts",
|
|
6157
|
+
"a.test.tsx",
|
|
6158
|
+
"a.spec.js",
|
|
6159
|
+
"test/a.test.mjs"
|
|
6160
|
+
];
|
|
6161
|
+
PYTEST_CANONICAL_SAMPLES = [
|
|
6162
|
+
"tests/test_a.py",
|
|
6163
|
+
"tests/a_test.py",
|
|
6164
|
+
"test_a.py",
|
|
6165
|
+
"src/tests/test_a.py",
|
|
6166
|
+
"tests/conftest.py"
|
|
6167
|
+
];
|
|
6168
|
+
isStr = (e) => ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e);
|
|
6169
|
+
PYTEST_INI_ORDER = ["pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"];
|
|
6170
|
+
pathGlobs = (p) => {
|
|
6171
|
+
const clean = p.replace(/^\.\//, "").replace(/::.*$/, "").replace(/\/+$/, "");
|
|
6172
|
+
return clean ? [clean, clean + "/**"] : [];
|
|
6188
6173
|
};
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6174
|
+
isWorkspaceFile = (path) => /^vitest\.workspace\./.test(path.split("/").pop() ?? path);
|
|
6175
|
+
empty = () => ({ include: null, regex: null, ignore: null, roots: null, base: null, projects: null, wide: [], opaque: false, present: false });
|
|
6176
|
+
stripRootDir = (g) => g.replace(/^<rootDir>\/?/, "");
|
|
6177
|
+
JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
|
|
6178
|
+
globs2 = (patterns) => {
|
|
6179
|
+
const ms = patterns.map((g) => {
|
|
6180
|
+
try {
|
|
6181
|
+
return picomatch(g, { dot: true });
|
|
6182
|
+
} catch {
|
|
6183
|
+
return () => false;
|
|
6184
|
+
}
|
|
6185
|
+
});
|
|
6186
|
+
return (path) => ms.some((m) => m(path));
|
|
6201
6187
|
};
|
|
6202
|
-
|
|
6188
|
+
globList2 = (patterns) => {
|
|
6189
|
+
const pos = globs2(patterns.filter((g) => !g.startsWith("!")));
|
|
6190
|
+
const neg = globs2(patterns.filter((g) => g.startsWith("!")).map((g) => g.slice(1)));
|
|
6191
|
+
return (path) => pos(path) && !neg(path);
|
|
6192
|
+
};
|
|
6193
|
+
regexes = (patterns) => {
|
|
6194
|
+
const rs = patterns.flatMap((p) => {
|
|
6195
|
+
try {
|
|
6196
|
+
return [new RegExp(p.replace(/<rootDir>/g, ""))];
|
|
6197
|
+
} catch {
|
|
6198
|
+
return [];
|
|
6199
|
+
}
|
|
6200
|
+
});
|
|
6201
|
+
return (path) => rs.some((r) => r.test("/" + path));
|
|
6202
|
+
};
|
|
6203
|
+
wideWhy = (hit) => `${hit.key} narrows the whole run (${JSON.stringify(hit.pattern.slice(1).replace(/^testNamePattern /, ""))})`;
|
|
6204
|
+
underDir = (path, dir) => dir === "" || path.startsWith(dir + "/");
|
|
6205
|
+
isLocalPath = (s) => /^(?:\.\.?\/|\/|<rootDir>)/.test(s);
|
|
6206
|
+
hasGlob = (s) => /[*?[\]{}]/.test(s);
|
|
6207
|
+
oneLine = (node) => node.getText().replace(/\s+/g, " ").slice(0, 120);
|
|
6208
|
+
JEST_SETUP_KEYS = /^(?:setupFiles|setupFilesAfterEnv|setupFilesAfterEach|globalSetup|globalTeardown|runner|testRunner|testEnvironment|reporters|testResultsProcessor|testSequencer|snapshotResolver|resolver)$/;
|
|
6209
|
+
VITEST_SETUP_KEYS = /^(?:setupFiles|setupFilesAfterEach|globalSetup|environment|runner|reporters)$/;
|
|
6210
|
+
namesFile = (s) => isLocalPath(s) || /\.[cm]?[jt]sx?$/.test(s) && !s.startsWith("@");
|
|
6203
6211
|
}
|
|
6204
6212
|
});
|
|
6205
6213
|
|
|
6206
|
-
// src/detectors/
|
|
6207
|
-
function
|
|
6208
|
-
|
|
6214
|
+
// src/detectors/config-weakening.ts
|
|
6215
|
+
function parseJsonc(path, text) {
|
|
6216
|
+
try {
|
|
6217
|
+
const r = ts.parseConfigFileTextToJson(path, text);
|
|
6218
|
+
if (r.error) return null;
|
|
6219
|
+
const cfg = r.config;
|
|
6220
|
+
return cfg;
|
|
6221
|
+
} catch {
|
|
6222
|
+
return null;
|
|
6223
|
+
}
|
|
6209
6224
|
}
|
|
6210
|
-
function
|
|
6211
|
-
|
|
6225
|
+
function parseYaml(text) {
|
|
6226
|
+
try {
|
|
6227
|
+
const v = yaml.parse(text);
|
|
6228
|
+
return v;
|
|
6229
|
+
} catch {
|
|
6230
|
+
return null;
|
|
6231
|
+
}
|
|
6212
6232
|
}
|
|
6213
|
-
function
|
|
6214
|
-
|
|
6215
|
-
const m = v.match(/^\$\{\{\s*(.+?)\s*\}\}$/);
|
|
6216
|
-
return m ? m[1] : v;
|
|
6233
|
+
function corePattern(p) {
|
|
6234
|
+
return p.trim().replace(/^!/, "").replace(/^(?:\.\/|\/)+/, "").replace(/^(?:\*\*\/)+/, "").replace(/(?:\/\*\*?)+\/?$/, "").replace(/\/+$/, "");
|
|
6217
6235
|
}
|
|
6218
|
-
function
|
|
6219
|
-
const
|
|
6220
|
-
if (
|
|
6221
|
-
const
|
|
6222
|
-
return
|
|
6236
|
+
function housekeepingByName(p) {
|
|
6237
|
+
const core = corePattern(p);
|
|
6238
|
+
if (core === "" || core === "**" || core === "*") return false;
|
|
6239
|
+
const first = core.split("/")[0];
|
|
6240
|
+
return HOUSEKEEPING_DIR.test(first) || HOUSEKEEPING_FILE.test(core);
|
|
6223
6241
|
}
|
|
6224
|
-
function
|
|
6225
|
-
const
|
|
6226
|
-
if (
|
|
6227
|
-
const
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
return f !== void 0 && truthy(f);
|
|
6242
|
+
function ignoreGlobs(p) {
|
|
6243
|
+
const core = corePattern(p);
|
|
6244
|
+
if (!core) return [];
|
|
6245
|
+
const anchored = p.trim().replace(/^!/, "").startsWith("/") || core.includes("/");
|
|
6246
|
+
const heads = anchored ? [core] : [core, `**/${core}`];
|
|
6247
|
+
return heads.flatMap((h) => [h, `${h}/**`]);
|
|
6231
6248
|
}
|
|
6232
|
-
function
|
|
6233
|
-
const
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6249
|
+
function hidesSource(pattern, dir, ctx) {
|
|
6250
|
+
const files = trackedFiles(ctx);
|
|
6251
|
+
if (!files) return housekeepingByName(pattern) ? null : pattern;
|
|
6252
|
+
const m = matcher(ignoreGlobs(pattern));
|
|
6253
|
+
const hit = files.find((f) => {
|
|
6254
|
+
if (dir && !f.startsWith(dir)) return false;
|
|
6255
|
+
const rel = f.slice(dir.length);
|
|
6256
|
+
if (!CODE_FILE.test(rel) || HOUSEKEEPING_FILE.test(rel)) return false;
|
|
6257
|
+
if (rel.split("/").slice(0, -1).some((seg) => HOUSEKEEPING_DIR.test(seg))) return false;
|
|
6258
|
+
return m(rel);
|
|
6259
|
+
});
|
|
6260
|
+
return hit === void 0 ? null : hit;
|
|
6241
6261
|
}
|
|
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];
|
|
6262
|
+
function grown(before, after) {
|
|
6263
|
+
const was = new Set(before.map(corePattern));
|
|
6264
|
+
return after.filter((e) => !was.has(corePattern(e)));
|
|
6254
6265
|
}
|
|
6255
|
-
function
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
}
|
|
6264
|
-
return out3;
|
|
6266
|
+
function tsconfigFacts(path, text) {
|
|
6267
|
+
if (text === null) return null;
|
|
6268
|
+
const cfg = parseJsonc(path, text);
|
|
6269
|
+
if (!isRecord(cfg)) return null;
|
|
6270
|
+
return {
|
|
6271
|
+
options: isRecord(cfg.compilerOptions) ? cfg.compilerOptions : {},
|
|
6272
|
+
exclude: strings(cfg.exclude),
|
|
6273
|
+
extends: strings(cfg.extends)
|
|
6274
|
+
};
|
|
6265
6275
|
}
|
|
6266
|
-
function
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6276
|
+
function effective(f, flag) {
|
|
6277
|
+
if (!f) return false;
|
|
6278
|
+
const v = f.options[flag];
|
|
6279
|
+
if (typeof v === "boolean") return v;
|
|
6280
|
+
return STRICT_FAMILY.has(flag) && f.options.strict === true;
|
|
6281
|
+
}
|
|
6282
|
+
function tsconfigWeakenings(c, all, policy, addedPaths, ctx, out3) {
|
|
6283
|
+
if (c.before === null || c.after === null) return;
|
|
6284
|
+
const b = tsconfigFacts(c.path, c.before);
|
|
6285
|
+
const a = tsconfigFacts(c.path, c.after);
|
|
6286
|
+
if (!b || !a) return;
|
|
6287
|
+
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) }));
|
|
6288
|
+
const movedTo = (flag) => others.find((o) => effective(o.a, flag) && !effective(o.b, flag));
|
|
6289
|
+
const push = (message, evidence, remediation) => out3.push(makeFinding(RULE6, policy, { file: c.path, message, evidence, remediation, defaultSeverity: "warn" }));
|
|
6290
|
+
const strictLowered = effective(b, "strict") && !effective(a, "strict");
|
|
6291
|
+
for (const flag of TRUE_IS_STRICT) {
|
|
6292
|
+
if (!effective(b, flag) || effective(a, flag)) continue;
|
|
6293
|
+
if (flag !== "strict" && STRICT_FAMILY.has(flag) && strictLowered && a.options[flag] === void 0) continue;
|
|
6294
|
+
if (movedTo(flag)) continue;
|
|
6295
|
+
const was = typeof b.options[flag] === "boolean" ? String(b.options[flag]) : "implied by strict";
|
|
6296
|
+
const now = a.options[flag] === void 0 ? "removed" : String(a.options[flag]);
|
|
6297
|
+
push(
|
|
6298
|
+
`tsconfig strictness lowered: compilerOptions.${flag} ${was} \u2192 ${now}.`,
|
|
6299
|
+
`${c.path}: ${flag} ${was} \u2192 ${now}`,
|
|
6300
|
+
`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.`
|
|
6301
|
+
);
|
|
6283
6302
|
}
|
|
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;
|
|
6303
|
+
for (const flag of TRUE_IS_WEAK) {
|
|
6304
|
+
if (b.options[flag] === true || a.options[flag] !== true) continue;
|
|
6305
|
+
push(
|
|
6306
|
+
`tsconfig check loosened: compilerOptions.${flag}: true added${flag === "skipLibCheck" ? " (declaration files are no longer type-checked)" : ""}.`,
|
|
6307
|
+
`${c.path}: ${flag}: true`,
|
|
6308
|
+
`Leave ${flag} off and fix what the compiler reports; a check switched off here is off for every build.`
|
|
6309
|
+
);
|
|
6312
6310
|
}
|
|
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;
|
|
6311
|
+
const dir = dirOf2(c.path);
|
|
6312
|
+
for (const e of grown(b.exclude, a.exclude)) {
|
|
6313
|
+
const hit = hidesSource(e, dir, ctx);
|
|
6314
|
+
if (hit === null) continue;
|
|
6315
|
+
push(
|
|
6316
|
+
`tsconfig exclude grown: ${JSON.stringify(e)} now hides ${hit === e ? "source" : hit} from the compiler.`,
|
|
6317
|
+
`${c.path}: exclude += ${JSON.stringify(e)}`,
|
|
6318
|
+
"Keep the compiler pointed at the whole source. A file excluded from tsconfig is not type-checked anywhere."
|
|
6319
|
+
);
|
|
6344
6320
|
}
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
return (p) => r.test(p);
|
|
6352
|
-
} catch {
|
|
6353
|
-
return () => false;
|
|
6321
|
+
if (b.extends.length && a.extends.length === 0) {
|
|
6322
|
+
push(
|
|
6323
|
+
`tsconfig no longer extends ${b.extends.map((x) => JSON.stringify(x)).join(", ")}: the strictness the base carried is gone with it.`,
|
|
6324
|
+
`${c.path}: extends removed`,
|
|
6325
|
+
"Keep the base config, or carry its compiler options over explicitly so the check stays as strict as it was."
|
|
6326
|
+
);
|
|
6354
6327
|
}
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6328
|
+
for (const target of a.extends.filter((t) => !b.extends.includes(t) && isLocalTarget(t))) {
|
|
6329
|
+
const cands = targetCandidates(c.path, target);
|
|
6330
|
+
const added = cands.find((f) => addedPaths.has(f));
|
|
6331
|
+
if (added === void 0 && cands.some((f) => isProtected(f, policy, "config"))) continue;
|
|
6332
|
+
push(
|
|
6333
|
+
`tsconfig now extends ${JSON.stringify(target)}, ${added !== void 0 ? `a file this change adds (${added})` : "a file no protected glob covers"}.`,
|
|
6334
|
+
`${c.path}: extends ${JSON.stringify(target)}`,
|
|
6335
|
+
"Extend a protected config, or protect the base (protected.config in .tamperward.yml); the strictness now lives where the gate cannot read it."
|
|
6336
|
+
);
|
|
6361
6337
|
}
|
|
6362
|
-
return on;
|
|
6363
6338
|
}
|
|
6364
|
-
function
|
|
6365
|
-
const
|
|
6366
|
-
if (
|
|
6367
|
-
|
|
6368
|
-
return
|
|
6339
|
+
function levelOf(v) {
|
|
6340
|
+
const head = Array.isArray(v) ? v[0] : v;
|
|
6341
|
+
if (head === "off" || head === 0) return "off";
|
|
6342
|
+
if (head === "warn" || head === "error" || head === 1 || head === 2) return "on";
|
|
6343
|
+
return null;
|
|
6369
6344
|
}
|
|
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`);
|
|
6345
|
+
function eslintObjectFacts(v, f, key2 = null) {
|
|
6346
|
+
if (Array.isArray(v)) {
|
|
6347
|
+
for (const el of v) eslintObjectFacts(el, f, null);
|
|
6348
|
+
return;
|
|
6378
6349
|
}
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6350
|
+
if (!isRecord(v)) return;
|
|
6351
|
+
if (key2 === "rules") {
|
|
6352
|
+
for (const [rule, level] of Object.entries(v)) {
|
|
6353
|
+
const l = levelOf(level);
|
|
6354
|
+
if (l === "off") f.off.set(rule, `${rule}: ${JSON.stringify(level)}`);
|
|
6355
|
+
else if (l === "on") f.on.add(rule);
|
|
6384
6356
|
}
|
|
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`);
|
|
6357
|
+
return;
|
|
6394
6358
|
}
|
|
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`);
|
|
6359
|
+
for (const [k, val] of Object.entries(v)) {
|
|
6360
|
+
if (k === "ignores" || k === "ignorePatterns") f.ignores.push(...strings(val));
|
|
6361
|
+
else eslintObjectFacts(val, f, k);
|
|
6400
6362
|
}
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6363
|
+
}
|
|
6364
|
+
function eslintAstFacts(text, f) {
|
|
6365
|
+
let sf = null;
|
|
6366
|
+
try {
|
|
6367
|
+
sf = parseSource("eslint.config.ts", text);
|
|
6368
|
+
} catch {
|
|
6369
|
+
return;
|
|
6404
6370
|
}
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
if (
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6371
|
+
if (!sf) return;
|
|
6372
|
+
const isStr2 = (e) => ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e);
|
|
6373
|
+
const keyText = (n) => ts.isIdentifier(n) || isStr2(n) || ts.isNumericLiteral(n) ? n.text : null;
|
|
6374
|
+
const literal2 = (e) => {
|
|
6375
|
+
if (isStr2(e)) return e.text;
|
|
6376
|
+
if (ts.isNumericLiteral(e)) return Number(e.text);
|
|
6377
|
+
if (ts.isArrayLiteralExpression(e)) return e.elements.map(literal2);
|
|
6378
|
+
return void 0;
|
|
6379
|
+
};
|
|
6380
|
+
const arrayStrings = (e) => ts.isArrayLiteralExpression(e) ? e.elements.filter(isStr2).map((s) => s.text) : isStr2(e) ? [e.text] : [];
|
|
6381
|
+
const visit = (node) => {
|
|
6382
|
+
if (ts.isPropertyAssignment(node)) {
|
|
6383
|
+
const k = keyText(node.name);
|
|
6384
|
+
if (k === "rules" && ts.isObjectLiteralExpression(node.initializer)) {
|
|
6385
|
+
for (const p of node.initializer.properties) {
|
|
6386
|
+
if (!ts.isPropertyAssignment(p)) continue;
|
|
6387
|
+
const rule = keyText(p.name);
|
|
6388
|
+
if (rule === null) continue;
|
|
6389
|
+
const l = levelOf(literal2(p.initializer));
|
|
6390
|
+
if (l === "off") f.off.set(rule, `${rule}: ${p.initializer.getText().replace(/\s+/g, " ").slice(0, 60)}`);
|
|
6391
|
+
else if (l === "on") f.on.add(rule);
|
|
6392
|
+
}
|
|
6393
|
+
} else if (k === "ignores" || k === "ignorePatterns") f.ignores.push(...arrayStrings(node.initializer));
|
|
6394
|
+
} else if (ts.isCallExpression(node) && /^(?:\w+\.)?globalIgnores$/.test(node.expression.getText()) && node.arguments[0]) {
|
|
6395
|
+
f.ignores.push(...arrayStrings(node.arguments[0]));
|
|
6414
6396
|
}
|
|
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;
|
|
6397
|
+
ts.forEachChild(node, visit);
|
|
6398
|
+
};
|
|
6399
|
+
visit(sf);
|
|
6436
6400
|
}
|
|
6437
|
-
function
|
|
6438
|
-
const
|
|
6439
|
-
if (
|
|
6440
|
-
const
|
|
6441
|
-
if (
|
|
6442
|
-
|
|
6401
|
+
function eslintFacts(path, text) {
|
|
6402
|
+
const f = emptyFacts();
|
|
6403
|
+
if (text === null) return f;
|
|
6404
|
+
const base = basename2(path);
|
|
6405
|
+
if (isEslintIgnore(path)) {
|
|
6406
|
+
f.ignores.push(...text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("!")));
|
|
6407
|
+
return f;
|
|
6408
|
+
}
|
|
6409
|
+
if (/\.(?:[cm]?[jt]s)$/.test(base)) eslintAstFacts(text, f);
|
|
6410
|
+
else if (/\.ya?ml$/.test(base)) eslintObjectFacts(parseYaml(text), f);
|
|
6411
|
+
else if (base === "package.json") {
|
|
6412
|
+
const pkg = parseJsonc(path, text);
|
|
6413
|
+
if (isRecord(pkg)) eslintObjectFacts(pkg.eslintConfig, f);
|
|
6414
|
+
} else {
|
|
6415
|
+
const j = parseJsonc(path, text);
|
|
6416
|
+
eslintObjectFacts(j !== null ? j : base === ".eslintrc" ? parseYaml(text) : null, f);
|
|
6417
|
+
}
|
|
6418
|
+
return f;
|
|
6419
|
+
}
|
|
6420
|
+
function biomeFacts(path, text) {
|
|
6421
|
+
const f = emptyFacts();
|
|
6422
|
+
if (text === null) return f;
|
|
6423
|
+
const cfg = parseJsonc(path, text);
|
|
6424
|
+
if (!isRecord(cfg)) return f;
|
|
6425
|
+
const readLinter = (linter) => {
|
|
6426
|
+
if (!isRecord(linter)) return;
|
|
6427
|
+
if (linter.enabled === false) f.linterOff = true;
|
|
6428
|
+
f.ignores.push(...strings(linter.ignore));
|
|
6429
|
+
const rules = linter.rules;
|
|
6430
|
+
if (!isRecord(rules)) return;
|
|
6431
|
+
if (rules.recommended === false) f.off.set("recommended", "rules.recommended: false");
|
|
6432
|
+
for (const [group, body] of Object.entries(rules)) {
|
|
6433
|
+
if (!isRecord(body)) continue;
|
|
6434
|
+
if (body.recommended === false) f.off.set(`${group}/recommended`, `${group}.recommended: false`);
|
|
6435
|
+
for (const [rule, level] of Object.entries(body)) {
|
|
6436
|
+
const l = level === "off" || isRecord(level) && level.level === "off" ? "off" : typeof level === "string" || isRecord(level) ? "on" : null;
|
|
6437
|
+
if (l === "off") f.off.set(`${group}/${rule}`, `${group}.${rule}: off`);
|
|
6438
|
+
else if (l === "on") f.on.add(`${group}/${rule}`);
|
|
6439
|
+
}
|
|
6440
|
+
}
|
|
6441
|
+
};
|
|
6442
|
+
readLinter(cfg.linter);
|
|
6443
|
+
if (Array.isArray(cfg.overrides)) {
|
|
6444
|
+
for (const o of cfg.overrides) if (isRecord(o)) readLinter(o.linter);
|
|
6445
|
+
}
|
|
6446
|
+
if (isRecord(cfg.files)) {
|
|
6447
|
+
f.ignores.push(...strings(cfg.files.ignore));
|
|
6448
|
+
f.ignores.push(...strings(cfg.files.includes).filter((p) => p.startsWith("!")));
|
|
6449
|
+
}
|
|
6450
|
+
return f;
|
|
6443
6451
|
}
|
|
6444
|
-
function
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
return d !== null && /^\s*defaults:\s*$/.test(lines[d]) ? d : null;
|
|
6452
|
+
function replacedByVariant(rule, on) {
|
|
6453
|
+
if (rule.includes("/")) return false;
|
|
6454
|
+
for (const r of on) if (r.endsWith(`/${rule}`)) return true;
|
|
6455
|
+
return false;
|
|
6449
6456
|
}
|
|
6450
|
-
function
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6457
|
+
function lintWeakenings(c, facts, priorAll, policy, ctx, out3) {
|
|
6458
|
+
if (c.after === null) return;
|
|
6459
|
+
const a = facts(c.path, c.after);
|
|
6460
|
+
const tool = isBiome(c.path) ? "biome" : "eslint";
|
|
6461
|
+
const push = (message, evidence, remediation) => out3.push(makeFinding(RULE6, policy, { file: c.path, message, evidence, remediation, defaultSeverity: "warn" }));
|
|
6462
|
+
if (a.linterOff && !priorAll.linterOff) {
|
|
6463
|
+
push(`${tool} linter switched off: linter.enabled: false.`, `${c.path}: linter.enabled: false`, "Keep the linter on and fix what it reports.");
|
|
6464
|
+
}
|
|
6465
|
+
for (const [rule, how] of a.off) {
|
|
6466
|
+
if (priorAll.off.has(rule) || replacedByVariant(rule, a.on)) continue;
|
|
6467
|
+
push(
|
|
6468
|
+
`${tool} rule turned off: ${rule}.`,
|
|
6469
|
+
`${c.path}: ${how}`,
|
|
6470
|
+
`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.`
|
|
6471
|
+
);
|
|
6472
|
+
}
|
|
6473
|
+
const dir = dirOf2(c.path);
|
|
6474
|
+
for (const e of grown(priorAll.ignores, a.ignores)) {
|
|
6475
|
+
const hit = hidesSource(e, dir, ctx);
|
|
6476
|
+
if (hit === null) continue;
|
|
6477
|
+
const key2 = isEslintIgnore(c.path) ? ".eslintignore" : tool === "biome" ? "files.ignore" : /\.eslintrc/.test(basename2(c.path)) ? "ignorePatterns" : "ignores";
|
|
6478
|
+
push(
|
|
6479
|
+
`${tool} ${key2} grown: ${JSON.stringify(e)} now hides ${hit === e ? "source" : hit} from the lint.`,
|
|
6480
|
+
`${c.path}: ${key2} += ${JSON.stringify(e)}`,
|
|
6481
|
+
"Keep the lint pointed at the whole source. A path ignored here is never linted, whatever the rules say."
|
|
6482
|
+
);
|
|
6458
6483
|
}
|
|
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
6484
|
}
|
|
6467
|
-
var
|
|
6468
|
-
var
|
|
6469
|
-
"src/detectors/
|
|
6485
|
+
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;
|
|
6486
|
+
var init_config_weakening = __esm({
|
|
6487
|
+
"src/detectors/config-weakening.ts"() {
|
|
6470
6488
|
"use strict";
|
|
6471
|
-
|
|
6489
|
+
init_lazy_deps();
|
|
6490
|
+
init_ts_lazy();
|
|
6472
6491
|
init_policy();
|
|
6492
|
+
init_narrow();
|
|
6473
6493
|
init_finding();
|
|
6474
|
-
init_files();
|
|
6475
|
-
init_hook_wiring();
|
|
6476
6494
|
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"
|
|
6495
|
+
init_suite_config();
|
|
6496
|
+
RULE6 = "config-weakening";
|
|
6497
|
+
basename2 = (path) => path.split("/").pop() ?? path;
|
|
6498
|
+
isTsconfig = (path) => /^tsconfig.*\.json$/.test(basename2(path));
|
|
6499
|
+
isEslintConfig = (path) => /^(?:\.eslintrc(?:\..*)?|eslint\.config\..*)$/.test(basename2(path));
|
|
6500
|
+
isEslintIgnore = (path) => basename2(path) === ".eslintignore";
|
|
6501
|
+
isBiome = (path) => /^biome\.jsonc?$/.test(basename2(path));
|
|
6502
|
+
CODE_FILE = /\.(?:[cm]?[jt]sx?|vue|svelte)$/;
|
|
6503
|
+
HOUSEKEEPING_DIR = /^(?:node_modules|dist|build|out|output|coverage|lib|esm|cjs|umd|target|tmp|temp|vendor|generated|__generated__|public|static|storybook-static|\..+)$/;
|
|
6504
|
+
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;
|
|
6505
|
+
matcher = (patterns) => {
|
|
6506
|
+
const ms = patterns.map((g) => {
|
|
6507
|
+
try {
|
|
6508
|
+
return picomatch(g, { dot: true });
|
|
6509
|
+
} catch {
|
|
6510
|
+
return () => false;
|
|
6511
|
+
}
|
|
6512
|
+
});
|
|
6513
|
+
return (path) => ms.some((m) => m(path));
|
|
6510
6514
|
};
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6515
|
+
dirOf2 = (path) => path.includes("/") ? path.slice(0, path.lastIndexOf("/") + 1) : "";
|
|
6516
|
+
strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : typeof v === "string" ? [v] : [];
|
|
6517
|
+
STRICT_FAMILY = /* @__PURE__ */ new Set([
|
|
6518
|
+
"noImplicitAny",
|
|
6519
|
+
"strictNullChecks",
|
|
6520
|
+
"strictFunctionTypes",
|
|
6521
|
+
"strictBindCallApply",
|
|
6522
|
+
"strictPropertyInitialization",
|
|
6523
|
+
"noImplicitThis",
|
|
6524
|
+
"alwaysStrict",
|
|
6525
|
+
"useUnknownInCatchVariables",
|
|
6526
|
+
"strictBuiltinIteratorReturn"
|
|
6527
|
+
]);
|
|
6528
|
+
TRUE_IS_STRICT = [
|
|
6529
|
+
"strict",
|
|
6530
|
+
...STRICT_FAMILY,
|
|
6531
|
+
"noUnusedLocals",
|
|
6532
|
+
"noUnusedParameters",
|
|
6533
|
+
"noImplicitReturns",
|
|
6534
|
+
"noFallthroughCasesInSwitch",
|
|
6535
|
+
"noUncheckedIndexedAccess",
|
|
6536
|
+
"noImplicitOverride",
|
|
6537
|
+
"noPropertyAccessFromIndexSignature",
|
|
6538
|
+
"exactOptionalPropertyTypes",
|
|
6539
|
+
"checkJs",
|
|
6540
|
+
"forceConsistentCasingInFileNames"
|
|
6541
|
+
];
|
|
6542
|
+
TRUE_IS_WEAK = ["skipLibCheck", "allowUnreachableCode", "allowUnusedLabels", "suppressImplicitAnyIndexErrors", "suppressExcessPropertyErrors", "noStrictGenericChecks"];
|
|
6543
|
+
isLocalTarget = (s) => /^(?:\.\.?\/|\/)/.test(s);
|
|
6544
|
+
emptyFacts = () => ({ off: /* @__PURE__ */ new Map(), on: /* @__PURE__ */ new Set(), ignores: [], linterOff: false });
|
|
6545
|
+
configWeakening = {
|
|
6546
|
+
id: RULE6,
|
|
6526
6547
|
surface: ["file"],
|
|
6527
6548
|
certainty: "mechanical",
|
|
6528
6549
|
run(changes, policy, _view, ctx) {
|
|
6529
6550
|
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
|
-
}
|
|
6551
|
+
const files = changes.filter((c) => c.kind === "file");
|
|
6552
|
+
const addedPaths = new Set(files.filter((c) => c.op === "add").map((c) => c.path));
|
|
6553
|
+
const tsconfigs = files.filter((c) => isTsconfig(c.path) && isProtected(c.path, policy, "config"));
|
|
6554
|
+
const eslints = files.filter((c) => (isEslintConfig(c.path) || isEslintIgnore(c.path)) && isProtected(c.path, policy, "config"));
|
|
6555
|
+
const biomes = files.filter((c) => isBiome(c.path) && isProtected(c.path, policy, "config"));
|
|
6556
|
+
const priorOf = (list3, facts) => {
|
|
6557
|
+
const all = emptyFacts();
|
|
6558
|
+
for (const c of list3) {
|
|
6559
|
+
const f = facts(c.path, c.before);
|
|
6560
|
+
for (const [k, v] of f.off) all.off.set(k, v);
|
|
6561
|
+
for (const k of f.on) all.on.add(k);
|
|
6562
|
+
all.ignores.push(...f.ignores);
|
|
6563
|
+
if (f.linterOff) all.linterOff = true;
|
|
6693
6564
|
}
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
})
|
|
6710
|
-
);
|
|
6711
|
-
continue;
|
|
6712
|
-
}
|
|
6565
|
+
return all;
|
|
6566
|
+
};
|
|
6567
|
+
const eslintPrior = priorOf(eslints, eslintFacts);
|
|
6568
|
+
const biomePrior = priorOf(biomes, biomeFacts);
|
|
6569
|
+
for (const c of tsconfigs) tsconfigWeakenings(c, tsconfigs, policy, addedPaths, ctx, out3);
|
|
6570
|
+
for (const c of eslints) lintWeakenings(c, eslintFacts, eslintPrior, policy, ctx, out3);
|
|
6571
|
+
for (const c of biomes) lintWeakenings(c, biomeFacts, biomePrior, policy, ctx, out3);
|
|
6572
|
+
for (const c of files) {
|
|
6573
|
+
if (c.after === null || !isProtected(c.path, policy, "config")) continue;
|
|
6574
|
+
const rn = runnerOf(c.path, c.after) ?? runnerOf(c.path, c.before);
|
|
6575
|
+
if (rn !== "jest" && rn !== "vitest") continue;
|
|
6576
|
+
for (const e of newSetupEntries(c.before, c.after, c.path)) {
|
|
6577
|
+
const cands = targetCandidates(c.path, e.target);
|
|
6578
|
+
const added = cands.find((f) => addedPaths.has(f));
|
|
6579
|
+
if (added === void 0 && cands.some((f) => isProtected(f, policy))) continue;
|
|
6713
6580
|
out3.push(
|
|
6714
|
-
makeFinding(
|
|
6581
|
+
makeFinding(RULE6, policy, {
|
|
6715
6582
|
file: c.path,
|
|
6716
|
-
message:
|
|
6717
|
-
evidence:
|
|
6718
|
-
remediation: "
|
|
6583
|
+
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.`,
|
|
6584
|
+
evidence: `${c.path}: ${e.key} += ${JSON.stringify(e.target)}`,
|
|
6585
|
+
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.",
|
|
6586
|
+
defaultSeverity: "warn"
|
|
6719
6587
|
})
|
|
6720
6588
|
);
|
|
6721
6589
|
}
|
|
@@ -6726,444 +6594,1130 @@ var init_ci_tampering = __esm({
|
|
|
6726
6594
|
}
|
|
6727
6595
|
});
|
|
6728
6596
|
|
|
6729
|
-
// src/detectors/
|
|
6730
|
-
function
|
|
6731
|
-
const
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6597
|
+
// src/detectors/coverage-exclusion.ts
|
|
6598
|
+
function isText(line, idx, lang) {
|
|
6599
|
+
const opener = LINE_COMMENT[lang];
|
|
6600
|
+
let quote = null;
|
|
6601
|
+
for (let j = 0; j < idx; j++) {
|
|
6602
|
+
const ch = line[j];
|
|
6603
|
+
if (quote) {
|
|
6604
|
+
if (ch === "\\") j++;
|
|
6605
|
+
else if (ch === quote) quote = null;
|
|
6606
|
+
continue;
|
|
6607
|
+
}
|
|
6608
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
6609
|
+
else if (opener && opener.test(line.slice(j, j + 2))) return true;
|
|
6610
|
+
}
|
|
6611
|
+
return quote !== null;
|
|
6612
|
+
}
|
|
6613
|
+
var RULE7, PATTERNS3, OUT_OF_SCOPE_PATH, LINE_COMMENT, coverageExclusion;
|
|
6614
|
+
var init_coverage_exclusion = __esm({
|
|
6615
|
+
"src/detectors/coverage-exclusion.ts"() {
|
|
6616
|
+
"use strict";
|
|
6617
|
+
init_select();
|
|
6618
|
+
init_files();
|
|
6619
|
+
init_policy();
|
|
6620
|
+
init_finding();
|
|
6621
|
+
RULE7 = "coverage-exclusion";
|
|
6622
|
+
PATTERNS3 = {
|
|
6623
|
+
js: [
|
|
6624
|
+
{ re: /\/\*\s*istanbul\s+ignore\b/, why: "`istanbul ignore` takes the next statement, branch or the whole file out of the coverage measurement" },
|
|
6625
|
+
{ 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" },
|
|
6626
|
+
{ 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" },
|
|
6627
|
+
{ re: /\/\*\s*node:coverage\s+(?:ignore|disable)\b/, why: "`node:coverage ignore/disable` takes the following code out of the coverage measurement" }
|
|
6628
|
+
],
|
|
6629
|
+
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" }],
|
|
6630
|
+
rs: [{ re: /#\s*!?\[[^\]]*\bcoverage\s*\(\s*off\s*\)/, why: "`#[coverage(off)]` takes the item out of the coverage measurement" }],
|
|
6631
|
+
go: [
|
|
6632
|
+
{
|
|
6633
|
+
re: /^\s*\/\/\s*(?:go:build\b|\+build\b)/,
|
|
6634
|
+
why: "a build constraint added to an existing file takes it out of the default build, and out of `go test -cover`",
|
|
6635
|
+
existingOnly: true
|
|
6636
|
+
}
|
|
6637
|
+
]
|
|
6638
|
+
};
|
|
6639
|
+
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)\.[^/]*$/;
|
|
6640
|
+
LINE_COMMENT = { js: /^\/\//, go: /^\/\//, rs: /^\/\//, py: /^#/ };
|
|
6641
|
+
coverageExclusion = {
|
|
6642
|
+
id: RULE7,
|
|
6643
|
+
surface: ["file"],
|
|
6644
|
+
certainty: "mechanical",
|
|
6645
|
+
run(changes, policy) {
|
|
6646
|
+
const out3 = [];
|
|
6647
|
+
for (const c of changes) {
|
|
6648
|
+
if (c.kind !== "file" || c.op === "delete") continue;
|
|
6649
|
+
const lang = langOf(c.path);
|
|
6650
|
+
if (!lang) continue;
|
|
6651
|
+
const patterns = PATTERNS3[lang];
|
|
6652
|
+
if (!patterns) continue;
|
|
6653
|
+
const category = protectedCategory(c.path, policy);
|
|
6654
|
+
if (OUT_OF_SCOPE_PATH.test(c.path) || category === "tests" || category === "config") continue;
|
|
6655
|
+
const removed = new Set(removedLines(c).map((l) => l.content.trim()));
|
|
6656
|
+
const before = c.before ?? "";
|
|
6657
|
+
for (const l of addedLines(c)) {
|
|
6658
|
+
const trimmed = l.content.trim();
|
|
6659
|
+
if (removed.has(trimmed)) continue;
|
|
6660
|
+
for (const p of patterns) {
|
|
6661
|
+
const m = p.re.exec(l.content);
|
|
6662
|
+
if (!m || isText(l.content, m.index, lang)) continue;
|
|
6663
|
+
if (p.existingOnly && (c.op === "add" || c.before == null || before.split("\n").some((b) => p.re.test(b)))) continue;
|
|
6664
|
+
out3.push(
|
|
6665
|
+
makeFinding(RULE7, policy, {
|
|
6666
|
+
file: c.path,
|
|
6667
|
+
line: l.newLine ?? void 0,
|
|
6668
|
+
message: `Coverage exclusion added: ${p.why}.`,
|
|
6669
|
+
evidence: trimmed,
|
|
6670
|
+
remediation: "Cover the code with a test instead of excluding it from the measurement; if it is genuinely unreachable, say so in review.",
|
|
6671
|
+
defaultSeverity: "warn"
|
|
6672
|
+
})
|
|
6673
|
+
);
|
|
6674
|
+
break;
|
|
6675
|
+
}
|
|
6676
|
+
}
|
|
6677
|
+
}
|
|
6678
|
+
return out3;
|
|
6679
|
+
}
|
|
6680
|
+
};
|
|
6681
|
+
}
|
|
6682
|
+
});
|
|
6683
|
+
|
|
6684
|
+
// src/detectors/gh-expression.ts
|
|
6685
|
+
function foldExpressions(s) {
|
|
6686
|
+
return s.replace(/\$\{\{([^}]*)\}\}/g, (_m, e) => {
|
|
6687
|
+
const v = foldConst(e.trim());
|
|
6688
|
+
if (v !== void 0 && v !== TRUTHY) return String(v);
|
|
6689
|
+
return /\b(?:matrix|strategy|inputs)\./.test(e) ? MATRIX_TOKEN : EXPR_TOKEN;
|
|
6690
|
+
});
|
|
6691
|
+
}
|
|
6692
|
+
function lex(src) {
|
|
6693
|
+
const out3 = [];
|
|
6694
|
+
let i = 0;
|
|
6695
|
+
while (i < src.length) {
|
|
6696
|
+
const ch = src[i];
|
|
6697
|
+
if (/\s/.test(ch)) {
|
|
6698
|
+
i++;
|
|
6699
|
+
continue;
|
|
6700
|
+
}
|
|
6701
|
+
if (ch === "'") {
|
|
6702
|
+
let j = i + 1;
|
|
6703
|
+
let s = "";
|
|
6704
|
+
while (j < src.length) {
|
|
6705
|
+
if (src[j] === "'" && src[j + 1] === "'") {
|
|
6706
|
+
s += "'";
|
|
6707
|
+
j += 2;
|
|
6708
|
+
} else if (src[j] === "'") break;
|
|
6709
|
+
else s += src[j++];
|
|
6710
|
+
}
|
|
6711
|
+
if (j >= src.length) return null;
|
|
6712
|
+
out3.push({ t: "str", v: s });
|
|
6713
|
+
i = j + 1;
|
|
6714
|
+
continue;
|
|
6715
|
+
}
|
|
6716
|
+
const num = src.slice(i).match(/^-?\d+(?:\.\d+)?/);
|
|
6717
|
+
if (num) {
|
|
6718
|
+
out3.push({ t: "num", v: num[0] });
|
|
6719
|
+
i += num[0].length;
|
|
6720
|
+
continue;
|
|
6721
|
+
}
|
|
6722
|
+
const op = src.slice(i).match(/^(?:==|!=|&&|\|\||<=|>=|[!()<>,[\]])/);
|
|
6723
|
+
if (op) {
|
|
6724
|
+
out3.push({ t: "op", v: op[0] });
|
|
6725
|
+
i += op[0].length;
|
|
6726
|
+
continue;
|
|
6727
|
+
}
|
|
6728
|
+
const id = src.slice(i).match(/^[A-Za-z_][\w.\-*]*/);
|
|
6729
|
+
if (id) {
|
|
6730
|
+
out3.push({ t: "id", v: id[0] });
|
|
6731
|
+
i += id[0].length;
|
|
6732
|
+
continue;
|
|
6733
|
+
}
|
|
6734
|
+
return null;
|
|
6735
|
+
}
|
|
6736
|
+
return out3;
|
|
6737
|
+
}
|
|
6738
|
+
function foldConst(src) {
|
|
6739
|
+
const toks = lex(src);
|
|
6740
|
+
if (!toks) return void 0;
|
|
6741
|
+
let p = 0;
|
|
6742
|
+
const peek = () => toks[p];
|
|
6743
|
+
const eat = (v2) => toks[p]?.t === "op" && toks[p].v === v2 ? (p++, true) : false;
|
|
6744
|
+
const num = (v2) => v2 === TRUTHY ? NaN : typeof v2 === "number" ? v2 : typeof v2 === "boolean" ? v2 ? 1 : 0 : v2 === null ? 0 : v2.trim() === "" ? 0 : Number(v2);
|
|
6745
|
+
const eq = (a, b) => {
|
|
6746
|
+
if (a === TRUTHY || b === TRUTHY) return void 0;
|
|
6747
|
+
if (typeof a === "string" && typeof b === "string") return a.toLowerCase() === b.toLowerCase();
|
|
6748
|
+
if (a === null && b === null) return true;
|
|
6749
|
+
const x = num(a);
|
|
6750
|
+
const y2 = num(b);
|
|
6751
|
+
return !Number.isNaN(x) && x === y2;
|
|
6752
|
+
};
|
|
6753
|
+
const or = () => {
|
|
6754
|
+
let l = and();
|
|
6755
|
+
while (eat("||")) {
|
|
6756
|
+
const r = and();
|
|
6757
|
+
if (l !== void 0 && truthy(l)) continue;
|
|
6758
|
+
if (l !== void 0) l = r;
|
|
6759
|
+
else l = r !== void 0 && truthy(r) ? TRUTHY : void 0;
|
|
6760
|
+
}
|
|
6761
|
+
return l;
|
|
6762
|
+
};
|
|
6763
|
+
const and = () => {
|
|
6764
|
+
let l = cmp();
|
|
6765
|
+
while (eat("&&")) {
|
|
6766
|
+
const r = cmp();
|
|
6767
|
+
if (l !== void 0 && !truthy(l)) continue;
|
|
6768
|
+
if (l !== void 0) l = r;
|
|
6769
|
+
else l = r !== void 0 && !truthy(r) ? false : void 0;
|
|
6770
|
+
}
|
|
6771
|
+
return l;
|
|
6772
|
+
};
|
|
6773
|
+
const cmp = () => {
|
|
6774
|
+
let l = unary();
|
|
6775
|
+
for (; ; ) {
|
|
6776
|
+
const t = peek();
|
|
6777
|
+
if (!t || t.t !== "op" || !/^(?:==|!=|<|>|<=|>=)$/.test(t.v)) return l;
|
|
6778
|
+
p++;
|
|
6779
|
+
const r = unary();
|
|
6780
|
+
if (l === void 0 || r === void 0) {
|
|
6781
|
+
l = void 0;
|
|
6782
|
+
continue;
|
|
6783
|
+
}
|
|
6784
|
+
if (t.v === "==" || t.v === "!=") {
|
|
6785
|
+
const e = eq(l, r);
|
|
6786
|
+
l = e === void 0 ? void 0 : t.v === "==" ? e : !e;
|
|
6787
|
+
} else {
|
|
6788
|
+
const x = num(l);
|
|
6789
|
+
const y2 = num(r);
|
|
6790
|
+
if (Number.isNaN(x) || Number.isNaN(y2)) l = void 0;
|
|
6791
|
+
else l = t.v === "<" ? x < y2 : t.v === ">" ? x > y2 : t.v === "<=" ? x <= y2 : x >= y2;
|
|
6792
|
+
}
|
|
6793
|
+
}
|
|
6794
|
+
};
|
|
6795
|
+
const unary = () => {
|
|
6796
|
+
if (eat("!")) {
|
|
6797
|
+
const v2 = unary();
|
|
6798
|
+
return v2 === void 0 ? void 0 : !truthy(v2);
|
|
6799
|
+
}
|
|
6800
|
+
return primary();
|
|
6801
|
+
};
|
|
6802
|
+
const primary = () => {
|
|
6803
|
+
const t = peek();
|
|
6804
|
+
if (!t) return void 0;
|
|
6805
|
+
if (eat("(")) {
|
|
6806
|
+
const v2 = or();
|
|
6807
|
+
return eat(")") ? v2 : void 0;
|
|
6808
|
+
}
|
|
6809
|
+
p++;
|
|
6810
|
+
if (t.t === "str") return t.v;
|
|
6811
|
+
if (t.t === "num") return Number(t.v);
|
|
6812
|
+
if (t.t === "id") {
|
|
6813
|
+
if (/^true$/i.test(t.v)) return true;
|
|
6814
|
+
if (/^false$/i.test(t.v)) return false;
|
|
6815
|
+
if (/^null$/i.test(t.v)) return null;
|
|
6816
|
+
if (eat("(")) {
|
|
6817
|
+
const args = [];
|
|
6818
|
+
if (!eat(")")) {
|
|
6819
|
+
for (; ; ) {
|
|
6820
|
+
args.push(or());
|
|
6821
|
+
if (eat(")")) break;
|
|
6822
|
+
if (!eat(",")) return void 0;
|
|
6823
|
+
}
|
|
6824
|
+
}
|
|
6825
|
+
let indexed = false;
|
|
6826
|
+
while (eat("[")) {
|
|
6827
|
+
or();
|
|
6828
|
+
indexed = true;
|
|
6829
|
+
if (!eat("]")) return void 0;
|
|
6830
|
+
}
|
|
6831
|
+
if (indexed) return void 0;
|
|
6832
|
+
return callConst(t.v, args);
|
|
6833
|
+
}
|
|
6834
|
+
while (eat("[")) {
|
|
6835
|
+
or();
|
|
6836
|
+
if (!eat("]")) return void 0;
|
|
6837
|
+
}
|
|
6838
|
+
return void 0;
|
|
6839
|
+
}
|
|
6840
|
+
return void 0;
|
|
6841
|
+
};
|
|
6842
|
+
const v = or();
|
|
6843
|
+
return p === toks.length ? v : void 0;
|
|
6844
|
+
}
|
|
6845
|
+
function callConst(name, args) {
|
|
6846
|
+
const vals = [];
|
|
6847
|
+
for (const a of args) {
|
|
6848
|
+
if (a === void 0 || a === TRUTHY) return void 0;
|
|
6849
|
+
vals.push(a);
|
|
6850
|
+
}
|
|
6851
|
+
const s = (i) => vals[i] == null ? "" : String(vals[i]);
|
|
6852
|
+
switch (name.toLowerCase()) {
|
|
6853
|
+
case "contains":
|
|
6854
|
+
return vals.length === 2 ? s(0).toLowerCase().includes(s(1).toLowerCase()) : void 0;
|
|
6855
|
+
case "startswith":
|
|
6856
|
+
return vals.length === 2 ? s(0).toLowerCase().startsWith(s(1).toLowerCase()) : void 0;
|
|
6857
|
+
case "endswith":
|
|
6858
|
+
return vals.length === 2 ? s(0).toLowerCase().endsWith(s(1).toLowerCase()) : void 0;
|
|
6859
|
+
case "format": {
|
|
6860
|
+
if (vals.length < 1) return void 0;
|
|
6861
|
+
return s(0).replace(/\{\{|\}\}|\{(\d+)\}/g, (m, i) => m === "{{" ? "{" : m === "}}" ? "}" : s(Number(i) + 1));
|
|
6862
|
+
}
|
|
6863
|
+
case "join":
|
|
6864
|
+
return vals.length >= 1 && vals.length <= 2 ? s(0) : void 0;
|
|
6865
|
+
case "tojson":
|
|
6866
|
+
return vals.length === 1 ? JSON.stringify(vals[0]) : void 0;
|
|
6867
|
+
case "fromjson": {
|
|
6868
|
+
if (vals.length !== 1) return void 0;
|
|
6869
|
+
try {
|
|
6870
|
+
const v = JSON.parse(s(0));
|
|
6871
|
+
if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
|
|
6872
|
+
return TRUTHY;
|
|
6873
|
+
} catch {
|
|
6874
|
+
return void 0;
|
|
6875
|
+
}
|
|
6876
|
+
}
|
|
6877
|
+
default:
|
|
6878
|
+
return void 0;
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6881
|
+
var EXPR_TOKEN, MATRIX_TOKEN, TRUTHY, truthy;
|
|
6882
|
+
var init_gh_expression = __esm({
|
|
6883
|
+
"src/detectors/gh-expression.ts"() {
|
|
6884
|
+
"use strict";
|
|
6885
|
+
EXPR_TOKEN = "__expr__";
|
|
6886
|
+
MATRIX_TOKEN = "__matrix__";
|
|
6887
|
+
TRUTHY = /* @__PURE__ */ Symbol("truthy");
|
|
6888
|
+
truthy = (v) => v === TRUTHY ? true : typeof v === "string" ? v !== "" : typeof v === "number" ? v !== 0 : v === true;
|
|
6889
|
+
}
|
|
6890
|
+
});
|
|
6891
|
+
|
|
6892
|
+
// src/detectors/invocation.ts
|
|
6893
|
+
function canonical(core) {
|
|
6894
|
+
let s = foldExpressions(core.replace(/^(['"])(.*)\1$/, "$2").trim()).replace(MATRIX_FLAG, " ").replace(/\s+/g, " ").trim();
|
|
6895
|
+
s = s.replace(/^(?:cd|pushd)\s+\S+\s*&&\s*/, "");
|
|
6896
|
+
s = s.replace(/^(?:npx|pnpm\s+(?:exec|dlx)|yarn\s+(?:exec|dlx)|bunx|bun\s+x)\s+/, "exec ");
|
|
6897
|
+
s = s.replace(/^(?:npm|pnpm|yarn|bun)\s+(?:run(?:-script)?\s+)?/, "exec ");
|
|
6898
|
+
s = s.replace(/^exec\s+(?:t|tst)(?=\s|$)/, "exec test");
|
|
6899
|
+
s = s.replace(/^exec\s+/, "");
|
|
6900
|
+
return s.replace(/\s+--\s*$/, "").trim();
|
|
6901
|
+
}
|
|
6902
|
+
function runnerNarrowed(cmd, args) {
|
|
6903
|
+
return runnerNarrowing(cmd, args) !== null;
|
|
6904
|
+
}
|
|
6905
|
+
function runnerNarrowing(cmd, args) {
|
|
6906
|
+
const t = RUNNER_NARROWINGS.find((x) => x.runner.test(cmd));
|
|
6907
|
+
const m = t ? (" " + args.trim()).match(t.flags) : null;
|
|
6908
|
+
return m ? m[0].trim().replace(/[=\s].*$/, "") : null;
|
|
6909
|
+
}
|
|
6910
|
+
function coverageFloorLowered(removed, added) {
|
|
6911
|
+
const was = removed.match(COVERAGE_FLOOR);
|
|
6912
|
+
const now = added.match(COVERAGE_FLOOR);
|
|
6913
|
+
return was !== null && now !== null && Number(now[1]) < Number(was[1]);
|
|
6914
|
+
}
|
|
6915
|
+
function inCommandPosition(core) {
|
|
6916
|
+
return canonical(core).split(/\s*(?:&&|\|\||;|\|)\s*/).some((seg) => CHECK_HEAD.test(canonical(seg.replace(COMMAND_PREFIX, ""))));
|
|
6917
|
+
}
|
|
6918
|
+
function narrowedByPositional(args) {
|
|
6919
|
+
return positionalOf(args) !== null;
|
|
6920
|
+
}
|
|
6921
|
+
function positionalOf(args) {
|
|
6922
|
+
const toks = args.trim().split(/\s+/).filter(Boolean);
|
|
6923
|
+
for (let i = 0; i < toks.length; i++) {
|
|
6924
|
+
const t = toks[i];
|
|
6925
|
+
if (t === "--" || t.startsWith("-")) continue;
|
|
6926
|
+
const prev = toks[i - 1];
|
|
6927
|
+
if (prev && (prev.startsWith("-") && prev !== "--" && !prev.includes("=") || /[<>]/.test(prev))) continue;
|
|
6928
|
+
if (/[<>|;&]/.test(t)) continue;
|
|
6929
|
+
if (PATH_SHAPED.test(t)) return t;
|
|
6930
|
+
}
|
|
6931
|
+
return null;
|
|
6932
|
+
}
|
|
6933
|
+
function checkKind(core) {
|
|
6934
|
+
if (!invokesCheck(core)) return null;
|
|
6935
|
+
const s = canonical(core);
|
|
6936
|
+
if (/\btamperward\b/.test(s)) return "gate";
|
|
6937
|
+
if (/\b(?:lint|eslint|biome|oxlint|golangci-lint|ruff)\b/i.test(s)) return "lint";
|
|
6938
|
+
if (/\b(?:typecheck|type-check|tsc|tsgo|mypy)\b|\bdeno\s+check\b/i.test(s)) return "types";
|
|
6939
|
+
return "test";
|
|
6940
|
+
}
|
|
6941
|
+
function survives(removedCore, isUses, afterCores, addedCores) {
|
|
6942
|
+
if (isUses) {
|
|
6943
|
+
const ref = usesRef(removedCore);
|
|
6944
|
+
return afterCores.some((a) => usesRef(a) === ref) ? { state: "kept" } : { state: "gone" };
|
|
6945
|
+
}
|
|
6946
|
+
const r = canonical(removedCore);
|
|
6947
|
+
const kind = checkKind(removedCore);
|
|
6948
|
+
const narrowed = (args) => kind === "test" && narrowedByPositional(args);
|
|
6949
|
+
let neutralised;
|
|
6950
|
+
for (const raw of afterCores) {
|
|
6951
|
+
const a = canonical(raw);
|
|
6952
|
+
if (a === r) return { state: "kept" };
|
|
6953
|
+
if (a.startsWith(r + " ")) {
|
|
6954
|
+
const rest = a.slice(r.length);
|
|
6955
|
+
if (NEUTRALISING_SUFFIX.test(rest) || narrowed(rest) || runnerNarrowed(a, rest)) neutralised ??= raw;
|
|
6956
|
+
else return { state: "kept" };
|
|
6957
|
+
} else if (r.startsWith(a + " ") && invokesCheck(a)) {
|
|
6958
|
+
return { state: "kept" };
|
|
6959
|
+
} else {
|
|
6960
|
+
const w = a.match(TIMEOUT_WRAP);
|
|
6961
|
+
if (w && (canonical(w[1]) === r || canonical(w[1]).startsWith(r + " "))) neutralised ??= raw;
|
|
6962
|
+
}
|
|
6963
|
+
}
|
|
6964
|
+
const maskOf = (args) => (" " + args).match(NEUTRALISING_SUFFIX)?.[0]?.trim() ?? null;
|
|
6965
|
+
const priorMask = r.includes(" ") ? maskOf(r.slice(r.indexOf(" ") + 1)) : null;
|
|
6966
|
+
let respelled = false;
|
|
6967
|
+
for (const raw of addedCores) {
|
|
6968
|
+
if (!kind || checkKind(raw) !== kind) continue;
|
|
6969
|
+
const a = canonical(raw);
|
|
6970
|
+
if (a.match(TIMEOUT_WRAP)) {
|
|
6971
|
+
neutralised ??= raw;
|
|
6972
|
+
continue;
|
|
6973
|
+
}
|
|
6974
|
+
if (!inCommandPosition(raw)) continue;
|
|
6975
|
+
if (coverageFloorLowered(r, a)) {
|
|
6976
|
+
neutralised ??= raw;
|
|
6977
|
+
continue;
|
|
6978
|
+
}
|
|
6979
|
+
const args = a.slice(a.indexOf(" ") + 1 || a.length);
|
|
6980
|
+
const mask = maskOf(args);
|
|
6981
|
+
const newMask = mask !== null && mask !== priorMask;
|
|
6982
|
+
if (a.includes(" ") && (newMask || narrowed(args) || runnerNarrowed(a, args))) neutralised ??= raw;
|
|
6983
|
+
else respelled = true;
|
|
6984
|
+
}
|
|
6985
|
+
if (respelled) return { state: "kept" };
|
|
6986
|
+
return neutralised ? { state: "neutralised", by: neutralised } : { state: "gone" };
|
|
6987
|
+
}
|
|
6988
|
+
function unwrap(seg) {
|
|
6989
|
+
const s = seg.trim().replace(PYTHON_M, "").replace(NODE_BIN, "$1").replace(BIN_OF, "$1$2");
|
|
6990
|
+
if (invokesCheck(s) || PRINTER.test(s) || /^["']/.test(s)) return s;
|
|
6991
|
+
const toks = s.split(/\s+/);
|
|
6992
|
+
for (let i = 1; i < toks.length && i <= 16; i++) {
|
|
6993
|
+
const rest = toks.slice(i).join(" ");
|
|
6994
|
+
if (invokesCheck(rest)) return rest;
|
|
6995
|
+
}
|
|
6996
|
+
return s;
|
|
6997
|
+
}
|
|
6998
|
+
function scriptKind3(inner) {
|
|
6999
|
+
const kind = checkKind(inner);
|
|
7000
|
+
if (!kind) return null;
|
|
7001
|
+
const head = canonical(inner).match(/^(tests?|lint|type-?check|coverage)(?=[\s:]|$)/i)?.[1];
|
|
7002
|
+
if (!head) return kind;
|
|
7003
|
+
return /^type/i.test(head) ? "types" : /^lint/i.test(head) ? "lint" : "test";
|
|
7004
|
+
}
|
|
7005
|
+
function runnerOfSegment(canon) {
|
|
7006
|
+
const m = canon.match(RUNNER_NAME);
|
|
7007
|
+
if (!m) return null;
|
|
7008
|
+
const n = m[1];
|
|
7009
|
+
return n === "jest" || n === "vitest" || n === "mocha" || n === "pytest" || n === "go" || n === "cargo" ? n : "node";
|
|
7010
|
+
}
|
|
7011
|
+
function statements(raw) {
|
|
7012
|
+
const out3 = [];
|
|
7013
|
+
let single = false;
|
|
7014
|
+
let double = false;
|
|
7015
|
+
let last = 0;
|
|
7016
|
+
for (let i = 0; i < raw.length; i++) {
|
|
7017
|
+
const ch = raw[i];
|
|
7018
|
+
if (ch === "\\" && !single) {
|
|
7019
|
+
i++;
|
|
7020
|
+
continue;
|
|
7021
|
+
}
|
|
7022
|
+
if (ch === "'" && !double) {
|
|
7023
|
+
single = !single;
|
|
7024
|
+
continue;
|
|
7025
|
+
}
|
|
7026
|
+
if (ch === '"' && !single) {
|
|
7027
|
+
double = !double;
|
|
7028
|
+
continue;
|
|
7029
|
+
}
|
|
7030
|
+
if (single || double) continue;
|
|
7031
|
+
const prev = raw[i - 1];
|
|
7032
|
+
const next = raw[i + 1];
|
|
7033
|
+
let op = null;
|
|
7034
|
+
if (ch === "&" && next === "&") op = "&&";
|
|
7035
|
+
else if (ch === "|" && next === "|") op = "||";
|
|
7036
|
+
else if (ch === "|" && next === "&") op = "|&";
|
|
7037
|
+
else if (ch === ";" || ch === "\n") op = ";";
|
|
7038
|
+
else if (ch === "|" && prev !== ">") op = "|";
|
|
7039
|
+
else if (ch === "&" && prev !== ">" && next !== ">") op = "&";
|
|
7040
|
+
if (!op) continue;
|
|
7041
|
+
out3.push({ seg: raw.slice(last, i).trim(), op });
|
|
7042
|
+
i += op.length - 1;
|
|
7043
|
+
last = i + 1;
|
|
7044
|
+
}
|
|
7045
|
+
out3.push({ seg: raw.slice(last).trim(), op: null });
|
|
7046
|
+
return out3.filter((s) => s.seg !== "");
|
|
7047
|
+
}
|
|
7048
|
+
function descriptors(cmd, runner, opts) {
|
|
7049
|
+
const out3 = [];
|
|
7050
|
+
const stmts = statements(cmd);
|
|
7051
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
7052
|
+
const { seg, op } = stmts[i];
|
|
7053
|
+
const wrapped = seg.match(TIMEOUT_WRAP);
|
|
7054
|
+
const inner = unwrap(wrapped ? wrapped[1] : seg);
|
|
7055
|
+
if (!invokesCheck(inner)) continue;
|
|
7056
|
+
const kind = scriptKind3(inner);
|
|
7057
|
+
const canon = canonical(inner);
|
|
7058
|
+
const rn = runner ?? runnerOfSegment(canon);
|
|
7059
|
+
const args = canon.includes(" ") ? " " + canon.slice(canon.indexOf(" ") + 1) : " ";
|
|
7060
|
+
if (wrapped) out3.push({ key: "timeout", what: "a timeout wrapper", direction: "added" });
|
|
7061
|
+
const following = stmts[i + 1]?.seg ?? "";
|
|
7062
|
+
if (op === "||" || op === "|" || op === "&" || op === ";" && !invokesCheck(unwrap(following))) {
|
|
7063
|
+
out3.push({ key: `op:${op}`, what: `${op} ${following.split(/\s+/).slice(0, 2).join(" ")}`.trim(), direction: "added" });
|
|
7064
|
+
}
|
|
7065
|
+
for (const re of MASKING_FLAGS) {
|
|
7066
|
+
const f = spelled(re, args);
|
|
7067
|
+
if (f) out3.push({ key: `flag:${f}`, what: f, direction: "added" });
|
|
7068
|
+
}
|
|
7069
|
+
for (const re of GATE_FLAGS) {
|
|
7070
|
+
const f = spelled(re, " " + seg);
|
|
7071
|
+
if (f) out3.push({ key: `gate:${f}`, what: f, direction: "removed" });
|
|
7072
|
+
}
|
|
7073
|
+
if (kind !== "test" || opts.sliceByDesign || opts.extended?.has(kind)) continue;
|
|
7074
|
+
const flags = [SUITE_NARROWING_FLAGS, ...NARROWING_BY_RUNNER["*"], ...rn ? NARROWING_BY_RUNNER[rn] : []];
|
|
7075
|
+
for (const re of flags) {
|
|
7076
|
+
const f = spelled(re, args);
|
|
7077
|
+
if (f && !opts.excuseFlag?.(f, canon)) out3.push({ key: `flag:${f}`, what: f, direction: "added" });
|
|
7078
|
+
}
|
|
7079
|
+
if (rn === "cargo" && CARGO_SKIP.test(args)) out3.push({ key: "flag:-- --skip", what: "-- --skip", direction: "added" });
|
|
7080
|
+
const shared = runnerNarrowing(canon, args);
|
|
7081
|
+
if (shared && !opts.excuseFlag?.(shared, canon)) out3.push({ key: `flag:${shared}`, what: shared, direction: "added" });
|
|
7082
|
+
if (opts.fresh) continue;
|
|
7083
|
+
const cfg = args.match(CONFIG_FLAG);
|
|
7084
|
+
if (cfg && !opts.configReviewed?.(cfg[3])) out3.push({ key: `config:${cfg[3]}`, what: `${cfg[1]} ${cfg[3]}`, direction: "added" });
|
|
7085
|
+
const pos = positionalOf(args);
|
|
7086
|
+
if (pos) out3.push({ key: "positional", what: `a path positional (${pos})`, direction: "added" });
|
|
6738
7087
|
}
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
7088
|
+
return out3;
|
|
7089
|
+
}
|
|
7090
|
+
function checkStatements(cmd) {
|
|
7091
|
+
const out3 = [];
|
|
7092
|
+
for (const seg of segments(cmd)) {
|
|
7093
|
+
const w = seg.match(TIMEOUT_WRAP);
|
|
7094
|
+
const inner = unwrap(w ? w[1] : seg);
|
|
7095
|
+
const kind = scriptKind3(inner);
|
|
7096
|
+
if (kind) out3.push({ kind, canon: canonical(inner) });
|
|
6743
7097
|
}
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
7098
|
+
return out3;
|
|
7099
|
+
}
|
|
7100
|
+
function checkKinds(cmd) {
|
|
7101
|
+
return new Set(checkStatements(cmd).map((s) => s.kind));
|
|
7102
|
+
}
|
|
7103
|
+
function failsLoudly(cmd) {
|
|
7104
|
+
const last = segments(cmd).pop();
|
|
7105
|
+
return last !== void 0 && FAILS_LOUDLY.test(last.trim());
|
|
7106
|
+
}
|
|
7107
|
+
function invocationWeakening(before, after, runner, opts = {}) {
|
|
7108
|
+
const was = checkKinds(before);
|
|
7109
|
+
const now = checkKinds(after);
|
|
7110
|
+
if (now.size === 0 && failsLoudly(after)) return { state: "kept" };
|
|
7111
|
+
for (const k of was) {
|
|
7112
|
+
if (now.has(k) || opts.relocated?.(k)) continue;
|
|
7113
|
+
const instead = [...now].filter((x) => !was.has(x));
|
|
7114
|
+
return { state: "removed", kind: k, what: instead.length ? `runs a ${instead.join("/")} check instead` : now.size ? "the check is gone" : "no check is run" };
|
|
6747
7115
|
}
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
7116
|
+
const survived = new Set(checkStatements(before).map((s) => s.canon));
|
|
7117
|
+
const extended = new Set(checkStatements(after).filter((s) => survived.has(s.canon)).map((s) => s.kind));
|
|
7118
|
+
const read = { ...opts, fresh: was.size === 0, extended };
|
|
7119
|
+
const beforeDs = descriptors(before, runner, read);
|
|
7120
|
+
const afterDs = descriptors(after, runner, read);
|
|
7121
|
+
const seenBefore = new Set(beforeDs.map((d) => d.key));
|
|
7122
|
+
const added = afterDs.find((d) => d.direction === "added" && !seenBefore.has(d.key));
|
|
7123
|
+
if (added) return { state: "neutralised", what: added.what, direction: "added" };
|
|
7124
|
+
if (coverageFloorLowered(before, after)) return { state: "neutralised", what: "a lowered --cov-fail-under", direction: "added" };
|
|
7125
|
+
const seenAfter = new Set(afterDs.map((d) => d.key));
|
|
7126
|
+
const dropped = beforeDs.find((d) => d.direction === "removed" && !seenAfter.has(d.key));
|
|
7127
|
+
if (dropped) return { state: "neutralised", what: dropped.what, direction: "removed" };
|
|
7128
|
+
return { state: "kept" };
|
|
7129
|
+
}
|
|
7130
|
+
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;
|
|
7131
|
+
var init_invocation = __esm({
|
|
7132
|
+
"src/detectors/invocation.ts"() {
|
|
7133
|
+
"use strict";
|
|
7134
|
+
init_gh_expression();
|
|
7135
|
+
init_command();
|
|
7136
|
+
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/;
|
|
7137
|
+
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/;
|
|
7138
|
+
invokesCheck = (line) => INVOKES_TOOL.test(line) || INVOKES_SCRIPT.test(line);
|
|
7139
|
+
MATRIX_FLAG = new RegExp(`(?:^|\\s)-{1,2}[\\w-]+(?:=|\\s+)${MATRIX_TOKEN}(?:/\\S*)?(?=\\s|$)`, "g");
|
|
7140
|
+
usesRef = (core) => core.replace(/@.*$/, "");
|
|
7141
|
+
SUITE_NARROWING_FLAGS = /--testPathIgnorePatterns\b|--testPathPattern\b|--testNamePattern\b|(?:^|\s)-t\s|--onlyChanged\b|--changed\b|--findRelatedTests\b|--exclude\b|--dir\b|--project\b/;
|
|
7142
|
+
NEUTRALISING_SUFFIX = new RegExp(
|
|
7143
|
+
`\\|\\||;|(?<!&)&(?!&)|\\|(?!\\|)|--passWithNoTests\\b|--coverage=false\\b|--coverageThreshold\\b|${SUITE_NARROWING_FLAGS.source}`
|
|
7144
|
+
);
|
|
7145
|
+
TIMEOUT_WRAP = /^timeout\s+(?:-{1,2}\S+\s+)*\d\S*\s+(.+)$/;
|
|
7146
|
+
RUNNER_NARROWINGS = [
|
|
7147
|
+
{
|
|
7148
|
+
// a package-manager script: `test --prefix dir`, `-w pkg`, `--workspace=pkg`,
|
|
7149
|
+
// `--if-present` (no script, no failure), pnpm's `--filter` / `-F`, npm's `-C`
|
|
7150
|
+
runner: /^(?:test|tests|coverage)(?::\S+)?(?=\s|$)/,
|
|
7151
|
+
flags: /\s--if-present(?=\s|$)|\s--prefix(?:=|\s)|\s-C(?:=|\s)|\s-w(?:=|\s)|\s--workspace(?:=|\s)|\s--filter(?:=|\s)|\s-F(?:=|\s)/
|
|
7152
|
+
},
|
|
7153
|
+
{
|
|
7154
|
+
runner: /^(?:jest|vitest)\b/,
|
|
7155
|
+
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)/
|
|
7156
|
+
},
|
|
7157
|
+
{ runner: /^playwright\b/, flags: /\s--shard(?:=|\s)|\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--grep-invert(?:=|\s)/ },
|
|
7158
|
+
{ runner: /^mocha\b/, flags: /\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--fgrep(?:=|\s)|\s-f(?:=|\s)/ },
|
|
7159
|
+
{ runner: /^ava\b/, flags: /\s--match(?:=|\s)|\s-m(?:=|\s)/ },
|
|
7160
|
+
{ runner: /^node --test\b/, flags: /\s--test-name-pattern(?:=|\s)|\s--test-skip-pattern(?:=|\s)|\s--test-only(?=\s|$)/ },
|
|
7161
|
+
{
|
|
7162
|
+
// `-p no:<plugin>` disables collection when the plugin is the collector
|
|
7163
|
+
// (`python`, `doctest`, `cov`); the cache, warning and output plugins select nothing
|
|
7164
|
+
runner: /^pytest\b/,
|
|
7165
|
+
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|$)/
|
|
7166
|
+
},
|
|
7167
|
+
{ runner: /^cargo test\b/, flags: /\s--\s(?:\S+\s+)*--skip(?:=|\s)|\s--exclude(?:=|\s)/ },
|
|
7168
|
+
{ runner: /^go test\b/, flags: /\s-run(?:=|\s)|\s-skip(?:=|\s)/ }
|
|
7169
|
+
];
|
|
7170
|
+
COVERAGE_FLOOR = /--cov-fail-under(?:=|\s+)(\d+(?:\.\d+)?)/;
|
|
7171
|
+
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+)*)*/;
|
|
7172
|
+
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)/;
|
|
7173
|
+
PATH_SHAPED = /\/|\.(?:test|spec)\./;
|
|
7174
|
+
NARROWING_BY_RUNNER = {
|
|
7175
|
+
"*": [/--shard\b/],
|
|
7176
|
+
jest: [/--root\b/, /--rootDir\b/, /--testMatch\b/, /--testRegex\b/, /--modulePathIgnorePatterns\b/, /--selectProjects\b/, /--ignoreProjects\b/, /--filter\b/],
|
|
7177
|
+
vitest: [/--grep\b/],
|
|
7178
|
+
mocha: [/--grep\b/, /(?:^|\s)-g(?:\s|=)/, /--ignore\b/, /(?:^|\s)-f(?:\s|=)/, /--fgrep\b/],
|
|
7179
|
+
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|$)/],
|
|
7180
|
+
go: [/(?:^|\s)-run(?:\s|=)/, /(?:^|\s)-skip(?:\s|=)/],
|
|
7181
|
+
cargo: [],
|
|
7182
|
+
node: [/--test-name-pattern\b/, /--test-skip-pattern\b/]
|
|
7183
|
+
};
|
|
7184
|
+
CARGO_SKIP = /\s--\s+(?:\S+\s+)*--skip\b/;
|
|
7185
|
+
MASKING_FLAGS = [/--passWithNoTests\b/, /--coverage=false\b/, /--coverageThreshold\b/, /--if-present\b/];
|
|
7186
|
+
GATE_FLAGS = [/--check-coverage\b/];
|
|
7187
|
+
CONFIG_FLAG = /(?:^|\s)(--config|-c)(?:=|\s+)(['"]?)([^\s'"]+)\2(?=\s|$)/;
|
|
7188
|
+
PRINTER = /^(?:echo|printf|true|false|:|cat|exit|return|sleep|test|\[|node|git|curl|wget)(?=\s|$)/;
|
|
7189
|
+
PYTHON_M = /^python\d?(?:\.\d+)?\s+-m\s+(?=pytest\b)/;
|
|
7190
|
+
NODE_BIN = /^node(?:\s+-\S+)*\s+\S*node_modules\/(?:\.bin\/)?(jest|vitest|mocha|ava)\b\S*/;
|
|
7191
|
+
BIN_OF = /\$\((?:yarn|npm|pnpm)\s+bin\s+(\w+)\)|\$\((?:yarn|npm|pnpm)\s+bin\)\/(\w+)/g;
|
|
7192
|
+
RUNNER_NAME = /^(jest|vitest|mocha|pytest|go|cargo|node)\b/;
|
|
7193
|
+
spelled = (re, s) => {
|
|
7194
|
+
const m = s.match(re);
|
|
7195
|
+
return m ? m[0].trim().replace(/[=\s].*$/, "") : null;
|
|
7196
|
+
};
|
|
7197
|
+
FAILS_LOUDLY = /^(?:exit\s+(?:[1-9]\d*)|false)\s*$/;
|
|
6751
7198
|
}
|
|
6752
|
-
|
|
7199
|
+
});
|
|
7200
|
+
|
|
7201
|
+
// src/detectors/ci-tampering.ts
|
|
7202
|
+
function isActiveWorkflow(path) {
|
|
7203
|
+
return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(path);
|
|
6753
7204
|
}
|
|
6754
|
-
function
|
|
6755
|
-
|
|
6756
|
-
return null;
|
|
7205
|
+
function commandCore(line) {
|
|
7206
|
+
return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses|script|command|before_script|after_script|sh|bash|pwsh|powershell):\s*/, "").replace(/\s+/g, " ").trim();
|
|
6757
7207
|
}
|
|
6758
|
-
function
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
let opaque = false;
|
|
6763
|
-
for (const el of e.elements) {
|
|
6764
|
-
if (isStr(el)) items.push(el.text);
|
|
6765
|
-
else if (ts.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
|
|
6766
|
-
items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
6767
|
-
} else opaque = true;
|
|
6768
|
-
}
|
|
6769
|
-
return { items, opaque };
|
|
7208
|
+
function expressionOf(raw) {
|
|
7209
|
+
const v = uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
7210
|
+
const m = v.match(/^\$\{\{\s*(.+?)\s*\}\}$/);
|
|
7211
|
+
return m ? m[1] : v;
|
|
6770
7212
|
}
|
|
6771
|
-
function
|
|
6772
|
-
const
|
|
6773
|
-
if (
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
return q && ts.isPropertyAssignment(q) ? keyName2(q.name) : null;
|
|
7213
|
+
function isAlwaysFalse(raw) {
|
|
7214
|
+
const e = expressionOf(raw);
|
|
7215
|
+
if (e === "") return false;
|
|
7216
|
+
const v = foldConst(e);
|
|
7217
|
+
return v !== void 0 && !truthy(v);
|
|
6777
7218
|
}
|
|
6778
|
-
function
|
|
6779
|
-
const
|
|
6780
|
-
|
|
7219
|
+
function isTruthy(raw) {
|
|
7220
|
+
const v = uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
7221
|
+
if (/^(?:true|yes|on|1)$/i.test(v)) return true;
|
|
7222
|
+
const m = v.match(/^\$\{\{\s*(.+?)\s*\}\}$/);
|
|
7223
|
+
if (!m) return false;
|
|
7224
|
+
const f = foldConst(m[1]);
|
|
7225
|
+
return f !== void 0 && truthy(f);
|
|
6781
7226
|
}
|
|
6782
|
-
function
|
|
6783
|
-
const
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
if (
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
if (base === "pyproject.toml" && /^\s*\[tool\.pytest\.ini_options\]/m.test(content)) return "pytest";
|
|
6792
|
-
return null;
|
|
7227
|
+
function stepOf(lines, i) {
|
|
7228
|
+
const ind = indentOf(lines[i]);
|
|
7229
|
+
let s = i - 1;
|
|
7230
|
+
while (s >= 0 && (lines[s].trim() === "" || indentOf(lines[s]) >= ind)) s--;
|
|
7231
|
+
if (s < 0 || !STEP_START.test(lines[s])) return null;
|
|
7232
|
+
const stepInd = indentOf(lines[s]);
|
|
7233
|
+
let e = i + 1;
|
|
7234
|
+
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > stepInd)) e++;
|
|
7235
|
+
return [s, e];
|
|
6793
7236
|
}
|
|
6794
|
-
function
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
7237
|
+
function jobOf(lines, i) {
|
|
7238
|
+
const ind = indentOf(lines[i]);
|
|
7239
|
+
let h = i - 1;
|
|
7240
|
+
while (h >= 0 && (lines[h].trim() === "" || indentOf(lines[h]) >= ind)) h--;
|
|
7241
|
+
if (h < 0 || !/^\s+[\w-]+:\s*(?:#.*)?$/.test(lines[h])) return null;
|
|
7242
|
+
let t = h - 1;
|
|
7243
|
+
while (t >= 0 && (lines[t].trim() === "" || indentOf(lines[t]) > 0)) t--;
|
|
7244
|
+
if (t < 0 || !/^jobs:/.test(lines[t])) return null;
|
|
7245
|
+
const hi = indentOf(lines[h]);
|
|
7246
|
+
let e = i + 1;
|
|
7247
|
+
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > hi)) e++;
|
|
7248
|
+
return [h, e];
|
|
6798
7249
|
}
|
|
6799
|
-
function
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
if (
|
|
6803
|
-
|
|
7250
|
+
function runBlocks(lines) {
|
|
7251
|
+
const out3 = [];
|
|
7252
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7253
|
+
if (!/^\s*-?\s*run:\s*[|>][-+]?\s*$/.test(lines[i])) continue;
|
|
7254
|
+
const ind = indentOf(lines[i].replace(/^(\s*)-\s+/, "$1 "));
|
|
7255
|
+
let e = i + 1;
|
|
7256
|
+
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > ind)) e++;
|
|
7257
|
+
out3.push({ start: i, end: e, body: lines.slice(i + 1, e) });
|
|
6804
7258
|
}
|
|
6805
|
-
return
|
|
6806
|
-
}
|
|
6807
|
-
function effectivePytestConfig(path, ctx) {
|
|
6808
|
-
if (path.includes("/")) return false;
|
|
6809
|
-
const base = path;
|
|
6810
|
-
const rank = PYTEST_INI_ORDER.findIndex((name) => name === base);
|
|
6811
|
-
if (rank < 0) return false;
|
|
6812
|
-
const files = ctx?.trackedFiles;
|
|
6813
|
-
if (!files) return true;
|
|
6814
|
-
const rootNames = new Set(files.filter((f) => !f.includes("/")));
|
|
6815
|
-
return !PYTEST_INI_ORDER.slice(0, rank).some((n) => rootNames.has(n));
|
|
7259
|
+
return out3;
|
|
6816
7260
|
}
|
|
6817
|
-
function
|
|
6818
|
-
const
|
|
6819
|
-
const
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
let key2 = null;
|
|
6824
|
-
for (i += 1; i < lines.length; i++) {
|
|
6825
|
-
const line = lines[i];
|
|
6826
|
-
if (/^\s*\[/.test(line)) break;
|
|
6827
|
-
if (/^\s*[#;]/.test(line) || !line.trim()) {
|
|
7261
|
+
function commandLines(lines) {
|
|
7262
|
+
const out3 = lines.slice();
|
|
7263
|
+
for (const b of runBlocks(lines)) {
|
|
7264
|
+
if (/^\s*-?\s*run:\s*>/.test(lines[b.start])) {
|
|
7265
|
+
out3[b.start + 1] = b.body.map((l) => l.trim()).filter(Boolean).join(" ");
|
|
7266
|
+
for (let k = b.start + 2; k < b.end; k++) out3[k] = "";
|
|
6828
7267
|
continue;
|
|
6829
7268
|
}
|
|
6830
|
-
const
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
7269
|
+
const indents = b.body.filter((l) => l.trim() !== "").map(indentOf);
|
|
7270
|
+
const ind = indents.length ? Math.min(...indents) : 0;
|
|
7271
|
+
const dedented = b.body.map((l) => l.slice(Math.min(ind, indentOf(l))));
|
|
7272
|
+
const kept = stripHeredocs(dedented);
|
|
7273
|
+
let j = 0;
|
|
7274
|
+
for (let k = 0; k < dedented.length; k++) {
|
|
7275
|
+
if (j < kept.length && kept[j] === dedented[k]) j++;
|
|
7276
|
+
else out3[b.start + 1 + k] = "";
|
|
6835
7277
|
}
|
|
6836
|
-
if (key2 && /^\s+\S/.test(line)) out3[key2] += " " + line.trim();
|
|
6837
7278
|
}
|
|
6838
7279
|
return out3;
|
|
6839
7280
|
}
|
|
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 };
|
|
7281
|
+
function flowList(v) {
|
|
7282
|
+
const m = v.trim().match(/^\[(.*)\]$/);
|
|
7283
|
+
if (!m) return null;
|
|
7284
|
+
return m[1].split(",").map((s) => s.trim().replace(/^(['"])(.*)\1$/, "$2")).filter(Boolean);
|
|
6887
7285
|
}
|
|
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;
|
|
7286
|
+
function parseTriggers(src) {
|
|
7287
|
+
const t = {
|
|
7288
|
+
events: /* @__PURE__ */ new Set(),
|
|
7289
|
+
branches: /* @__PURE__ */ new Map(),
|
|
7290
|
+
branchesIgnore: /* @__PURE__ */ new Map(),
|
|
7291
|
+
pathsIgnore: /* @__PURE__ */ new Map(),
|
|
7292
|
+
paths: /* @__PURE__ */ new Map(),
|
|
7293
|
+
types: /* @__PURE__ */ new Map(),
|
|
7294
|
+
tags: /* @__PURE__ */ new Map(),
|
|
7295
|
+
present: false
|
|
6906
7296
|
};
|
|
6907
|
-
const
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
7297
|
+
const lines = src.split("\n");
|
|
7298
|
+
const i = lines.findIndex((l) => /^(?:on|"on"|'on'|true):/.test(l));
|
|
7299
|
+
if (i < 0) return t;
|
|
7300
|
+
t.present = true;
|
|
7301
|
+
const head = lines[i].replace(/^(?:on|"on"|'on'|true):\s*/, "");
|
|
7302
|
+
const inline = uncommented(head);
|
|
7303
|
+
if (inline) {
|
|
7304
|
+
const list3 = flowList(inline);
|
|
7305
|
+
for (const e of list3 ?? [inline]) t.events.add(e);
|
|
7306
|
+
return t;
|
|
7307
|
+
}
|
|
7308
|
+
let event = null;
|
|
7309
|
+
let listKey = null;
|
|
7310
|
+
let listInd = -1;
|
|
7311
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
7312
|
+
const l = lines[j];
|
|
7313
|
+
if (l.trim() === "" || /^\s*#/.test(l)) continue;
|
|
7314
|
+
const ind = indentOf(l);
|
|
7315
|
+
if (ind === 0) break;
|
|
7316
|
+
const body = uncommented(l);
|
|
7317
|
+
const item = body.match(/^\s*-\s+(.+)$/);
|
|
7318
|
+
if (item && listKey && event && ind > listInd) {
|
|
7319
|
+
const target = t[listKey].get(event) ?? [];
|
|
7320
|
+
target.push(item[1].replace(/^(['"])(.*)\1$/, "$2"));
|
|
7321
|
+
t[listKey].set(event, target);
|
|
7322
|
+
continue;
|
|
6915
7323
|
}
|
|
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
|
-
}
|
|
7324
|
+
const kv = body.match(/^\s*([\w-]+):\s*(.*)$/);
|
|
7325
|
+
if (!kv) continue;
|
|
7326
|
+
if (ind === 2 || event === null && ind < 4) {
|
|
7327
|
+
event = kv[1];
|
|
7328
|
+
t.events.add(event);
|
|
7329
|
+
listKey = null;
|
|
7330
|
+
continue;
|
|
6945
7331
|
}
|
|
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;
|
|
7332
|
+
const key2 = LIST_KEYS[kv[1]];
|
|
7333
|
+
if (event && key2) {
|
|
7334
|
+
listKey = key2;
|
|
7335
|
+
listInd = ind;
|
|
7336
|
+
const list3 = flowList(kv[2]);
|
|
7337
|
+
t[listKey].set(event, list3 ?? []);
|
|
7338
|
+
} else listKey = null;
|
|
6957
7339
|
}
|
|
6958
|
-
|
|
6959
|
-
return x;
|
|
7340
|
+
return t;
|
|
6960
7341
|
}
|
|
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";
|
|
7342
|
+
function ghGlob(g) {
|
|
7343
|
+
const re = g.split("**").map((part) => part.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")).join(".*");
|
|
7344
|
+
try {
|
|
7345
|
+
const r = new RegExp(`^${re}$`);
|
|
7346
|
+
return (p) => r.test(p);
|
|
7347
|
+
} catch {
|
|
7348
|
+
return () => false;
|
|
6970
7349
|
}
|
|
6971
|
-
return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
|
|
6972
7350
|
}
|
|
6973
|
-
function
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
if (
|
|
6978
|
-
return "path";
|
|
7351
|
+
function admits(globs4, name) {
|
|
7352
|
+
let on = false;
|
|
7353
|
+
for (const g of globs4) {
|
|
7354
|
+
const neg = g.startsWith("!");
|
|
7355
|
+
if (ghGlob(neg ? g.slice(1) : g)(name)) on = !neg;
|
|
6979
7356
|
}
|
|
6980
|
-
return
|
|
7357
|
+
return on;
|
|
6981
7358
|
}
|
|
6982
|
-
function
|
|
6983
|
-
const
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
7359
|
+
function sourceProbes(ctx) {
|
|
7360
|
+
const files = trackedFiles(ctx);
|
|
7361
|
+
if (!files) return CODE_PROBES;
|
|
7362
|
+
const code2 = files.filter((f) => langOf(f) !== null);
|
|
7363
|
+
return code2.length ? code2 : CODE_PROBES;
|
|
7364
|
+
}
|
|
7365
|
+
function triggerNarrowings(before, after, opts) {
|
|
7366
|
+
const out3 = [];
|
|
7367
|
+
if (!before.present || !after.present) return out3;
|
|
7368
|
+
const isDefault = (b) => opts.defaultBranch ? b === opts.defaultBranch : DEFAULT_BRANCH.test(b);
|
|
7369
|
+
const candidates = opts.defaultBranch ? [opts.defaultBranch] : ["main", "master"];
|
|
7370
|
+
const namesDefault = (l) => candidates.some((b) => admits(l, b));
|
|
7371
|
+
for (const e of CODE_EVENTS) {
|
|
7372
|
+
if (before.events.has(e) && !after.events.has(e)) out3.push(`the ${e} trigger was removed \u2014 the workflow no longer runs on it`);
|
|
7373
|
+
}
|
|
7374
|
+
for (const [e, list3] of after.pathsIgnore) {
|
|
7375
|
+
const had = before.pathsIgnore.get(e) ?? [];
|
|
7376
|
+
if (list3.some((g) => ALL(g) && !had.includes(g))) {
|
|
7377
|
+
out3.push(`on.${e}.paths-ignore now ignores every path \u2014 the workflow never runs on ${e}`);
|
|
7378
|
+
continue;
|
|
7379
|
+
}
|
|
7380
|
+
if (!CODE_EVENTS.includes(e) || !after.events.has(e)) continue;
|
|
7381
|
+
if (had.length && ignoresEverySource(had, opts.sources)) continue;
|
|
7382
|
+
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`);
|
|
7383
|
+
}
|
|
7384
|
+
for (const [e, list3] of after.paths) {
|
|
7385
|
+
if (!CODE_EVENTS.includes(e) || !after.events.has(e)) continue;
|
|
7386
|
+
const had = before.paths.get(e);
|
|
7387
|
+
if (had && !pathsMatchSource(had, opts.sources)) continue;
|
|
7388
|
+
if (!pathsMatchSource(list3, opts.sources)) out3.push(`on.${e}.paths matches no source file (now [${list3.join(", ")}]) \u2014 the workflow never runs on code`);
|
|
6993
7389
|
}
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
root.opaque = true;
|
|
7000
|
-
return [];
|
|
7390
|
+
for (const [e, list3] of after.types) {
|
|
7391
|
+
if (!/^pull_request(?:_target)?$/.test(e) || !after.events.has(e)) continue;
|
|
7392
|
+
const had = before.types.get(e);
|
|
7393
|
+
if (had && !had.some((t) => PR_CODE_TYPES.test(t))) continue;
|
|
7394
|
+
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
7395
|
}
|
|
7002
|
-
const list3
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
if (ts.isObjectLiteralExpression(lit)) list3.push(readProject(lit, root, runner));
|
|
7006
|
-
else root.opaque = true;
|
|
7396
|
+
for (const [e, list3] of after.branchesIgnore) {
|
|
7397
|
+
const had = before.branchesIgnore.get(e) ?? [];
|
|
7398
|
+
if (list3.some((g) => (ALL(g) || isDefault(g)) && !had.includes(g))) out3.push(`on.${e}.branches-ignore now covers the default branch`);
|
|
7007
7399
|
}
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
if (
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
e = e.expression;
|
|
7400
|
+
const events = /* @__PURE__ */ new Set([...before.branches.keys(), ...CODE_EVENTS.filter((e) => before.events.has(e))]);
|
|
7401
|
+
for (const e of events) {
|
|
7402
|
+
const was = before.branches.get(e) ?? (before.tags.has(e) ? null : ["**"]);
|
|
7403
|
+
if (!was || !after.events.has(e)) continue;
|
|
7404
|
+
const now = after.branches.get(e);
|
|
7405
|
+
if (!now) {
|
|
7406
|
+
const tags = after.tags.get(e);
|
|
7407
|
+
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`);
|
|
7408
|
+
continue;
|
|
7018
7409
|
}
|
|
7019
|
-
|
|
7020
|
-
|
|
7410
|
+
const renamed = (b) => DEFAULT_BRANCH.test(b) && now.includes(b) && admits(now, b) && opts.hasBranch(b) !== false;
|
|
7411
|
+
const lost = candidates.filter((b) => admits(was, b) && !admits(now, b));
|
|
7412
|
+
const lostDefault = lost.length > 0 && !["main", "master"].some(renamed);
|
|
7413
|
+
const replaced = !namesDefault(was) && !namesDefault(now) && was.length > 0 && !was.some((b) => now.includes(b));
|
|
7414
|
+
if (!lostDefault && !replaced) continue;
|
|
7415
|
+
const dropped = was.filter((b) => !now.includes(b) && !ALL(b));
|
|
7416
|
+
out3.push(
|
|
7417
|
+
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`
|
|
7418
|
+
);
|
|
7021
7419
|
}
|
|
7022
|
-
return
|
|
7420
|
+
return out3;
|
|
7023
7421
|
}
|
|
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;
|
|
7422
|
+
function localUses(line) {
|
|
7423
|
+
const wf = line.match(REUSED_WORKFLOW)?.[1];
|
|
7424
|
+
if (wf) return [wf];
|
|
7425
|
+
const action = line.match(LOCAL_ACTION)?.[1];
|
|
7426
|
+
return action ? [`${action}/action.yml`, `${action}/action.yaml`] : [];
|
|
7041
7427
|
}
|
|
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
|
-
};
|
|
7428
|
+
function literalValue(raw) {
|
|
7429
|
+
const v = foldExpressions(uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim());
|
|
7430
|
+
return v.includes(EXPR_TOKEN) || v.includes(MATRIX_TOKEN) ? null : v;
|
|
7080
7431
|
}
|
|
7081
|
-
function
|
|
7082
|
-
const
|
|
7083
|
-
if (!
|
|
7084
|
-
const
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
const
|
|
7090
|
-
|
|
7091
|
-
|
|
7432
|
+
function dirRunsNothing(dir, ctx) {
|
|
7433
|
+
const files = trackedFiles(ctx);
|
|
7434
|
+
if (!files) return true;
|
|
7435
|
+
const d = dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
7436
|
+
if (d === "" || d === ".") return false;
|
|
7437
|
+
return !files.some((f) => f.startsWith(d + "/") && (langOf(f) !== null || MANIFEST.test(f)));
|
|
7438
|
+
}
|
|
7439
|
+
function defaultsOf(lines, i) {
|
|
7440
|
+
const run = ancestorOf(lines, i);
|
|
7441
|
+
if (run === null || !/^\s*run:\s*$/.test(lines[run])) return null;
|
|
7442
|
+
const d = ancestorOf(lines, run);
|
|
7443
|
+
return d !== null && /^\s*defaults:\s*$/.test(lines[d]) ? d : null;
|
|
7444
|
+
}
|
|
7445
|
+
function enclosingStep(lines, i) {
|
|
7446
|
+
let h = i;
|
|
7447
|
+
while ((h = ancestorOf(lines, h)) !== null) {
|
|
7448
|
+
if (!STEP_START.test(lines[h])) continue;
|
|
7449
|
+
const ind = indentOf(lines[h]);
|
|
7450
|
+
let e = i + 1;
|
|
7451
|
+
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > ind)) e++;
|
|
7452
|
+
return [h, e];
|
|
7092
7453
|
}
|
|
7093
|
-
return
|
|
7454
|
+
return null;
|
|
7094
7455
|
}
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7456
|
+
function ancestorOf(lines, i) {
|
|
7457
|
+
const ind = indentOf(lines[i]);
|
|
7458
|
+
let h = i - 1;
|
|
7459
|
+
while (h >= 0 && (lines[h].trim() === "" || /^\s*#/.test(lines[h]) || indentOf(lines[h]) >= ind)) h--;
|
|
7460
|
+
return h >= 0 ? h : null;
|
|
7461
|
+
}
|
|
7462
|
+
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;
|
|
7463
|
+
var init_ci_tampering = __esm({
|
|
7464
|
+
"src/detectors/ci-tampering.ts"() {
|
|
7098
7465
|
"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
|
-
"
|
|
7466
|
+
init_select();
|
|
7467
|
+
init_policy();
|
|
7468
|
+
init_finding();
|
|
7469
|
+
init_files();
|
|
7470
|
+
init_hook_wiring();
|
|
7471
|
+
init_repo();
|
|
7472
|
+
init_gh_expression();
|
|
7473
|
+
init_invocation();
|
|
7474
|
+
init_invocation();
|
|
7475
|
+
RULE8 = "ci-tampering";
|
|
7476
|
+
USES = /^\s*-\s*uses:/;
|
|
7477
|
+
CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
|
|
7478
|
+
YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
|
|
7479
|
+
isCompositeAction = (path) => /(^|\/)\.github\/actions\/.+\/action\.ya?ml$/.test(path);
|
|
7480
|
+
isGithubFile = (path) => /(^|\/)\.github\//.test(path);
|
|
7481
|
+
GH_RUN_KEY = /^\s*-?\s*run:/;
|
|
7482
|
+
GENERIC_RUN_KEY = /^\s*-?\s*(?:run|script|command|before_script|after_script|sh|bash|pwsh|powershell):/;
|
|
7483
|
+
invokesCheck2 = (line) => invokesCheck(line) || INVOKES_TOOL.test(commandCore(line));
|
|
7484
|
+
uncommented = (v) => v.replace(/\s+#.*$/, "").trim();
|
|
7485
|
+
SHELL_ERREXIT = /(?:^|\s)-[a-zA-Z]*e[a-zA-Z]*(?:\s|$)|-o\s+errexit\b/;
|
|
7486
|
+
indentOf = (l) => l.length - l.trimStart().length;
|
|
7487
|
+
STEP_START = /^\s*-\s+(?:name|uses|run|if|id|with|env|shell|continue-on-error|working-directory|timeout-minutes):/;
|
|
7488
|
+
REPORTER_ACTION = /^\s*-?\s*uses:\s*\S*?(?:[\w-]*-reporter|[\w-]*-results?[\w-]*|upload-[\w-]*|codecov[\w-]*|coveralls[\w-]*)(?:@|\s|$)/i;
|
|
7489
|
+
usesCheck = (l) => USES.test(l) && CHECK.test(l) && !REPORTER_ACTION.test(l);
|
|
7490
|
+
stepHasCheck = (lines, range) => range === null || lines.slice(range[0], range[1]).some((l) => invokesCheck2(l) || usesCheck(l));
|
|
7491
|
+
jobHasCheck = (lines, range) => lines.slice(range[0], range[1]).some((l) => invokesCheck2(l) || usesCheck(l) || /^\s*uses:\s*\S/.test(l));
|
|
7492
|
+
NEUTRALISERS = [
|
|
7493
|
+
{ re: /^\s*set\s+\+e\b/, what: "set +e \u2014 a failing check no longer fails the step" },
|
|
7494
|
+
{ re: /^\s*exit\s+0\s*(?:$|[;#])/, what: "exit 0 \u2014 the step ends green whatever the check did" },
|
|
7495
|
+
{ re: /^\s*if\s+false\s*;?\s*then\b/, what: "if false; then \u2014 the check is never run" },
|
|
7496
|
+
{ re: /^\s*:\s*<<\s*['"]?\w+/, what: "a heredoc fed to `:` \u2014 the check is commented out" }
|
|
7130
7497
|
];
|
|
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);
|
|
7498
|
+
LIST_KEYS = {
|
|
7499
|
+
branches: "branches",
|
|
7500
|
+
"branches-ignore": "branchesIgnore",
|
|
7501
|
+
"paths-ignore": "pathsIgnore",
|
|
7502
|
+
paths: "paths",
|
|
7503
|
+
types: "types",
|
|
7504
|
+
tags: "tags"
|
|
7155
7505
|
};
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7506
|
+
DEFAULT_BRANCH = /^(?:main|master)$/;
|
|
7507
|
+
ALL = (g) => /^\*\*?$|^\*\*\/\*$/.test(g);
|
|
7508
|
+
CODE_EVENTS = ["push", "pull_request", "pull_request_target"];
|
|
7509
|
+
PR_CODE_TYPES = /^(?:opened|synchronize|reopened)$/;
|
|
7510
|
+
pathsMatchSource = (globs4, sources) => sources.some((s) => admits(globs4, s));
|
|
7511
|
+
ignoresEverySource = (globs4, sources) => sources.length > 0 && sources.every((s) => admits(globs4, s));
|
|
7512
|
+
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"];
|
|
7513
|
+
REUSED_WORKFLOW = /^\s*-?\s*uses:\s*\.\/(\.github\/workflows\/[^\s@]+)/;
|
|
7514
|
+
LOCAL_ACTION = /^\s*-?\s*uses:\s*\.\/(\.github\/actions\/[^\s@]+?)\/?\s*(?:#.*)?$/;
|
|
7515
|
+
MANIFEST = /(?:^|\/)(?:package\.json|pyproject\.toml|setup\.py|setup\.cfg|Cargo\.toml|go\.mod|Makefile|build\.gradle(?:\.kts)?|pom\.xml|Gemfile|deno\.jsonc?)$/;
|
|
7516
|
+
WORKDIR_LINE = /^\s*-?\s*working-directory:\s*(.+?)\s*$/;
|
|
7517
|
+
REF_LINE = /^\s*ref:\s*(.+?)\s*$/;
|
|
7518
|
+
CHECKOUT = /^\s*-?\s*uses:\s*actions\/checkout(?:@|\s|$)/;
|
|
7519
|
+
ciTampering = {
|
|
7520
|
+
id: RULE8,
|
|
7521
|
+
surface: ["file"],
|
|
7522
|
+
certainty: "mechanical",
|
|
7523
|
+
run(changes, policy, _view, ctx) {
|
|
7524
|
+
const out3 = [];
|
|
7525
|
+
const afterByPath = /* @__PURE__ */ new Map();
|
|
7526
|
+
for (const c of changes) if (c.kind === "file" && c.after != null) afterByPath.set(c.path, c.after);
|
|
7527
|
+
let triggerOpts = null;
|
|
7528
|
+
for (const c of changes) {
|
|
7529
|
+
if (c.kind !== "file") continue;
|
|
7530
|
+
if (c.op === "rename" && c.oldPath && isActiveWorkflow(c.oldPath) && !isActiveWorkflow(c.path)) {
|
|
7531
|
+
out3.push(
|
|
7532
|
+
makeFinding(RULE8, policy, {
|
|
7533
|
+
file: c.path,
|
|
7534
|
+
message: `A CI workflow was renamed so it no longer runs (${c.oldPath} \u2192 ${c.path}).`,
|
|
7535
|
+
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
7536
|
+
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."
|
|
7537
|
+
})
|
|
7538
|
+
);
|
|
7539
|
+
continue;
|
|
7540
|
+
}
|
|
7541
|
+
if (!isProtected(c.path, policy, "ci")) continue;
|
|
7542
|
+
const afterLines = c.after != null ? c.after.split("\n") : null;
|
|
7543
|
+
const addedAt = new Set(addedLines(c).map((l) => l.newLine).filter((n) => n != null));
|
|
7544
|
+
const github = isGithubFile(c.path);
|
|
7545
|
+
for (const l of github ? addedLines(c) : []) {
|
|
7546
|
+
const coe = l.content.match(/^\s*-?\s*continue-on-error:\s*(.+?)\s*$/);
|
|
7547
|
+
const cond = l.content.match(/^\s*-?\s*if:\s*(.+?)\s*$/);
|
|
7548
|
+
if (!coe && !cond) continue;
|
|
7549
|
+
const step = afterLines && l.newLine != null ? stepOf(afterLines, l.newLine - 1) : null;
|
|
7550
|
+
if (afterLines && step && !stepHasCheck(afterLines, step)) continue;
|
|
7551
|
+
if (afterLines && !step && l.newLine != null) {
|
|
7552
|
+
const job = jobOf(afterLines, l.newLine - 1);
|
|
7553
|
+
if (job && !jobHasCheck(afterLines, job)) continue;
|
|
7554
|
+
}
|
|
7555
|
+
if (coe && isTruthy(coe[1])) {
|
|
7556
|
+
out3.push(
|
|
7557
|
+
makeFinding(RULE8, policy, {
|
|
7558
|
+
file: c.path,
|
|
7559
|
+
line: l.newLine ?? void 0,
|
|
7560
|
+
message: "continue-on-error: true added \u2014 failures will no longer fail the job.",
|
|
7561
|
+
evidence: l.content.trim(),
|
|
7562
|
+
remediation: "Remove it. A check that cannot fail is not a check."
|
|
7563
|
+
})
|
|
7564
|
+
);
|
|
7565
|
+
} else if (cond && isAlwaysFalse(cond[1])) {
|
|
7566
|
+
out3.push(
|
|
7567
|
+
makeFinding(RULE8, policy, {
|
|
7568
|
+
file: c.path,
|
|
7569
|
+
line: l.newLine ?? void 0,
|
|
7570
|
+
message: `if: ${uncommented(cond[1])} added \u2014 this step can never run.`,
|
|
7571
|
+
evidence: l.content.trim(),
|
|
7572
|
+
remediation: "Re-enable the step instead of conditioning it off."
|
|
7573
|
+
})
|
|
7574
|
+
);
|
|
7575
|
+
}
|
|
7576
|
+
}
|
|
7577
|
+
if (afterLines && github) {
|
|
7578
|
+
for (const b of runBlocks(afterLines)) {
|
|
7579
|
+
if (!b.body.some(invokesCheck2)) continue;
|
|
7580
|
+
const propagates = b.body.some((l) => /\bexit\s+"?\$/.test(l));
|
|
7581
|
+
for (let k = 0; k < b.body.length; k++) {
|
|
7582
|
+
const idx = b.start + 1 + k;
|
|
7583
|
+
if (!addedAt.has(idx + 1)) continue;
|
|
7584
|
+
const n = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
|
|
7585
|
+
if (!n || propagates && /set\s\+e/.test(b.body[k])) continue;
|
|
7586
|
+
out3.push(
|
|
7587
|
+
makeFinding(RULE8, policy, {
|
|
7588
|
+
file: c.path,
|
|
7589
|
+
line: idx + 1,
|
|
7590
|
+
message: `A check's run block was neutralised: ${n.what}.`,
|
|
7591
|
+
evidence: b.body[k].trim(),
|
|
7592
|
+
remediation: "Let the check decide the step. Masking its exit status is the same tamper as removing it."
|
|
7593
|
+
})
|
|
7594
|
+
);
|
|
7595
|
+
}
|
|
7596
|
+
}
|
|
7597
|
+
for (const l of addedLines(c)) {
|
|
7598
|
+
if (!/^\s*-?\s*shell:\s*.*\{0\}/.test(l.content) || l.newLine == null) continue;
|
|
7599
|
+
if (SHELL_ERREXIT.test(uncommented(l.content.replace(/^\s*-?\s*shell:\s*/, "")))) continue;
|
|
7600
|
+
const step = stepOf(afterLines, l.newLine - 1);
|
|
7601
|
+
if (step && !stepHasCheck(afterLines, step)) continue;
|
|
7602
|
+
out3.push(
|
|
7603
|
+
makeFinding(RULE8, policy, {
|
|
7604
|
+
file: c.path,
|
|
7605
|
+
line: l.newLine,
|
|
7606
|
+
message: "shell: \u2026 {0} added on a check step \u2014 the shell no longer stops on the first failure.",
|
|
7607
|
+
evidence: l.content.trim(),
|
|
7608
|
+
remediation: "Keep the default shell (fail-fast) on steps that run checks."
|
|
7609
|
+
})
|
|
7610
|
+
);
|
|
7611
|
+
}
|
|
7612
|
+
for (const l of addedLines(c)) {
|
|
7613
|
+
if (l.newLine == null) continue;
|
|
7614
|
+
const i = l.newLine - 1;
|
|
7615
|
+
const wd = l.content.match(WORKDIR_LINE);
|
|
7616
|
+
if (wd) {
|
|
7617
|
+
const dir = literalValue(wd[1]);
|
|
7618
|
+
if (dir === null || !dirRunsNothing(dir, ctx)) continue;
|
|
7619
|
+
const step2 = stepOf(afterLines, i);
|
|
7620
|
+
if (step2) {
|
|
7621
|
+
if (!stepHasCheck(afterLines, step2)) continue;
|
|
7622
|
+
} else {
|
|
7623
|
+
const d = defaultsOf(afterLines, i);
|
|
7624
|
+
if (d === null) continue;
|
|
7625
|
+
const job = jobOf(afterLines, d);
|
|
7626
|
+
if (job ? !jobHasCheck(afterLines, job) : !afterLines.some((x) => invokesCheck2(x) || usesCheck(x))) continue;
|
|
7627
|
+
}
|
|
7628
|
+
out3.push(
|
|
7629
|
+
makeFinding(RULE8, policy, {
|
|
7630
|
+
file: c.path,
|
|
7631
|
+
line: l.newLine,
|
|
7632
|
+
message: `A check was pointed at ${dir} by working-directory \u2014 a directory holding no code or package the check could run.`,
|
|
7633
|
+
evidence: l.content.trim(),
|
|
7634
|
+
remediation: "Run the check where the code is. A suite pointed at an empty directory passes on nothing."
|
|
7635
|
+
})
|
|
7636
|
+
);
|
|
7637
|
+
continue;
|
|
7638
|
+
}
|
|
7639
|
+
const ref = l.content.match(REF_LINE);
|
|
7640
|
+
if (!ref) continue;
|
|
7641
|
+
const step = enclosingStep(afterLines, i);
|
|
7642
|
+
if (!step || !afterLines.slice(step[0], step[1]).some((x) => CHECKOUT.test(x))) continue;
|
|
7643
|
+
const target = literalValue(ref[1]);
|
|
7644
|
+
if (target === null) continue;
|
|
7645
|
+
out3.push(
|
|
7646
|
+
makeFinding(RULE8, policy, {
|
|
7647
|
+
file: c.path,
|
|
7648
|
+
line: l.newLine,
|
|
7649
|
+
message: `actions/checkout was pinned to ref: ${target} \u2014 the checks run against that ref, not against the change under review.`,
|
|
7650
|
+
evidence: l.content.trim(),
|
|
7651
|
+
remediation: "Let checkout take the event's ref (its default). A suite that checks out main proves nothing about the pull request."
|
|
7652
|
+
})
|
|
7653
|
+
);
|
|
7654
|
+
}
|
|
7655
|
+
if (c.before != null && c.after != null && !isCompositeAction(c.path)) {
|
|
7656
|
+
triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
|
|
7657
|
+
for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
|
|
7658
|
+
out3.push(
|
|
7659
|
+
makeFinding(RULE8, policy, {
|
|
7660
|
+
file: c.path,
|
|
7661
|
+
message: `The workflow's triggers were narrowed: ${reason}.`,
|
|
7662
|
+
evidence: reason,
|
|
7663
|
+
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."
|
|
7664
|
+
})
|
|
7665
|
+
);
|
|
7666
|
+
}
|
|
7667
|
+
}
|
|
7668
|
+
}
|
|
7669
|
+
const afterCommands = afterLines ? commandLines(afterLines) : null;
|
|
7670
|
+
const afterCores = (afterCommands ?? addedLines(c).map((l) => l.content)).map(commandCore);
|
|
7671
|
+
const addedCores = addedLines(c).map((l) => commandCore(afterCommands && l.newLine != null ? afterCommands[l.newLine - 1] : l.content));
|
|
7672
|
+
for (const l of afterLines ?? []) {
|
|
7673
|
+
for (const reused of localUses(l)) {
|
|
7674
|
+
const content = afterByPath.get(reused);
|
|
7675
|
+
if (content == null || reused === c.path) continue;
|
|
7676
|
+
const cores = commandLines(content.split("\n")).map(commandCore);
|
|
7677
|
+
afterCores.push(...cores);
|
|
7678
|
+
addedCores.push(...cores);
|
|
7679
|
+
}
|
|
7680
|
+
}
|
|
7681
|
+
if (!github && c.after == null) {
|
|
7682
|
+
for (const o of changes) {
|
|
7683
|
+
if (o.kind !== "file" || o.op !== "add" || o.after == null || !isProtected(o.path, policy, "ci")) continue;
|
|
7684
|
+
const cores = commandLines(o.after.split("\n")).map(commandCore);
|
|
7685
|
+
afterCores.push(...cores);
|
|
7686
|
+
addedCores.push(...cores);
|
|
7687
|
+
}
|
|
7688
|
+
}
|
|
7689
|
+
const runKey = github ? GH_RUN_KEY : GENERIC_RUN_KEY;
|
|
7690
|
+
for (const l of removedLines(c)) {
|
|
7691
|
+
if (/^\s*#/.test(l.content)) continue;
|
|
7692
|
+
const isUsesLine = usesCheck(l.content);
|
|
7693
|
+
const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? runKey.test(l.content) : true) && invokesCheck2(l.content);
|
|
7694
|
+
if (!isUsesLine && !isCheckCommand) continue;
|
|
7695
|
+
const s = survives(commandCore(l.content), isUsesLine, afterCores, addedCores);
|
|
7696
|
+
if (s.state === "kept") continue;
|
|
7697
|
+
if (s.state === "neutralised") {
|
|
7698
|
+
out3.push(
|
|
7699
|
+
makeFinding(RULE8, policy, {
|
|
7700
|
+
file: c.path,
|
|
7701
|
+
message: "A CI check command was neutralised in place: it still runs, but its result no longer decides (or its suite was narrowed).",
|
|
7702
|
+
evidence: `${l.content.trim()} \u2192 ${s.by}`,
|
|
7703
|
+
remediation: "Let the check decide the step; do not mask its status, cut it short, or narrow what it runs."
|
|
7704
|
+
})
|
|
7705
|
+
);
|
|
7706
|
+
continue;
|
|
7707
|
+
}
|
|
7708
|
+
out3.push(
|
|
7709
|
+
makeFinding(RULE8, policy, {
|
|
7710
|
+
file: c.path,
|
|
7711
|
+
message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
|
|
7712
|
+
evidence: l.content.trim(),
|
|
7713
|
+
remediation: "Restore the step. Removing the check that protects main is itself the tamper."
|
|
7714
|
+
})
|
|
7715
|
+
);
|
|
7716
|
+
}
|
|
7162
7717
|
}
|
|
7163
|
-
|
|
7164
|
-
|
|
7718
|
+
return out3;
|
|
7719
|
+
}
|
|
7165
7720
|
};
|
|
7166
|
-
underDir = (path, dir) => dir === "" || path.startsWith(dir + "/");
|
|
7167
7721
|
}
|
|
7168
7722
|
});
|
|
7169
7723
|
|
|
@@ -7191,7 +7745,7 @@ function safeParse(src) {
|
|
|
7191
7745
|
return null;
|
|
7192
7746
|
}
|
|
7193
7747
|
}
|
|
7194
|
-
function
|
|
7748
|
+
function effective2(raw) {
|
|
7195
7749
|
const v = raw.version;
|
|
7196
7750
|
const version = typeof v === "number" && Number.isInteger(v) && v >= 1 ? v : 1;
|
|
7197
7751
|
const base = defaultPolicy(version);
|
|
@@ -7256,8 +7810,8 @@ function policyWeakening(before, after, probe) {
|
|
|
7256
7810
|
if (b === null) return null;
|
|
7257
7811
|
const a = safeParse(after);
|
|
7258
7812
|
if (a === null) return ["the policy no longer parses as a valid policy (emptied or corrupted)"];
|
|
7259
|
-
const be =
|
|
7260
|
-
const ae =
|
|
7813
|
+
const be = effective2(b);
|
|
7814
|
+
const ae = effective2(a);
|
|
7261
7815
|
const reasons = [];
|
|
7262
7816
|
if (ae.version < be.version) {
|
|
7263
7817
|
reasons.push(
|
|
@@ -7520,7 +8074,7 @@ function entryShape(h) {
|
|
|
7520
8074
|
}
|
|
7521
8075
|
return { problems, runs };
|
|
7522
8076
|
}
|
|
7523
|
-
function judgeGateEntry(event,
|
|
8077
|
+
function judgeGateEntry(event, matcher2, h) {
|
|
7524
8078
|
const cmd = typeof h.command === "string" ? h.command : "";
|
|
7525
8079
|
const a = analyseGateCommand(cmd, event);
|
|
7526
8080
|
const shape = entryShape(h);
|
|
@@ -7528,13 +8082,13 @@ function judgeGateEntry(event, matcher, h) {
|
|
|
7528
8082
|
let runs = a.runs && shape.runs;
|
|
7529
8083
|
let tools = null;
|
|
7530
8084
|
if (event === "PreToolUse") {
|
|
7531
|
-
const sel = matcherSelection(
|
|
8085
|
+
const sel = matcherSelection(matcher2);
|
|
7532
8086
|
tools = sel.tools;
|
|
7533
8087
|
if (sel.problem) {
|
|
7534
8088
|
problems.push(sel.problem);
|
|
7535
8089
|
}
|
|
7536
|
-
} else if (
|
|
7537
|
-
problems.push(`a \`matcher\` (${JSON.stringify(
|
|
8090
|
+
} else if (matcher2 !== void 0 && !(typeof matcher2 === "string" && (matcher2 === "" || matcher2 === "*"))) {
|
|
8091
|
+
problems.push(`a \`matcher\` (${JSON.stringify(matcher2)}) on the Stop entry`);
|
|
7538
8092
|
runs = false;
|
|
7539
8093
|
}
|
|
7540
8094
|
return { runs, problems, pin: a.pin, tools };
|
|
@@ -7741,7 +8295,7 @@ function claudeSettingsFindings(c, policy) {
|
|
|
7741
8295
|
if (c.after == null) return null;
|
|
7742
8296
|
const after = parseSettings(c.after);
|
|
7743
8297
|
const before = c.before != null ? parseSettings(c.before) : null;
|
|
7744
|
-
const finding = (reason) => makeFinding(
|
|
8298
|
+
const finding = (reason) => makeFinding(RULE9, policy, {
|
|
7745
8299
|
file: c.path,
|
|
7746
8300
|
message: `The Claude Code hook wiring was weakened: ${reason}.`,
|
|
7747
8301
|
evidence: reason,
|
|
@@ -7766,7 +8320,7 @@ function policyFindings(c, policy, ctx) {
|
|
|
7766
8320
|
const reasons = c.before == null ? policyAddWeakening(c.after, probe) : policyWeakening(c.before, c.after, probe);
|
|
7767
8321
|
if (reasons === null) return null;
|
|
7768
8322
|
return reasons.map(
|
|
7769
|
-
(reason) => makeFinding(
|
|
8323
|
+
(reason) => makeFinding(RULE9, policy, {
|
|
7770
8324
|
file: c.path,
|
|
7771
8325
|
message: added ? `A policy file was added that weakens the baseline: ${reason}.` : `The policy was weakened: ${reason}.`,
|
|
7772
8326
|
evidence: reason,
|
|
@@ -7876,7 +8430,7 @@ function configFindings(c, changes, policy, ctx) {
|
|
|
7876
8430
|
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
8431
|
}
|
|
7878
8432
|
return reasons.map(
|
|
7879
|
-
(reason) => makeFinding(
|
|
8433
|
+
(reason) => makeFinding(RULE9, policy, {
|
|
7880
8434
|
file: c.path,
|
|
7881
8435
|
message: `The hook configuration was weakened: ${reason}.`,
|
|
7882
8436
|
evidence: reason,
|
|
@@ -7927,7 +8481,7 @@ function huskyRuntimeWrite(path, content, ctx) {
|
|
|
7927
8481
|
(i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
|
|
7928
8482
|
);
|
|
7929
8483
|
}
|
|
7930
|
-
var
|
|
8484
|
+
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
8485
|
var init_hook_tampering = __esm({
|
|
7932
8486
|
"src/detectors/hook-tampering.ts"() {
|
|
7933
8487
|
"use strict";
|
|
@@ -7940,7 +8494,7 @@ var init_hook_tampering = __esm({
|
|
|
7940
8494
|
init_command();
|
|
7941
8495
|
init_repo();
|
|
7942
8496
|
init_hook_wiring();
|
|
7943
|
-
|
|
8497
|
+
RULE9 = "hook-tampering";
|
|
7944
8498
|
ownerExec2 = (mode) => (parseInt(mode.slice(-3), 8) & 64) !== 0;
|
|
7945
8499
|
EVENTS = ["PreToolUse", "Stop"];
|
|
7946
8500
|
isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
@@ -8008,7 +8562,7 @@ They WILL FAIL in v10.0.0
|
|
|
8008
8562
|
isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
|
|
8009
8563
|
isHookNote = (path) => !/(?:^|\/)\.husky\/_\//.test(path) && /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
|
|
8010
8564
|
hookTampering = {
|
|
8011
|
-
id:
|
|
8565
|
+
id: RULE9,
|
|
8012
8566
|
surface: ["file", "command"],
|
|
8013
8567
|
certainty: "mechanical",
|
|
8014
8568
|
run(changes, policy, _view, ctx) {
|
|
@@ -8021,7 +8575,7 @@ They WILL FAIL in v10.0.0
|
|
|
8021
8575
|
if (isPackageJson(c.path) && c.before != null && c.after != null) {
|
|
8022
8576
|
for (const reason of packageJsonWeakening(c.before, c.after)) {
|
|
8023
8577
|
out3.push(
|
|
8024
|
-
makeFinding(
|
|
8578
|
+
makeFinding(RULE9, policy, {
|
|
8025
8579
|
file: c.path,
|
|
8026
8580
|
message: `The hook installation was weakened: ${reason}.`,
|
|
8027
8581
|
evidence: reason,
|
|
@@ -8034,7 +8588,7 @@ They WILL FAIL in v10.0.0
|
|
|
8034
8588
|
}
|
|
8035
8589
|
if (c.op !== "delete" && c.oldMode && c.newMode && ownerExec2(c.oldMode) && !ownerExec2(c.newMode)) {
|
|
8036
8590
|
out3.push(
|
|
8037
|
-
makeFinding(
|
|
8591
|
+
makeFinding(RULE9, policy, {
|
|
8038
8592
|
file: c.path,
|
|
8039
8593
|
message: `Execute permission was removed from a protected hook (${c.oldMode} \u2192 ${c.newMode}) \u2014 git will skip it.`,
|
|
8040
8594
|
evidence: `${c.path}: mode ${c.oldMode} \u2192 ${c.newMode}`,
|
|
@@ -8044,7 +8598,7 @@ They WILL FAIL in v10.0.0
|
|
|
8044
8598
|
}
|
|
8045
8599
|
if (c.op === "delete") {
|
|
8046
8600
|
out3.push(
|
|
8047
|
-
makeFinding(
|
|
8601
|
+
makeFinding(RULE9, policy, {
|
|
8048
8602
|
file: c.path,
|
|
8049
8603
|
message: "A protected hook/policy file was deleted.",
|
|
8050
8604
|
evidence: c.path,
|
|
@@ -8053,7 +8607,7 @@ They WILL FAIL in v10.0.0
|
|
|
8053
8607
|
);
|
|
8054
8608
|
} else if (c.op === "rename" && !renameKept(c)) {
|
|
8055
8609
|
out3.push(
|
|
8056
|
-
makeFinding(
|
|
8610
|
+
makeFinding(RULE9, policy, {
|
|
8057
8611
|
file: c.path,
|
|
8058
8612
|
message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
|
|
8059
8613
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -8067,7 +8621,7 @@ They WILL FAIL in v10.0.0
|
|
|
8067
8621
|
if (c.before != null && c.after != null) {
|
|
8068
8622
|
for (const reason of codeownersWeakening(c.before, c.after, [...repoGateFiles(ctx), c.path])) {
|
|
8069
8623
|
out3.push(
|
|
8070
|
-
makeFinding(
|
|
8624
|
+
makeFinding(RULE9, policy, {
|
|
8071
8625
|
file: c.path,
|
|
8072
8626
|
message: `CODEOWNERS was weakened: ${reason}.`,
|
|
8073
8627
|
evidence: reason,
|
|
@@ -8089,7 +8643,7 @@ They WILL FAIL in v10.0.0
|
|
|
8089
8643
|
const kept = afterPin !== null && (beforePin === "" ? afterPin === "" || PLAIN_SEMVER.test(afterPin) : pinNotBelow(afterPin, beforePin));
|
|
8090
8644
|
if (!kept) {
|
|
8091
8645
|
out3.push(
|
|
8092
|
-
makeFinding(
|
|
8646
|
+
makeFinding(RULE9, policy, {
|
|
8093
8647
|
file: c.path,
|
|
8094
8648
|
message: "A protected hook script no longer matches the shape `tamperward init` writes.",
|
|
8095
8649
|
evidence: "the gate script no longer matches the shape init wrote; re-run init or sign off",
|
|
@@ -8112,7 +8666,7 @@ They WILL FAIL in v10.0.0
|
|
|
8112
8666
|
if (shebang || !live) {
|
|
8113
8667
|
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
8668
|
out3.push(
|
|
8115
|
-
makeFinding(
|
|
8669
|
+
makeFinding(RULE9, policy, {
|
|
8116
8670
|
file: c.path,
|
|
8117
8671
|
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
8672
|
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 +8675,7 @@ They WILL FAIL in v10.0.0
|
|
|
8121
8675
|
);
|
|
8122
8676
|
} else if (earlyExit) {
|
|
8123
8677
|
out3.push(
|
|
8124
|
-
makeFinding(
|
|
8678
|
+
makeFinding(RULE9, policy, {
|
|
8125
8679
|
file: c.path,
|
|
8126
8680
|
line: earlyExit.newLine ?? void 0,
|
|
8127
8681
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -8143,7 +8697,7 @@ They WILL FAIL in v10.0.0
|
|
|
8143
8697
|
deadGuard2 ? `a guard that can never be true was inserted (\`${deadGuard2.content.trim()}\`)` : null
|
|
8144
8698
|
].filter((d) => d !== null);
|
|
8145
8699
|
out3.push(
|
|
8146
|
-
makeFinding(
|
|
8700
|
+
makeFinding(RULE9, policy, {
|
|
8147
8701
|
file: c.path,
|
|
8148
8702
|
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
8703
|
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 +8709,7 @@ They WILL FAIL in v10.0.0
|
|
|
8155
8709
|
}
|
|
8156
8710
|
if (shebangChanged && c.after != null) {
|
|
8157
8711
|
out3.push(
|
|
8158
|
-
makeFinding(
|
|
8712
|
+
makeFinding(RULE9, policy, {
|
|
8159
8713
|
file: c.path,
|
|
8160
8714
|
line: 1,
|
|
8161
8715
|
message: `A protected hook's interpreter was changed: ${shebang}.`,
|
|
@@ -8166,7 +8720,7 @@ They WILL FAIL in v10.0.0
|
|
|
8166
8720
|
}
|
|
8167
8721
|
if (earlyExit) {
|
|
8168
8722
|
out3.push(
|
|
8169
|
-
makeFinding(
|
|
8723
|
+
makeFinding(RULE9, policy, {
|
|
8170
8724
|
file: c.path,
|
|
8171
8725
|
line: earlyExit.newLine ?? void 0,
|
|
8172
8726
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -8178,7 +8732,7 @@ They WILL FAIL in v10.0.0
|
|
|
8178
8732
|
const deadGuard = c.before == null || c.after == null ? added.find((l) => insertsDeadGuard(l.content)) : void 0;
|
|
8179
8733
|
if (deadGuard) {
|
|
8180
8734
|
out3.push(
|
|
8181
|
-
makeFinding(
|
|
8735
|
+
makeFinding(RULE9, policy, {
|
|
8182
8736
|
file: c.path,
|
|
8183
8737
|
line: deadGuard.newLine ?? void 0,
|
|
8184
8738
|
message: "A guard that can never be true was inserted into a protected hook \u2014 what it wraps no longer runs.",
|
|
@@ -8190,7 +8744,7 @@ They WILL FAIL in v10.0.0
|
|
|
8190
8744
|
const lost = scriptWeakening(beforeLines, afterLines, opts);
|
|
8191
8745
|
if (lost.length) {
|
|
8192
8746
|
out3.push(
|
|
8193
|
-
makeFinding(
|
|
8747
|
+
makeFinding(RULE9, policy, {
|
|
8194
8748
|
file: c.path,
|
|
8195
8749
|
message: `A check invocation was removed from a protected hook: ${lost[0].reason}.`,
|
|
8196
8750
|
evidence: lost[0].evidence,
|
|
@@ -8209,7 +8763,7 @@ They WILL FAIL in v10.0.0
|
|
|
8209
8763
|
const m = l.content.match(INLINE);
|
|
8210
8764
|
if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
|
|
8211
8765
|
out3.push(
|
|
8212
|
-
makeFinding(
|
|
8766
|
+
makeFinding(RULE9, policy, {
|
|
8213
8767
|
file: c.path,
|
|
8214
8768
|
line: l.newLine ?? void 0,
|
|
8215
8769
|
message: `Rule "${m[1]}" was lowered from block to warn in the policy.`,
|
|
@@ -8219,7 +8773,7 @@ They WILL FAIL in v10.0.0
|
|
|
8219
8773
|
);
|
|
8220
8774
|
} else if (/enabled:\s*false\b/.test(l.content)) {
|
|
8221
8775
|
out3.push(
|
|
8222
|
-
makeFinding(
|
|
8776
|
+
makeFinding(RULE9, policy, {
|
|
8223
8777
|
file: c.path,
|
|
8224
8778
|
line: l.newLine ?? void 0,
|
|
8225
8779
|
message: "A rule was disabled (enabled: false) in the policy.",
|
|
@@ -8237,7 +8791,7 @@ They WILL FAIL in v10.0.0
|
|
|
8237
8791
|
const hiding = updateIndexHiding(toks, policy);
|
|
8238
8792
|
if (hiding) {
|
|
8239
8793
|
out3.push(
|
|
8240
|
-
makeFinding(
|
|
8794
|
+
makeFinding(RULE9, policy, {
|
|
8241
8795
|
file: repoRelative(hiding.file, ctx),
|
|
8242
8796
|
message: `Hook tampering via shell: ${hiding.reason}.`,
|
|
8243
8797
|
evidence: seg,
|
|
@@ -8253,7 +8807,7 @@ They WILL FAIL in v10.0.0
|
|
|
8253
8807
|
const file = target ? repoRelative(target, ctx) : void 0;
|
|
8254
8808
|
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
8809
|
out3.push(
|
|
8256
|
-
makeFinding(
|
|
8810
|
+
makeFinding(RULE9, policy, {
|
|
8257
8811
|
...file !== void 0 ? { file } : {},
|
|
8258
8812
|
message: `Hook tampering via shell: ${why}${outside}.`,
|
|
8259
8813
|
evidence: seg,
|
|
@@ -8801,7 +9355,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
8801
9355
|
if (!was || !now || was.path === now.path) return [];
|
|
8802
9356
|
const samples = runnerSamples(now.path, policy, ctx, "pytest");
|
|
8803
9357
|
return suiteNarrowings(was.content, now.content, now.path, samples).map(
|
|
8804
|
-
(n) => makeFinding(
|
|
9358
|
+
(n) => makeFinding(RULE10, policy, {
|
|
8805
9359
|
file: now.path,
|
|
8806
9360
|
message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
|
|
8807
9361
|
evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
|
|
@@ -8809,7 +9363,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
8809
9363
|
})
|
|
8810
9364
|
);
|
|
8811
9365
|
}
|
|
8812
|
-
var
|
|
9366
|
+
var RULE10, JVM_TEST, TEST_DEFS, isTableOf, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
|
|
8813
9367
|
var init_test_deletion = __esm({
|
|
8814
9368
|
"src/detectors/test-deletion.ts"() {
|
|
8815
9369
|
"use strict";
|
|
@@ -8826,7 +9380,7 @@ var init_test_deletion = __esm({
|
|
|
8826
9380
|
init_narrow();
|
|
8827
9381
|
init_reachability();
|
|
8828
9382
|
init_runner_chain();
|
|
8829
|
-
|
|
9383
|
+
RULE10 = "test-deletion";
|
|
8830
9384
|
JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
|
|
8831
9385
|
TEST_DEFS = {
|
|
8832
9386
|
py: /^\s*(?:async\s+)?def\s+test\w*\s*\(/,
|
|
@@ -8849,7 +9403,7 @@ var init_test_deletion = __esm({
|
|
|
8849
9403
|
SLICE_SCRIPT = /^test:(?:unit|integration)$/;
|
|
8850
9404
|
SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
|
|
8851
9405
|
testDeletion = {
|
|
8852
|
-
id:
|
|
9406
|
+
id: RULE10,
|
|
8853
9407
|
surface: ["file", "command"],
|
|
8854
9408
|
certainty: "mechanical",
|
|
8855
9409
|
run(changes, policy, _view, ctx) {
|
|
@@ -8881,7 +9435,7 @@ var init_test_deletion = __esm({
|
|
|
8881
9435
|
if (c.op === "delete" && isTest) {
|
|
8882
9436
|
if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
|
|
8883
9437
|
out3.push(
|
|
8884
|
-
makeFinding(
|
|
9438
|
+
makeFinding(RULE10, policy, {
|
|
8885
9439
|
file: c.path,
|
|
8886
9440
|
message: "A test file was deleted.",
|
|
8887
9441
|
evidence: c.path,
|
|
@@ -8890,7 +9444,7 @@ var init_test_deletion = __esm({
|
|
|
8890
9444
|
);
|
|
8891
9445
|
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && isSpecShaped(c.oldPath, c.before, c.after) && !isSpec(c.path)) {
|
|
8892
9446
|
out3.push(
|
|
8893
|
-
makeFinding(
|
|
9447
|
+
makeFinding(RULE10, policy, {
|
|
8894
9448
|
file: c.path,
|
|
8895
9449
|
message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
|
|
8896
9450
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -8899,7 +9453,7 @@ var init_test_deletion = __esm({
|
|
|
8899
9453
|
);
|
|
8900
9454
|
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && isTest && leavesWalk(c.oldPath, c.path)) {
|
|
8901
9455
|
out3.push(
|
|
8902
|
-
makeFinding(
|
|
9456
|
+
makeFinding(RULE10, policy, {
|
|
8903
9457
|
file: c.path,
|
|
8904
9458
|
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
9459
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -8914,7 +9468,7 @@ var init_test_deletion = __esm({
|
|
|
8914
9468
|
const removed = new Set([...significantLines(c.before, c.path)].filter((l) => !afterLines.has(l)));
|
|
8915
9469
|
if (isRelocation(removed, before.min - after.min)) continue;
|
|
8916
9470
|
out3.push(
|
|
8917
|
-
makeFinding(
|
|
9471
|
+
makeFinding(RULE10, policy, {
|
|
8918
9472
|
file: c.path,
|
|
8919
9473
|
message: `Test blocks removed: ${before.min} \u2192 ${after.min} it()/test() in this spec (each-table rows count as tests).`,
|
|
8920
9474
|
evidence: `${before.min - after.min} test block(s) removed from ${c.path}`,
|
|
@@ -8925,9 +9479,22 @@ var init_test_deletion = __esm({
|
|
|
8925
9479
|
} else if (c.op !== "delete" && c.after != null && !isSpec(c.path) && isProtected(c.path, policy, "config") && runnerOf(c.path, c.after)) {
|
|
8926
9480
|
const rn = runnerOf(c.path, c.after);
|
|
8927
9481
|
const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
|
|
9482
|
+
for (const d of suiteDelegations(c.before, c.after, c.path)) {
|
|
9483
|
+
const cands = targetCandidates(c.path, d.target);
|
|
9484
|
+
const added = cands.find((f) => addedPaths.has(f));
|
|
9485
|
+
if (added === void 0 && cands.some((f) => isProtected(f, policy, "config"))) continue;
|
|
9486
|
+
out3.push(
|
|
9487
|
+
makeFinding(RULE10, policy, {
|
|
9488
|
+
file: c.path,
|
|
9489
|
+
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}.`,
|
|
9490
|
+
evidence: `${c.path}: ${d.how}`,
|
|
9491
|
+
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."
|
|
9492
|
+
})
|
|
9493
|
+
);
|
|
9494
|
+
}
|
|
8928
9495
|
for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
|
|
8929
9496
|
out3.push(
|
|
8930
|
-
makeFinding(
|
|
9497
|
+
makeFinding(RULE10, policy, {
|
|
8931
9498
|
file: c.path,
|
|
8932
9499
|
message: `The runner no longer selects ${n.path}: ${n.reason}.`,
|
|
8933
9500
|
evidence: `${n.path} dropped from the suite by ${c.path}`,
|
|
@@ -8939,12 +9506,12 @@ var init_test_deletion = __esm({
|
|
|
8939
9506
|
const w = n.weakening;
|
|
8940
9507
|
const evidence = `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${n.after === "" ? "deleted" : JSON.stringify(n.after)}`;
|
|
8941
9508
|
out3.push(
|
|
8942
|
-
w.state === "removed" ? makeFinding(
|
|
9509
|
+
w.state === "removed" ? makeFinding(RULE10, policy, {
|
|
8943
9510
|
file: c.path,
|
|
8944
9511
|
message: `The test script no longer runs the ${w.kind} check: scripts.${n.script} ${w.what}.`,
|
|
8945
9512
|
evidence,
|
|
8946
9513
|
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(
|
|
9514
|
+
}) : makeFinding(RULE10, policy, {
|
|
8948
9515
|
file: c.path,
|
|
8949
9516
|
message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${n.script}.`,
|
|
8950
9517
|
evidence,
|
|
@@ -9079,7 +9646,7 @@ var init_test_deletion = __esm({
|
|
|
9079
9646
|
}
|
|
9080
9647
|
if (why) {
|
|
9081
9648
|
out3.push(
|
|
9082
|
-
makeFinding(
|
|
9649
|
+
makeFinding(RULE10, policy, {
|
|
9083
9650
|
message: `Test removed via shell: ${why}.`,
|
|
9084
9651
|
evidence,
|
|
9085
9652
|
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 +9705,7 @@ function significantLinesOrdered(src, path) {
|
|
|
9138
9705
|
function liveSignificantLines(src, path) {
|
|
9139
9706
|
return significantLinesOrdered(partition(src, path).live, path);
|
|
9140
9707
|
}
|
|
9141
|
-
var
|
|
9708
|
+
var RULE11, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, KeptPool, testContentRemoval;
|
|
9142
9709
|
var init_test_content_removal = __esm({
|
|
9143
9710
|
"src/detectors/test-content-removal.ts"() {
|
|
9144
9711
|
"use strict";
|
|
@@ -9149,7 +9716,7 @@ var init_test_content_removal = __esm({
|
|
|
9149
9716
|
init_files();
|
|
9150
9717
|
init_spec_shape();
|
|
9151
9718
|
init_reachability();
|
|
9152
|
-
|
|
9719
|
+
RULE11 = "test-content-removal";
|
|
9153
9720
|
MIN_REMOVED_LINES = 3;
|
|
9154
9721
|
MIN_REMOVED_ROWS = 2;
|
|
9155
9722
|
ws = (s) => s.replace(/\s+/g, "");
|
|
@@ -9190,7 +9757,7 @@ var init_test_content_removal = __esm({
|
|
|
9190
9757
|
}
|
|
9191
9758
|
};
|
|
9192
9759
|
testContentRemoval = {
|
|
9193
|
-
id:
|
|
9760
|
+
id: RULE11,
|
|
9194
9761
|
surface: ["file"],
|
|
9195
9762
|
certainty: "mechanical",
|
|
9196
9763
|
run(changes, policy) {
|
|
@@ -9234,7 +9801,7 @@ var init_test_content_removal = __esm({
|
|
|
9234
9801
|
const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
|
|
9235
9802
|
if (goneRows.length >= MIN_REMOVED_ROWS) {
|
|
9236
9803
|
out3.push(
|
|
9237
|
-
makeFinding(
|
|
9804
|
+
makeFinding(RULE11, policy, {
|
|
9238
9805
|
file: c.path,
|
|
9239
9806
|
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
9807
|
evidence: goneRows.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9245,7 +9812,7 @@ var init_test_content_removal = __esm({
|
|
|
9245
9812
|
}
|
|
9246
9813
|
if (hidden.length >= MIN_REMOVED_LINES) {
|
|
9247
9814
|
out3.push(
|
|
9248
|
-
makeFinding(
|
|
9815
|
+
makeFinding(RULE11, policy, {
|
|
9249
9816
|
file: c.path,
|
|
9250
9817
|
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
9818
|
evidence: hidden.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9256,7 +9823,7 @@ var init_test_content_removal = __esm({
|
|
|
9256
9823
|
}
|
|
9257
9824
|
if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
|
|
9258
9825
|
out3.push(
|
|
9259
|
-
makeFinding(
|
|
9826
|
+
makeFinding(RULE11, policy, {
|
|
9260
9827
|
file: c.path,
|
|
9261
9828
|
message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
|
|
9262
9829
|
evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9279,14 +9846,14 @@ function namesProtectedSnapshot(seg, policy) {
|
|
|
9279
9846
|
}
|
|
9280
9847
|
return null;
|
|
9281
9848
|
}
|
|
9282
|
-
var
|
|
9849
|
+
var RULE12, RUNNER3, PM_TEST_SCRIPT, UPDATE_ANY, UPDATE_WITH_RUNNER, INSTA_ACCEPT, UPDATE_EXPECT, REGEN_SCRIPT, MUTATE, snapshotRewrite;
|
|
9283
9850
|
var init_snapshot_rewrite = __esm({
|
|
9284
9851
|
"src/detectors/snapshot-rewrite.ts"() {
|
|
9285
9852
|
"use strict";
|
|
9286
9853
|
init_policy();
|
|
9287
9854
|
init_finding();
|
|
9288
9855
|
init_command();
|
|
9289
|
-
|
|
9856
|
+
RULE12 = "snapshot-rewrite";
|
|
9290
9857
|
RUNNER3 = /\b(?:jest|vitest|playwright|ava)\b/;
|
|
9291
9858
|
PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
|
|
9292
9859
|
UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
|
|
@@ -9296,12 +9863,12 @@ var init_snapshot_rewrite = __esm({
|
|
|
9296
9863
|
REGEN_SCRIPT = /\b(?:update|regen(?:erate)?|re?bless)[-_.]?(?:golden|snapshots?|baselines?|expected)\b/i;
|
|
9297
9864
|
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
9865
|
snapshotRewrite = {
|
|
9299
|
-
id:
|
|
9866
|
+
id: RULE12,
|
|
9300
9867
|
surface: ["command", "file"],
|
|
9301
9868
|
certainty: "mechanical",
|
|
9302
9869
|
run(changes, policy) {
|
|
9303
9870
|
const out3 = [];
|
|
9304
|
-
const warn = (input) => out3.push(makeFinding(
|
|
9871
|
+
const warn = (input) => out3.push(makeFinding(RULE12, policy, { ...input, defaultSeverity: "warn" }));
|
|
9305
9872
|
for (const c of changes) {
|
|
9306
9873
|
if (c.kind === "command") {
|
|
9307
9874
|
for (const seg of segments(c.raw)) {
|
|
@@ -9349,16 +9916,16 @@ function isSnapshotChange(c, policy) {
|
|
|
9349
9916
|
if (isProtected(c.path, policy, "snapshots")) return true;
|
|
9350
9917
|
return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
|
|
9351
9918
|
}
|
|
9352
|
-
var
|
|
9919
|
+
var RULE13, COMMIT_VIEWS, snapshotOnlyRewrite;
|
|
9353
9920
|
var init_snapshot_only = __esm({
|
|
9354
9921
|
"src/detectors/snapshot-only.ts"() {
|
|
9355
9922
|
"use strict";
|
|
9356
9923
|
init_policy();
|
|
9357
9924
|
init_finding();
|
|
9358
|
-
|
|
9925
|
+
RULE13 = "snapshot-only-rewrite";
|
|
9359
9926
|
COMMIT_VIEWS = ["staged", "range"];
|
|
9360
9927
|
snapshotOnlyRewrite = {
|
|
9361
|
-
id:
|
|
9928
|
+
id: RULE13,
|
|
9362
9929
|
surface: ["file"],
|
|
9363
9930
|
certainty: "mechanical",
|
|
9364
9931
|
run(changes, policy, view) {
|
|
@@ -9371,7 +9938,7 @@ var init_snapshot_only = __esm({
|
|
|
9371
9938
|
if (!first) return [];
|
|
9372
9939
|
const names = snaps.map((c) => c.path).slice(0, 3).join(", ");
|
|
9373
9940
|
return [
|
|
9374
|
-
makeFinding(
|
|
9941
|
+
makeFinding(RULE13, policy, {
|
|
9375
9942
|
file: first.path,
|
|
9376
9943
|
message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
|
|
9377
9944
|
evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
|
|
@@ -9428,7 +9995,7 @@ function staticFacts(node) {
|
|
|
9428
9995
|
}
|
|
9429
9996
|
function expectAssertion(node, sf) {
|
|
9430
9997
|
if (!ts.isPropertyAccessExpression(node.expression)) return null;
|
|
9431
|
-
const
|
|
9998
|
+
const matcher2 = node.expression.name.text;
|
|
9432
9999
|
let receiver = node.expression.expression;
|
|
9433
10000
|
let polarity = "";
|
|
9434
10001
|
if (ts.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
|
|
@@ -9443,10 +10010,10 @@ function expectAssertion(node, sf) {
|
|
|
9443
10010
|
return {
|
|
9444
10011
|
subject,
|
|
9445
10012
|
polarity,
|
|
9446
|
-
matcher,
|
|
10013
|
+
matcher: matcher2,
|
|
9447
10014
|
args,
|
|
9448
10015
|
firstArgFacts: staticFacts(node.arguments[0]),
|
|
9449
|
-
canonical: [subject, polarity,
|
|
10016
|
+
canonical: [subject, polarity, matcher2, ...args].join("|"),
|
|
9450
10017
|
text: node.getText(sf).replace(/\s+/g, " ")
|
|
9451
10018
|
};
|
|
9452
10019
|
}
|
|
@@ -9549,7 +10116,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
9549
10116
|
const weakened2 = directionalWeakening(before.assertions, after.assertions);
|
|
9550
10117
|
if (weakened2) {
|
|
9551
10118
|
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(
|
|
10119
|
+
return makeFinding(RULE14, policy, {
|
|
9553
10120
|
file: path,
|
|
9554
10121
|
message,
|
|
9555
10122
|
evidence: `${weakened2.before.text} -> ${weakened2.after.text}`,
|
|
@@ -9558,7 +10125,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
9558
10125
|
}
|
|
9559
10126
|
const removed = pureRemoval(before.assertions, after.assertions);
|
|
9560
10127
|
if (removed) {
|
|
9561
|
-
return makeFinding(
|
|
10128
|
+
return makeFinding(RULE14, policy, {
|
|
9562
10129
|
file: path,
|
|
9563
10130
|
message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
|
|
9564
10131
|
evidence: removed.text,
|
|
@@ -9567,7 +10134,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
9567
10134
|
}
|
|
9568
10135
|
return null;
|
|
9569
10136
|
}
|
|
9570
|
-
var
|
|
10137
|
+
var RULE14, TEST_CALLS, SUITE_CALLS, EXACT_MATCHERS, WEAK_MATCHERS, compact, assertionWeakening;
|
|
9571
10138
|
var init_assertion_weakening = __esm({
|
|
9572
10139
|
"src/detectors/assertion-weakening.ts"() {
|
|
9573
10140
|
"use strict";
|
|
@@ -9576,14 +10143,14 @@ var init_assertion_weakening = __esm({
|
|
|
9576
10143
|
init_finding();
|
|
9577
10144
|
init_files();
|
|
9578
10145
|
init_runner_chain();
|
|
9579
|
-
|
|
10146
|
+
RULE14 = "assertion-weakening";
|
|
9580
10147
|
TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
9581
10148
|
SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
|
|
9582
10149
|
EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
|
|
9583
10150
|
WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
|
|
9584
10151
|
compact = (s) => s.replace(/\s+/g, "");
|
|
9585
10152
|
assertionWeakening = {
|
|
9586
|
-
id:
|
|
10153
|
+
id: RULE14,
|
|
9587
10154
|
surface: ["file"],
|
|
9588
10155
|
certainty: "heuristic",
|
|
9589
10156
|
run(changes, policy) {
|
|
@@ -9617,7 +10184,7 @@ function significant(src, path) {
|
|
|
9617
10184
|
}
|
|
9618
10185
|
return out3;
|
|
9619
10186
|
}
|
|
9620
|
-
var
|
|
10187
|
+
var RULE15, MIN_REMOVED_LINES2, ws2, testSupport;
|
|
9621
10188
|
var init_test_support = __esm({
|
|
9622
10189
|
"src/detectors/test-support.ts"() {
|
|
9623
10190
|
"use strict";
|
|
@@ -9625,11 +10192,11 @@ var init_test_support = __esm({
|
|
|
9625
10192
|
init_finding();
|
|
9626
10193
|
init_spec_shape();
|
|
9627
10194
|
init_files();
|
|
9628
|
-
|
|
10195
|
+
RULE15 = "test-support";
|
|
9629
10196
|
MIN_REMOVED_LINES2 = 3;
|
|
9630
10197
|
ws2 = (s) => s.replace(/\s+/g, "");
|
|
9631
10198
|
testSupport = {
|
|
9632
|
-
id:
|
|
10199
|
+
id: RULE15,
|
|
9633
10200
|
surface: ["file"],
|
|
9634
10201
|
certainty: "mechanical",
|
|
9635
10202
|
run(changes, policy) {
|
|
@@ -9644,7 +10211,7 @@ var init_test_support = __esm({
|
|
|
9644
10211
|
if (c.op === "delete") {
|
|
9645
10212
|
if (!isSupport(c.path, c.before, null)) continue;
|
|
9646
10213
|
out3.push(
|
|
9647
|
-
makeFinding(
|
|
10214
|
+
makeFinding(RULE15, policy, {
|
|
9648
10215
|
file: c.path,
|
|
9649
10216
|
message: "A test-support file was deleted (a helper, setup module or fixture under the test globs; it defines no test).",
|
|
9650
10217
|
evidence: c.path,
|
|
@@ -9654,7 +10221,7 @@ var init_test_support = __esm({
|
|
|
9654
10221
|
);
|
|
9655
10222
|
} else if (c.op === "rename" && c.oldPath && isSupport(c.oldPath, c.before, c.after) && !isProtected(c.path, policy, "tests")) {
|
|
9656
10223
|
out3.push(
|
|
9657
|
-
makeFinding(
|
|
10224
|
+
makeFinding(RULE15, policy, {
|
|
9658
10225
|
file: c.path,
|
|
9659
10226
|
message: `A test-support file was renamed out of the test globs (${c.oldPath} \u2192 ${c.path}).`,
|
|
9660
10227
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -9672,7 +10239,7 @@ var init_test_support = __esm({
|
|
|
9672
10239
|
const gone = [...new Set(beforeSig)].filter((l) => !afterSet.has(l) && !kept.includes(ws2(l)));
|
|
9673
10240
|
if (gone.length < MIN_REMOVED_LINES2) continue;
|
|
9674
10241
|
out3.push(
|
|
9675
|
-
makeFinding(
|
|
10242
|
+
makeFinding(RULE15, policy, {
|
|
9676
10243
|
file: c.path,
|
|
9677
10244
|
message: `Test-support content removed: ${gone.length} significant line(s) gone from a helper, setup module or fixture under the test globs.`,
|
|
9678
10245
|
evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9699,6 +10266,7 @@ var init_detectors = __esm({
|
|
|
9699
10266
|
init_lint_suppression();
|
|
9700
10267
|
init_test_skip();
|
|
9701
10268
|
init_coverage_lowering();
|
|
10269
|
+
init_config_weakening();
|
|
9702
10270
|
init_coverage_exclusion();
|
|
9703
10271
|
init_ci_tampering();
|
|
9704
10272
|
init_hook_tampering();
|
|
@@ -9715,6 +10283,7 @@ var init_detectors = __esm({
|
|
|
9715
10283
|
lintSuppression,
|
|
9716
10284
|
testSkip,
|
|
9717
10285
|
coverageLowering,
|
|
10286
|
+
configWeakening,
|
|
9718
10287
|
coverageExclusion,
|
|
9719
10288
|
ciTampering,
|
|
9720
10289
|
hookTampering,
|
|
@@ -11188,7 +11757,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
|
|
|
11188
11757
|
};
|
|
11189
11758
|
}
|
|
11190
11759
|
function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
11191
|
-
if (!isEnabled(
|
|
11760
|
+
if (!isEnabled(RULE16, policy)) return [];
|
|
11192
11761
|
const byPath = /* @__PURE__ */ new Map();
|
|
11193
11762
|
for (const e of events) {
|
|
11194
11763
|
if (!isProtected(e.path, policy)) continue;
|
|
@@ -11208,7 +11777,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
11208
11777
|
const modeChurn = modes.size > 1;
|
|
11209
11778
|
if (!contentChurn && !modeChurn) continue;
|
|
11210
11779
|
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(
|
|
11780
|
+
const f = makeFinding(RULE16, policy, {
|
|
11212
11781
|
file: path,
|
|
11213
11782
|
message: `Transient protected mutation: ${what}, leaving no net diff.`,
|
|
11214
11783
|
evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
|
|
@@ -11223,14 +11792,14 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
11223
11792
|
}
|
|
11224
11793
|
return out3;
|
|
11225
11794
|
}
|
|
11226
|
-
var
|
|
11795
|
+
var RULE16, MAX_EVENT_READ_BYTES, MAX_EVENT_SWEEP_BYTES, DEFAULT_EVENT_READ_IO;
|
|
11227
11796
|
var init_fs_events = __esm({
|
|
11228
11797
|
"src/detectors/fs-events.ts"() {
|
|
11229
11798
|
"use strict";
|
|
11230
11799
|
init_policy();
|
|
11231
11800
|
init_finding();
|
|
11232
11801
|
init_narrow();
|
|
11233
|
-
|
|
11802
|
+
RULE16 = "transient-protected-mutation";
|
|
11234
11803
|
MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
|
|
11235
11804
|
MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
|
|
11236
11805
|
DEFAULT_EVENT_READ_IO = {
|
|
@@ -12263,7 +12832,7 @@ var init_safe_write = __esm({
|
|
|
12263
12832
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
12264
12833
|
import { createHash as createHash6 } from "node:crypto";
|
|
12265
12834
|
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync4 } from "node:fs";
|
|
12266
|
-
import { basename as
|
|
12835
|
+
import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute8, join as join14, relative as relative3, resolve as resolve4 } from "node:path";
|
|
12267
12836
|
function gitConfig(cwd, key2) {
|
|
12268
12837
|
try {
|
|
12269
12838
|
return execFileSync6("git", ["config", "--get", key2], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -12322,13 +12891,13 @@ function readWorkflowMark(src) {
|
|
|
12322
12891
|
if (!m) return null;
|
|
12323
12892
|
return { version: m[1], hash: m[2], body: src.replace(WORKFLOW_MARK_RE, "") };
|
|
12324
12893
|
}
|
|
12325
|
-
function toolSet(
|
|
12326
|
-
const m = String(
|
|
12894
|
+
function toolSet(matcher2) {
|
|
12895
|
+
const m = String(matcher2 ?? "").trim();
|
|
12327
12896
|
if (m === "" || m === "*") return null;
|
|
12328
12897
|
return new Set(m.split(/[|,]/).map((t) => t.trim()).filter(Boolean));
|
|
12329
12898
|
}
|
|
12330
|
-
function missingTools(
|
|
12331
|
-
const have = toolSet(
|
|
12899
|
+
function missingTools(matcher2) {
|
|
12900
|
+
const have = toolSet(matcher2);
|
|
12332
12901
|
if (have === null) return [];
|
|
12333
12902
|
return PRE_MATCHER.split("|").filter((t) => !have.has(t));
|
|
12334
12903
|
}
|
|
@@ -12643,7 +13212,7 @@ function planPreCommit(cwd) {
|
|
|
12643
13212
|
}
|
|
12644
13213
|
const dir = hooksDir(cwd);
|
|
12645
13214
|
if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
|
|
12646
|
-
if (
|
|
13215
|
+
if (basename3(dir) === "_" && basename3(dirname4(dir)) === ".husky") {
|
|
12647
13216
|
path = join14(dirname4(dir), "pre-commit");
|
|
12648
13217
|
note = "husky";
|
|
12649
13218
|
} else {
|
|
@@ -13100,7 +13669,7 @@ import {
|
|
|
13100
13669
|
statSync as statSync3
|
|
13101
13670
|
} from "node:fs";
|
|
13102
13671
|
import {
|
|
13103
|
-
basename as
|
|
13672
|
+
basename as basename4,
|
|
13104
13673
|
delimiter,
|
|
13105
13674
|
dirname as dirname5,
|
|
13106
13675
|
isAbsolute as isAbsolute9,
|
|
@@ -13322,23 +13891,23 @@ function pythonName(name) {
|
|
|
13322
13891
|
return /^(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)$/.test(name.toLowerCase());
|
|
13323
13892
|
}
|
|
13324
13893
|
function looksPython(command, executable, token) {
|
|
13325
|
-
const exe = executable ?
|
|
13326
|
-
const raw = token ?
|
|
13894
|
+
const exe = executable ? basename4(executable) : "";
|
|
13895
|
+
const raw = token ? basename4(token) : "";
|
|
13327
13896
|
if (pythonName(exe) || pythonName(raw)) return true;
|
|
13328
13897
|
return /(?:^|[\s;&|()])(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)(?=$|[\s;&|()])/.test(command);
|
|
13329
13898
|
}
|
|
13330
13899
|
function inferredVenvFromToken(token, cwd) {
|
|
13331
13900
|
if (!token || !token.includes("/") && !token.includes("\\")) return null;
|
|
13332
13901
|
const path = isAbsolute9(token) ? token : resolve5(cwd, token);
|
|
13333
|
-
if (!pythonName(
|
|
13902
|
+
if (!pythonName(basename4(path))) return null;
|
|
13334
13903
|
const parent = dirname5(path);
|
|
13335
|
-
const parentName =
|
|
13904
|
+
const parentName = basename4(parent).toLowerCase();
|
|
13336
13905
|
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
13337
13906
|
const root = dirname5(parent);
|
|
13338
13907
|
return existsSync12(join15(root, "pyvenv.cfg")) ? root : null;
|
|
13339
13908
|
}
|
|
13340
13909
|
function unsupportedEcosystem(command, token) {
|
|
13341
|
-
const name = token ?
|
|
13910
|
+
const name = token ? basename4(token).toLowerCase() : "";
|
|
13342
13911
|
const raw = command.toLowerCase();
|
|
13343
13912
|
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
13913
|
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 +13917,7 @@ function unsupportedEcosystem(command, token) {
|
|
|
13348
13917
|
function inferredVenv(executable) {
|
|
13349
13918
|
if (!executable) return null;
|
|
13350
13919
|
const parent = dirname5(executable);
|
|
13351
|
-
const parentName =
|
|
13920
|
+
const parentName = basename4(parent).toLowerCase();
|
|
13352
13921
|
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
13353
13922
|
const root = dirname5(parent);
|
|
13354
13923
|
return existsSync12(join15(root, "pyvenv.cfg")) ? root : null;
|