tamperward 2.7.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +244 -70
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1186,6 +1186,50 @@ function trackedFiles(ctx) {
|
|
|
1186
1186
|
return files;
|
|
1187
1187
|
}
|
|
1188
1188
|
var branchCache = /* @__PURE__ */ new Map();
|
|
1189
|
+
var refsCache = /* @__PURE__ */ new Map();
|
|
1190
|
+
function branchRefs(ctx) {
|
|
1191
|
+
if (!ctx?.cwd) return null;
|
|
1192
|
+
const hit = refsCache.get(ctx.cwd);
|
|
1193
|
+
if (hit !== void 0) return hit;
|
|
1194
|
+
let refs = null;
|
|
1195
|
+
try {
|
|
1196
|
+
const out2 = execFileSync2("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads", "refs/remotes"], {
|
|
1197
|
+
cwd: ctx.cwd,
|
|
1198
|
+
encoding: "utf8",
|
|
1199
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1200
|
+
});
|
|
1201
|
+
refs = new Set(out2.split("\n").map((l) => l.trim()).filter(Boolean));
|
|
1202
|
+
} catch {
|
|
1203
|
+
refs = null;
|
|
1204
|
+
}
|
|
1205
|
+
refsCache.set(ctx.cwd, refs);
|
|
1206
|
+
return refs;
|
|
1207
|
+
}
|
|
1208
|
+
function branchExists(name, ctx) {
|
|
1209
|
+
const refs = branchRefs(ctx);
|
|
1210
|
+
if (!refs) return null;
|
|
1211
|
+
if (refs.has(name)) return true;
|
|
1212
|
+
for (const r of refs) if (r.endsWith("/" + name)) return true;
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
function revIsHead(rev, ctx) {
|
|
1216
|
+
if (!ctx?.cwd || rev.startsWith("-")) return null;
|
|
1217
|
+
const parse3 = (r) => {
|
|
1218
|
+
try {
|
|
1219
|
+
return execFileSync2("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${r}^{commit}`], {
|
|
1220
|
+
cwd: ctx.cwd,
|
|
1221
|
+
encoding: "utf8",
|
|
1222
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1223
|
+
}).trim();
|
|
1224
|
+
} catch {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
const head = parse3("HEAD");
|
|
1229
|
+
const target = parse3(rev);
|
|
1230
|
+
if (!head || !target) return null;
|
|
1231
|
+
return head === target;
|
|
1232
|
+
}
|
|
1189
1233
|
function defaultBranch(ctx) {
|
|
1190
1234
|
if (!ctx?.cwd) return null;
|
|
1191
1235
|
const hit = branchCache.get(ctx.cwd);
|
|
@@ -1205,15 +1249,16 @@ function defaultBranch(ctx) {
|
|
|
1205
1249
|
return name;
|
|
1206
1250
|
}
|
|
1207
1251
|
var dirOf = (t) => t.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1208
|
-
function containsProtected(token, policy, category, ctx) {
|
|
1252
|
+
function containsProtected(token, policy, category, ctx, except) {
|
|
1209
1253
|
const dir = dirOf(token);
|
|
1210
1254
|
if (!dir || dir === "." || dir === "/" || dir.startsWith("-")) return false;
|
|
1255
|
+
const counts = (p) => isProtected(p, policy, category) && !(except && isProtected(p, policy, except));
|
|
1211
1256
|
const files = trackedFiles(ctx);
|
|
1212
1257
|
if (files) {
|
|
1213
1258
|
const prefix = dir + "/";
|
|
1214
|
-
return files.some((f) => f.startsWith(prefix) &&
|
|
1259
|
+
return files.some((f) => f.startsWith(prefix) && counts(f));
|
|
1215
1260
|
}
|
|
1216
|
-
if (
|
|
1261
|
+
if (counts(dir + "/probe")) return true;
|
|
1217
1262
|
const base = dir.split("/").pop() ?? "";
|
|
1218
1263
|
return category === "tests" && /^(?:test|tests|__tests__|spec|specs)$/i.test(base);
|
|
1219
1264
|
}
|
|
@@ -1316,6 +1361,10 @@ function parseThresholds(src) {
|
|
|
1316
1361
|
if (!ts2.isPropertyAssignment(p)) continue;
|
|
1317
1362
|
const k = keyName(p.name);
|
|
1318
1363
|
if (k === null || isMetric(k)) continue;
|
|
1364
|
+
if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts2.SyntaxKind.TrueKeyword) {
|
|
1365
|
+
res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1319
1368
|
const m = metricsOf(p.initializer, resolve5);
|
|
1320
1369
|
if (!m) continue;
|
|
1321
1370
|
if (k === "global") res.global = merge(res.global, m);
|
|
@@ -1409,7 +1458,7 @@ function sectionText(src, key2) {
|
|
|
1409
1458
|
return "";
|
|
1410
1459
|
}
|
|
1411
1460
|
}
|
|
1412
|
-
var BENIGN_EXEMPTION = /node_modules|(?:^|[/!])(?:dist|build|out|coverage|tmp|temp|vendor|test|tests|__tests__|__mocks__|__fixtures__|fixtures?|mocks?|spec|specs|e2e|cypress|playwright|storybook|scripts?|examples?|docs?|bin|types?|typings)(?:\/|$|\*)|\.(?:test|spec|stories|d)\.|\.(?:config|setup)
|
|
1461
|
+
var BENIGN_EXEMPTION = /node_modules|(?:^|[/!])(?:dist|build|out|coverage|tmp|temp|vendor|test|tests|__tests__|__mocks__|__fixtures__|fixtures?|mocks?|spec|specs|e2e|cypress|playwright|storybook|scripts?|examples?|docs?|bin|types?|typings|generated|__generated__|codegen|migrations?)(?:\/|$|\*)|\.(?:test|spec|stories|d|gen|generated|pb|e2e-spec)\.|\.(?:config|setup)\.|(?:^|[/!*])(?:manage|wsgi|asgi|__init__)\.py$|(?:^|[/!*])main\.[jt]sx?$|\.git\b|\.cache|\.next|\.nuxt|\.output|\.idea|\.d\.ts|\.json$|\.md$|\.css$|\.s[ac]ss$/i;
|
|
1413
1462
|
function compareMetrics(scope, before, after, out2) {
|
|
1414
1463
|
if (!before) return;
|
|
1415
1464
|
for (const m of METRICS) {
|
|
@@ -1553,20 +1602,28 @@ var SIMPLE_FILES = [
|
|
|
1553
1602
|
];
|
|
1554
1603
|
var lessThan = (a, b) => a === "auto" ? b !== "auto" : b === "auto" ? false : a < b;
|
|
1555
1604
|
var numOf = (s) => s === "auto" ? "auto" : Number(s);
|
|
1556
|
-
function
|
|
1557
|
-
const out2 = [];
|
|
1605
|
+
function* scopedLines(src) {
|
|
1558
1606
|
const stack = [];
|
|
1559
1607
|
for (const line of src.split("\n")) {
|
|
1560
|
-
|
|
1561
|
-
if (
|
|
1562
|
-
const
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
while (stack.length && stack[stack.length - 1].ind >= ind) stack.pop();
|
|
1566
|
-
stack.push({ ind, key: m[2] });
|
|
1567
|
-
}
|
|
1568
|
-
scope = [...stack.slice(0, -1)].reverse().find((s) => /^(?:project|patch|changes)$/.test(s.key))?.key ?? "";
|
|
1608
|
+
const m = line.match(/^(\s*)([\w-]+):/);
|
|
1609
|
+
if (m) {
|
|
1610
|
+
const ind = m[1].length;
|
|
1611
|
+
while (stack.length && stack[stack.length - 1].ind >= ind) stack.pop();
|
|
1612
|
+
stack.push({ ind, key: m[2] });
|
|
1569
1613
|
}
|
|
1614
|
+
const scope = [...stack.slice(0, -1)].reverse().find((s) => /^(?:project|patch|changes)$/.test(s.key))?.key ?? "";
|
|
1615
|
+
yield [line, scope];
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function scopedKeys(src, key2) {
|
|
1619
|
+
const out2 = [];
|
|
1620
|
+
for (const [line, scope] of scopedLines(src)) if (key2.test(line)) out2.push(scope);
|
|
1621
|
+
return out2;
|
|
1622
|
+
}
|
|
1623
|
+
function numbersOf(src, key2, scoped = false) {
|
|
1624
|
+
const out2 = [];
|
|
1625
|
+
for (const [line, inScope] of scopedLines(src)) {
|
|
1626
|
+
const scope = scoped ? inScope : "";
|
|
1570
1627
|
if (key2.global) {
|
|
1571
1628
|
for (const m of line.matchAll(key2)) out2.push({ v: numOf(m[1]), scope });
|
|
1572
1629
|
} else {
|
|
@@ -1645,6 +1702,9 @@ function simpleNarrowings(path, before, after) {
|
|
|
1645
1702
|
const off2 = (s) => new Set(s.split("\n").map((l) => l.match(/^\s*(project|patch):\s*off\b/)?.[1]).filter((x) => !!x));
|
|
1646
1703
|
const had = off2(before);
|
|
1647
1704
|
for (const s of off2(after)) if (!had.has(s)) out2.push(`codecov status.${s} switched off`);
|
|
1705
|
+
const informational = (s) => new Set(scopedKeys(s, /^\s*informational:\s*true\b/).filter((scope) => scope !== "patch" && scope !== "changes"));
|
|
1706
|
+
const was = informational(before);
|
|
1707
|
+
for (const scope of informational(after)) if (!was.has(scope)) out2.push(`codecov ${scope || "project"} status informational: true added \u2014 the status can no longer fail`);
|
|
1648
1708
|
}
|
|
1649
1709
|
return out2;
|
|
1650
1710
|
}
|
|
@@ -1688,13 +1748,14 @@ var SWITCHES = [
|
|
|
1688
1748
|
re: /"?check-coverage"?\s*[:=]\s*false\b/,
|
|
1689
1749
|
message: "check-coverage: false \u2014 nyc will no longer fail on the thresholds.",
|
|
1690
1750
|
remediation: "Keep check-coverage on; raise real coverage instead."
|
|
1691
|
-
},
|
|
1692
|
-
{
|
|
1693
|
-
re: /^\s*informational:\s*true\b/,
|
|
1694
|
-
message: "informational: true added \u2014 the codecov status can no longer fail.",
|
|
1695
|
-
remediation: "Remove informational; a status that cannot fail is not a gate."
|
|
1696
1751
|
}
|
|
1697
1752
|
];
|
|
1753
|
+
function dirHasSpec(configPath, policy, ctx) {
|
|
1754
|
+
const files = trackedFiles(ctx);
|
|
1755
|
+
if (!files) return null;
|
|
1756
|
+
const root = configPath.includes("/") ? configPath.slice(0, configPath.lastIndexOf("/") + 1) : "";
|
|
1757
|
+
return files.some((f) => f.startsWith(root) && isProtected(f, policy, "tests"));
|
|
1758
|
+
}
|
|
1698
1759
|
var isJsConfig = (path) => /\.(?:[cm]?[jt]sx?|json)$/.test(path);
|
|
1699
1760
|
function gateMovedElsewhere(c, changes, policy) {
|
|
1700
1761
|
const before = c.before ?? "";
|
|
@@ -1787,6 +1848,7 @@ var coverageLowering = {
|
|
|
1787
1848
|
for (const s of SWITCHES) {
|
|
1788
1849
|
if (!s.re.test(l.content)) continue;
|
|
1789
1850
|
if (removed.some((r) => s.re.test(r.content))) continue;
|
|
1851
|
+
if (/passWithNoTests/.test(s.re.source) && dirHasSpec(c.path, policy, ctx) === false) continue;
|
|
1790
1852
|
out2.push(
|
|
1791
1853
|
makeFinding(RULE4, policy, {
|
|
1792
1854
|
file: c.path,
|
|
@@ -1838,7 +1900,7 @@ var RULE5 = "ci-tampering";
|
|
|
1838
1900
|
var USES = /^\s*-\s*uses:/;
|
|
1839
1901
|
var CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
|
|
1840
1902
|
var YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
|
|
1841
|
-
var INVOKES_TOOL = /(?:^\s*|[;&|`]\s*|\$\(\s*|\b(?:npx|yarn|pnpm|bunx?)\s+)(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward)\b/;
|
|
1903
|
+
var 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/;
|
|
1842
1904
|
var 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/;
|
|
1843
1905
|
function isActiveWorkflow(path) {
|
|
1844
1906
|
return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(path);
|
|
@@ -1847,8 +1909,19 @@ var invokesCheck = (line) => INVOKES_TOOL.test(line) || INVOKES_SCRIPT.test(line
|
|
|
1847
1909
|
function commandCore(line) {
|
|
1848
1910
|
return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses):\s*/, "").replace(/\s+/g, " ").trim();
|
|
1849
1911
|
}
|
|
1912
|
+
function foldExpressions(s) {
|
|
1913
|
+
return s.replace(/\$\{\{([^}]*)\}\}/g, (_m, e) => {
|
|
1914
|
+
const v = foldConst(e.trim());
|
|
1915
|
+
if (v !== void 0 && v !== TRUTHY) return String(v);
|
|
1916
|
+
return /\b(?:matrix|strategy|inputs)\./.test(e) ? MATRIX_TOKEN : EXPR_TOKEN;
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
var EXPR_TOKEN = "__expr__";
|
|
1920
|
+
var MATRIX_TOKEN = "__matrix__";
|
|
1921
|
+
var MATRIX_FLAG = new RegExp(`(?:^|\\s)-{1,2}[\\w-]+(?:=|\\s+)${MATRIX_TOKEN}(?:/\\S*)?(?=\\s|$)`, "g");
|
|
1850
1922
|
function canonical(core) {
|
|
1851
|
-
let s = core.replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
1923
|
+
let s = foldExpressions(core.replace(/^(['"])(.*)\1$/, "$2").trim()).replace(MATRIX_FLAG, " ").replace(/\s+/g, " ").trim();
|
|
1924
|
+
s = s.replace(/^(?:cd|pushd)\s+\S+\s*&&\s*/, "");
|
|
1852
1925
|
s = s.replace(/^(?:npx|pnpm\s+(?:exec|dlx)|yarn\s+(?:exec|dlx)|bunx|bun\s+x)\s+/, "exec ");
|
|
1853
1926
|
s = s.replace(/^(?:npm|pnpm|yarn|bun)\s+(?:run(?:-script)?\s+)?/, "exec ");
|
|
1854
1927
|
s = s.replace(/^exec\s+(?:t|tst)(?=\s|$)/, "exec test");
|
|
@@ -1878,8 +1951,8 @@ function checkKind(core) {
|
|
|
1878
1951
|
if (!invokesCheck(core)) return null;
|
|
1879
1952
|
const s = canonical(core);
|
|
1880
1953
|
if (/\btamperward\b/.test(s)) return "gate";
|
|
1881
|
-
if (/\b(?:lint|eslint)\b/i.test(s)) return "lint";
|
|
1882
|
-
if (/\b(?:typecheck|type-check|tsc)\b/i.test(s)) return "types";
|
|
1954
|
+
if (/\b(?:lint|eslint|biome|oxlint|golangci-lint|ruff)\b/i.test(s)) return "lint";
|
|
1955
|
+
if (/\b(?:typecheck|type-check|tsc|tsgo|mypy)\b|\bdeno\s+check\b/i.test(s)) return "types";
|
|
1883
1956
|
return "test";
|
|
1884
1957
|
}
|
|
1885
1958
|
function survives(removedCore, isUses, afterCores, addedCores) {
|
|
@@ -1888,13 +1961,15 @@ function survives(removedCore, isUses, afterCores, addedCores) {
|
|
|
1888
1961
|
return afterCores.some((a) => usesRef(a) === ref) ? { state: "kept" } : { state: "gone" };
|
|
1889
1962
|
}
|
|
1890
1963
|
const r = canonical(removedCore);
|
|
1964
|
+
const kind = checkKind(removedCore);
|
|
1965
|
+
const narrowed = (args) => kind === "test" && narrowedByPositional(args);
|
|
1891
1966
|
let neutralised;
|
|
1892
1967
|
for (const raw of afterCores) {
|
|
1893
1968
|
const a = canonical(raw);
|
|
1894
1969
|
if (a === r) return { state: "kept" };
|
|
1895
1970
|
if (a.startsWith(r + " ")) {
|
|
1896
1971
|
const rest = a.slice(r.length);
|
|
1897
|
-
if (NEUTRALISING_SUFFIX.test(rest) ||
|
|
1972
|
+
if (NEUTRALISING_SUFFIX.test(rest) || narrowed(rest)) neutralised ??= raw;
|
|
1898
1973
|
else return { state: "kept" };
|
|
1899
1974
|
} else if (r.startsWith(a + " ") && invokesCheck(a)) {
|
|
1900
1975
|
return { state: "kept" };
|
|
@@ -1903,7 +1978,6 @@ function survives(removedCore, isUses, afterCores, addedCores) {
|
|
|
1903
1978
|
if (w && (canonical(w[1]) === r || canonical(w[1]).startsWith(r + " "))) neutralised ??= raw;
|
|
1904
1979
|
}
|
|
1905
1980
|
}
|
|
1906
|
-
const kind = checkKind(removedCore);
|
|
1907
1981
|
let respelled = false;
|
|
1908
1982
|
for (const raw of addedCores) {
|
|
1909
1983
|
if (!kind || checkKind(raw) !== kind) continue;
|
|
@@ -1913,7 +1987,7 @@ function survives(removedCore, isUses, afterCores, addedCores) {
|
|
|
1913
1987
|
continue;
|
|
1914
1988
|
}
|
|
1915
1989
|
const args = a.slice(a.indexOf(" ") + 1 || a.length);
|
|
1916
|
-
if (a.includes(" ") && (NEUTRALISING_SUFFIX.test(" " + args) ||
|
|
1990
|
+
if (a.includes(" ") && (NEUTRALISING_SUFFIX.test(" " + args) || narrowed(args))) neutralised ??= raw;
|
|
1917
1991
|
else respelled = true;
|
|
1918
1992
|
}
|
|
1919
1993
|
if (respelled) return { state: "kept" };
|
|
@@ -2085,6 +2159,7 @@ function isTruthy(raw) {
|
|
|
2085
2159
|
const f = foldConst(m[1]);
|
|
2086
2160
|
return f !== void 0 && truthy(f);
|
|
2087
2161
|
}
|
|
2162
|
+
var SHELL_ERREXIT = /(?:^|\s)-[a-zA-Z]*e[a-zA-Z]*(?:\s|$)|-o\s+errexit\b/;
|
|
2088
2163
|
var indentOf = (l) => l.match(/^\s*/)[0].length;
|
|
2089
2164
|
var STEP_START = /^\s*-\s+(?:name|uses|run|if|id|with|env|shell|continue-on-error|working-directory|timeout-minutes):/;
|
|
2090
2165
|
function stepOf(lines, i) {
|
|
@@ -2110,8 +2185,10 @@ function jobOf(lines, i) {
|
|
|
2110
2185
|
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > hi)) e++;
|
|
2111
2186
|
return [h, e];
|
|
2112
2187
|
}
|
|
2113
|
-
var
|
|
2114
|
-
var
|
|
2188
|
+
var REPORTER_ACTION = /^\s*-?\s*uses:\s*\S*?(?:[\w-]*-reporter|[\w-]*-results?[\w-]*|upload-[\w-]*|codecov[\w-]*|coveralls[\w-]*)(?:@|\s|$)/i;
|
|
2189
|
+
var usesCheck = (l) => USES.test(l) && CHECK.test(l) && !REPORTER_ACTION.test(l);
|
|
2190
|
+
var stepHasCheck = (lines, range) => range === null || lines.slice(range[0], range[1]).some((l) => invokesCheck(l) || usesCheck(l));
|
|
2191
|
+
var jobHasCheck = (lines, range) => lines.slice(range[0], range[1]).some((l) => invokesCheck(l) || usesCheck(l) || /^\s*uses:\s*\S/.test(l));
|
|
2115
2192
|
function runBlocks(lines) {
|
|
2116
2193
|
const out2 = [];
|
|
2117
2194
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -2256,7 +2333,8 @@ function triggerNarrowings(before, after, opts) {
|
|
|
2256
2333
|
const now = after.branches.get(e);
|
|
2257
2334
|
if (!now || !after.events.has(e)) continue;
|
|
2258
2335
|
if (now.some(ALL)) continue;
|
|
2259
|
-
const
|
|
2336
|
+
const renamed = (b) => DEFAULT_BRANCH.test(b) && opts.hasBranch(b) !== false;
|
|
2337
|
+
const lostDefault = namesDefault(was) && !namesDefault(now) && !now.some(renamed);
|
|
2260
2338
|
const replaced = !namesDefault(was) && !namesDefault(now) && was.length > 0 && !was.some((b) => now.includes(b));
|
|
2261
2339
|
if (lostDefault || replaced) out2.push(`on.${e}.branches no longer names ${was.filter((b) => !now.includes(b)).join(", ")} (now [${now.join(", ")}])`);
|
|
2262
2340
|
}
|
|
@@ -2342,6 +2420,7 @@ var ciTampering = {
|
|
|
2342
2420
|
}
|
|
2343
2421
|
for (const l of addedLines(c)) {
|
|
2344
2422
|
if (!/^\s*-?\s*shell:\s*.*\{0\}/.test(l.content) || l.newLine == null) continue;
|
|
2423
|
+
if (SHELL_ERREXIT.test(uncommented(l.content.replace(/^\s*-?\s*shell:\s*/, "")))) continue;
|
|
2345
2424
|
const step = stepOf(afterLines, l.newLine - 1);
|
|
2346
2425
|
if (step && !stepHasCheck(afterLines, step)) continue;
|
|
2347
2426
|
out2.push(
|
|
@@ -2355,7 +2434,7 @@ var ciTampering = {
|
|
|
2355
2434
|
);
|
|
2356
2435
|
}
|
|
2357
2436
|
if (c.before != null) {
|
|
2358
|
-
triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx) };
|
|
2437
|
+
triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
|
|
2359
2438
|
for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
|
|
2360
2439
|
out2.push(
|
|
2361
2440
|
makeFinding(RULE5, policy, {
|
|
@@ -2380,7 +2459,7 @@ var ciTampering = {
|
|
|
2380
2459
|
}
|
|
2381
2460
|
for (const l of removedLines(c)) {
|
|
2382
2461
|
if (/^\s*#/.test(l.content)) continue;
|
|
2383
|
-
const isUsesLine =
|
|
2462
|
+
const isUsesLine = usesCheck(l.content);
|
|
2384
2463
|
const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? /^\s*-?\s*run:/.test(l.content) : true) && invokesCheck(l.content);
|
|
2385
2464
|
if (!isUsesLine && !isCheckCommand) continue;
|
|
2386
2465
|
const s = survives(commandCore(l.content), isUsesLine, afterCores, addedCores);
|
|
@@ -4442,21 +4521,20 @@ function collect(root, runner) {
|
|
|
4442
4521
|
const entries = take(key2, e).map((pattern) => ({ pattern, key: key2 }));
|
|
4443
4522
|
sel.ignore = [...sel.ignore ?? [], ...entries];
|
|
4444
4523
|
};
|
|
4524
|
+
let projectList = null;
|
|
4445
4525
|
const projects = (e) => {
|
|
4446
4526
|
sel.present = true;
|
|
4447
|
-
|
|
4527
|
+
projectList = e;
|
|
4528
|
+
};
|
|
4529
|
+
const visit = (node) => {
|
|
4530
|
+
if (ts3.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
|
|
4531
|
+
sel.present = true;
|
|
4448
4532
|
sel.opaque = true;
|
|
4449
|
-
return;
|
|
4450
4533
|
}
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
else sel.opaque = true;
|
|
4534
|
+
if (ts3.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts3.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
|
|
4535
|
+
sel.present = true;
|
|
4536
|
+
sel.opaque = true;
|
|
4455
4537
|
}
|
|
4456
|
-
sel.projects = list2;
|
|
4457
|
-
if (list2.some((p) => p.opaque)) sel.opaque = true;
|
|
4458
|
-
};
|
|
4459
|
-
const visit = (node) => {
|
|
4460
4538
|
if (ts3.isPropertyAssignment(node)) {
|
|
4461
4539
|
const k = keyName2(node.name);
|
|
4462
4540
|
if (runner === "jest") {
|
|
@@ -4476,7 +4554,7 @@ function collect(root, runner) {
|
|
|
4476
4554
|
} else if (ownerKey2(node) === "test") {
|
|
4477
4555
|
if (k === "include") sel.include = take(k, node.initializer);
|
|
4478
4556
|
else if (k === "exclude") ignores(k, node.initializer);
|
|
4479
|
-
else if (k === "dir") {
|
|
4557
|
+
else if (k === "dir" || k === "root") {
|
|
4480
4558
|
const { dir, opaque } = dirValue(node.initializer);
|
|
4481
4559
|
sel.present = true;
|
|
4482
4560
|
if (opaque) sel.opaque = true;
|
|
@@ -4490,8 +4568,68 @@ function collect(root, runner) {
|
|
|
4490
4568
|
ts3.forEachChild(node, visit);
|
|
4491
4569
|
};
|
|
4492
4570
|
visit(root);
|
|
4571
|
+
if (projectList) sel.projects = readProjects(projectList, sel, runner);
|
|
4572
|
+
if (sel.projects?.some((p) => p.opaque)) sel.opaque = true;
|
|
4493
4573
|
return sel;
|
|
4494
4574
|
}
|
|
4575
|
+
function unwrapConfigCall(e) {
|
|
4576
|
+
let x = e;
|
|
4577
|
+
while (ts3.isParenthesizedExpression(x) || ts3.isAsExpression(x) || x.kind === ts3.SyntaxKind.SatisfiesExpression) {
|
|
4578
|
+
x = x.expression;
|
|
4579
|
+
}
|
|
4580
|
+
if (ts3.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
|
|
4581
|
+
return x;
|
|
4582
|
+
}
|
|
4583
|
+
var JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
|
|
4584
|
+
function spreadsIntoConfig(node, runner) {
|
|
4585
|
+
const obj = node.parent;
|
|
4586
|
+
if (!ts3.isObjectLiteralExpression(obj)) return false;
|
|
4587
|
+
const keys = obj.properties.map((p) => ts3.isPropertyAssignment(p) ? keyName2(p.name) : null);
|
|
4588
|
+
if (runner === "vitest") {
|
|
4589
|
+
if (keys.includes("test")) return true;
|
|
4590
|
+
let q = obj.parent;
|
|
4591
|
+
while (q && (ts3.isParenthesizedExpression(q) || ts3.isAsExpression(q) || q.kind === ts3.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
4592
|
+
return !!q && ts3.isPropertyAssignment(q) && keyName2(q.name) === "test";
|
|
4593
|
+
}
|
|
4594
|
+
return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
|
|
4595
|
+
}
|
|
4596
|
+
function extendsOf(el) {
|
|
4597
|
+
for (const p of el.properties) {
|
|
4598
|
+
if (!ts3.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
|
|
4599
|
+
if (p.initializer.kind === ts3.SyntaxKind.TrueKeyword) return true;
|
|
4600
|
+
if (p.initializer.kind === ts3.SyntaxKind.FalseKeyword) return null;
|
|
4601
|
+
return "path";
|
|
4602
|
+
}
|
|
4603
|
+
return null;
|
|
4604
|
+
}
|
|
4605
|
+
function readProject(el, root, runner) {
|
|
4606
|
+
const p = collect(el, runner);
|
|
4607
|
+
const ext = extendsOf(el);
|
|
4608
|
+
if (ext === "path") p.opaque = true;
|
|
4609
|
+
else if (ext === true && root) {
|
|
4610
|
+
p.include ??= root.include;
|
|
4611
|
+
p.regex ??= root.regex;
|
|
4612
|
+
p.ignore ??= root.ignore;
|
|
4613
|
+
p.roots ??= root.roots;
|
|
4614
|
+
p.base ??= root.base;
|
|
4615
|
+
if (root.opaque) p.opaque = true;
|
|
4616
|
+
}
|
|
4617
|
+
p.present = true;
|
|
4618
|
+
return p;
|
|
4619
|
+
}
|
|
4620
|
+
function readProjects(e, root, runner) {
|
|
4621
|
+
if (!ts3.isArrayLiteralExpression(e)) {
|
|
4622
|
+
root.opaque = true;
|
|
4623
|
+
return [];
|
|
4624
|
+
}
|
|
4625
|
+
const list2 = [];
|
|
4626
|
+
for (const el of e.elements) {
|
|
4627
|
+
const lit = unwrapConfigCall(el);
|
|
4628
|
+
if (ts3.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
|
|
4629
|
+
else root.opaque = true;
|
|
4630
|
+
}
|
|
4631
|
+
return list2;
|
|
4632
|
+
}
|
|
4495
4633
|
function workspaceArray(sf) {
|
|
4496
4634
|
for (const st of sf.statements) {
|
|
4497
4635
|
let e;
|
|
@@ -4513,14 +4651,9 @@ function parseSelection(src, runner, path = "") {
|
|
|
4513
4651
|
if (runner === "vitest" && isWorkspaceFile(path)) {
|
|
4514
4652
|
const arr = workspaceArray(sf);
|
|
4515
4653
|
if (arr) {
|
|
4516
|
-
|
|
4517
|
-
for (const el of arr.elements) {
|
|
4518
|
-
if (ts3.isObjectLiteralExpression(el)) list2.push(collect(el, runner));
|
|
4519
|
-
else sel.opaque = true;
|
|
4520
|
-
}
|
|
4521
|
-
sel.projects = list2;
|
|
4654
|
+
sel.projects = readProjects(arr, sel, runner);
|
|
4522
4655
|
sel.present = true;
|
|
4523
|
-
if (
|
|
4656
|
+
if (sel.projects.some((p) => p.opaque)) sel.opaque = true;
|
|
4524
4657
|
} else if (src.trim()) sel.opaque = true;
|
|
4525
4658
|
} else sel = collect(sf, runner);
|
|
4526
4659
|
} catch {
|
|
@@ -4580,8 +4713,8 @@ function predicate(sel, runner) {
|
|
|
4580
4713
|
if (!inRoots(r)) return `roots no longer covers it (${JSON.stringify(sel.roots)})`;
|
|
4581
4714
|
const hit = ignoredBy(r);
|
|
4582
4715
|
if (hit) return `${hit.key === "exclude" ? "test.exclude" : hit.key} now matches it (${JSON.stringify(hit.pattern)})`;
|
|
4583
|
-
const key2 = sel.include ?
|
|
4584
|
-
return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? [])})`;
|
|
4716
|
+
const key2 = runner === "jest" ? sel.include ? "testMatch" : sel.regex ? "testRegex" : "testMatch (default)" : sel.include ? "test.include" : "test.include (default)";
|
|
4717
|
+
return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? includeList ?? [])})`;
|
|
4585
4718
|
}
|
|
4586
4719
|
};
|
|
4587
4720
|
}
|
|
@@ -4620,8 +4753,21 @@ function calleeName(expr) {
|
|
|
4620
4753
|
}
|
|
4621
4754
|
return null;
|
|
4622
4755
|
}
|
|
4623
|
-
var
|
|
4624
|
-
var
|
|
4756
|
+
var TABLE_METHOD = /^(?:each|for)$/;
|
|
4757
|
+
var isEachOf = (expr) => ts4.isPropertyAccessExpression(expr) && ts4.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
|
|
4758
|
+
var isDescribeEach = (expr) => ts4.isPropertyAccessExpression(expr) && ts4.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
|
|
4759
|
+
function loopRows(iterable) {
|
|
4760
|
+
if (ts4.isArrayLiteralExpression(iterable)) {
|
|
4761
|
+
let n = 0;
|
|
4762
|
+
let open = false;
|
|
4763
|
+
for (const el of iterable.elements) {
|
|
4764
|
+
if (ts4.isSpreadElement(el)) open = true;
|
|
4765
|
+
else n++;
|
|
4766
|
+
}
|
|
4767
|
+
return { n: Math.max(n, open ? 0 : 1), open };
|
|
4768
|
+
}
|
|
4769
|
+
return { n: 1, open: true };
|
|
4770
|
+
}
|
|
4625
4771
|
function templateRows(node) {
|
|
4626
4772
|
const rows = node.template.getText().split("\n").filter((l) => /\S/.test(l.replace(/[`]/g, ""))).length - 1;
|
|
4627
4773
|
return Math.max(rows, 1);
|
|
@@ -4659,7 +4805,23 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
|
|
|
4659
4805
|
let n = 0;
|
|
4660
4806
|
let open = false;
|
|
4661
4807
|
const visit = (node, mult) => {
|
|
4808
|
+
if (ts4.isForOfStatement(node)) {
|
|
4809
|
+
const r = loopRows(node.expression);
|
|
4810
|
+
if (r.open) open = true;
|
|
4811
|
+
visit(node.statement, mult * r.n);
|
|
4812
|
+
return;
|
|
4813
|
+
}
|
|
4814
|
+
if (ts4.isForStatement(node) || ts4.isForInStatement(node) || ts4.isWhileStatement(node) || ts4.isDoStatement(node)) {
|
|
4815
|
+
open = true;
|
|
4816
|
+
}
|
|
4662
4817
|
if (ts4.isCallExpression(node)) {
|
|
4818
|
+
const callee = node.expression;
|
|
4819
|
+
if (ts4.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
|
|
4820
|
+
const r = loopRows(callee.expression);
|
|
4821
|
+
if (r.open) open = true;
|
|
4822
|
+
for (const a of node.arguments) visit(a, mult * r.n);
|
|
4823
|
+
return;
|
|
4824
|
+
}
|
|
4663
4825
|
const head = node.expression;
|
|
4664
4826
|
if (ts4.isCallExpression(head) && isDescribeEach(head.expression)) {
|
|
4665
4827
|
const r = eachRows(head);
|
|
@@ -4707,6 +4869,12 @@ function runnerSamples(configPath, policy, ctx) {
|
|
|
4707
4869
|
const own = files.filter((f) => f.startsWith(root) && langOf(f) === "js" && isProtected(f, policy, "tests") && !OTHER_RUNNER_DIR.test(f)).map((f) => f.slice(root.length));
|
|
4708
4870
|
return own.length ? own : CANONICAL_SAMPLES;
|
|
4709
4871
|
}
|
|
4872
|
+
var WHOLE_SUITE_SCRIPT = /^test(?::ci)?$/;
|
|
4873
|
+
function excludesOtherRunner(flag, cmd) {
|
|
4874
|
+
if (flag !== "--exclude") return false;
|
|
4875
|
+
const values = [...cmd.matchAll(/--exclude(?:=|\s+)(['"]?)(\S+?)\1(?=\s|$)/g)].map((m) => m[2]);
|
|
4876
|
+
return values.length > 0 && values.every((v) => OTHER_RUNNER_DIR.test(v.replace(/^\.\//, "").replace(/(?:\/\*+)*\/?$/, "") + "/"));
|
|
4877
|
+
}
|
|
4710
4878
|
function scriptNarrowings(before, after, path) {
|
|
4711
4879
|
if ((path.split("/").pop() ?? path) !== "package.json") return [];
|
|
4712
4880
|
const scriptsOf = (src) => {
|
|
@@ -4721,7 +4889,7 @@ function scriptNarrowings(before, after, path) {
|
|
|
4721
4889
|
const now = scriptsOf(after);
|
|
4722
4890
|
const out2 = [];
|
|
4723
4891
|
for (const [name, cmd] of Object.entries(now)) {
|
|
4724
|
-
if (
|
|
4892
|
+
if (!WHOLE_SUITE_SCRIPT.test(name) || !/\b(?:jest|vitest)\b/.test(cmd)) continue;
|
|
4725
4893
|
const prev = was[name] ?? "";
|
|
4726
4894
|
const flags = new RegExp(SUITE_NARROWING_FLAGS.source, "g");
|
|
4727
4895
|
for (const m of cmd.matchAll(flags)) {
|
|
@@ -4729,6 +4897,7 @@ function scriptNarrowings(before, after, path) {
|
|
|
4729
4897
|
if (!flag) continue;
|
|
4730
4898
|
const one = new RegExp(SUITE_NARROWING_FLAGS.source);
|
|
4731
4899
|
if (one.test(prev) && prev.includes(flag)) continue;
|
|
4900
|
+
if (excludesOtherRunner(flag, cmd)) continue;
|
|
4732
4901
|
out2.push({ script: name, flag, before: prev, after: cmd });
|
|
4733
4902
|
break;
|
|
4734
4903
|
}
|
|
@@ -4760,10 +4929,11 @@ var testDeletion = {
|
|
|
4760
4929
|
certainty: "mechanical",
|
|
4761
4930
|
run(changes, policy, _view, ctx) {
|
|
4762
4931
|
const out2 = [];
|
|
4932
|
+
const isSpec = (p) => isProtected(p, policy, "tests") && !isProtected(p, policy, "snapshots");
|
|
4763
4933
|
const addedTestLines = /* @__PURE__ */ new Set();
|
|
4764
4934
|
let addedTestBlocks = 0;
|
|
4765
4935
|
for (const c of changes) {
|
|
4766
|
-
if (c.kind !== "file" || c.after == null || !
|
|
4936
|
+
if (c.kind !== "file" || c.after == null || !isSpec(c.path)) continue;
|
|
4767
4937
|
if (c.op === "add") {
|
|
4768
4938
|
for (const l of significantLines(c.after, c.path)) addedTestLines.add(l);
|
|
4769
4939
|
addedTestBlocks += countTests(c.after, c.path, true).min;
|
|
@@ -4781,7 +4951,7 @@ var testDeletion = {
|
|
|
4781
4951
|
};
|
|
4782
4952
|
for (const c of changes) {
|
|
4783
4953
|
if (c.kind === "file") {
|
|
4784
|
-
const isTest =
|
|
4954
|
+
const isTest = isSpec(c.path);
|
|
4785
4955
|
if (c.op === "delete" && isTest) {
|
|
4786
4956
|
if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
|
|
4787
4957
|
out2.push(
|
|
@@ -4792,7 +4962,7 @@ var testDeletion = {
|
|
|
4792
4962
|
remediation: "Fix the code under test, not the suite. Deleting a failing test to pass is the move this blocks."
|
|
4793
4963
|
})
|
|
4794
4964
|
);
|
|
4795
|
-
} else if (c.op === "rename" && c.oldPath &&
|
|
4965
|
+
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && !isTest) {
|
|
4796
4966
|
out2.push(
|
|
4797
4967
|
makeFinding(RULE7, policy, {
|
|
4798
4968
|
file: c.path,
|
|
@@ -4853,22 +5023,22 @@ var testDeletion = {
|
|
|
4853
5023
|
const testToks = toks.filter((t) => {
|
|
4854
5024
|
const resolved = root !== null && /(?:^|\/)\.{1,2}(?:\/|$)/.test(t) ? joinRoot(root, t) : null;
|
|
4855
5025
|
const probe = resolved ?? t;
|
|
4856
|
-
if (!
|
|
5026
|
+
if (!isSpec(probe)) return false;
|
|
4857
5027
|
if (!listing || root === null) return true;
|
|
4858
5028
|
const looksLikeFile = /\.[A-Za-z0-9]+$/.test(probe.split("/").pop() ?? "");
|
|
4859
5029
|
if (looksLikeFile || listing.includes(resolved ?? inRepo(t))) return true;
|
|
4860
|
-
return containsProtected(resolved ?? inRepo(t), policy, "tests", ctx);
|
|
5030
|
+
return containsProtected(resolved ?? inRepo(t), policy, "tests", ctx, "snapshots");
|
|
4861
5031
|
});
|
|
4862
|
-
const rootHasProtected = () => listing ? listing.some(
|
|
5032
|
+
const rootHasProtected = () => listing ? listing.some(isSpec) : true;
|
|
4863
5033
|
const dirToks = toks.filter((t) => {
|
|
4864
5034
|
if (t.startsWith("-") || testToks.includes(t)) return false;
|
|
4865
5035
|
const whole = /^(?:\.|\.\/|\*|\.\/\*)$/.test(t);
|
|
4866
5036
|
if (!whole && /[*?{}[\]]/.test(t)) return false;
|
|
4867
|
-
if (root === null) return whole ? false : containsProtected(t, policy, "tests", ctx);
|
|
5037
|
+
if (root === null) return whole ? false : containsProtected(t, policy, "tests", ctx, "snapshots");
|
|
4868
5038
|
const dir = whole ? root : joinRoot(root, t);
|
|
4869
5039
|
if (dir === null) return false;
|
|
4870
5040
|
if (dir === "") return rootHasProtected();
|
|
4871
|
-
return containsProtected(dir, policy, "tests", ctx);
|
|
5041
|
+
return containsProtected(dir, policy, "tests", ctx, "snapshots");
|
|
4872
5042
|
});
|
|
4873
5043
|
if (testToks.length === 0 && dirToks.length === 0) continue;
|
|
4874
5044
|
const named = [...testToks, ...dirToks];
|
|
@@ -4876,13 +5046,13 @@ var testDeletion = {
|
|
|
4876
5046
|
let why = null;
|
|
4877
5047
|
let evidence = seg;
|
|
4878
5048
|
const redirectTarget = seg.match(/>\s*(\S+)/)?.[1];
|
|
4879
|
-
const redirectsOntoTest = redirectTarget ?
|
|
5049
|
+
const redirectsOntoTest = redirectTarget ? isSpec(unquote(redirectTarget)) : false;
|
|
4880
5050
|
const findDeletes = cmd === "find" && /\s(?:-delete\b|-exec\s+(?:rm|unlink)\b|-execdir\s+(?:rm|unlink)\b)/.test(seg);
|
|
4881
5051
|
const gitRestoresOld = cmd === "git" && /^git\s+(?:checkout|restore)\b/.test(seg) && (() => {
|
|
4882
5052
|
const src = seg.match(/--source[= ]\s*(\S+)/)?.[1];
|
|
4883
5053
|
const revIdx = toks.findIndex((t, i) => i >= 2 && !t.startsWith("-") && !named.includes(t) && t !== "--");
|
|
4884
5054
|
const rev = src ?? (revIdx >= 0 ? toks[revIdx] : void 0);
|
|
4885
|
-
if (!rev || rev === "HEAD") return false;
|
|
5055
|
+
if (!rev || rev === "HEAD" || rev === "@" || revIsHead(rev, ctx) === true) return false;
|
|
4886
5056
|
if (src || seg.includes(" -- ")) return true;
|
|
4887
5057
|
return revIdx >= 0 && toks.some((t, i) => i > revIdx && named.includes(t));
|
|
4888
5058
|
})();
|
|
@@ -4898,7 +5068,7 @@ var testDeletion = {
|
|
|
4898
5068
|
why = "truncate empties a test file";
|
|
4899
5069
|
} else if (redirectsOntoTest) {
|
|
4900
5070
|
why = "a redirect overwrites/empties a test file";
|
|
4901
|
-
} else if (cmd === "cp" && testToks.length > 0 &&
|
|
5071
|
+
} else if (cmd === "cp" && testToks.length > 0 && isSpec(toks[toks.length - 1] ?? "") && !isSpec(toks[toks.length - 2] ?? "")) {
|
|
4902
5072
|
why = "cp overwrites a test file with something else";
|
|
4903
5073
|
} else if (cmd === "tee" && !toks.includes("-a") && !toks.includes("--append")) {
|
|
4904
5074
|
why = "tee overwrites a test file";
|
|
@@ -4908,7 +5078,7 @@ var testDeletion = {
|
|
|
4908
5078
|
why = "git checkout <rev> replaces a test file with an older version";
|
|
4909
5079
|
} else if (/\bmv\b/.test(seg)) {
|
|
4910
5080
|
const dest = toks[toks.length - 1];
|
|
4911
|
-
if (dest && testToks.length > 0 && !
|
|
5081
|
+
if (dest && testToks.length > 0 && !isSpec(dest)) {
|
|
4912
5082
|
why = "mv renames a test file out of the test glob";
|
|
4913
5083
|
evidence = `${testToks[0]} \u2192 ${dest}`;
|
|
4914
5084
|
} else if (dest && testToks.length === 0 && dirToks[0] !== dest && leavesGlob(dirToks[0] ?? "", dest, policy, ctx)) {
|
|
@@ -4945,7 +5115,7 @@ function eachTables(src, path) {
|
|
|
4945
5115
|
try {
|
|
4946
5116
|
const sf = ts5.createSourceFile("spec.ts", src, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
|
|
4947
5117
|
const visit = (node) => {
|
|
4948
|
-
if (ts5.isCallExpression(node) && ts5.isPropertyAccessExpression(node.expression) && node.expression.name.text
|
|
5118
|
+
if (ts5.isCallExpression(node) && ts5.isPropertyAccessExpression(node.expression) && /^(?:each|for)$/.test(node.expression.name.text) && ts5.isIdentifier(node.expression.expression) && /^(?:it|test|describe)$/.test(node.expression.expression.text)) {
|
|
4949
5119
|
const arg = node.arguments[0];
|
|
4950
5120
|
if (arg && ts5.isArrayLiteralExpression(arg)) {
|
|
4951
5121
|
for (const el of arg.elements) {
|
|
@@ -4966,6 +5136,7 @@ function eachTables(src, path) {
|
|
|
4966
5136
|
}
|
|
4967
5137
|
return { rows, open };
|
|
4968
5138
|
}
|
|
5139
|
+
var TEST_DIR_FILE = /(?:^|\/)(?:test|tests|__tests__|spec|specs)\//;
|
|
4969
5140
|
function significantLinesOrdered(src, path) {
|
|
4970
5141
|
const out2 = [];
|
|
4971
5142
|
const lang = langOf(path);
|
|
@@ -4986,11 +5157,14 @@ var testContentRemoval = {
|
|
|
4986
5157
|
for (const c of changes) {
|
|
4987
5158
|
if (c.kind !== "file" || c.after == null) continue;
|
|
4988
5159
|
if (isProtected(c.path, policy, "tests")) keptPool += ws(significantLinesOrdered(c.after, c.path).join("\n"));
|
|
4989
|
-
|
|
5160
|
+
let addedHere = "";
|
|
5161
|
+
if (c.op === "add") addedHere = ws(c.after);
|
|
4990
5162
|
else if (c.before != null) {
|
|
4991
5163
|
const had = new Set(c.before.split("\n").map((l) => l.trim()));
|
|
4992
|
-
|
|
5164
|
+
addedHere = ws(c.after.split("\n").filter((l) => !had.has(l.trim())).join("\n"));
|
|
4993
5165
|
}
|
|
5166
|
+
addedPool += addedHere;
|
|
5167
|
+
if (!isProtected(c.path, policy, "tests") && TEST_DIR_FILE.test(c.path) && langOf(c.path) === "js") keptPool += addedHere;
|
|
4994
5168
|
}
|
|
4995
5169
|
for (const c of changes) {
|
|
4996
5170
|
if (c.kind !== "file" || c.op !== "modify" && c.op !== "rename" || c.before == null || c.after == null) continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "hexrift",
|