tamperward 2.25.1 → 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 +2098 -1488
- 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
|
|
@@ -526,7 +532,22 @@ function defaultPolicy(version = 1) {
|
|
|
526
532
|
"**/codecov.yml",
|
|
527
533
|
"**/.codecov.yml"
|
|
528
534
|
],
|
|
529
|
-
|
|
535
|
+
// A workflow's checks can live in a composite action it `uses:` (the workflow
|
|
536
|
+
// keeps `uses: ./.github/actions/test` while the action's `run: npm test`
|
|
537
|
+
// becomes `echo ok`), and in the entry file of a CI system that is not GitHub
|
|
538
|
+
// Actions at all. Each is read by ci-tampering: the action's `runs.steps` get
|
|
539
|
+
// the workflow's removal/neutralisation pass, the other systems' entry files
|
|
540
|
+
// the generic check-line pass and no trigger logic (issue #437).
|
|
541
|
+
ci: [
|
|
542
|
+
".github/workflows/**",
|
|
543
|
+
".github/actions/**/action.y?(a)ml",
|
|
544
|
+
".gitlab-ci.yml",
|
|
545
|
+
".circleci/config.yml",
|
|
546
|
+
"Jenkinsfile",
|
|
547
|
+
"azure-pipelines.yml",
|
|
548
|
+
"bitbucket-pipelines.yml",
|
|
549
|
+
".travis.yml"
|
|
550
|
+
],
|
|
530
551
|
// Recorded expected outputs. An assertion stored as data is still an assertion;
|
|
531
552
|
// rewriting it from current output is the snapshot-update move the affordance
|
|
532
553
|
// experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
|
|
@@ -579,6 +600,11 @@ function defaultPolicy(version = 1) {
|
|
|
579
600
|
// harness/fp-study/CAST-GROWTH-CORPUS.md), so it is a review prompt, not a gate.
|
|
580
601
|
"ts-cast-growth": { severity: "warn" },
|
|
581
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" },
|
|
582
608
|
"coverage-lowering": { severity: "block" },
|
|
583
609
|
// the per-function form of coverage-lowering: an inline `istanbul ignore` /
|
|
584
610
|
// `c8 ignore` / `v8 ignore` / `node:coverage ignore` / `# pragma: no cover` /
|
|
@@ -4185,24 +4211,24 @@ function topLevelStaticStrings(ctx) {
|
|
|
4185
4211
|
}
|
|
4186
4212
|
return resolved;
|
|
4187
4213
|
}
|
|
4188
|
-
function staticPropertyName(expr, ctx,
|
|
4214
|
+
function staticPropertyName(expr, ctx, strings2) {
|
|
4189
4215
|
if (ts.isStringLiteralLike(expr)) return { name: expr.text, causes: [] };
|
|
4190
|
-
if (ts.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx,
|
|
4216
|
+
if (ts.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx, strings2);
|
|
4191
4217
|
if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
4192
|
-
const left = staticPropertyName(expr.left, ctx,
|
|
4193
|
-
const right = staticPropertyName(expr.right, ctx,
|
|
4218
|
+
const left = staticPropertyName(expr.left, ctx, strings2);
|
|
4219
|
+
const right = staticPropertyName(expr.right, ctx, strings2);
|
|
4194
4220
|
if (!left || !right) return null;
|
|
4195
4221
|
return { name: left.name + right.name, causes: [...left.causes, ...right.causes] };
|
|
4196
4222
|
}
|
|
4197
4223
|
if (ts.isIdentifier(expr)) {
|
|
4198
4224
|
const sym = symbolAt(ctx, expr);
|
|
4199
4225
|
if (!sym) return null;
|
|
4200
|
-
const value =
|
|
4226
|
+
const value = strings2.get(sym);
|
|
4201
4227
|
return value ? { name: value.value, causes: value.causes } : null;
|
|
4202
4228
|
}
|
|
4203
4229
|
return null;
|
|
4204
4230
|
}
|
|
4205
|
-
function runnerChain(expr, ctx, binder,
|
|
4231
|
+
function runnerChain(expr, ctx, binder, strings2) {
|
|
4206
4232
|
const props = [];
|
|
4207
4233
|
const propNodes = [];
|
|
4208
4234
|
const causes = [];
|
|
@@ -4217,7 +4243,7 @@ function runnerChain(expr, ctx, binder, strings) {
|
|
|
4217
4243
|
continue;
|
|
4218
4244
|
}
|
|
4219
4245
|
if (!cur.argumentExpression) return null;
|
|
4220
|
-
const property = staticPropertyName(cur.argumentExpression, ctx,
|
|
4246
|
+
const property = staticPropertyName(cur.argumentExpression, ctx, strings2);
|
|
4221
4247
|
if (!property) return null;
|
|
4222
4248
|
props.unshift(property.name);
|
|
4223
4249
|
propNodes.unshift(cur.argumentExpression);
|
|
@@ -4271,7 +4297,7 @@ function structuralNodeKey(node, sf) {
|
|
|
4271
4297
|
return `${node.kind}(${children.map((child) => structuralNodeKey(child, sf)).join(",")})`;
|
|
4272
4298
|
}
|
|
4273
4299
|
function semanticSkipHits(ctx, binder) {
|
|
4274
|
-
const
|
|
4300
|
+
const strings2 = topLevelStaticStrings(ctx);
|
|
4275
4301
|
const hits = [];
|
|
4276
4302
|
const unowned = /* @__PURE__ */ new Set();
|
|
4277
4303
|
const callOrdinals = /* @__PURE__ */ new Map();
|
|
@@ -4283,7 +4309,7 @@ function semanticSkipHits(ctx, binder) {
|
|
|
4283
4309
|
const ordinal = (callOrdinals.get(callIdentity) ?? 0) + 1;
|
|
4284
4310
|
callOrdinals.set(callIdentity, ordinal);
|
|
4285
4311
|
const callKey = `${callIdentity}\0${ordinal}`;
|
|
4286
|
-
const chain = runnerChain(node.expression, ctx, binder,
|
|
4312
|
+
const chain = runnerChain(node.expression, ctx, binder, strings2);
|
|
4287
4313
|
if (chain === "unknown") {
|
|
4288
4314
|
const callee = node.expression;
|
|
4289
4315
|
const first = lineOf(callee.getStart(ctx.sf));
|
|
@@ -4321,7 +4347,7 @@ function semanticSkipHits(ctx, binder) {
|
|
|
4321
4347
|
if (ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name)) {
|
|
4322
4348
|
name = prop.name.text;
|
|
4323
4349
|
} else if (ts.isComputedPropertyName(prop.name)) {
|
|
4324
|
-
const resolved = staticPropertyName(prop.name.expression, ctx,
|
|
4350
|
+
const resolved = staticPropertyName(prop.name.expression, ctx, strings2);
|
|
4325
4351
|
name = resolved?.name ?? null;
|
|
4326
4352
|
propertyCauses = resolved?.causes ?? [];
|
|
4327
4353
|
}
|
|
@@ -5584,1097 +5610,980 @@ var init_coverage_lowering = __esm({
|
|
|
5584
5610
|
}
|
|
5585
5611
|
});
|
|
5586
5612
|
|
|
5587
|
-
// src/detectors/
|
|
5588
|
-
function
|
|
5589
|
-
const
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
continue;
|
|
5597
|
-
}
|
|
5598
|
-
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
5599
|
-
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";
|
|
5600
5622
|
}
|
|
5601
|
-
|
|
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;
|
|
5602
5637
|
}
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
{ re: /\/\*\s*node:coverage\s+(?:ignore|disable)\b/, why: "`node:coverage ignore/disable` takes the following code out of the coverage measurement" }
|
|
5618
|
-
],
|
|
5619
|
-
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" }],
|
|
5620
|
-
rs: [{ re: /#\s*!?\[[^\]]*\bcoverage\s*\(\s*off\s*\)/, why: "`#[coverage(off)]` takes the item out of the coverage measurement" }],
|
|
5621
|
-
go: [
|
|
5622
|
-
{
|
|
5623
|
-
re: /^\s*\/\/\s*(?:go:build\b|\+build\b)/,
|
|
5624
|
-
why: "a build constraint added to an existing file takes it out of the default build, and out of `go test -cover`",
|
|
5625
|
-
existingOnly: true
|
|
5626
|
-
}
|
|
5627
|
-
]
|
|
5628
|
-
};
|
|
5629
|
-
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)\.[^/]*$/;
|
|
5630
|
-
LINE_COMMENT = { js: /^\/\//, go: /^\/\//, rs: /^\/\//, py: /^#/ };
|
|
5631
|
-
coverageExclusion = {
|
|
5632
|
-
id: RULE6,
|
|
5633
|
-
surface: ["file"],
|
|
5634
|
-
certainty: "mechanical",
|
|
5635
|
-
run(changes, policy) {
|
|
5636
|
-
const out3 = [];
|
|
5637
|
-
for (const c of changes) {
|
|
5638
|
-
if (c.kind !== "file" || c.op === "delete") continue;
|
|
5639
|
-
const lang = langOf(c.path);
|
|
5640
|
-
if (!lang) continue;
|
|
5641
|
-
const patterns = PATTERNS3[lang];
|
|
5642
|
-
if (!patterns) continue;
|
|
5643
|
-
const category = protectedCategory(c.path, policy);
|
|
5644
|
-
if (OUT_OF_SCOPE_PATH.test(c.path) || category === "tests" || category === "config") continue;
|
|
5645
|
-
const removed = new Set(removedLines(c).map((l) => l.content.trim()));
|
|
5646
|
-
const before = c.before ?? "";
|
|
5647
|
-
for (const l of addedLines(c)) {
|
|
5648
|
-
const trimmed = l.content.trim();
|
|
5649
|
-
if (removed.has(trimmed)) continue;
|
|
5650
|
-
for (const p of patterns) {
|
|
5651
|
-
const m = p.re.exec(l.content);
|
|
5652
|
-
if (!m || isText(l.content, m.index, lang)) continue;
|
|
5653
|
-
if (p.existingOnly && (c.op === "add" || c.before == null || before.split("\n").some((b) => p.re.test(b)))) continue;
|
|
5654
|
-
out3.push(
|
|
5655
|
-
makeFinding(RULE6, policy, {
|
|
5656
|
-
file: c.path,
|
|
5657
|
-
line: l.newLine ?? void 0,
|
|
5658
|
-
message: `Coverage exclusion added: ${p.why}.`,
|
|
5659
|
-
evidence: trimmed,
|
|
5660
|
-
remediation: "Cover the code with a test instead of excluding it from the measurement; if it is genuinely unreachable, say so in review.",
|
|
5661
|
-
defaultSeverity: "warn"
|
|
5662
|
-
})
|
|
5663
|
-
);
|
|
5664
|
-
break;
|
|
5665
|
-
}
|
|
5666
|
-
}
|
|
5667
|
-
}
|
|
5668
|
-
return out3;
|
|
5669
|
-
}
|
|
5670
|
-
};
|
|
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;
|
|
5671
5652
|
}
|
|
5672
|
-
}
|
|
5673
|
-
|
|
5674
|
-
// src/detectors/gh-expression.ts
|
|
5675
|
-
function foldExpressions(s) {
|
|
5676
|
-
return s.replace(/\$\{\{([^}]*)\}\}/g, (_m, e) => {
|
|
5677
|
-
const v = foldConst(e.trim());
|
|
5678
|
-
if (v !== void 0 && v !== TRUTHY) return String(v);
|
|
5679
|
-
return /\b(?:matrix|strategy|inputs)\./.test(e) ? MATRIX_TOKEN : EXPR_TOKEN;
|
|
5680
|
-
});
|
|
5653
|
+
return { items, opaque };
|
|
5681
5654
|
}
|
|
5682
|
-
function
|
|
5683
|
-
const
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
const
|
|
5713
|
-
if (
|
|
5714
|
-
|
|
5715
|
-
|
|
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()) {
|
|
5716
5712
|
continue;
|
|
5717
5713
|
}
|
|
5718
|
-
const
|
|
5719
|
-
if (
|
|
5720
|
-
|
|
5721
|
-
|
|
5714
|
+
const m = line.match(/^(\w[\w.-]*)\s*[=:]\s*(.*)$/);
|
|
5715
|
+
if (m) {
|
|
5716
|
+
key2 = m[1];
|
|
5717
|
+
out3[key2] = m[2].trim();
|
|
5722
5718
|
continue;
|
|
5723
5719
|
}
|
|
5724
|
-
|
|
5720
|
+
if (key2 && /^\s+\S/.test(line)) out3[key2] += " " + line.trim();
|
|
5725
5721
|
}
|
|
5726
5722
|
return out3;
|
|
5727
5723
|
}
|
|
5728
|
-
function
|
|
5729
|
-
const
|
|
5730
|
-
if (
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
const
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
}
|
|
5761
|
-
|
|
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);
|
|
5762
5781
|
};
|
|
5763
|
-
const
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
const t = peek();
|
|
5767
|
-
if (!t || t.t !== "op" || !/^(?:==|!=|<|>|<=|>=)$/.test(t.v)) return l;
|
|
5768
|
-
p++;
|
|
5769
|
-
const r = unary();
|
|
5770
|
-
if (l === void 0 || r === void 0) {
|
|
5771
|
-
l = void 0;
|
|
5772
|
-
continue;
|
|
5773
|
-
}
|
|
5774
|
-
if (t.v === "==" || t.v === "!=") {
|
|
5775
|
-
const e = eq(l, r);
|
|
5776
|
-
l = e === void 0 ? void 0 : t.v === "==" ? e : !e;
|
|
5777
|
-
} else {
|
|
5778
|
-
const x = num(l);
|
|
5779
|
-
const y2 = num(r);
|
|
5780
|
-
if (Number.isNaN(x) || Number.isNaN(y2)) l = void 0;
|
|
5781
|
-
else l = t.v === "<" ? x < y2 : t.v === ">" ? x > y2 : t.v === "<=" ? x <= y2 : x >= y2;
|
|
5782
|
-
}
|
|
5783
|
-
}
|
|
5782
|
+
const ignores = (key2, e) => {
|
|
5783
|
+
const entries = take(key2, e).map((pattern) => ({ pattern, key: key2 }));
|
|
5784
|
+
sel.ignore = [...sel.ignore ?? [], ...entries];
|
|
5784
5785
|
};
|
|
5785
|
-
const
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
return v2 === void 0 ? void 0 : !truthy(v2);
|
|
5789
|
-
}
|
|
5790
|
-
return primary();
|
|
5786
|
+
const namePattern = (e) => {
|
|
5787
|
+
sel.present = true;
|
|
5788
|
+
sel.wide.push({ pattern: "\0testNamePattern " + (isStr(e) ? e.text : e.getText()), key: "testNamePattern" });
|
|
5791
5789
|
};
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
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;
|
|
5798
5799
|
}
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
if (
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
if (
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
if (
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
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);
|
|
5827
5832
|
}
|
|
5828
|
-
return void 0;
|
|
5829
5833
|
}
|
|
5830
|
-
|
|
5834
|
+
ts.forEachChild(node, visit);
|
|
5831
5835
|
};
|
|
5832
|
-
|
|
5833
|
-
|
|
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;
|
|
5834
5840
|
}
|
|
5835
|
-
function
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
vals.push(a);
|
|
5840
|
-
}
|
|
5841
|
-
const s = (i) => vals[i] == null ? "" : String(vals[i]);
|
|
5842
|
-
switch (name.toLowerCase()) {
|
|
5843
|
-
case "contains":
|
|
5844
|
-
return vals.length === 2 ? s(0).toLowerCase().includes(s(1).toLowerCase()) : void 0;
|
|
5845
|
-
case "startswith":
|
|
5846
|
-
return vals.length === 2 ? s(0).toLowerCase().startsWith(s(1).toLowerCase()) : void 0;
|
|
5847
|
-
case "endswith":
|
|
5848
|
-
return vals.length === 2 ? s(0).toLowerCase().endsWith(s(1).toLowerCase()) : void 0;
|
|
5849
|
-
case "format": {
|
|
5850
|
-
if (vals.length < 1) return void 0;
|
|
5851
|
-
return s(0).replace(/\{\{|\}\}|\{(\d+)\}/g, (m, i) => m === "{{" ? "{" : m === "}}" ? "}" : s(Number(i) + 1));
|
|
5852
|
-
}
|
|
5853
|
-
case "join":
|
|
5854
|
-
return vals.length >= 1 && vals.length <= 2 ? s(0) : void 0;
|
|
5855
|
-
case "tojson":
|
|
5856
|
-
return vals.length === 1 ? JSON.stringify(vals[0]) : void 0;
|
|
5857
|
-
case "fromjson": {
|
|
5858
|
-
if (vals.length !== 1) return void 0;
|
|
5859
|
-
try {
|
|
5860
|
-
const v = JSON.parse(s(0));
|
|
5861
|
-
if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
|
|
5862
|
-
return TRUTHY;
|
|
5863
|
-
} catch {
|
|
5864
|
-
return void 0;
|
|
5865
|
-
}
|
|
5866
|
-
}
|
|
5867
|
-
default:
|
|
5868
|
-
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;
|
|
5869
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;
|
|
5870
5848
|
}
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
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";
|
|
5879
5858
|
}
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
// src/detectors/invocation.ts
|
|
5883
|
-
function canonical(core) {
|
|
5884
|
-
let s = foldExpressions(core.replace(/^(['"])(.*)\1$/, "$2").trim()).replace(MATRIX_FLAG, " ").replace(/\s+/g, " ").trim();
|
|
5885
|
-
s = s.replace(/^(?:cd|pushd)\s+\S+\s*&&\s*/, "");
|
|
5886
|
-
s = s.replace(/^(?:npx|pnpm\s+(?:exec|dlx)|yarn\s+(?:exec|dlx)|bunx|bun\s+x)\s+/, "exec ");
|
|
5887
|
-
s = s.replace(/^(?:npm|pnpm|yarn|bun)\s+(?:run(?:-script)?\s+)?/, "exec ");
|
|
5888
|
-
s = s.replace(/^exec\s+(?:t|tst)(?=\s|$)/, "exec test");
|
|
5889
|
-
s = s.replace(/^exec\s+/, "");
|
|
5890
|
-
return s.replace(/\s+--\s*$/, "").trim();
|
|
5859
|
+
return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
|
|
5891
5860
|
}
|
|
5892
|
-
function
|
|
5893
|
-
|
|
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;
|
|
5894
5869
|
}
|
|
5895
|
-
function
|
|
5896
|
-
const
|
|
5897
|
-
const
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
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;
|
|
5907
5884
|
}
|
|
5908
|
-
function
|
|
5909
|
-
|
|
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;
|
|
5910
5897
|
}
|
|
5911
|
-
function
|
|
5912
|
-
const
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
if (
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
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;
|
|
5920
5909
|
}
|
|
5921
5910
|
return null;
|
|
5922
5911
|
}
|
|
5923
|
-
function
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
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 {
|
|
5935
5927
|
}
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
const
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
else return { state: "kept" };
|
|
5947
|
-
} else if (r.startsWith(a + " ") && invokesCheck(a)) {
|
|
5948
|
-
return { state: "kept" };
|
|
5949
|
-
} else {
|
|
5950
|
-
const w = a.match(TIMEOUT_WRAP);
|
|
5951
|
-
if (w && (canonical(w[1]) === r || canonical(w[1]).startsWith(r + " "))) neutralised ??= raw;
|
|
5952
|
-
}
|
|
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
|
+
};
|
|
5953
5938
|
}
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
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 ?? [])})`;
|
|
5966
5967
|
}
|
|
5967
|
-
|
|
5968
|
-
if (a.includes(" ") && (NEUTRALISING_SUFFIX.test(" " + args) || narrowed(args) || runnerNarrowed(a, args))) neutralised ??= raw;
|
|
5969
|
-
else respelled = true;
|
|
5970
|
-
}
|
|
5971
|
-
if (respelled) return { state: "kept" };
|
|
5972
|
-
return neutralised ? { state: "neutralised", by: neutralised } : { state: "gone" };
|
|
5968
|
+
};
|
|
5973
5969
|
}
|
|
5974
|
-
function
|
|
5975
|
-
const
|
|
5976
|
-
if (
|
|
5977
|
-
const
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
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) });
|
|
5981
5981
|
}
|
|
5982
|
-
return
|
|
5983
|
-
}
|
|
5984
|
-
function scriptKind3(inner) {
|
|
5985
|
-
const kind = checkKind(inner);
|
|
5986
|
-
if (!kind) return null;
|
|
5987
|
-
const head = canonical(inner).match(/^(tests?|lint|type-?check|coverage)(?=[\s:]|$)/i)?.[1];
|
|
5988
|
-
if (!head) return kind;
|
|
5989
|
-
return /^type/i.test(head) ? "types" : /^lint/i.test(head) ? "lint" : "test";
|
|
5982
|
+
return out3;
|
|
5990
5983
|
}
|
|
5991
|
-
function
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
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;
|
|
5996
5992
|
}
|
|
5997
|
-
function
|
|
5993
|
+
function delegationsIn(src, runner) {
|
|
5998
5994
|
const out3 = [];
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
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
|
+
}
|
|
6011
6025
|
}
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
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
|
+
}
|
|
6015
6061
|
}
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
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);
|
|
6030
6081
|
}
|
|
6031
|
-
|
|
6032
|
-
|
|
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}`)];
|
|
6033
6086
|
}
|
|
6034
|
-
function
|
|
6087
|
+
function setupEntriesIn(src, runner, path) {
|
|
6035
6088
|
const out3 = [];
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
const following = stmts[i + 1]?.seg ?? "";
|
|
6048
|
-
if (op === "||" || op === "|" || op === "&" || op === ";" && !invokesCheck(unwrap(following))) {
|
|
6049
|
-
out3.push({ key: `op:${op}`, what: `${op} ${following.split(/\s+/).slice(0, 2).join(" ")}`.trim(), direction: "added" });
|
|
6050
|
-
}
|
|
6051
|
-
for (const re of MASKING_FLAGS) {
|
|
6052
|
-
const f = spelled(re, args);
|
|
6053
|
-
if (f) out3.push({ key: `flag:${f}`, what: f, direction: "added" });
|
|
6054
|
-
}
|
|
6055
|
-
for (const re of GATE_FLAGS) {
|
|
6056
|
-
const f = spelled(re, " " + seg);
|
|
6057
|
-
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;
|
|
6058
6100
|
}
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
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
|
+
}
|
|
6064
6120
|
}
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
if (opts.fresh) continue;
|
|
6069
|
-
const cfg = args.match(CONFIG_FLAG);
|
|
6070
|
-
if (cfg && !opts.configReviewed?.(cfg[3])) out3.push({ key: `config:${cfg[3]}`, what: `${cfg[1]} ${cfg[3]}`, direction: "added" });
|
|
6071
|
-
const pos = positionalOf(args);
|
|
6072
|
-
if (pos) out3.push({ key: "positional", what: `a path positional (${pos})`, direction: "added" });
|
|
6073
|
-
}
|
|
6074
|
-
return out3;
|
|
6075
|
-
}
|
|
6076
|
-
function checkStatements(cmd) {
|
|
6077
|
-
const out3 = [];
|
|
6078
|
-
for (const seg of segments(cmd)) {
|
|
6079
|
-
const w = seg.match(TIMEOUT_WRAP);
|
|
6080
|
-
const inner = unwrap(w ? w[1] : seg);
|
|
6081
|
-
const kind = scriptKind3(inner);
|
|
6082
|
-
if (kind) out3.push({ kind, canon: canonical(inner) });
|
|
6083
|
-
}
|
|
6121
|
+
ts.forEachChild(node, visit);
|
|
6122
|
+
};
|
|
6123
|
+
visit(sf);
|
|
6084
6124
|
return out3;
|
|
6085
6125
|
}
|
|
6086
|
-
function
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
return last !== void 0 && FAILS_LOUDLY.test(last.trim());
|
|
6092
|
-
}
|
|
6093
|
-
function invocationWeakening(before, after, runner, opts = {}) {
|
|
6094
|
-
const was = checkKinds(before);
|
|
6095
|
-
const now = checkKinds(after);
|
|
6096
|
-
if (now.size === 0 && failsLoudly(after)) return { state: "kept" };
|
|
6097
|
-
for (const k of was) {
|
|
6098
|
-
if (now.has(k) || opts.relocated?.(k)) continue;
|
|
6099
|
-
const instead = [...now].filter((x) => !was.has(x));
|
|
6100
|
-
return { state: "removed", kind: k, what: instead.length ? `runs a ${instead.join("/")} check instead` : now.size ? "the check is gone" : "no check is run" };
|
|
6101
|
-
}
|
|
6102
|
-
const survived = new Set(checkStatements(before).map((s) => s.canon));
|
|
6103
|
-
const extended = new Set(checkStatements(after).filter((s) => survived.has(s.canon)).map((s) => s.kind));
|
|
6104
|
-
const read = { ...opts, fresh: was.size === 0, extended };
|
|
6105
|
-
const beforeDs = descriptors(before, runner, read);
|
|
6106
|
-
const afterDs = descriptors(after, runner, read);
|
|
6107
|
-
const seenBefore = new Set(beforeDs.map((d) => d.key));
|
|
6108
|
-
const added = afterDs.find((d) => d.direction === "added" && !seenBefore.has(d.key));
|
|
6109
|
-
if (added) return { state: "neutralised", what: added.what, direction: "added" };
|
|
6110
|
-
if (coverageFloorLowered(before, after)) return { state: "neutralised", what: "a lowered --cov-fail-under", direction: "added" };
|
|
6111
|
-
const seenAfter = new Set(afterDs.map((d) => d.key));
|
|
6112
|
-
const dropped = beforeDs.find((d) => d.direction === "removed" && !seenAfter.has(d.key));
|
|
6113
|
-
if (dropped) return { state: "neutralised", what: dropped.what, direction: "removed" };
|
|
6114
|
-
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}`));
|
|
6115
6131
|
}
|
|
6116
|
-
var
|
|
6117
|
-
var
|
|
6118
|
-
"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"() {
|
|
6119
6135
|
"use strict";
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
{
|
|
6134
|
-
// a package-manager script: `test --prefix dir`, `-w pkg`, `--workspace=pkg`,
|
|
6135
|
-
// `--if-present` (no script, no failure), pnpm's `--filter` / `-F`, npm's `-C`
|
|
6136
|
-
runner: /^(?:test|tests|coverage)(?::\S+)?(?=\s|$)/,
|
|
6137
|
-
flags: /\s--if-present(?=\s|$)|\s--prefix(?:=|\s)|\s-C(?:=|\s)|\s-w(?:=|\s)|\s--workspace(?:=|\s)|\s--filter(?:=|\s)|\s-F(?:=|\s)/
|
|
6138
|
-
},
|
|
6139
|
-
{
|
|
6140
|
-
runner: /^(?:jest|vitest)\b/,
|
|
6141
|
-
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)/
|
|
6142
|
-
},
|
|
6143
|
-
{ runner: /^playwright\b/, flags: /\s--shard(?:=|\s)|\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--grep-invert(?:=|\s)/ },
|
|
6144
|
-
{ runner: /^mocha\b/, flags: /\s--grep(?:=|\s)|\s-g(?:=|\s)|\s--fgrep(?:=|\s)|\s-f(?:=|\s)/ },
|
|
6145
|
-
{ runner: /^ava\b/, flags: /\s--match(?:=|\s)|\s-m(?:=|\s)/ },
|
|
6146
|
-
{ runner: /^node --test\b/, flags: /\s--test-name-pattern(?:=|\s)|\s--test-skip-pattern(?:=|\s)|\s--test-only(?=\s|$)/ },
|
|
6147
|
-
{
|
|
6148
|
-
// `-p no:<plugin>` disables collection when the plugin is the collector
|
|
6149
|
-
// (`python`, `doctest`, `cov`); the cache, warning and output plugins select nothing
|
|
6150
|
-
runner: /^pytest\b/,
|
|
6151
|
-
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|$)/
|
|
6152
|
-
},
|
|
6153
|
-
{ runner: /^cargo test\b/, flags: /\s--\s(?:\S+\s+)*--skip(?:=|\s)|\s--exclude(?:=|\s)/ },
|
|
6154
|
-
{ 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.*"
|
|
6155
6149
|
];
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
"
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
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 + "/**"] : [];
|
|
6169
6173
|
};
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
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));
|
|
6182
6187
|
};
|
|
6183
|
-
|
|
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("@");
|
|
6184
6211
|
}
|
|
6185
6212
|
});
|
|
6186
6213
|
|
|
6187
|
-
// src/detectors/
|
|
6188
|
-
function
|
|
6189
|
-
|
|
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
|
+
}
|
|
6190
6224
|
}
|
|
6191
|
-
function
|
|
6192
|
-
|
|
6225
|
+
function parseYaml(text) {
|
|
6226
|
+
try {
|
|
6227
|
+
const v = yaml.parse(text);
|
|
6228
|
+
return v;
|
|
6229
|
+
} catch {
|
|
6230
|
+
return null;
|
|
6231
|
+
}
|
|
6193
6232
|
}
|
|
6194
|
-
function
|
|
6195
|
-
|
|
6196
|
-
const m = v.match(/^\$\{\{\s*(.+?)\s*\}\}$/);
|
|
6197
|
-
return m ? m[1] : v;
|
|
6233
|
+
function corePattern(p) {
|
|
6234
|
+
return p.trim().replace(/^!/, "").replace(/^(?:\.\/|\/)+/, "").replace(/^(?:\*\*\/)+/, "").replace(/(?:\/\*\*?)+\/?$/, "").replace(/\/+$/, "");
|
|
6198
6235
|
}
|
|
6199
|
-
function
|
|
6200
|
-
const
|
|
6201
|
-
if (
|
|
6202
|
-
const
|
|
6203
|
-
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);
|
|
6204
6241
|
}
|
|
6205
|
-
function
|
|
6206
|
-
const
|
|
6207
|
-
if (
|
|
6208
|
-
const
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
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}/**`]);
|
|
6212
6248
|
}
|
|
6213
|
-
function
|
|
6214
|
-
const
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
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;
|
|
6222
6261
|
}
|
|
6223
|
-
function
|
|
6224
|
-
const
|
|
6225
|
-
|
|
6226
|
-
while (h >= 0 && (lines[h].trim() === "" || indentOf(lines[h]) >= ind)) h--;
|
|
6227
|
-
if (h < 0 || !/^\s+[\w-]+:\s*(?:#.*)?$/.test(lines[h])) return null;
|
|
6228
|
-
let t = h - 1;
|
|
6229
|
-
while (t >= 0 && (lines[t].trim() === "" || indentOf(lines[t]) > 0)) t--;
|
|
6230
|
-
if (t < 0 || !/^jobs:/.test(lines[t])) return null;
|
|
6231
|
-
const hi = indentOf(lines[h]);
|
|
6232
|
-
let e = i + 1;
|
|
6233
|
-
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > hi)) e++;
|
|
6234
|
-
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)));
|
|
6235
6265
|
}
|
|
6236
|
-
function
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
}
|
|
6245
|
-
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
|
+
};
|
|
6246
6275
|
}
|
|
6247
|
-
function
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
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
|
+
);
|
|
6264
6302
|
}
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
function parseTriggers(src) {
|
|
6273
|
-
const t = {
|
|
6274
|
-
events: /* @__PURE__ */ new Set(),
|
|
6275
|
-
branches: /* @__PURE__ */ new Map(),
|
|
6276
|
-
branchesIgnore: /* @__PURE__ */ new Map(),
|
|
6277
|
-
pathsIgnore: /* @__PURE__ */ new Map(),
|
|
6278
|
-
paths: /* @__PURE__ */ new Map(),
|
|
6279
|
-
types: /* @__PURE__ */ new Map(),
|
|
6280
|
-
tags: /* @__PURE__ */ new Map(),
|
|
6281
|
-
present: false
|
|
6282
|
-
};
|
|
6283
|
-
const lines = src.split("\n");
|
|
6284
|
-
const i = lines.findIndex((l) => /^(?:on|"on"|'on'|true):/.test(l));
|
|
6285
|
-
if (i < 0) return t;
|
|
6286
|
-
t.present = true;
|
|
6287
|
-
const head = lines[i].replace(/^(?:on|"on"|'on'|true):\s*/, "");
|
|
6288
|
-
const inline = uncommented(head);
|
|
6289
|
-
if (inline) {
|
|
6290
|
-
const list3 = flowList(inline);
|
|
6291
|
-
for (const e of list3 ?? [inline]) t.events.add(e);
|
|
6292
|
-
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
|
+
);
|
|
6293
6310
|
}
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
const item = body.match(/^\s*-\s+(.+)$/);
|
|
6304
|
-
if (item && listKey && event && ind > listInd) {
|
|
6305
|
-
const target = t[listKey].get(event) ?? [];
|
|
6306
|
-
target.push(item[1].replace(/^(['"])(.*)\1$/, "$2"));
|
|
6307
|
-
t[listKey].set(event, target);
|
|
6308
|
-
continue;
|
|
6309
|
-
}
|
|
6310
|
-
const kv = body.match(/^\s*([\w-]+):\s*(.*)$/);
|
|
6311
|
-
if (!kv) continue;
|
|
6312
|
-
if (ind === 2 || event === null && ind < 4) {
|
|
6313
|
-
event = kv[1];
|
|
6314
|
-
t.events.add(event);
|
|
6315
|
-
listKey = null;
|
|
6316
|
-
continue;
|
|
6317
|
-
}
|
|
6318
|
-
const key2 = LIST_KEYS[kv[1]];
|
|
6319
|
-
if (event && key2) {
|
|
6320
|
-
listKey = key2;
|
|
6321
|
-
listInd = ind;
|
|
6322
|
-
const list3 = flowList(kv[2]);
|
|
6323
|
-
t[listKey].set(event, list3 ?? []);
|
|
6324
|
-
} 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
|
+
);
|
|
6325
6320
|
}
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
return (p) => r.test(p);
|
|
6333
|
-
} catch {
|
|
6334
|
-
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
|
+
);
|
|
6335
6327
|
}
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
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
|
+
);
|
|
6342
6337
|
}
|
|
6343
|
-
return on;
|
|
6344
6338
|
}
|
|
6345
|
-
function
|
|
6346
|
-
const
|
|
6347
|
-
if (
|
|
6348
|
-
|
|
6349
|
-
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;
|
|
6350
6344
|
}
|
|
6351
|
-
function
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
const candidates = opts.defaultBranch ? [opts.defaultBranch] : ["main", "master"];
|
|
6356
|
-
const namesDefault = (l) => candidates.some((b) => admits(l, b));
|
|
6357
|
-
for (const e of CODE_EVENTS) {
|
|
6358
|
-
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;
|
|
6359
6349
|
}
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
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);
|
|
6365
6356
|
}
|
|
6366
|
-
|
|
6367
|
-
if (had.length && ignoresEverySource(had, opts.sources)) continue;
|
|
6368
|
-
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`);
|
|
6369
|
-
}
|
|
6370
|
-
for (const [e, list3] of after.paths) {
|
|
6371
|
-
if (!CODE_EVENTS.includes(e) || !after.events.has(e)) continue;
|
|
6372
|
-
const had = before.paths.get(e);
|
|
6373
|
-
if (had && !pathsMatchSource(had, opts.sources)) continue;
|
|
6374
|
-
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;
|
|
6375
6358
|
}
|
|
6376
|
-
for (const [
|
|
6377
|
-
if (
|
|
6378
|
-
|
|
6379
|
-
if (had && !had.some((t) => PR_CODE_TYPES.test(t))) continue;
|
|
6380
|
-
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);
|
|
6381
6362
|
}
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6363
|
+
}
|
|
6364
|
+
function eslintAstFacts(text, f) {
|
|
6365
|
+
let sf = null;
|
|
6366
|
+
try {
|
|
6367
|
+
sf = parseSource("eslint.config.ts", text);
|
|
6368
|
+
} catch {
|
|
6369
|
+
return;
|
|
6385
6370
|
}
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
if (
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
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]));
|
|
6395
6396
|
}
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
const replaced = !namesDefault(was) && !namesDefault(now) && was.length > 0 && !was.some((b) => now.includes(b));
|
|
6400
|
-
if (!lostDefault && !replaced) continue;
|
|
6401
|
-
const dropped = was.filter((b) => !now.includes(b) && !ALL(b));
|
|
6402
|
-
out3.push(
|
|
6403
|
-
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`
|
|
6404
|
-
);
|
|
6405
|
-
}
|
|
6406
|
-
return out3;
|
|
6407
|
-
}
|
|
6408
|
-
function literalValue(raw) {
|
|
6409
|
-
const v = foldExpressions(uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim());
|
|
6410
|
-
return v.includes(EXPR_TOKEN) || v.includes(MATRIX_TOKEN) ? null : v;
|
|
6397
|
+
ts.forEachChild(node, visit);
|
|
6398
|
+
};
|
|
6399
|
+
visit(sf);
|
|
6411
6400
|
}
|
|
6412
|
-
function
|
|
6413
|
-
const
|
|
6414
|
-
if (
|
|
6415
|
-
const
|
|
6416
|
-
if (
|
|
6417
|
-
|
|
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;
|
|
6418
6451
|
}
|
|
6419
|
-
function
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
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;
|
|
6424
6456
|
}
|
|
6425
|
-
function
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
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
|
+
);
|
|
6433
6483
|
}
|
|
6434
|
-
return null;
|
|
6435
|
-
}
|
|
6436
|
-
function ancestorOf(lines, i) {
|
|
6437
|
-
const ind = indentOf(lines[i]);
|
|
6438
|
-
let h = i - 1;
|
|
6439
|
-
while (h >= 0 && (lines[h].trim() === "" || /^\s*#/.test(lines[h]) || indentOf(lines[h]) >= ind)) h--;
|
|
6440
|
-
return h >= 0 ? h : null;
|
|
6441
6484
|
}
|
|
6442
|
-
var
|
|
6443
|
-
var
|
|
6444
|
-
"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"() {
|
|
6445
6488
|
"use strict";
|
|
6446
|
-
|
|
6489
|
+
init_lazy_deps();
|
|
6490
|
+
init_ts_lazy();
|
|
6447
6491
|
init_policy();
|
|
6492
|
+
init_narrow();
|
|
6448
6493
|
init_finding();
|
|
6449
|
-
init_files();
|
|
6450
|
-
init_hook_wiring();
|
|
6451
6494
|
init_repo();
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
{ re: /^\s*if\s+false\s*;?\s*then\b/, what: "if false; then \u2014 the check is never run" },
|
|
6472
|
-
{ re: /^\s*:\s*<<\s*['"]?\w+/, what: "a heredoc fed to `:` \u2014 the check is commented out" }
|
|
6473
|
-
];
|
|
6474
|
-
LIST_KEYS = {
|
|
6475
|
-
branches: "branches",
|
|
6476
|
-
"branches-ignore": "branchesIgnore",
|
|
6477
|
-
"paths-ignore": "pathsIgnore",
|
|
6478
|
-
paths: "paths",
|
|
6479
|
-
types: "types",
|
|
6480
|
-
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));
|
|
6481
6514
|
};
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
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,
|
|
6496
6547
|
surface: ["file"],
|
|
6497
6548
|
certainty: "mechanical",
|
|
6498
6549
|
run(changes, policy, _view, ctx) {
|
|
6499
6550
|
const out3 = [];
|
|
6500
|
-
const
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
);
|
|
6514
|
-
continue;
|
|
6515
|
-
}
|
|
6516
|
-
if (!isProtected(c.path, policy, "ci")) continue;
|
|
6517
|
-
const afterLines = c.after != null ? c.after.split("\n") : null;
|
|
6518
|
-
const addedAt = new Set(addedLines(c).map((l) => l.newLine).filter((n) => n != null));
|
|
6519
|
-
for (const l of addedLines(c)) {
|
|
6520
|
-
const coe = l.content.match(/^\s*-?\s*continue-on-error:\s*(.+?)\s*$/);
|
|
6521
|
-
const cond = l.content.match(/^\s*-?\s*if:\s*(.+?)\s*$/);
|
|
6522
|
-
if (!coe && !cond) continue;
|
|
6523
|
-
const step = afterLines && l.newLine != null ? stepOf(afterLines, l.newLine - 1) : null;
|
|
6524
|
-
if (afterLines && step && !stepHasCheck(afterLines, step)) continue;
|
|
6525
|
-
if (afterLines && !step && l.newLine != null) {
|
|
6526
|
-
const job = jobOf(afterLines, l.newLine - 1);
|
|
6527
|
-
if (job && !jobHasCheck(afterLines, job)) continue;
|
|
6528
|
-
}
|
|
6529
|
-
if (coe && isTruthy(coe[1])) {
|
|
6530
|
-
out3.push(
|
|
6531
|
-
makeFinding(RULE7, policy, {
|
|
6532
|
-
file: c.path,
|
|
6533
|
-
line: l.newLine ?? void 0,
|
|
6534
|
-
message: "continue-on-error: true added \u2014 failures will no longer fail the job.",
|
|
6535
|
-
evidence: l.content.trim(),
|
|
6536
|
-
remediation: "Remove it. A check that cannot fail is not a check."
|
|
6537
|
-
})
|
|
6538
|
-
);
|
|
6539
|
-
} else if (cond && isAlwaysFalse(cond[1])) {
|
|
6540
|
-
out3.push(
|
|
6541
|
-
makeFinding(RULE7, policy, {
|
|
6542
|
-
file: c.path,
|
|
6543
|
-
line: l.newLine ?? void 0,
|
|
6544
|
-
message: `if: ${uncommented(cond[1])} added \u2014 this step can never run.`,
|
|
6545
|
-
evidence: l.content.trim(),
|
|
6546
|
-
remediation: "Re-enable the step instead of conditioning it off."
|
|
6547
|
-
})
|
|
6548
|
-
);
|
|
6549
|
-
}
|
|
6550
|
-
}
|
|
6551
|
-
if (afterLines) {
|
|
6552
|
-
for (const b of runBlocks(afterLines)) {
|
|
6553
|
-
if (!b.body.some(invokesCheck2)) continue;
|
|
6554
|
-
const propagates = b.body.some((l) => /\bexit\s+"?\$/.test(l));
|
|
6555
|
-
for (let k = 0; k < b.body.length; k++) {
|
|
6556
|
-
const idx = b.start + 1 + k;
|
|
6557
|
-
if (!addedAt.has(idx + 1)) continue;
|
|
6558
|
-
const n = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
|
|
6559
|
-
if (!n || propagates && /set\s\+e/.test(b.body[k])) continue;
|
|
6560
|
-
out3.push(
|
|
6561
|
-
makeFinding(RULE7, policy, {
|
|
6562
|
-
file: c.path,
|
|
6563
|
-
line: idx + 1,
|
|
6564
|
-
message: `A check's run block was neutralised: ${n.what}.`,
|
|
6565
|
-
evidence: b.body[k].trim(),
|
|
6566
|
-
remediation: "Let the check decide the step. Masking its exit status is the same tamper as removing it."
|
|
6567
|
-
})
|
|
6568
|
-
);
|
|
6569
|
-
}
|
|
6570
|
-
}
|
|
6571
|
-
for (const l of addedLines(c)) {
|
|
6572
|
-
if (!/^\s*-?\s*shell:\s*.*\{0\}/.test(l.content) || l.newLine == null) continue;
|
|
6573
|
-
if (SHELL_ERREXIT.test(uncommented(l.content.replace(/^\s*-?\s*shell:\s*/, "")))) continue;
|
|
6574
|
-
const step = stepOf(afterLines, l.newLine - 1);
|
|
6575
|
-
if (step && !stepHasCheck(afterLines, step)) continue;
|
|
6576
|
-
out3.push(
|
|
6577
|
-
makeFinding(RULE7, policy, {
|
|
6578
|
-
file: c.path,
|
|
6579
|
-
line: l.newLine,
|
|
6580
|
-
message: "shell: \u2026 {0} added on a check step \u2014 the shell no longer stops on the first failure.",
|
|
6581
|
-
evidence: l.content.trim(),
|
|
6582
|
-
remediation: "Keep the default shell (fail-fast) on steps that run checks."
|
|
6583
|
-
})
|
|
6584
|
-
);
|
|
6585
|
-
}
|
|
6586
|
-
for (const l of addedLines(c)) {
|
|
6587
|
-
if (l.newLine == null) continue;
|
|
6588
|
-
const i = l.newLine - 1;
|
|
6589
|
-
const wd = l.content.match(WORKDIR_LINE);
|
|
6590
|
-
if (wd) {
|
|
6591
|
-
const dir = literalValue(wd[1]);
|
|
6592
|
-
if (dir === null || !dirRunsNothing(dir, ctx)) continue;
|
|
6593
|
-
const step2 = stepOf(afterLines, i);
|
|
6594
|
-
if (step2) {
|
|
6595
|
-
if (!stepHasCheck(afterLines, step2)) continue;
|
|
6596
|
-
} else {
|
|
6597
|
-
const d = defaultsOf(afterLines, i);
|
|
6598
|
-
if (d === null) continue;
|
|
6599
|
-
const job = jobOf(afterLines, d);
|
|
6600
|
-
if (job ? !jobHasCheck(afterLines, job) : !afterLines.some((x) => invokesCheck2(x) || usesCheck(x))) continue;
|
|
6601
|
-
}
|
|
6602
|
-
out3.push(
|
|
6603
|
-
makeFinding(RULE7, policy, {
|
|
6604
|
-
file: c.path,
|
|
6605
|
-
line: l.newLine,
|
|
6606
|
-
message: `A check was pointed at ${dir} by working-directory \u2014 a directory holding no code or package the check could run.`,
|
|
6607
|
-
evidence: l.content.trim(),
|
|
6608
|
-
remediation: "Run the check where the code is. A suite pointed at an empty directory passes on nothing."
|
|
6609
|
-
})
|
|
6610
|
-
);
|
|
6611
|
-
continue;
|
|
6612
|
-
}
|
|
6613
|
-
const ref = l.content.match(REF_LINE);
|
|
6614
|
-
if (!ref) continue;
|
|
6615
|
-
const step = enclosingStep(afterLines, i);
|
|
6616
|
-
if (!step || !afterLines.slice(step[0], step[1]).some((x) => CHECKOUT.test(x))) continue;
|
|
6617
|
-
const target = literalValue(ref[1]);
|
|
6618
|
-
if (target === null) continue;
|
|
6619
|
-
out3.push(
|
|
6620
|
-
makeFinding(RULE7, policy, {
|
|
6621
|
-
file: c.path,
|
|
6622
|
-
line: l.newLine,
|
|
6623
|
-
message: `actions/checkout was pinned to ref: ${target} \u2014 the checks run against that ref, not against the change under review.`,
|
|
6624
|
-
evidence: l.content.trim(),
|
|
6625
|
-
remediation: "Let checkout take the event's ref (its default). A suite that checks out main proves nothing about the pull request."
|
|
6626
|
-
})
|
|
6627
|
-
);
|
|
6628
|
-
}
|
|
6629
|
-
if (c.before != null && c.after != null) {
|
|
6630
|
-
triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
|
|
6631
|
-
for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
|
|
6632
|
-
out3.push(
|
|
6633
|
-
makeFinding(RULE7, policy, {
|
|
6634
|
-
file: c.path,
|
|
6635
|
-
message: `The workflow's triggers were narrowed: ${reason}.`,
|
|
6636
|
-
evidence: reason,
|
|
6637
|
-
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."
|
|
6638
|
-
})
|
|
6639
|
-
);
|
|
6640
|
-
}
|
|
6641
|
-
}
|
|
6642
|
-
}
|
|
6643
|
-
const afterCommands = afterLines ? commandLines(afterLines) : null;
|
|
6644
|
-
const afterCores = (afterCommands ?? addedLines(c).map((l) => l.content)).map(commandCore);
|
|
6645
|
-
const addedCores = addedLines(c).map((l) => commandCore(afterCommands && l.newLine != null ? afterCommands[l.newLine - 1] : l.content));
|
|
6646
|
-
for (const l of afterLines ?? []) {
|
|
6647
|
-
const reused = l.match(REUSED_WORKFLOW)?.[1];
|
|
6648
|
-
const content = reused ? afterByPath.get(reused) : void 0;
|
|
6649
|
-
if (content == null || reused === c.path) continue;
|
|
6650
|
-
const cores = commandLines(content.split("\n")).map(commandCore);
|
|
6651
|
-
afterCores.push(...cores);
|
|
6652
|
-
addedCores.push(...cores);
|
|
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;
|
|
6653
6564
|
}
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
);
|
|
6670
|
-
continue;
|
|
6671
|
-
}
|
|
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;
|
|
6672
6580
|
out3.push(
|
|
6673
|
-
makeFinding(
|
|
6581
|
+
makeFinding(RULE6, policy, {
|
|
6674
6582
|
file: c.path,
|
|
6675
|
-
message:
|
|
6676
|
-
evidence:
|
|
6677
|
-
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"
|
|
6678
6587
|
})
|
|
6679
6588
|
);
|
|
6680
6589
|
}
|
|
@@ -6685,444 +6594,1130 @@ var init_ci_tampering = __esm({
|
|
|
6685
6594
|
}
|
|
6686
6595
|
});
|
|
6687
6596
|
|
|
6688
|
-
// src/detectors/
|
|
6689
|
-
function
|
|
6690
|
-
const
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
if (
|
|
6701
|
-
return `go test skips ${hit === "testdata" ? "testdata/" : hit.startsWith("_") ? "_*" : ".*"} paths (${JSON.stringify(hit)})`;
|
|
6702
|
-
}
|
|
6703
|
-
if (/\.py$/.test(path)) {
|
|
6704
|
-
const hit = PYTEST_DEFAULT_IGNORE.find((g) => globs2([g])(path));
|
|
6705
|
-
return hit === void 0 ? null : `pytest's default norecursedirs matches it (${JSON.stringify(hit)})`;
|
|
6706
|
-
}
|
|
6707
|
-
if (/\.[cm]?[jt]sx?$/.test(path)) {
|
|
6708
|
-
const hit = JS_UNWALKED.find((g) => globs2([g])(path));
|
|
6709
|
-
return hit === void 0 ? null : `the runner's default exclude matches it (${JSON.stringify(hit)})`;
|
|
6710
|
-
}
|
|
6711
|
-
return null;
|
|
6712
|
-
}
|
|
6713
|
-
function keyName2(name) {
|
|
6714
|
-
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
|
|
6715
|
-
return null;
|
|
6716
|
-
}
|
|
6717
|
-
function literals2(e) {
|
|
6718
|
-
if (isStr(e)) return { items: [e.text], opaque: false };
|
|
6719
|
-
if (!ts.isArrayLiteralExpression(e)) return { items: [], opaque: true };
|
|
6720
|
-
const items = [];
|
|
6721
|
-
let opaque = false;
|
|
6722
|
-
for (const el of e.elements) {
|
|
6723
|
-
if (isStr(el)) items.push(el.text);
|
|
6724
|
-
else if (ts.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
|
|
6725
|
-
items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
6726
|
-
} else opaque = true;
|
|
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;
|
|
6727
6610
|
}
|
|
6728
|
-
return
|
|
6729
|
-
}
|
|
6730
|
-
function ownerKey2(node) {
|
|
6731
|
-
const obj = node.parent;
|
|
6732
|
-
if (!obj || !ts.isObjectLiteralExpression(obj)) return null;
|
|
6733
|
-
let q = obj.parent;
|
|
6734
|
-
while (q && (ts.isParenthesizedExpression(q) || ts.isAsExpression(q) || q.kind === ts.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
6735
|
-
return q && ts.isPropertyAssignment(q) ? keyName2(q.name) : null;
|
|
6611
|
+
return quote !== null;
|
|
6736
6612
|
}
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
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
|
+
});
|
|
6740
6691
|
}
|
|
6741
|
-
function
|
|
6742
|
-
const
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
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
|
+
}
|
|
6751
6931
|
return null;
|
|
6752
6932
|
}
|
|
6753
|
-
function
|
|
6754
|
-
if (
|
|
6755
|
-
const
|
|
6756
|
-
|
|
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" };
|
|
6757
6987
|
}
|
|
6758
|
-
function
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
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;
|
|
6763
6995
|
}
|
|
6764
|
-
return
|
|
6996
|
+
return s;
|
|
6765
6997
|
}
|
|
6766
|
-
function
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
const
|
|
6770
|
-
if (
|
|
6771
|
-
|
|
6772
|
-
if (!files) return true;
|
|
6773
|
-
const rootNames = new Set(files.filter((f) => !f.includes("/")));
|
|
6774
|
-
return !PYTEST_INI_ORDER.slice(0, rank).some((n) => rootNames.has(n));
|
|
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";
|
|
6775
7004
|
}
|
|
6776
|
-
function
|
|
6777
|
-
const
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
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++;
|
|
6787
7020
|
continue;
|
|
6788
7021
|
}
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
key2 = m[1];
|
|
6792
|
-
out3[key2] = m[2].trim();
|
|
7022
|
+
if (ch === "'" && !double) {
|
|
7023
|
+
single = !single;
|
|
6793
7024
|
continue;
|
|
6794
7025
|
}
|
|
6795
|
-
if (
|
|
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" });
|
|
6796
7087
|
}
|
|
6797
7088
|
return out3;
|
|
6798
7089
|
}
|
|
6799
|
-
function
|
|
6800
|
-
const
|
|
6801
|
-
|
|
6802
|
-
|
|
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) });
|
|
6803
7097
|
}
|
|
6804
|
-
|
|
6805
|
-
return [...unq2.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "").filter(Boolean);
|
|
7098
|
+
return out3;
|
|
6806
7099
|
}
|
|
6807
|
-
function
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
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" };
|
|
6817
7115
|
}
|
|
6818
|
-
const
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
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*$/;
|
|
6837
7198
|
}
|
|
6838
|
-
|
|
6839
|
-
|
|
7199
|
+
});
|
|
7200
|
+
|
|
7201
|
+
// src/detectors/ci-tampering.ts
|
|
7202
|
+
function isActiveWorkflow(path) {
|
|
7203
|
+
return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(path);
|
|
6840
7204
|
}
|
|
6841
|
-
function
|
|
6842
|
-
|
|
6843
|
-
const d = stripRootDir(e.text).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
6844
|
-
if (d === "." || d === "" || d.split("/").includes("..")) return { dir: null, opaque: false };
|
|
6845
|
-
return { dir: d, opaque: false };
|
|
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();
|
|
6846
7207
|
}
|
|
6847
|
-
function
|
|
6848
|
-
const
|
|
6849
|
-
const
|
|
6850
|
-
|
|
6851
|
-
const { items, opaque } = literals2(e);
|
|
6852
|
-
if (opaque || seen.has(key2)) sel.opaque = true;
|
|
6853
|
-
seen.add(key2);
|
|
6854
|
-
sel.present = true;
|
|
6855
|
-
return items.map(stripRootDir);
|
|
6856
|
-
};
|
|
6857
|
-
const ignores = (key2, e) => {
|
|
6858
|
-
const entries = take(key2, e).map((pattern) => ({ pattern, key: key2 }));
|
|
6859
|
-
sel.ignore = [...sel.ignore ?? [], ...entries];
|
|
6860
|
-
};
|
|
6861
|
-
let projectList = null;
|
|
6862
|
-
const projects = (e) => {
|
|
6863
|
-
sel.present = true;
|
|
6864
|
-
projectList = e;
|
|
6865
|
-
};
|
|
6866
|
-
const visit = (node) => {
|
|
6867
|
-
if (ts.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
|
|
6868
|
-
sel.present = true;
|
|
6869
|
-
sel.opaque = true;
|
|
6870
|
-
}
|
|
6871
|
-
if (ts.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
|
|
6872
|
-
sel.present = true;
|
|
6873
|
-
sel.opaque = true;
|
|
6874
|
-
}
|
|
6875
|
-
if (ts.isPropertyAssignment(node)) {
|
|
6876
|
-
const k = keyName2(node.name);
|
|
6877
|
-
if (runner === "jest") {
|
|
6878
|
-
if (k === "testMatch") sel.include = take(k, node.initializer);
|
|
6879
|
-
else if (k === "testRegex") sel.regex = take(k, node.initializer);
|
|
6880
|
-
else if (k === "testPathIgnorePatterns" || k === "modulePathIgnorePatterns") ignores(k, node.initializer);
|
|
6881
|
-
else if (k === "roots") sel.roots = take(k, node.initializer);
|
|
6882
|
-
else if (k === "rootDir") {
|
|
6883
|
-
const { dir, opaque } = dirValue(node.initializer);
|
|
6884
|
-
sel.present = true;
|
|
6885
|
-
if (opaque) sel.opaque = true;
|
|
6886
|
-
else sel.base = dir;
|
|
6887
|
-
} else if (k === "projects") {
|
|
6888
|
-
projects(node.initializer);
|
|
6889
|
-
return;
|
|
6890
|
-
}
|
|
6891
|
-
} else if (ownerKey2(node) === "test") {
|
|
6892
|
-
if (k === "include") sel.include = take(k, node.initializer);
|
|
6893
|
-
else if (k === "exclude") ignores(k, node.initializer);
|
|
6894
|
-
else if (k === "dir" || k === "root") {
|
|
6895
|
-
const { dir, opaque } = dirValue(node.initializer);
|
|
6896
|
-
sel.present = true;
|
|
6897
|
-
if (opaque) sel.opaque = true;
|
|
6898
|
-
else sel.base = dir;
|
|
6899
|
-
} else if (k === "projects") {
|
|
6900
|
-
projects(node.initializer);
|
|
6901
|
-
return;
|
|
6902
|
-
}
|
|
6903
|
-
}
|
|
6904
|
-
}
|
|
6905
|
-
ts.forEachChild(node, visit);
|
|
6906
|
-
};
|
|
6907
|
-
visit(root);
|
|
6908
|
-
if (projectList) sel.projects = readProjects(projectList, sel, runner);
|
|
6909
|
-
if (sel.projects?.some((p) => p.opaque)) sel.opaque = true;
|
|
6910
|
-
return sel;
|
|
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;
|
|
6911
7212
|
}
|
|
6912
|
-
function
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
if (ts.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
|
|
6918
|
-
return x;
|
|
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);
|
|
6919
7218
|
}
|
|
6920
|
-
function
|
|
6921
|
-
const
|
|
6922
|
-
if (
|
|
6923
|
-
const
|
|
6924
|
-
if (
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
while (q && (ts.isParenthesizedExpression(q) || ts.isAsExpression(q) || q.kind === ts.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
6928
|
-
return !!q && ts.isPropertyAssignment(q) && keyName2(q.name) === "test";
|
|
6929
|
-
}
|
|
6930
|
-
return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
|
|
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);
|
|
6931
7226
|
}
|
|
6932
|
-
function
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
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];
|
|
6940
7236
|
}
|
|
6941
|
-
function
|
|
6942
|
-
const
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
p.present = true;
|
|
6954
|
-
return p;
|
|
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];
|
|
6955
7249
|
}
|
|
6956
|
-
function
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
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) });
|
|
6960
7258
|
}
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
7259
|
+
return out3;
|
|
7260
|
+
}
|
|
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] = "";
|
|
7267
|
+
continue;
|
|
7268
|
+
}
|
|
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] = "";
|
|
7277
|
+
}
|
|
6966
7278
|
}
|
|
6967
|
-
return
|
|
7279
|
+
return out3;
|
|
6968
7280
|
}
|
|
6969
|
-
function
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
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);
|
|
7285
|
+
}
|
|
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
|
|
7296
|
+
};
|
|
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;
|
|
7323
|
+
}
|
|
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;
|
|
6977
7331
|
}
|
|
6978
|
-
|
|
6979
|
-
|
|
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;
|
|
6980
7339
|
}
|
|
6981
|
-
return
|
|
7340
|
+
return t;
|
|
6982
7341
|
}
|
|
6983
|
-
function
|
|
6984
|
-
|
|
6985
|
-
if (runner === "pytest") return pytestSelection(src);
|
|
7342
|
+
function ghGlob(g) {
|
|
7343
|
+
const re = g.split("**").map((part) => part.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")).join(".*");
|
|
6986
7344
|
try {
|
|
6987
|
-
const
|
|
6988
|
-
|
|
6989
|
-
if (runner === "vitest" && isWorkspaceFile(path)) {
|
|
6990
|
-
const arr = workspaceArray(sf);
|
|
6991
|
-
if (arr) {
|
|
6992
|
-
sel.projects = readProjects(arr, sel, runner);
|
|
6993
|
-
sel.present = true;
|
|
6994
|
-
if (sel.projects.some((p) => p.opaque)) sel.opaque = true;
|
|
6995
|
-
} else if (src.trim()) sel.opaque = true;
|
|
6996
|
-
} else sel = collect(sf, runner);
|
|
7345
|
+
const r = new RegExp(`^${re}$`);
|
|
7346
|
+
return (p) => r.test(p);
|
|
6997
7347
|
} catch {
|
|
7348
|
+
return () => false;
|
|
6998
7349
|
}
|
|
6999
|
-
return sel;
|
|
7000
7350
|
}
|
|
7001
|
-
function
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
why: (p) => ps.length ? `no project selects it (${ps[0].why(p)})` : "the projects list is empty"
|
|
7007
|
-
};
|
|
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;
|
|
7008
7356
|
}
|
|
7009
|
-
|
|
7010
|
-
const ignoreList = sel.ignore ?? (runner === "jest" ? JEST_DEFAULT_IGNORE : runner === "pytest" ? PYTEST_DEFAULT_IGNORE : VITEST_DEFAULT_EXCLUDE).map((pattern) => ({
|
|
7011
|
-
pattern,
|
|
7012
|
-
key: runner === "jest" ? "testPathIgnorePatterns" : runner === "pytest" ? "norecursedirs" : "test.exclude"
|
|
7013
|
-
}));
|
|
7014
|
-
const blockWide = ignoreList.filter((e) => e.pattern.startsWith("\0"));
|
|
7015
|
-
const included = includeList ? globList2(includeList) : sel.regex ? regexes(sel.regex) : () => true;
|
|
7016
|
-
const ignoredBy = (p) => blockWide[0] ?? ignoreList.filter((e) => !e.pattern.startsWith("\0")).find((e) => (runner === "jest" ? regexes([e.pattern]) : globs2([e.pattern]))(p));
|
|
7017
|
-
const roots = sel.roots?.map((r) => r.replace(/^\.\//, "").replace(/\/+$/, "")).map((r) => r === "." ? "" : r) ?? null;
|
|
7018
|
-
const rebase = (p) => sel.base === null ? p : underDir(p, sel.base) ? p.slice(sel.base.length + 1) : null;
|
|
7019
|
-
const inRoots = (p) => roots === null || roots.some((r) => underDir(p, r));
|
|
7020
|
-
return {
|
|
7021
|
-
selects: (p) => {
|
|
7022
|
-
const r = rebase(p);
|
|
7023
|
-
return r !== null && inRoots(r) && included(r) && !ignoredBy(r);
|
|
7024
|
-
},
|
|
7025
|
-
why: (p) => {
|
|
7026
|
-
const r = rebase(p);
|
|
7027
|
-
const baseKey = runner === "jest" ? "rootDir" : "test.dir";
|
|
7028
|
-
if (r === null) return `${baseKey} no longer covers it (${JSON.stringify(sel.base)})`;
|
|
7029
|
-
if (!inRoots(r)) return `roots no longer covers it (${JSON.stringify(sel.roots)})`;
|
|
7030
|
-
const hit = ignoredBy(r);
|
|
7031
|
-
if (hit) {
|
|
7032
|
-
if (hit.pattern.startsWith("\0")) return `${hit.key} narrows the whole run (${JSON.stringify(hit.pattern.slice(1))})`;
|
|
7033
|
-
return `${hit.key === "exclude" ? "test.exclude" : hit.key} now matches it (${JSON.stringify(hit.pattern)})`;
|
|
7034
|
-
}
|
|
7035
|
-
const key2 = runner === "jest" ? sel.include ? "testMatch" : sel.regex ? "testRegex" : "testMatch (default)" : sel.include ? "test.include" : "test.include (default)";
|
|
7036
|
-
return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? includeList ?? [])})`;
|
|
7037
|
-
}
|
|
7038
|
-
};
|
|
7357
|
+
return on;
|
|
7039
7358
|
}
|
|
7040
|
-
function
|
|
7041
|
-
const
|
|
7042
|
-
if (!
|
|
7043
|
-
const
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
const pa = predicate(a, runner);
|
|
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) {
|
|
7048
7366
|
const out3 = [];
|
|
7049
|
-
|
|
7050
|
-
|
|
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`);
|
|
7389
|
+
}
|
|
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`);
|
|
7395
|
+
}
|
|
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`);
|
|
7399
|
+
}
|
|
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;
|
|
7409
|
+
}
|
|
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
|
+
);
|
|
7051
7419
|
}
|
|
7052
7420
|
return out3;
|
|
7053
7421
|
}
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
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`] : [];
|
|
7427
|
+
}
|
|
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;
|
|
7431
|
+
}
|
|
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];
|
|
7453
|
+
}
|
|
7454
|
+
return null;
|
|
7455
|
+
}
|
|
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"() {
|
|
7057
7465
|
"use strict";
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
];
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
"
|
|
7086
|
-
"
|
|
7087
|
-
"
|
|
7088
|
-
"
|
|
7089
|
-
];
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
empty = () => ({ include: null, regex: null, ignore: null, roots: null, base: null, projects: null, opaque: false, present: false });
|
|
7098
|
-
stripRootDir = (g) => g.replace(/^<rootDir>\/?/, "");
|
|
7099
|
-
JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
|
|
7100
|
-
globs2 = (patterns) => {
|
|
7101
|
-
const ms = patterns.map((g) => {
|
|
7102
|
-
try {
|
|
7103
|
-
return picomatch(g, { dot: true });
|
|
7104
|
-
} catch {
|
|
7105
|
-
return () => false;
|
|
7106
|
-
}
|
|
7107
|
-
});
|
|
7108
|
-
return (path) => ms.some((m) => m(path));
|
|
7109
|
-
};
|
|
7110
|
-
globList2 = (patterns) => {
|
|
7111
|
-
const pos = globs2(patterns.filter((g) => !g.startsWith("!")));
|
|
7112
|
-
const neg = globs2(patterns.filter((g) => g.startsWith("!")).map((g) => g.slice(1)));
|
|
7113
|
-
return (path) => pos(path) && !neg(path);
|
|
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" }
|
|
7497
|
+
];
|
|
7498
|
+
LIST_KEYS = {
|
|
7499
|
+
branches: "branches",
|
|
7500
|
+
"branches-ignore": "branchesIgnore",
|
|
7501
|
+
"paths-ignore": "pathsIgnore",
|
|
7502
|
+
paths: "paths",
|
|
7503
|
+
types: "types",
|
|
7504
|
+
tags: "tags"
|
|
7114
7505
|
};
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
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
|
+
}
|
|
7121
7717
|
}
|
|
7122
|
-
|
|
7123
|
-
|
|
7718
|
+
return out3;
|
|
7719
|
+
}
|
|
7124
7720
|
};
|
|
7125
|
-
underDir = (path, dir) => dir === "" || path.startsWith(dir + "/");
|
|
7126
7721
|
}
|
|
7127
7722
|
});
|
|
7128
7723
|
|
|
@@ -7150,7 +7745,7 @@ function safeParse(src) {
|
|
|
7150
7745
|
return null;
|
|
7151
7746
|
}
|
|
7152
7747
|
}
|
|
7153
|
-
function
|
|
7748
|
+
function effective2(raw) {
|
|
7154
7749
|
const v = raw.version;
|
|
7155
7750
|
const version = typeof v === "number" && Number.isInteger(v) && v >= 1 ? v : 1;
|
|
7156
7751
|
const base = defaultPolicy(version);
|
|
@@ -7215,8 +7810,8 @@ function policyWeakening(before, after, probe) {
|
|
|
7215
7810
|
if (b === null) return null;
|
|
7216
7811
|
const a = safeParse(after);
|
|
7217
7812
|
if (a === null) return ["the policy no longer parses as a valid policy (emptied or corrupted)"];
|
|
7218
|
-
const be =
|
|
7219
|
-
const ae =
|
|
7813
|
+
const be = effective2(b);
|
|
7814
|
+
const ae = effective2(a);
|
|
7220
7815
|
const reasons = [];
|
|
7221
7816
|
if (ae.version < be.version) {
|
|
7222
7817
|
reasons.push(
|
|
@@ -7479,7 +8074,7 @@ function entryShape(h) {
|
|
|
7479
8074
|
}
|
|
7480
8075
|
return { problems, runs };
|
|
7481
8076
|
}
|
|
7482
|
-
function judgeGateEntry(event,
|
|
8077
|
+
function judgeGateEntry(event, matcher2, h) {
|
|
7483
8078
|
const cmd = typeof h.command === "string" ? h.command : "";
|
|
7484
8079
|
const a = analyseGateCommand(cmd, event);
|
|
7485
8080
|
const shape = entryShape(h);
|
|
@@ -7487,13 +8082,13 @@ function judgeGateEntry(event, matcher, h) {
|
|
|
7487
8082
|
let runs = a.runs && shape.runs;
|
|
7488
8083
|
let tools = null;
|
|
7489
8084
|
if (event === "PreToolUse") {
|
|
7490
|
-
const sel = matcherSelection(
|
|
8085
|
+
const sel = matcherSelection(matcher2);
|
|
7491
8086
|
tools = sel.tools;
|
|
7492
8087
|
if (sel.problem) {
|
|
7493
8088
|
problems.push(sel.problem);
|
|
7494
8089
|
}
|
|
7495
|
-
} else if (
|
|
7496
|
-
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`);
|
|
7497
8092
|
runs = false;
|
|
7498
8093
|
}
|
|
7499
8094
|
return { runs, problems, pin: a.pin, tools };
|
|
@@ -7700,7 +8295,7 @@ function claudeSettingsFindings(c, policy) {
|
|
|
7700
8295
|
if (c.after == null) return null;
|
|
7701
8296
|
const after = parseSettings(c.after);
|
|
7702
8297
|
const before = c.before != null ? parseSettings(c.before) : null;
|
|
7703
|
-
const finding = (reason) => makeFinding(
|
|
8298
|
+
const finding = (reason) => makeFinding(RULE9, policy, {
|
|
7704
8299
|
file: c.path,
|
|
7705
8300
|
message: `The Claude Code hook wiring was weakened: ${reason}.`,
|
|
7706
8301
|
evidence: reason,
|
|
@@ -7725,7 +8320,7 @@ function policyFindings(c, policy, ctx) {
|
|
|
7725
8320
|
const reasons = c.before == null ? policyAddWeakening(c.after, probe) : policyWeakening(c.before, c.after, probe);
|
|
7726
8321
|
if (reasons === null) return null;
|
|
7727
8322
|
return reasons.map(
|
|
7728
|
-
(reason) => makeFinding(
|
|
8323
|
+
(reason) => makeFinding(RULE9, policy, {
|
|
7729
8324
|
file: c.path,
|
|
7730
8325
|
message: added ? `A policy file was added that weakens the baseline: ${reason}.` : `The policy was weakened: ${reason}.`,
|
|
7731
8326
|
evidence: reason,
|
|
@@ -7835,7 +8430,7 @@ function configFindings(c, changes, policy, ctx) {
|
|
|
7835
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);
|
|
7836
8431
|
}
|
|
7837
8432
|
return reasons.map(
|
|
7838
|
-
(reason) => makeFinding(
|
|
8433
|
+
(reason) => makeFinding(RULE9, policy, {
|
|
7839
8434
|
file: c.path,
|
|
7840
8435
|
message: `The hook configuration was weakened: ${reason}.`,
|
|
7841
8436
|
evidence: reason,
|
|
@@ -7886,7 +8481,7 @@ function huskyRuntimeWrite(path, content, ctx) {
|
|
|
7886
8481
|
(i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
|
|
7887
8482
|
);
|
|
7888
8483
|
}
|
|
7889
|
-
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;
|
|
7890
8485
|
var init_hook_tampering = __esm({
|
|
7891
8486
|
"src/detectors/hook-tampering.ts"() {
|
|
7892
8487
|
"use strict";
|
|
@@ -7899,7 +8494,7 @@ var init_hook_tampering = __esm({
|
|
|
7899
8494
|
init_command();
|
|
7900
8495
|
init_repo();
|
|
7901
8496
|
init_hook_wiring();
|
|
7902
|
-
|
|
8497
|
+
RULE9 = "hook-tampering";
|
|
7903
8498
|
ownerExec2 = (mode) => (parseInt(mode.slice(-3), 8) & 64) !== 0;
|
|
7904
8499
|
EVENTS = ["PreToolUse", "Stop"];
|
|
7905
8500
|
isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
@@ -7967,7 +8562,7 @@ They WILL FAIL in v10.0.0
|
|
|
7967
8562
|
isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
|
|
7968
8563
|
isHookNote = (path) => !/(?:^|\/)\.husky\/_\//.test(path) && /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
|
|
7969
8564
|
hookTampering = {
|
|
7970
|
-
id:
|
|
8565
|
+
id: RULE9,
|
|
7971
8566
|
surface: ["file", "command"],
|
|
7972
8567
|
certainty: "mechanical",
|
|
7973
8568
|
run(changes, policy, _view, ctx) {
|
|
@@ -7980,7 +8575,7 @@ They WILL FAIL in v10.0.0
|
|
|
7980
8575
|
if (isPackageJson(c.path) && c.before != null && c.after != null) {
|
|
7981
8576
|
for (const reason of packageJsonWeakening(c.before, c.after)) {
|
|
7982
8577
|
out3.push(
|
|
7983
|
-
makeFinding(
|
|
8578
|
+
makeFinding(RULE9, policy, {
|
|
7984
8579
|
file: c.path,
|
|
7985
8580
|
message: `The hook installation was weakened: ${reason}.`,
|
|
7986
8581
|
evidence: reason,
|
|
@@ -7993,7 +8588,7 @@ They WILL FAIL in v10.0.0
|
|
|
7993
8588
|
}
|
|
7994
8589
|
if (c.op !== "delete" && c.oldMode && c.newMode && ownerExec2(c.oldMode) && !ownerExec2(c.newMode)) {
|
|
7995
8590
|
out3.push(
|
|
7996
|
-
makeFinding(
|
|
8591
|
+
makeFinding(RULE9, policy, {
|
|
7997
8592
|
file: c.path,
|
|
7998
8593
|
message: `Execute permission was removed from a protected hook (${c.oldMode} \u2192 ${c.newMode}) \u2014 git will skip it.`,
|
|
7999
8594
|
evidence: `${c.path}: mode ${c.oldMode} \u2192 ${c.newMode}`,
|
|
@@ -8003,7 +8598,7 @@ They WILL FAIL in v10.0.0
|
|
|
8003
8598
|
}
|
|
8004
8599
|
if (c.op === "delete") {
|
|
8005
8600
|
out3.push(
|
|
8006
|
-
makeFinding(
|
|
8601
|
+
makeFinding(RULE9, policy, {
|
|
8007
8602
|
file: c.path,
|
|
8008
8603
|
message: "A protected hook/policy file was deleted.",
|
|
8009
8604
|
evidence: c.path,
|
|
@@ -8012,7 +8607,7 @@ They WILL FAIL in v10.0.0
|
|
|
8012
8607
|
);
|
|
8013
8608
|
} else if (c.op === "rename" && !renameKept(c)) {
|
|
8014
8609
|
out3.push(
|
|
8015
|
-
makeFinding(
|
|
8610
|
+
makeFinding(RULE9, policy, {
|
|
8016
8611
|
file: c.path,
|
|
8017
8612
|
message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
|
|
8018
8613
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -8026,7 +8621,7 @@ They WILL FAIL in v10.0.0
|
|
|
8026
8621
|
if (c.before != null && c.after != null) {
|
|
8027
8622
|
for (const reason of codeownersWeakening(c.before, c.after, [...repoGateFiles(ctx), c.path])) {
|
|
8028
8623
|
out3.push(
|
|
8029
|
-
makeFinding(
|
|
8624
|
+
makeFinding(RULE9, policy, {
|
|
8030
8625
|
file: c.path,
|
|
8031
8626
|
message: `CODEOWNERS was weakened: ${reason}.`,
|
|
8032
8627
|
evidence: reason,
|
|
@@ -8048,7 +8643,7 @@ They WILL FAIL in v10.0.0
|
|
|
8048
8643
|
const kept = afterPin !== null && (beforePin === "" ? afterPin === "" || PLAIN_SEMVER.test(afterPin) : pinNotBelow(afterPin, beforePin));
|
|
8049
8644
|
if (!kept) {
|
|
8050
8645
|
out3.push(
|
|
8051
|
-
makeFinding(
|
|
8646
|
+
makeFinding(RULE9, policy, {
|
|
8052
8647
|
file: c.path,
|
|
8053
8648
|
message: "A protected hook script no longer matches the shape `tamperward init` writes.",
|
|
8054
8649
|
evidence: "the gate script no longer matches the shape init wrote; re-run init or sign off",
|
|
@@ -8071,7 +8666,7 @@ They WILL FAIL in v10.0.0
|
|
|
8071
8666
|
if (shebang || !live) {
|
|
8072
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`");
|
|
8073
8668
|
out3.push(
|
|
8074
|
-
makeFinding(
|
|
8669
|
+
makeFinding(RULE9, policy, {
|
|
8075
8670
|
file: c.path,
|
|
8076
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.`,
|
|
8077
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`,
|
|
@@ -8080,7 +8675,7 @@ They WILL FAIL in v10.0.0
|
|
|
8080
8675
|
);
|
|
8081
8676
|
} else if (earlyExit) {
|
|
8082
8677
|
out3.push(
|
|
8083
|
-
makeFinding(
|
|
8678
|
+
makeFinding(RULE9, policy, {
|
|
8084
8679
|
file: c.path,
|
|
8085
8680
|
line: earlyExit.newLine ?? void 0,
|
|
8086
8681
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -8102,7 +8697,7 @@ They WILL FAIL in v10.0.0
|
|
|
8102
8697
|
deadGuard2 ? `a guard that can never be true was inserted (\`${deadGuard2.content.trim()}\`)` : null
|
|
8103
8698
|
].filter((d) => d !== null);
|
|
8104
8699
|
out3.push(
|
|
8105
|
-
makeFinding(
|
|
8700
|
+
makeFinding(RULE9, policy, {
|
|
8106
8701
|
file: c.path,
|
|
8107
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.`,
|
|
8108
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`,
|
|
@@ -8114,7 +8709,7 @@ They WILL FAIL in v10.0.0
|
|
|
8114
8709
|
}
|
|
8115
8710
|
if (shebangChanged && c.after != null) {
|
|
8116
8711
|
out3.push(
|
|
8117
|
-
makeFinding(
|
|
8712
|
+
makeFinding(RULE9, policy, {
|
|
8118
8713
|
file: c.path,
|
|
8119
8714
|
line: 1,
|
|
8120
8715
|
message: `A protected hook's interpreter was changed: ${shebang}.`,
|
|
@@ -8125,7 +8720,7 @@ They WILL FAIL in v10.0.0
|
|
|
8125
8720
|
}
|
|
8126
8721
|
if (earlyExit) {
|
|
8127
8722
|
out3.push(
|
|
8128
|
-
makeFinding(
|
|
8723
|
+
makeFinding(RULE9, policy, {
|
|
8129
8724
|
file: c.path,
|
|
8130
8725
|
line: earlyExit.newLine ?? void 0,
|
|
8131
8726
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -8137,7 +8732,7 @@ They WILL FAIL in v10.0.0
|
|
|
8137
8732
|
const deadGuard = c.before == null || c.after == null ? added.find((l) => insertsDeadGuard(l.content)) : void 0;
|
|
8138
8733
|
if (deadGuard) {
|
|
8139
8734
|
out3.push(
|
|
8140
|
-
makeFinding(
|
|
8735
|
+
makeFinding(RULE9, policy, {
|
|
8141
8736
|
file: c.path,
|
|
8142
8737
|
line: deadGuard.newLine ?? void 0,
|
|
8143
8738
|
message: "A guard that can never be true was inserted into a protected hook \u2014 what it wraps no longer runs.",
|
|
@@ -8149,7 +8744,7 @@ They WILL FAIL in v10.0.0
|
|
|
8149
8744
|
const lost = scriptWeakening(beforeLines, afterLines, opts);
|
|
8150
8745
|
if (lost.length) {
|
|
8151
8746
|
out3.push(
|
|
8152
|
-
makeFinding(
|
|
8747
|
+
makeFinding(RULE9, policy, {
|
|
8153
8748
|
file: c.path,
|
|
8154
8749
|
message: `A check invocation was removed from a protected hook: ${lost[0].reason}.`,
|
|
8155
8750
|
evidence: lost[0].evidence,
|
|
@@ -8168,7 +8763,7 @@ They WILL FAIL in v10.0.0
|
|
|
8168
8763
|
const m = l.content.match(INLINE);
|
|
8169
8764
|
if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
|
|
8170
8765
|
out3.push(
|
|
8171
|
-
makeFinding(
|
|
8766
|
+
makeFinding(RULE9, policy, {
|
|
8172
8767
|
file: c.path,
|
|
8173
8768
|
line: l.newLine ?? void 0,
|
|
8174
8769
|
message: `Rule "${m[1]}" was lowered from block to warn in the policy.`,
|
|
@@ -8178,7 +8773,7 @@ They WILL FAIL in v10.0.0
|
|
|
8178
8773
|
);
|
|
8179
8774
|
} else if (/enabled:\s*false\b/.test(l.content)) {
|
|
8180
8775
|
out3.push(
|
|
8181
|
-
makeFinding(
|
|
8776
|
+
makeFinding(RULE9, policy, {
|
|
8182
8777
|
file: c.path,
|
|
8183
8778
|
line: l.newLine ?? void 0,
|
|
8184
8779
|
message: "A rule was disabled (enabled: false) in the policy.",
|
|
@@ -8196,7 +8791,7 @@ They WILL FAIL in v10.0.0
|
|
|
8196
8791
|
const hiding = updateIndexHiding(toks, policy);
|
|
8197
8792
|
if (hiding) {
|
|
8198
8793
|
out3.push(
|
|
8199
|
-
makeFinding(
|
|
8794
|
+
makeFinding(RULE9, policy, {
|
|
8200
8795
|
file: repoRelative(hiding.file, ctx),
|
|
8201
8796
|
message: `Hook tampering via shell: ${hiding.reason}.`,
|
|
8202
8797
|
evidence: seg,
|
|
@@ -8212,7 +8807,7 @@ They WILL FAIL in v10.0.0
|
|
|
8212
8807
|
const file = target ? repoRelative(target, ctx) : void 0;
|
|
8213
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` : "";
|
|
8214
8809
|
out3.push(
|
|
8215
|
-
makeFinding(
|
|
8810
|
+
makeFinding(RULE9, policy, {
|
|
8216
8811
|
...file !== void 0 ? { file } : {},
|
|
8217
8812
|
message: `Hook tampering via shell: ${why}${outside}.`,
|
|
8218
8813
|
evidence: seg,
|
|
@@ -8760,7 +9355,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
8760
9355
|
if (!was || !now || was.path === now.path) return [];
|
|
8761
9356
|
const samples = runnerSamples(now.path, policy, ctx, "pytest");
|
|
8762
9357
|
return suiteNarrowings(was.content, now.content, now.path, samples).map(
|
|
8763
|
-
(n) => makeFinding(
|
|
9358
|
+
(n) => makeFinding(RULE10, policy, {
|
|
8764
9359
|
file: now.path,
|
|
8765
9360
|
message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
|
|
8766
9361
|
evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
|
|
@@ -8768,7 +9363,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
8768
9363
|
})
|
|
8769
9364
|
);
|
|
8770
9365
|
}
|
|
8771
|
-
var
|
|
9366
|
+
var RULE10, JVM_TEST, TEST_DEFS, isTableOf, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
|
|
8772
9367
|
var init_test_deletion = __esm({
|
|
8773
9368
|
"src/detectors/test-deletion.ts"() {
|
|
8774
9369
|
"use strict";
|
|
@@ -8785,7 +9380,7 @@ var init_test_deletion = __esm({
|
|
|
8785
9380
|
init_narrow();
|
|
8786
9381
|
init_reachability();
|
|
8787
9382
|
init_runner_chain();
|
|
8788
|
-
|
|
9383
|
+
RULE10 = "test-deletion";
|
|
8789
9384
|
JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
|
|
8790
9385
|
TEST_DEFS = {
|
|
8791
9386
|
py: /^\s*(?:async\s+)?def\s+test\w*\s*\(/,
|
|
@@ -8808,7 +9403,7 @@ var init_test_deletion = __esm({
|
|
|
8808
9403
|
SLICE_SCRIPT = /^test:(?:unit|integration)$/;
|
|
8809
9404
|
SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
|
|
8810
9405
|
testDeletion = {
|
|
8811
|
-
id:
|
|
9406
|
+
id: RULE10,
|
|
8812
9407
|
surface: ["file", "command"],
|
|
8813
9408
|
certainty: "mechanical",
|
|
8814
9409
|
run(changes, policy, _view, ctx) {
|
|
@@ -8840,7 +9435,7 @@ var init_test_deletion = __esm({
|
|
|
8840
9435
|
if (c.op === "delete" && isTest) {
|
|
8841
9436
|
if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
|
|
8842
9437
|
out3.push(
|
|
8843
|
-
makeFinding(
|
|
9438
|
+
makeFinding(RULE10, policy, {
|
|
8844
9439
|
file: c.path,
|
|
8845
9440
|
message: "A test file was deleted.",
|
|
8846
9441
|
evidence: c.path,
|
|
@@ -8849,7 +9444,7 @@ var init_test_deletion = __esm({
|
|
|
8849
9444
|
);
|
|
8850
9445
|
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && isSpecShaped(c.oldPath, c.before, c.after) && !isSpec(c.path)) {
|
|
8851
9446
|
out3.push(
|
|
8852
|
-
makeFinding(
|
|
9447
|
+
makeFinding(RULE10, policy, {
|
|
8853
9448
|
file: c.path,
|
|
8854
9449
|
message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
|
|
8855
9450
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -8858,7 +9453,7 @@ var init_test_deletion = __esm({
|
|
|
8858
9453
|
);
|
|
8859
9454
|
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && isTest && leavesWalk(c.oldPath, c.path)) {
|
|
8860
9455
|
out3.push(
|
|
8861
|
-
makeFinding(
|
|
9456
|
+
makeFinding(RULE10, policy, {
|
|
8862
9457
|
file: c.path,
|
|
8863
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)}.`,
|
|
8864
9459
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -8873,7 +9468,7 @@ var init_test_deletion = __esm({
|
|
|
8873
9468
|
const removed = new Set([...significantLines(c.before, c.path)].filter((l) => !afterLines.has(l)));
|
|
8874
9469
|
if (isRelocation(removed, before.min - after.min)) continue;
|
|
8875
9470
|
out3.push(
|
|
8876
|
-
makeFinding(
|
|
9471
|
+
makeFinding(RULE10, policy, {
|
|
8877
9472
|
file: c.path,
|
|
8878
9473
|
message: `Test blocks removed: ${before.min} \u2192 ${after.min} it()/test() in this spec (each-table rows count as tests).`,
|
|
8879
9474
|
evidence: `${before.min - after.min} test block(s) removed from ${c.path}`,
|
|
@@ -8884,9 +9479,22 @@ var init_test_deletion = __esm({
|
|
|
8884
9479
|
} else if (c.op !== "delete" && c.after != null && !isSpec(c.path) && isProtected(c.path, policy, "config") && runnerOf(c.path, c.after)) {
|
|
8885
9480
|
const rn = runnerOf(c.path, c.after);
|
|
8886
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
|
+
}
|
|
8887
9495
|
for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
|
|
8888
9496
|
out3.push(
|
|
8889
|
-
makeFinding(
|
|
9497
|
+
makeFinding(RULE10, policy, {
|
|
8890
9498
|
file: c.path,
|
|
8891
9499
|
message: `The runner no longer selects ${n.path}: ${n.reason}.`,
|
|
8892
9500
|
evidence: `${n.path} dropped from the suite by ${c.path}`,
|
|
@@ -8898,12 +9506,12 @@ var init_test_deletion = __esm({
|
|
|
8898
9506
|
const w = n.weakening;
|
|
8899
9507
|
const evidence = `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${n.after === "" ? "deleted" : JSON.stringify(n.after)}`;
|
|
8900
9508
|
out3.push(
|
|
8901
|
-
w.state === "removed" ? makeFinding(
|
|
9509
|
+
w.state === "removed" ? makeFinding(RULE10, policy, {
|
|
8902
9510
|
file: c.path,
|
|
8903
9511
|
message: `The test script no longer runs the ${w.kind} check: scripts.${n.script} ${w.what}.`,
|
|
8904
9512
|
evidence,
|
|
8905
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."
|
|
8906
|
-
}) : makeFinding(
|
|
9514
|
+
}) : makeFinding(RULE10, policy, {
|
|
8907
9515
|
file: c.path,
|
|
8908
9516
|
message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${n.script}.`,
|
|
8909
9517
|
evidence,
|
|
@@ -9038,7 +9646,7 @@ var init_test_deletion = __esm({
|
|
|
9038
9646
|
}
|
|
9039
9647
|
if (why) {
|
|
9040
9648
|
out3.push(
|
|
9041
|
-
makeFinding(
|
|
9649
|
+
makeFinding(RULE10, policy, {
|
|
9042
9650
|
message: `Test removed via shell: ${why}.`,
|
|
9043
9651
|
evidence,
|
|
9044
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."
|
|
@@ -9097,7 +9705,7 @@ function significantLinesOrdered(src, path) {
|
|
|
9097
9705
|
function liveSignificantLines(src, path) {
|
|
9098
9706
|
return significantLinesOrdered(partition(src, path).live, path);
|
|
9099
9707
|
}
|
|
9100
|
-
var
|
|
9708
|
+
var RULE11, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, KeptPool, testContentRemoval;
|
|
9101
9709
|
var init_test_content_removal = __esm({
|
|
9102
9710
|
"src/detectors/test-content-removal.ts"() {
|
|
9103
9711
|
"use strict";
|
|
@@ -9108,7 +9716,7 @@ var init_test_content_removal = __esm({
|
|
|
9108
9716
|
init_files();
|
|
9109
9717
|
init_spec_shape();
|
|
9110
9718
|
init_reachability();
|
|
9111
|
-
|
|
9719
|
+
RULE11 = "test-content-removal";
|
|
9112
9720
|
MIN_REMOVED_LINES = 3;
|
|
9113
9721
|
MIN_REMOVED_ROWS = 2;
|
|
9114
9722
|
ws = (s) => s.replace(/\s+/g, "");
|
|
@@ -9149,7 +9757,7 @@ var init_test_content_removal = __esm({
|
|
|
9149
9757
|
}
|
|
9150
9758
|
};
|
|
9151
9759
|
testContentRemoval = {
|
|
9152
|
-
id:
|
|
9760
|
+
id: RULE11,
|
|
9153
9761
|
surface: ["file"],
|
|
9154
9762
|
certainty: "mechanical",
|
|
9155
9763
|
run(changes, policy) {
|
|
@@ -9193,7 +9801,7 @@ var init_test_content_removal = __esm({
|
|
|
9193
9801
|
const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
|
|
9194
9802
|
if (goneRows.length >= MIN_REMOVED_ROWS) {
|
|
9195
9803
|
out3.push(
|
|
9196
|
-
makeFinding(
|
|
9804
|
+
makeFinding(RULE11, policy, {
|
|
9197
9805
|
file: c.path,
|
|
9198
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.`,
|
|
9199
9807
|
evidence: goneRows.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9204,7 +9812,7 @@ var init_test_content_removal = __esm({
|
|
|
9204
9812
|
}
|
|
9205
9813
|
if (hidden.length >= MIN_REMOVED_LINES) {
|
|
9206
9814
|
out3.push(
|
|
9207
|
-
makeFinding(
|
|
9815
|
+
makeFinding(RULE11, policy, {
|
|
9208
9816
|
file: c.path,
|
|
9209
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).`,
|
|
9210
9818
|
evidence: hidden.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9215,7 +9823,7 @@ var init_test_content_removal = __esm({
|
|
|
9215
9823
|
}
|
|
9216
9824
|
if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
|
|
9217
9825
|
out3.push(
|
|
9218
|
-
makeFinding(
|
|
9826
|
+
makeFinding(RULE11, policy, {
|
|
9219
9827
|
file: c.path,
|
|
9220
9828
|
message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
|
|
9221
9829
|
evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9238,14 +9846,14 @@ function namesProtectedSnapshot(seg, policy) {
|
|
|
9238
9846
|
}
|
|
9239
9847
|
return null;
|
|
9240
9848
|
}
|
|
9241
|
-
var
|
|
9849
|
+
var RULE12, RUNNER3, PM_TEST_SCRIPT, UPDATE_ANY, UPDATE_WITH_RUNNER, INSTA_ACCEPT, UPDATE_EXPECT, REGEN_SCRIPT, MUTATE, snapshotRewrite;
|
|
9242
9850
|
var init_snapshot_rewrite = __esm({
|
|
9243
9851
|
"src/detectors/snapshot-rewrite.ts"() {
|
|
9244
9852
|
"use strict";
|
|
9245
9853
|
init_policy();
|
|
9246
9854
|
init_finding();
|
|
9247
9855
|
init_command();
|
|
9248
|
-
|
|
9856
|
+
RULE12 = "snapshot-rewrite";
|
|
9249
9857
|
RUNNER3 = /\b(?:jest|vitest|playwright|ava)\b/;
|
|
9250
9858
|
PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
|
|
9251
9859
|
UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
|
|
@@ -9255,12 +9863,12 @@ var init_snapshot_rewrite = __esm({
|
|
|
9255
9863
|
REGEN_SCRIPT = /\b(?:update|regen(?:erate)?|re?bless)[-_.]?(?:golden|snapshots?|baselines?|expected)\b/i;
|
|
9256
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/;
|
|
9257
9865
|
snapshotRewrite = {
|
|
9258
|
-
id:
|
|
9866
|
+
id: RULE12,
|
|
9259
9867
|
surface: ["command", "file"],
|
|
9260
9868
|
certainty: "mechanical",
|
|
9261
9869
|
run(changes, policy) {
|
|
9262
9870
|
const out3 = [];
|
|
9263
|
-
const warn = (input) => out3.push(makeFinding(
|
|
9871
|
+
const warn = (input) => out3.push(makeFinding(RULE12, policy, { ...input, defaultSeverity: "warn" }));
|
|
9264
9872
|
for (const c of changes) {
|
|
9265
9873
|
if (c.kind === "command") {
|
|
9266
9874
|
for (const seg of segments(c.raw)) {
|
|
@@ -9308,16 +9916,16 @@ function isSnapshotChange(c, policy) {
|
|
|
9308
9916
|
if (isProtected(c.path, policy, "snapshots")) return true;
|
|
9309
9917
|
return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
|
|
9310
9918
|
}
|
|
9311
|
-
var
|
|
9919
|
+
var RULE13, COMMIT_VIEWS, snapshotOnlyRewrite;
|
|
9312
9920
|
var init_snapshot_only = __esm({
|
|
9313
9921
|
"src/detectors/snapshot-only.ts"() {
|
|
9314
9922
|
"use strict";
|
|
9315
9923
|
init_policy();
|
|
9316
9924
|
init_finding();
|
|
9317
|
-
|
|
9925
|
+
RULE13 = "snapshot-only-rewrite";
|
|
9318
9926
|
COMMIT_VIEWS = ["staged", "range"];
|
|
9319
9927
|
snapshotOnlyRewrite = {
|
|
9320
|
-
id:
|
|
9928
|
+
id: RULE13,
|
|
9321
9929
|
surface: ["file"],
|
|
9322
9930
|
certainty: "mechanical",
|
|
9323
9931
|
run(changes, policy, view) {
|
|
@@ -9330,7 +9938,7 @@ var init_snapshot_only = __esm({
|
|
|
9330
9938
|
if (!first) return [];
|
|
9331
9939
|
const names = snaps.map((c) => c.path).slice(0, 3).join(", ");
|
|
9332
9940
|
return [
|
|
9333
|
-
makeFinding(
|
|
9941
|
+
makeFinding(RULE13, policy, {
|
|
9334
9942
|
file: first.path,
|
|
9335
9943
|
message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
|
|
9336
9944
|
evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
|
|
@@ -9387,7 +9995,7 @@ function staticFacts(node) {
|
|
|
9387
9995
|
}
|
|
9388
9996
|
function expectAssertion(node, sf) {
|
|
9389
9997
|
if (!ts.isPropertyAccessExpression(node.expression)) return null;
|
|
9390
|
-
const
|
|
9998
|
+
const matcher2 = node.expression.name.text;
|
|
9391
9999
|
let receiver = node.expression.expression;
|
|
9392
10000
|
let polarity = "";
|
|
9393
10001
|
if (ts.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
|
|
@@ -9402,10 +10010,10 @@ function expectAssertion(node, sf) {
|
|
|
9402
10010
|
return {
|
|
9403
10011
|
subject,
|
|
9404
10012
|
polarity,
|
|
9405
|
-
matcher,
|
|
10013
|
+
matcher: matcher2,
|
|
9406
10014
|
args,
|
|
9407
10015
|
firstArgFacts: staticFacts(node.arguments[0]),
|
|
9408
|
-
canonical: [subject, polarity,
|
|
10016
|
+
canonical: [subject, polarity, matcher2, ...args].join("|"),
|
|
9409
10017
|
text: node.getText(sf).replace(/\s+/g, " ")
|
|
9410
10018
|
};
|
|
9411
10019
|
}
|
|
@@ -9508,7 +10116,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
9508
10116
|
const weakened2 = directionalWeakening(before.assertions, after.assertions);
|
|
9509
10117
|
if (weakened2) {
|
|
9510
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.`;
|
|
9511
|
-
return makeFinding(
|
|
10119
|
+
return makeFinding(RULE14, policy, {
|
|
9512
10120
|
file: path,
|
|
9513
10121
|
message,
|
|
9514
10122
|
evidence: `${weakened2.before.text} -> ${weakened2.after.text}`,
|
|
@@ -9517,7 +10125,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
9517
10125
|
}
|
|
9518
10126
|
const removed = pureRemoval(before.assertions, after.assertions);
|
|
9519
10127
|
if (removed) {
|
|
9520
|
-
return makeFinding(
|
|
10128
|
+
return makeFinding(RULE14, policy, {
|
|
9521
10129
|
file: path,
|
|
9522
10130
|
message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
|
|
9523
10131
|
evidence: removed.text,
|
|
@@ -9526,7 +10134,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
9526
10134
|
}
|
|
9527
10135
|
return null;
|
|
9528
10136
|
}
|
|
9529
|
-
var
|
|
10137
|
+
var RULE14, TEST_CALLS, SUITE_CALLS, EXACT_MATCHERS, WEAK_MATCHERS, compact, assertionWeakening;
|
|
9530
10138
|
var init_assertion_weakening = __esm({
|
|
9531
10139
|
"src/detectors/assertion-weakening.ts"() {
|
|
9532
10140
|
"use strict";
|
|
@@ -9535,14 +10143,14 @@ var init_assertion_weakening = __esm({
|
|
|
9535
10143
|
init_finding();
|
|
9536
10144
|
init_files();
|
|
9537
10145
|
init_runner_chain();
|
|
9538
|
-
|
|
10146
|
+
RULE14 = "assertion-weakening";
|
|
9539
10147
|
TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
9540
10148
|
SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
|
|
9541
10149
|
EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
|
|
9542
10150
|
WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
|
|
9543
10151
|
compact = (s) => s.replace(/\s+/g, "");
|
|
9544
10152
|
assertionWeakening = {
|
|
9545
|
-
id:
|
|
10153
|
+
id: RULE14,
|
|
9546
10154
|
surface: ["file"],
|
|
9547
10155
|
certainty: "heuristic",
|
|
9548
10156
|
run(changes, policy) {
|
|
@@ -9576,7 +10184,7 @@ function significant(src, path) {
|
|
|
9576
10184
|
}
|
|
9577
10185
|
return out3;
|
|
9578
10186
|
}
|
|
9579
|
-
var
|
|
10187
|
+
var RULE15, MIN_REMOVED_LINES2, ws2, testSupport;
|
|
9580
10188
|
var init_test_support = __esm({
|
|
9581
10189
|
"src/detectors/test-support.ts"() {
|
|
9582
10190
|
"use strict";
|
|
@@ -9584,11 +10192,11 @@ var init_test_support = __esm({
|
|
|
9584
10192
|
init_finding();
|
|
9585
10193
|
init_spec_shape();
|
|
9586
10194
|
init_files();
|
|
9587
|
-
|
|
10195
|
+
RULE15 = "test-support";
|
|
9588
10196
|
MIN_REMOVED_LINES2 = 3;
|
|
9589
10197
|
ws2 = (s) => s.replace(/\s+/g, "");
|
|
9590
10198
|
testSupport = {
|
|
9591
|
-
id:
|
|
10199
|
+
id: RULE15,
|
|
9592
10200
|
surface: ["file"],
|
|
9593
10201
|
certainty: "mechanical",
|
|
9594
10202
|
run(changes, policy) {
|
|
@@ -9603,7 +10211,7 @@ var init_test_support = __esm({
|
|
|
9603
10211
|
if (c.op === "delete") {
|
|
9604
10212
|
if (!isSupport(c.path, c.before, null)) continue;
|
|
9605
10213
|
out3.push(
|
|
9606
|
-
makeFinding(
|
|
10214
|
+
makeFinding(RULE15, policy, {
|
|
9607
10215
|
file: c.path,
|
|
9608
10216
|
message: "A test-support file was deleted (a helper, setup module or fixture under the test globs; it defines no test).",
|
|
9609
10217
|
evidence: c.path,
|
|
@@ -9613,7 +10221,7 @@ var init_test_support = __esm({
|
|
|
9613
10221
|
);
|
|
9614
10222
|
} else if (c.op === "rename" && c.oldPath && isSupport(c.oldPath, c.before, c.after) && !isProtected(c.path, policy, "tests")) {
|
|
9615
10223
|
out3.push(
|
|
9616
|
-
makeFinding(
|
|
10224
|
+
makeFinding(RULE15, policy, {
|
|
9617
10225
|
file: c.path,
|
|
9618
10226
|
message: `A test-support file was renamed out of the test globs (${c.oldPath} \u2192 ${c.path}).`,
|
|
9619
10227
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -9631,7 +10239,7 @@ var init_test_support = __esm({
|
|
|
9631
10239
|
const gone = [...new Set(beforeSig)].filter((l) => !afterSet.has(l) && !kept.includes(ws2(l)));
|
|
9632
10240
|
if (gone.length < MIN_REMOVED_LINES2) continue;
|
|
9633
10241
|
out3.push(
|
|
9634
|
-
makeFinding(
|
|
10242
|
+
makeFinding(RULE15, policy, {
|
|
9635
10243
|
file: c.path,
|
|
9636
10244
|
message: `Test-support content removed: ${gone.length} significant line(s) gone from a helper, setup module or fixture under the test globs.`,
|
|
9637
10245
|
evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -9658,6 +10266,7 @@ var init_detectors = __esm({
|
|
|
9658
10266
|
init_lint_suppression();
|
|
9659
10267
|
init_test_skip();
|
|
9660
10268
|
init_coverage_lowering();
|
|
10269
|
+
init_config_weakening();
|
|
9661
10270
|
init_coverage_exclusion();
|
|
9662
10271
|
init_ci_tampering();
|
|
9663
10272
|
init_hook_tampering();
|
|
@@ -9674,6 +10283,7 @@ var init_detectors = __esm({
|
|
|
9674
10283
|
lintSuppression,
|
|
9675
10284
|
testSkip,
|
|
9676
10285
|
coverageLowering,
|
|
10286
|
+
configWeakening,
|
|
9677
10287
|
coverageExclusion,
|
|
9678
10288
|
ciTampering,
|
|
9679
10289
|
hookTampering,
|
|
@@ -11147,7 +11757,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
|
|
|
11147
11757
|
};
|
|
11148
11758
|
}
|
|
11149
11759
|
function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
11150
|
-
if (!isEnabled(
|
|
11760
|
+
if (!isEnabled(RULE16, policy)) return [];
|
|
11151
11761
|
const byPath = /* @__PURE__ */ new Map();
|
|
11152
11762
|
for (const e of events) {
|
|
11153
11763
|
if (!isProtected(e.path, policy)) continue;
|
|
@@ -11167,7 +11777,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
11167
11777
|
const modeChurn = modes.size > 1;
|
|
11168
11778
|
if (!contentChurn && !modeChurn) continue;
|
|
11169
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 ")})`;
|
|
11170
|
-
const f = makeFinding(
|
|
11780
|
+
const f = makeFinding(RULE16, policy, {
|
|
11171
11781
|
file: path,
|
|
11172
11782
|
message: `Transient protected mutation: ${what}, leaving no net diff.`,
|
|
11173
11783
|
evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
|
|
@@ -11182,14 +11792,14 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
11182
11792
|
}
|
|
11183
11793
|
return out3;
|
|
11184
11794
|
}
|
|
11185
|
-
var
|
|
11795
|
+
var RULE16, MAX_EVENT_READ_BYTES, MAX_EVENT_SWEEP_BYTES, DEFAULT_EVENT_READ_IO;
|
|
11186
11796
|
var init_fs_events = __esm({
|
|
11187
11797
|
"src/detectors/fs-events.ts"() {
|
|
11188
11798
|
"use strict";
|
|
11189
11799
|
init_policy();
|
|
11190
11800
|
init_finding();
|
|
11191
11801
|
init_narrow();
|
|
11192
|
-
|
|
11802
|
+
RULE16 = "transient-protected-mutation";
|
|
11193
11803
|
MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
|
|
11194
11804
|
MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
|
|
11195
11805
|
DEFAULT_EVENT_READ_IO = {
|
|
@@ -12222,7 +12832,7 @@ var init_safe_write = __esm({
|
|
|
12222
12832
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
12223
12833
|
import { createHash as createHash6 } from "node:crypto";
|
|
12224
12834
|
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync4 } from "node:fs";
|
|
12225
|
-
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";
|
|
12226
12836
|
function gitConfig(cwd, key2) {
|
|
12227
12837
|
try {
|
|
12228
12838
|
return execFileSync6("git", ["config", "--get", key2], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -12281,13 +12891,13 @@ function readWorkflowMark(src) {
|
|
|
12281
12891
|
if (!m) return null;
|
|
12282
12892
|
return { version: m[1], hash: m[2], body: src.replace(WORKFLOW_MARK_RE, "") };
|
|
12283
12893
|
}
|
|
12284
|
-
function toolSet(
|
|
12285
|
-
const m = String(
|
|
12894
|
+
function toolSet(matcher2) {
|
|
12895
|
+
const m = String(matcher2 ?? "").trim();
|
|
12286
12896
|
if (m === "" || m === "*") return null;
|
|
12287
12897
|
return new Set(m.split(/[|,]/).map((t) => t.trim()).filter(Boolean));
|
|
12288
12898
|
}
|
|
12289
|
-
function missingTools(
|
|
12290
|
-
const have = toolSet(
|
|
12899
|
+
function missingTools(matcher2) {
|
|
12900
|
+
const have = toolSet(matcher2);
|
|
12291
12901
|
if (have === null) return [];
|
|
12292
12902
|
return PRE_MATCHER.split("|").filter((t) => !have.has(t));
|
|
12293
12903
|
}
|
|
@@ -12602,7 +13212,7 @@ function planPreCommit(cwd) {
|
|
|
12602
13212
|
}
|
|
12603
13213
|
const dir = hooksDir(cwd);
|
|
12604
13214
|
if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
|
|
12605
|
-
if (
|
|
13215
|
+
if (basename3(dir) === "_" && basename3(dirname4(dir)) === ".husky") {
|
|
12606
13216
|
path = join14(dirname4(dir), "pre-commit");
|
|
12607
13217
|
note = "husky";
|
|
12608
13218
|
} else {
|
|
@@ -13059,7 +13669,7 @@ import {
|
|
|
13059
13669
|
statSync as statSync3
|
|
13060
13670
|
} from "node:fs";
|
|
13061
13671
|
import {
|
|
13062
|
-
basename as
|
|
13672
|
+
basename as basename4,
|
|
13063
13673
|
delimiter,
|
|
13064
13674
|
dirname as dirname5,
|
|
13065
13675
|
isAbsolute as isAbsolute9,
|
|
@@ -13281,23 +13891,23 @@ function pythonName(name) {
|
|
|
13281
13891
|
return /^(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)$/.test(name.toLowerCase());
|
|
13282
13892
|
}
|
|
13283
13893
|
function looksPython(command, executable, token) {
|
|
13284
|
-
const exe = executable ?
|
|
13285
|
-
const raw = token ?
|
|
13894
|
+
const exe = executable ? basename4(executable) : "";
|
|
13895
|
+
const raw = token ? basename4(token) : "";
|
|
13286
13896
|
if (pythonName(exe) || pythonName(raw)) return true;
|
|
13287
13897
|
return /(?:^|[\s;&|()])(?:python(?:\d+(?:\.\d+)*)?|pytest|py\.test|pip(?:\d+(?:\.\d+)*)?)(?=$|[\s;&|()])/.test(command);
|
|
13288
13898
|
}
|
|
13289
13899
|
function inferredVenvFromToken(token, cwd) {
|
|
13290
13900
|
if (!token || !token.includes("/") && !token.includes("\\")) return null;
|
|
13291
13901
|
const path = isAbsolute9(token) ? token : resolve5(cwd, token);
|
|
13292
|
-
if (!pythonName(
|
|
13902
|
+
if (!pythonName(basename4(path))) return null;
|
|
13293
13903
|
const parent = dirname5(path);
|
|
13294
|
-
const parentName =
|
|
13904
|
+
const parentName = basename4(parent).toLowerCase();
|
|
13295
13905
|
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
13296
13906
|
const root = dirname5(parent);
|
|
13297
13907
|
return existsSync12(join15(root, "pyvenv.cfg")) ? root : null;
|
|
13298
13908
|
}
|
|
13299
13909
|
function unsupportedEcosystem(command, token) {
|
|
13300
|
-
const name = token ?
|
|
13910
|
+
const name = token ? basename4(token).toLowerCase() : "";
|
|
13301
13911
|
const raw = command.toLowerCase();
|
|
13302
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";
|
|
13303
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";
|
|
@@ -13307,7 +13917,7 @@ function unsupportedEcosystem(command, token) {
|
|
|
13307
13917
|
function inferredVenv(executable) {
|
|
13308
13918
|
if (!executable) return null;
|
|
13309
13919
|
const parent = dirname5(executable);
|
|
13310
|
-
const parentName =
|
|
13920
|
+
const parentName = basename4(parent).toLowerCase();
|
|
13311
13921
|
if (parentName !== "bin" && parentName !== "scripts") return null;
|
|
13312
13922
|
const root = dirname5(parent);
|
|
13313
13923
|
return existsSync12(join15(root, "pyvenv.cfg")) ? root : null;
|