tamperward 2.16.5 → 2.17.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/README.md +16 -6
- package/dist/cli/index.js +700 -133
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1385,6 +1385,7 @@ var lintSuppression = {
|
|
|
1385
1385
|
};
|
|
1386
1386
|
|
|
1387
1387
|
// src/detectors/test-skip.ts
|
|
1388
|
+
import ts2 from "typescript";
|
|
1388
1389
|
var RULE3 = "test-skip";
|
|
1389
1390
|
var acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
|
|
1390
1391
|
var JS_RUNNER = "\\b(?:it|test|describe|suite)";
|
|
@@ -1393,10 +1394,10 @@ var PATTERNS2 = {
|
|
|
1393
1394
|
js: [
|
|
1394
1395
|
// The skip/only/todo family on a runner, by dot-or-bracket access, with one optional
|
|
1395
1396
|
// vitest concurrency modifier hop: test.skip, test . skip, test['skip'], it.concurrent.skip.
|
|
1396
|
-
{ re: new RegExp(JS_RUNNER + acc(JS_MOD) + "?" + acc("skip|only|todo")), why: "a .skip/.only/.todo marker" },
|
|
1397
|
-
{ re: new RegExp(JS_RUNNER + acc("skipIf|runIf") + "\\s*\\("), why: "a .skipIf()/.runIf() condition (the test runs only when the condition allows)" },
|
|
1398
|
-
{ re: new RegExp(JS_RUNNER + acc("fails|failing") + "\\s*\\("), why: "a .fails/.failing marker (the test now passes by failing)" },
|
|
1399
|
-
{ re: /\b(?:it|test|describe)\.each\s*\(\s*\[\s*\]\s*\)/, why: "an empty .each table (no case ever runs)" },
|
|
1397
|
+
{ re: new RegExp(JS_RUNNER + acc(JS_MOD) + "?" + acc("skip|only|todo")), why: "a .skip/.only/.todo marker", astOwned: true },
|
|
1398
|
+
{ re: new RegExp(JS_RUNNER + acc("skipIf|runIf") + "\\s*\\("), why: "a .skipIf()/.runIf() condition (the test runs only when the condition allows)", astOwned: true },
|
|
1399
|
+
{ re: new RegExp(JS_RUNNER + acc("fails|failing") + "\\s*\\("), why: "a .fails/.failing marker (the test now passes by failing)", astOwned: true },
|
|
1400
|
+
{ re: /\b(?:it|test|describe)\.each\s*\(\s*\[\s*\]\s*\)/, why: "an empty .each table (no case ever runs)", astOwned: true },
|
|
1400
1401
|
{ re: /\b(?:xit|xtest|xdescribe)\s*\(/, why: "an x-prefixed disabled test" },
|
|
1401
1402
|
{ re: /\b(?:fit|ftest|fdescribe)\s*\(/, why: "an f-prefixed focused test (narrows the suite)" },
|
|
1402
1403
|
{ re: /\bpending\(\s*\)/, why: "a pending() marker" },
|
|
@@ -1406,7 +1407,7 @@ var PATTERNS2 = {
|
|
|
1406
1407
|
// In options position (right after the title) ANY value but false/0 skips — a
|
|
1407
1408
|
// non-literal `{ skip: process.env.CI }` included. Elsewhere on a line only the
|
|
1408
1409
|
// literal forms count: `list({ skip: 1, take: 5 })` is pagination.
|
|
1409
|
-
{ re: /\b(?:it|test|describe|suite)\s*\(\s*(['"`])(?:(?!\1).)*\1\s*,\s*\{[^}]*?\bskip:(?!\s*(?:false|0)\b)/, why: "a { skip: ... } option disabling the test (node:test options form)" },
|
|
1410
|
+
{ re: /\b(?:it|test|describe|suite)\s*\(\s*(['"`])(?:(?!\1).)*\1\s*,\s*\{[^}]*?\bskip:(?!\s*(?:false|0)\b)/, why: "a { skip: ... } option disabling the test (node:test options form)", astOwned: true },
|
|
1410
1411
|
{ re: /\bskip:\s*(?:true|['"`])/, why: "a { skip: ... } option disabling the test (node:test options form)" },
|
|
1411
1412
|
{ re: /\btodo:\s*(?:true|['"`])/, why: "a { todo: ... } option (the test no longer fails the run)" },
|
|
1412
1413
|
{ re: new RegExp("\\b(?:t|ctx|context|this)" + acc("skip") + "\\s*\\("), why: "a runtime t.skip()/this.skip() call" }
|
|
@@ -1477,6 +1478,343 @@ function matchesOutsideString(p, content, lang) {
|
|
|
1477
1478
|
}
|
|
1478
1479
|
return false;
|
|
1479
1480
|
}
|
|
1481
|
+
var JS_RUNNERS = /* @__PURE__ */ new Set(["it", "test", "describe", "suite"]);
|
|
1482
|
+
var JS_CHAIN_MODIFIERS = /* @__PURE__ */ new Set(["concurrent", "sequential", "shuffle"]);
|
|
1483
|
+
var JS_TEST_MODULES = /* @__PURE__ */ new Set([
|
|
1484
|
+
"vitest",
|
|
1485
|
+
"@jest/globals",
|
|
1486
|
+
"node:test",
|
|
1487
|
+
"node:test/promises",
|
|
1488
|
+
"mocha",
|
|
1489
|
+
"bun:test",
|
|
1490
|
+
"@playwright/test"
|
|
1491
|
+
]);
|
|
1492
|
+
function scriptKind(path) {
|
|
1493
|
+
if (/\.tsx$/i.test(path)) return ts2.ScriptKind.TSX;
|
|
1494
|
+
if (/\.jsx$/i.test(path)) return ts2.ScriptKind.JSX;
|
|
1495
|
+
if (/\.(?:js|mjs|cjs)$/i.test(path)) return ts2.ScriptKind.JS;
|
|
1496
|
+
return ts2.ScriptKind.TS;
|
|
1497
|
+
}
|
|
1498
|
+
function addedLineNumbers(c) {
|
|
1499
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
1500
|
+
for (const h of c.hunks) {
|
|
1501
|
+
for (const line of h.lines) {
|
|
1502
|
+
if (line.type === "add" && line.newLine != null) out2.add(line.newLine);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return out2;
|
|
1506
|
+
}
|
|
1507
|
+
function moduleName(expr) {
|
|
1508
|
+
return ts2.isStringLiteralLike(expr) ? expr.text : null;
|
|
1509
|
+
}
|
|
1510
|
+
function requiredModule(expr) {
|
|
1511
|
+
if (!ts2.isCallExpression(expr) || !ts2.isIdentifier(expr.expression) || expr.expression.text !== "require" || expr.arguments.length !== 1) return null;
|
|
1512
|
+
return moduleName(expr.arguments[0]);
|
|
1513
|
+
}
|
|
1514
|
+
function astContext(path, source) {
|
|
1515
|
+
const rootName = path.startsWith("/") ? path : "/tamperward/" + path;
|
|
1516
|
+
const sf = ts2.createSourceFile(
|
|
1517
|
+
rootName,
|
|
1518
|
+
source,
|
|
1519
|
+
ts2.ScriptTarget.Latest,
|
|
1520
|
+
true,
|
|
1521
|
+
scriptKind(path)
|
|
1522
|
+
);
|
|
1523
|
+
const diagnostics = sf.parseDiagnostics ?? [];
|
|
1524
|
+
if (diagnostics.length > 0) return null;
|
|
1525
|
+
const options = {
|
|
1526
|
+
allowJs: true,
|
|
1527
|
+
checkJs: false,
|
|
1528
|
+
noLib: true,
|
|
1529
|
+
noResolve: true,
|
|
1530
|
+
target: ts2.ScriptTarget.Latest,
|
|
1531
|
+
module: ts2.ModuleKind.ESNext
|
|
1532
|
+
};
|
|
1533
|
+
const host = ts2.createCompilerHost(options, true);
|
|
1534
|
+
host.getSourceFile = (fileName) => fileName === rootName ? sf : void 0;
|
|
1535
|
+
host.fileExists = (fileName) => fileName === rootName;
|
|
1536
|
+
host.readFile = (fileName) => fileName === rootName ? source : void 0;
|
|
1537
|
+
host.writeFile = () => {
|
|
1538
|
+
};
|
|
1539
|
+
host.getDefaultLibFileName = () => "";
|
|
1540
|
+
host.getCurrentDirectory = () => "/tamperward";
|
|
1541
|
+
host.getCanonicalFileName = (fileName) => fileName;
|
|
1542
|
+
host.useCaseSensitiveFileNames = () => true;
|
|
1543
|
+
host.getNewLine = () => "\n";
|
|
1544
|
+
const program = ts2.createProgram([rootName], options, host);
|
|
1545
|
+
return { sf, checker: program.getTypeChecker() };
|
|
1546
|
+
}
|
|
1547
|
+
function symbolAt(ctx, node) {
|
|
1548
|
+
return ctx.checker.getSymbolAtLocation(node) ?? null;
|
|
1549
|
+
}
|
|
1550
|
+
function importAliases(ctx) {
|
|
1551
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
1552
|
+
const simpleAliases = [];
|
|
1553
|
+
const add = (id, causes = [id]) => {
|
|
1554
|
+
const sym = symbolAt(ctx, id);
|
|
1555
|
+
if (sym) out2.set(sym, { causes });
|
|
1556
|
+
};
|
|
1557
|
+
for (const stmt of ctx.sf.statements) {
|
|
1558
|
+
if (ts2.isImportDeclaration(stmt) && ts2.isStringLiteralLike(stmt.moduleSpecifier)) {
|
|
1559
|
+
if (!JS_TEST_MODULES.has(stmt.moduleSpecifier.text)) continue;
|
|
1560
|
+
const clause = stmt.importClause;
|
|
1561
|
+
if (!clause) continue;
|
|
1562
|
+
if (clause.name && stmt.moduleSpecifier.text.startsWith("node:test")) add(clause.name);
|
|
1563
|
+
const bindings = clause.namedBindings;
|
|
1564
|
+
if (bindings && ts2.isNamedImports(bindings)) {
|
|
1565
|
+
for (const el of bindings.elements) {
|
|
1566
|
+
const imported = el.propertyName?.text ?? el.name.text;
|
|
1567
|
+
if (JS_RUNNERS.has(imported)) add(el.name);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
if (!ts2.isVariableStatement(stmt)) continue;
|
|
1573
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
1574
|
+
if (ts2.isObjectBindingPattern(decl.name) && decl.initializer) {
|
|
1575
|
+
const mod = requiredModule(decl.initializer);
|
|
1576
|
+
if (!mod || !JS_TEST_MODULES.has(mod)) continue;
|
|
1577
|
+
for (const el of decl.name.elements) {
|
|
1578
|
+
if (!ts2.isIdentifier(el.name)) continue;
|
|
1579
|
+
const imported = el.propertyName && ts2.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
1580
|
+
if (JS_RUNNERS.has(imported)) add(el.name);
|
|
1581
|
+
}
|
|
1582
|
+
} else if (ts2.isIdentifier(decl.name) && decl.initializer && ts2.isIdentifier(decl.initializer)) {
|
|
1583
|
+
simpleAliases.push({ local: decl.name, source: decl.initializer });
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
for (let pass = 0; pass < simpleAliases.length + 1; pass++) {
|
|
1588
|
+
let changed = false;
|
|
1589
|
+
for (const alias of simpleAliases) {
|
|
1590
|
+
const localSym = symbolAt(ctx, alias.local);
|
|
1591
|
+
if (!localSym || out2.has(localSym)) continue;
|
|
1592
|
+
const sourceSym = symbolAt(ctx, alias.source);
|
|
1593
|
+
const sourceBinding = sourceSym ? out2.get(sourceSym) : void 0;
|
|
1594
|
+
const implicitGlobal = sourceSym == null && JS_RUNNERS.has(alias.source.text);
|
|
1595
|
+
if (!sourceBinding && !implicitGlobal) continue;
|
|
1596
|
+
out2.set(localSym, {
|
|
1597
|
+
causes: [alias.local, ...sourceBinding?.causes ?? []]
|
|
1598
|
+
});
|
|
1599
|
+
changed = true;
|
|
1600
|
+
}
|
|
1601
|
+
if (!changed) break;
|
|
1602
|
+
}
|
|
1603
|
+
return out2;
|
|
1604
|
+
}
|
|
1605
|
+
function topLevelStaticStrings(ctx) {
|
|
1606
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1607
|
+
for (const stmt of ctx.sf.statements) {
|
|
1608
|
+
if (!ts2.isVariableStatement(stmt)) continue;
|
|
1609
|
+
const isConst = (stmt.declarationList.flags & ts2.NodeFlags.Const) !== 0;
|
|
1610
|
+
if (!isConst) continue;
|
|
1611
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
1612
|
+
if (!ts2.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
1613
|
+
const sym = symbolAt(ctx, decl.name);
|
|
1614
|
+
if (sym) pending.set(sym, { decl: decl.name, expr: decl.initializer });
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1618
|
+
const resolving = /* @__PURE__ */ new Set();
|
|
1619
|
+
const valueOf = (expr) => {
|
|
1620
|
+
if (ts2.isStringLiteralLike(expr)) return { value: expr.text, causes: [] };
|
|
1621
|
+
if (ts2.isParenthesizedExpression(expr)) return valueOf(expr.expression);
|
|
1622
|
+
if (ts2.isBinaryExpression(expr) && expr.operatorToken.kind === ts2.SyntaxKind.PlusToken) {
|
|
1623
|
+
const left = valueOf(expr.left);
|
|
1624
|
+
const right = valueOf(expr.right);
|
|
1625
|
+
if (!left || !right) return null;
|
|
1626
|
+
return { value: left.value + right.value, causes: [...left.causes, ...right.causes] };
|
|
1627
|
+
}
|
|
1628
|
+
if (ts2.isIdentifier(expr)) {
|
|
1629
|
+
const sym = symbolAt(ctx, expr);
|
|
1630
|
+
if (!sym) return null;
|
|
1631
|
+
const cached = resolved.get(sym);
|
|
1632
|
+
if (cached) return cached;
|
|
1633
|
+
const item = pending.get(sym);
|
|
1634
|
+
if (!item || resolving.has(sym)) return null;
|
|
1635
|
+
resolving.add(sym);
|
|
1636
|
+
const nested = valueOf(item.expr);
|
|
1637
|
+
resolving.delete(sym);
|
|
1638
|
+
if (!nested) return null;
|
|
1639
|
+
const result = { value: nested.value, causes: [item.decl, ...nested.causes] };
|
|
1640
|
+
resolved.set(sym, result);
|
|
1641
|
+
return result;
|
|
1642
|
+
}
|
|
1643
|
+
return null;
|
|
1644
|
+
};
|
|
1645
|
+
for (const [sym, item] of pending) {
|
|
1646
|
+
if (resolved.has(sym)) continue;
|
|
1647
|
+
resolving.add(sym);
|
|
1648
|
+
const nested = valueOf(item.expr);
|
|
1649
|
+
resolving.delete(sym);
|
|
1650
|
+
if (nested) resolved.set(sym, { value: nested.value, causes: [item.decl, ...nested.causes] });
|
|
1651
|
+
}
|
|
1652
|
+
return resolved;
|
|
1653
|
+
}
|
|
1654
|
+
function staticPropertyName(expr, ctx, strings) {
|
|
1655
|
+
if (ts2.isStringLiteralLike(expr)) return { name: expr.text, causes: [] };
|
|
1656
|
+
if (ts2.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx, strings);
|
|
1657
|
+
if (ts2.isBinaryExpression(expr) && expr.operatorToken.kind === ts2.SyntaxKind.PlusToken) {
|
|
1658
|
+
const left = staticPropertyName(expr.left, ctx, strings);
|
|
1659
|
+
const right = staticPropertyName(expr.right, ctx, strings);
|
|
1660
|
+
if (!left || !right) return null;
|
|
1661
|
+
return { name: left.name + right.name, causes: [...left.causes, ...right.causes] };
|
|
1662
|
+
}
|
|
1663
|
+
if (ts2.isIdentifier(expr)) {
|
|
1664
|
+
const sym = symbolAt(ctx, expr);
|
|
1665
|
+
if (!sym) return null;
|
|
1666
|
+
const value = strings.get(sym);
|
|
1667
|
+
return value ? { name: value.value, causes: value.causes } : null;
|
|
1668
|
+
}
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
function runnerChain(expr, ctx, runners, strings) {
|
|
1672
|
+
const props = [];
|
|
1673
|
+
const causes = [];
|
|
1674
|
+
let cur = expr;
|
|
1675
|
+
let terminalNode = expr;
|
|
1676
|
+
while (ts2.isPropertyAccessExpression(cur) || ts2.isElementAccessExpression(cur)) {
|
|
1677
|
+
if (ts2.isPropertyAccessExpression(cur)) {
|
|
1678
|
+
props.unshift(cur.name.text);
|
|
1679
|
+
if (props.length === 1) terminalNode = cur.name;
|
|
1680
|
+
cur = cur.expression;
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
if (!cur.argumentExpression) return null;
|
|
1684
|
+
const property = staticPropertyName(cur.argumentExpression, ctx, strings);
|
|
1685
|
+
if (!property) return null;
|
|
1686
|
+
props.unshift(property.name);
|
|
1687
|
+
causes.push(...property.causes);
|
|
1688
|
+
if (props.length === 1) terminalNode = cur.argumentExpression;
|
|
1689
|
+
cur = cur.expression;
|
|
1690
|
+
}
|
|
1691
|
+
if (!ts2.isIdentifier(cur)) return null;
|
|
1692
|
+
const sym = symbolAt(ctx, cur);
|
|
1693
|
+
if (sym) {
|
|
1694
|
+
const binding = runners.get(sym);
|
|
1695
|
+
if (!binding) return null;
|
|
1696
|
+
causes.push(...binding.causes);
|
|
1697
|
+
} else if (!JS_RUNNERS.has(cur.text)) {
|
|
1698
|
+
return null;
|
|
1699
|
+
}
|
|
1700
|
+
return { root: cur.text, props, terminalNode, causes };
|
|
1701
|
+
}
|
|
1702
|
+
function optionDisables(value) {
|
|
1703
|
+
return !(value.kind === ts2.SyntaxKind.FalseKeyword || ts2.isNumericLiteral(value) && Number(value.text) === 0);
|
|
1704
|
+
}
|
|
1705
|
+
function shorthandOptionDisables(prop, ctx) {
|
|
1706
|
+
const sym = ctx.checker.getShorthandAssignmentValueSymbol(prop) ?? symbolAt(ctx, prop.name);
|
|
1707
|
+
const decl = sym?.valueDeclaration ?? sym?.declarations?.find((d) => ts2.isVariableDeclaration(d));
|
|
1708
|
+
if (!decl || !ts2.isVariableDeclaration(decl) || !decl.initializer) return true;
|
|
1709
|
+
return optionDisables(decl.initializer);
|
|
1710
|
+
}
|
|
1711
|
+
function structuralNodeKey(node, sf) {
|
|
1712
|
+
const children = node.getChildren(sf);
|
|
1713
|
+
const text = node.text;
|
|
1714
|
+
if (children.length === 0) {
|
|
1715
|
+
return typeof text === "string" ? `${node.kind}:${JSON.stringify(text)}` : String(node.kind);
|
|
1716
|
+
}
|
|
1717
|
+
return `${node.kind}(${children.map((child) => structuralNodeKey(child, sf)).join(",")})`;
|
|
1718
|
+
}
|
|
1719
|
+
function semanticSkipHits(ctx) {
|
|
1720
|
+
const runners = importAliases(ctx);
|
|
1721
|
+
const strings = topLevelStaticStrings(ctx);
|
|
1722
|
+
const hits = [];
|
|
1723
|
+
const callOrdinals = /* @__PURE__ */ new Map();
|
|
1724
|
+
const visit = (node) => {
|
|
1725
|
+
if (ts2.isCallExpression(node)) {
|
|
1726
|
+
const callText = node.getText(ctx.sf).trim();
|
|
1727
|
+
const callIdentity = structuralNodeKey(node, ctx.sf);
|
|
1728
|
+
const ordinal = (callOrdinals.get(callIdentity) ?? 0) + 1;
|
|
1729
|
+
callOrdinals.set(callIdentity, ordinal);
|
|
1730
|
+
const callKey = `${callIdentity}\0${ordinal}`;
|
|
1731
|
+
const chain = runnerChain(node.expression, ctx, runners, strings);
|
|
1732
|
+
if (chain) {
|
|
1733
|
+
const { props, terminalNode, causes } = chain;
|
|
1734
|
+
const terminal = props.at(-1);
|
|
1735
|
+
const push = (target, extraCauses, why) => {
|
|
1736
|
+
hits.push({
|
|
1737
|
+
semanticKey: `${callKey}\0${why}`,
|
|
1738
|
+
terminalNode: target,
|
|
1739
|
+
causeNodes: [...causes, ...extraCauses],
|
|
1740
|
+
why,
|
|
1741
|
+
evidence: callText
|
|
1742
|
+
});
|
|
1743
|
+
};
|
|
1744
|
+
if (terminal && (terminal === "skip" || terminal === "only" || terminal === "todo") && props.slice(0, -1).every((p) => JS_CHAIN_MODIFIERS.has(p))) {
|
|
1745
|
+
push(terminalNode, [], "a .skip/.only/.todo marker");
|
|
1746
|
+
} else if (terminal && ["skipIf", "runIf"].includes(terminal) && props.length === 1) {
|
|
1747
|
+
push(terminalNode, [], "a .skipIf()/.runIf() condition (the test runs only when the condition allows)");
|
|
1748
|
+
} else if (terminal && ["fails", "failing"].includes(terminal) && props.length === 1) {
|
|
1749
|
+
push(terminalNode, [], "a .fails/.failing marker (the test now passes by failing)");
|
|
1750
|
+
} else if (terminal === "each" && props.length === 1 && node.arguments.length > 0 && ts2.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
|
|
1751
|
+
push(terminalNode, [], "an empty .each table (no case ever runs)");
|
|
1752
|
+
}
|
|
1753
|
+
if (props.length === 0) {
|
|
1754
|
+
for (const arg of node.arguments) {
|
|
1755
|
+
if (!ts2.isObjectLiteralExpression(arg)) continue;
|
|
1756
|
+
for (const prop of arg.properties) {
|
|
1757
|
+
if (ts2.isPropertyAssignment(prop)) {
|
|
1758
|
+
let name = null;
|
|
1759
|
+
let propertyCauses = [];
|
|
1760
|
+
if (ts2.isIdentifier(prop.name) || ts2.isStringLiteralLike(prop.name)) {
|
|
1761
|
+
name = prop.name.text;
|
|
1762
|
+
} else if (ts2.isComputedPropertyName(prop.name)) {
|
|
1763
|
+
const resolved = staticPropertyName(prop.name.expression, ctx, strings);
|
|
1764
|
+
name = resolved?.name ?? null;
|
|
1765
|
+
propertyCauses = resolved?.causes ?? [];
|
|
1766
|
+
}
|
|
1767
|
+
if (!name || !["skip", "todo", "only"].includes(name)) continue;
|
|
1768
|
+
if (!optionDisables(prop.initializer)) continue;
|
|
1769
|
+
const why = name === "only" ? "an { only: ... } option focusing the test" : name === "todo" ? "a { todo: ... } option (the test no longer fails the run)" : "a { skip: ... } option disabling the test";
|
|
1770
|
+
push(prop.name, propertyCauses, why);
|
|
1771
|
+
} else if (ts2.isShorthandPropertyAssignment(prop) && ["skip", "todo", "only"].includes(prop.name.text)) {
|
|
1772
|
+
if (!shorthandOptionDisables(prop, ctx)) continue;
|
|
1773
|
+
push(prop.name, [], `a { ${prop.name.text} } option that conditionally narrows the test run`);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
ts2.forEachChild(node, visit);
|
|
1781
|
+
};
|
|
1782
|
+
visit(ctx.sf);
|
|
1783
|
+
return hits;
|
|
1784
|
+
}
|
|
1785
|
+
function astSkipHits(c) {
|
|
1786
|
+
if (c.after == null) return { hits: [], authoritative: false };
|
|
1787
|
+
const added = addedLineNumbers(c);
|
|
1788
|
+
if (added.size === 0) return { hits: [], authoritative: false };
|
|
1789
|
+
const afterCtx = astContext(c.path, c.after);
|
|
1790
|
+
if (!afterCtx) return { hits: [], authoritative: false };
|
|
1791
|
+
const beforeCtx = c.before == null ? null : astContext(c.path, c.before);
|
|
1792
|
+
const beforeKeys = beforeCtx == null ? null : new Set(semanticSkipHits(beforeCtx).map((hit) => hit.semanticKey));
|
|
1793
|
+
const lineOf = (node) => afterCtx.sf.getLineAndCharacterOfPosition(node.getStart(afterCtx.sf)).line + 1;
|
|
1794
|
+
const hits = [];
|
|
1795
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1796
|
+
for (const hit of semanticSkipHits(afterCtx)) {
|
|
1797
|
+
const directLine = lineOf(hit.terminalNode);
|
|
1798
|
+
let findingLine = added.has(directLine) ? directLine : null;
|
|
1799
|
+
if (findingLine == null) {
|
|
1800
|
+
for (const cause of hit.causeNodes) {
|
|
1801
|
+
const line = lineOf(cause);
|
|
1802
|
+
if (added.has(line)) {
|
|
1803
|
+
findingLine = line;
|
|
1804
|
+
break;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
if (findingLine == null) continue;
|
|
1808
|
+
if (beforeKeys == null) continue;
|
|
1809
|
+
}
|
|
1810
|
+
if (beforeKeys?.has(hit.semanticKey)) continue;
|
|
1811
|
+
const dedupe = `${findingLine}\0${hit.semanticKey}`;
|
|
1812
|
+
if (seen.has(dedupe)) continue;
|
|
1813
|
+
seen.add(dedupe);
|
|
1814
|
+
hits.push({ line: findingLine, why: hit.why, evidence: hit.evidence });
|
|
1815
|
+
}
|
|
1816
|
+
return { hits, authoritative: true };
|
|
1817
|
+
}
|
|
1480
1818
|
var testSkip = {
|
|
1481
1819
|
id: RULE3,
|
|
1482
1820
|
surface: ["file"],
|
|
@@ -1488,9 +1826,29 @@ var testSkip = {
|
|
|
1488
1826
|
if (!isProtected(c.path, policy, "tests")) continue;
|
|
1489
1827
|
const lang = langOf(c.path);
|
|
1490
1828
|
const patterns = PATTERNS2[lang ?? "js"];
|
|
1829
|
+
const astHitLines = /* @__PURE__ */ new Set();
|
|
1830
|
+
let astAuthoritative = false;
|
|
1831
|
+
if (lang === "js" && c.after != null) {
|
|
1832
|
+
const analysis = astSkipHits(c);
|
|
1833
|
+
astAuthoritative = analysis.authoritative;
|
|
1834
|
+
for (const hit of analysis.hits) {
|
|
1835
|
+
astHitLines.add(hit.line);
|
|
1836
|
+
out2.push(
|
|
1837
|
+
makeFinding(RULE3, policy, {
|
|
1838
|
+
file: c.path,
|
|
1839
|
+
line: hit.line,
|
|
1840
|
+
message: `Test skipped or narrowed: ${hit.why}.`,
|
|
1841
|
+
evidence: hit.evidence,
|
|
1842
|
+
remediation: "Make the test pass rather than skipping it. If it is genuinely obsolete, a human must sign off."
|
|
1843
|
+
})
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1491
1847
|
for (const l of addedLines(c)) {
|
|
1848
|
+
if (l.newLine != null && astHitLines.has(l.newLine)) continue;
|
|
1492
1849
|
const comment = isCommentLine(l.content.trim(), lang);
|
|
1493
1850
|
for (const p of patterns) {
|
|
1851
|
+
if (astAuthoritative && p.astOwned) continue;
|
|
1494
1852
|
if (comment && !p.comment) continue;
|
|
1495
1853
|
if (matchesOutsideString(p, l.content, lang)) {
|
|
1496
1854
|
out2.push(
|
|
@@ -1513,7 +1871,7 @@ var testSkip = {
|
|
|
1513
1871
|
|
|
1514
1872
|
// src/detectors/coverage-lowering.ts
|
|
1515
1873
|
import picomatch2 from "picomatch";
|
|
1516
|
-
import
|
|
1874
|
+
import ts3 from "typescript";
|
|
1517
1875
|
|
|
1518
1876
|
// src/detectors/repo.ts
|
|
1519
1877
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
@@ -1641,29 +1999,29 @@ var METRICS = ["branches", "functions", "lines", "statements"];
|
|
|
1641
1999
|
var isMetric = (k) => METRICS.includes(k);
|
|
1642
2000
|
var norm = (p) => p.replace(/^\.\//, "");
|
|
1643
2001
|
function keyName(name) {
|
|
1644
|
-
if (
|
|
2002
|
+
if (ts3.isIdentifier(name) || ts3.isStringLiteral(name) || ts3.isNumericLiteral(name)) return name.text;
|
|
1645
2003
|
return null;
|
|
1646
2004
|
}
|
|
1647
2005
|
function numericOf(e) {
|
|
1648
|
-
if (
|
|
1649
|
-
if (
|
|
1650
|
-
if (
|
|
2006
|
+
if (ts3.isNumericLiteral(e)) return Number(e.text);
|
|
2007
|
+
if (ts3.isStringLiteral(e) && /^-?\d+(?:\.\d+)?$/.test(e.text.trim())) return Number(e.text);
|
|
2008
|
+
if (ts3.isPrefixUnaryExpression(e) && e.operator === ts3.SyntaxKind.MinusToken && ts3.isNumericLiteral(e.operand)) {
|
|
1651
2009
|
return -Number(e.operand.text);
|
|
1652
2010
|
}
|
|
1653
2011
|
return void 0;
|
|
1654
2012
|
}
|
|
1655
2013
|
function metricsOf(expr, resolve9) {
|
|
1656
2014
|
const obj = resolve9(expr);
|
|
1657
|
-
if (!
|
|
2015
|
+
if (!ts3.isObjectLiteralExpression(obj)) return void 0;
|
|
1658
2016
|
const out2 = { values: {}, opaque: false };
|
|
1659
2017
|
let any = false;
|
|
1660
2018
|
for (const p of obj.properties) {
|
|
1661
|
-
if (
|
|
2019
|
+
if (ts3.isSpreadAssignment(p)) {
|
|
1662
2020
|
out2.opaque = true;
|
|
1663
2021
|
any = true;
|
|
1664
2022
|
continue;
|
|
1665
2023
|
}
|
|
1666
|
-
if (!
|
|
2024
|
+
if (!ts3.isPropertyAssignment(p)) continue;
|
|
1667
2025
|
const k = keyName(p.name);
|
|
1668
2026
|
if (k === null || !isMetric(k)) continue;
|
|
1669
2027
|
any = true;
|
|
@@ -1675,34 +2033,34 @@ function metricsOf(expr, resolve9) {
|
|
|
1675
2033
|
}
|
|
1676
2034
|
function underKey(node, key2) {
|
|
1677
2035
|
for (let p = node.parent; p; p = p.parent) {
|
|
1678
|
-
if (
|
|
2036
|
+
if (ts3.isPropertyAssignment(p) && keyName(p.name) === key2) return true;
|
|
1679
2037
|
}
|
|
1680
2038
|
return false;
|
|
1681
2039
|
}
|
|
1682
2040
|
function ownerKey(node) {
|
|
1683
2041
|
const obj = node.parent;
|
|
1684
|
-
if (!obj || !
|
|
2042
|
+
if (!obj || !ts3.isObjectLiteralExpression(obj)) return null;
|
|
1685
2043
|
let q = obj.parent;
|
|
1686
|
-
while (q && (
|
|
1687
|
-
return q &&
|
|
2044
|
+
while (q && (ts3.isParenthesizedExpression(q) || ts3.isAsExpression(q) || q.kind === ts3.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
2045
|
+
return q && ts3.isPropertyAssignment(q) ? keyName(q.name) : null;
|
|
1688
2046
|
}
|
|
1689
2047
|
function asExpression(src) {
|
|
1690
2048
|
const t = src.trim();
|
|
1691
2049
|
return t.startsWith("{") ? `(${t})` : src;
|
|
1692
2050
|
}
|
|
1693
2051
|
function sourceOf(src) {
|
|
1694
|
-
return
|
|
2052
|
+
return ts3.createSourceFile("cfg.ts", asExpression(src), ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
|
|
1695
2053
|
}
|
|
1696
2054
|
function resolverFor(sf) {
|
|
1697
2055
|
const consts = /* @__PURE__ */ new Map();
|
|
1698
2056
|
for (const st of sf.statements) {
|
|
1699
|
-
if (!
|
|
2057
|
+
if (!ts3.isVariableStatement(st)) continue;
|
|
1700
2058
|
for (const d of st.declarationList.declarations) {
|
|
1701
|
-
if (
|
|
2059
|
+
if (ts3.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
|
|
1702
2060
|
}
|
|
1703
2061
|
}
|
|
1704
2062
|
return (e) => {
|
|
1705
|
-
if (
|
|
2063
|
+
if (ts3.isIdentifier(e)) {
|
|
1706
2064
|
const bound = consts.get(e.text);
|
|
1707
2065
|
return bound ?? e;
|
|
1708
2066
|
}
|
|
@@ -1715,25 +2073,25 @@ function parseThresholds(src) {
|
|
|
1715
2073
|
const sf = sourceOf(src);
|
|
1716
2074
|
const resolve9 = resolverFor(sf);
|
|
1717
2075
|
const visit = (node) => {
|
|
1718
|
-
if (
|
|
2076
|
+
if (ts3.isPropertyAssignment(node)) {
|
|
1719
2077
|
const key2 = keyName(node.name);
|
|
1720
2078
|
if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
|
|
1721
2079
|
res.present = true;
|
|
1722
2080
|
const init = resolve9(node.initializer);
|
|
1723
|
-
if (!
|
|
2081
|
+
if (!ts3.isObjectLiteralExpression(init)) {
|
|
1724
2082
|
res.opaque = true;
|
|
1725
2083
|
} else {
|
|
1726
2084
|
const flat = metricsOf(init, resolve9);
|
|
1727
2085
|
if (flat) res.global = merge(res.global, flat);
|
|
1728
2086
|
for (const p of init.properties) {
|
|
1729
|
-
if (
|
|
2087
|
+
if (ts3.isSpreadAssignment(p)) {
|
|
1730
2088
|
res.opaque = true;
|
|
1731
2089
|
continue;
|
|
1732
2090
|
}
|
|
1733
|
-
if (!
|
|
2091
|
+
if (!ts3.isPropertyAssignment(p)) continue;
|
|
1734
2092
|
const k = keyName(p.name);
|
|
1735
2093
|
if (k === null || isMetric(k)) continue;
|
|
1736
|
-
if (key2 === "thresholds" && k === "100" && p.initializer.kind ===
|
|
2094
|
+
if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts3.SyntaxKind.TrueKeyword) {
|
|
1737
2095
|
res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
|
|
1738
2096
|
continue;
|
|
1739
2097
|
}
|
|
@@ -1745,7 +2103,7 @@ function parseThresholds(src) {
|
|
|
1745
2103
|
}
|
|
1746
2104
|
}
|
|
1747
2105
|
}
|
|
1748
|
-
|
|
2106
|
+
ts3.forEachChild(node, visit);
|
|
1749
2107
|
};
|
|
1750
2108
|
visit(sf);
|
|
1751
2109
|
} catch {
|
|
@@ -1756,12 +2114,12 @@ function merge(a, b) {
|
|
|
1756
2114
|
return { values: { ...a?.values ?? {}, ...b.values }, opaque: (a?.opaque ?? false) || b.opaque };
|
|
1757
2115
|
}
|
|
1758
2116
|
function literals(e) {
|
|
1759
|
-
if (
|
|
1760
|
-
if (!
|
|
2117
|
+
if (ts3.isStringLiteral(e)) return { items: [e.text], opaque: false };
|
|
2118
|
+
if (!ts3.isArrayLiteralExpression(e)) return { items: [], opaque: true };
|
|
1761
2119
|
const items = [];
|
|
1762
2120
|
let opaque = false;
|
|
1763
2121
|
for (const el of e.elements) {
|
|
1764
|
-
if (
|
|
2122
|
+
if (ts3.isStringLiteral(el) || ts3.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
|
|
1765
2123
|
else opaque = true;
|
|
1766
2124
|
}
|
|
1767
2125
|
return { items, opaque };
|
|
@@ -1772,7 +2130,7 @@ function parseLists(src) {
|
|
|
1772
2130
|
const sf = sourceOf(src);
|
|
1773
2131
|
const resolve9 = resolverFor(sf);
|
|
1774
2132
|
const visit = (node) => {
|
|
1775
|
-
if (
|
|
2133
|
+
if (ts3.isPropertyAssignment(node)) {
|
|
1776
2134
|
const k = keyName(node.name);
|
|
1777
2135
|
const vitestCoverage = ownerKey(node) === "coverage";
|
|
1778
2136
|
if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
|
|
@@ -1786,7 +2144,7 @@ function parseLists(src) {
|
|
|
1786
2144
|
if (opaque) res.opaqueIgnore = true;
|
|
1787
2145
|
}
|
|
1788
2146
|
}
|
|
1789
|
-
|
|
2147
|
+
ts3.forEachChild(node, visit);
|
|
1790
2148
|
};
|
|
1791
2149
|
visit(sf);
|
|
1792
2150
|
} catch {
|
|
@@ -1797,15 +2155,15 @@ function parseSwitches(src) {
|
|
|
1797
2155
|
const res = {};
|
|
1798
2156
|
try {
|
|
1799
2157
|
const sf = sourceOf(src);
|
|
1800
|
-
const bool = (e) => e.kind ===
|
|
2158
|
+
const bool = (e) => e.kind === ts3.SyntaxKind.TrueKeyword ? true : e.kind === ts3.SyntaxKind.FalseKeyword ? false : void 0;
|
|
1801
2159
|
const visit = (node) => {
|
|
1802
|
-
if (
|
|
2160
|
+
if (ts3.isPropertyAssignment(node)) {
|
|
1803
2161
|
const k = keyName(node.name);
|
|
1804
2162
|
const owner = ownerKey(node);
|
|
1805
2163
|
if (k === "enabled" && owner === "coverage") res.enabled = bool(node.initializer);
|
|
1806
2164
|
else if (k === "perFile" && owner === "thresholds" && underKey(node, "coverage")) res.perFile = bool(node.initializer);
|
|
1807
2165
|
}
|
|
1808
|
-
|
|
2166
|
+
ts3.forEachChild(node, visit);
|
|
1809
2167
|
};
|
|
1810
2168
|
visit(sf);
|
|
1811
2169
|
} catch {
|
|
@@ -1818,11 +2176,11 @@ function sectionText(src, key2) {
|
|
|
1818
2176
|
let out2 = "";
|
|
1819
2177
|
const visit = (node) => {
|
|
1820
2178
|
if (out2) return;
|
|
1821
|
-
if (
|
|
2179
|
+
if (ts3.isPropertyAssignment(node) && keyName(node.name) === key2 && ts3.isObjectLiteralExpression(node.initializer)) {
|
|
1822
2180
|
out2 = node.initializer.getText(sf);
|
|
1823
2181
|
return;
|
|
1824
2182
|
}
|
|
1825
|
-
|
|
2183
|
+
ts3.forEachChild(node, visit);
|
|
1826
2184
|
};
|
|
1827
2185
|
visit(sf);
|
|
1828
2186
|
return out2;
|
|
@@ -5172,11 +5530,11 @@ var hookTampering = {
|
|
|
5172
5530
|
};
|
|
5173
5531
|
|
|
5174
5532
|
// src/detectors/test-deletion.ts
|
|
5175
|
-
import
|
|
5533
|
+
import ts5 from "typescript";
|
|
5176
5534
|
|
|
5177
5535
|
// src/detectors/suite-config.ts
|
|
5178
5536
|
import picomatch3 from "picomatch";
|
|
5179
|
-
import
|
|
5537
|
+
import ts4 from "typescript";
|
|
5180
5538
|
var JEST_DEFAULT_MATCH = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"];
|
|
5181
5539
|
var JEST_DEFAULT_IGNORE = ["/node_modules/"];
|
|
5182
5540
|
var VITEST_DEFAULT_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
|
|
@@ -5207,18 +5565,18 @@ var PYTEST_CANONICAL_SAMPLES = [
|
|
|
5207
5565
|
"tests/conftest.py"
|
|
5208
5566
|
];
|
|
5209
5567
|
function keyName2(name) {
|
|
5210
|
-
if (
|
|
5568
|
+
if (ts4.isIdentifier(name) || ts4.isStringLiteral(name)) return name.text;
|
|
5211
5569
|
return null;
|
|
5212
5570
|
}
|
|
5213
|
-
var isStr = (e) =>
|
|
5571
|
+
var isStr = (e) => ts4.isStringLiteral(e) || ts4.isNoSubstitutionTemplateLiteral(e);
|
|
5214
5572
|
function literals2(e) {
|
|
5215
5573
|
if (isStr(e)) return { items: [e.text], opaque: false };
|
|
5216
|
-
if (!
|
|
5574
|
+
if (!ts4.isArrayLiteralExpression(e)) return { items: [], opaque: true };
|
|
5217
5575
|
const items = [];
|
|
5218
5576
|
let opaque = false;
|
|
5219
5577
|
for (const el of e.elements) {
|
|
5220
5578
|
if (isStr(el)) items.push(el.text);
|
|
5221
|
-
else if (
|
|
5579
|
+
else if (ts4.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
|
|
5222
5580
|
items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
5223
5581
|
} else opaque = true;
|
|
5224
5582
|
}
|
|
@@ -5226,10 +5584,10 @@ function literals2(e) {
|
|
|
5226
5584
|
}
|
|
5227
5585
|
function ownerKey2(node) {
|
|
5228
5586
|
const obj = node.parent;
|
|
5229
|
-
if (!obj || !
|
|
5587
|
+
if (!obj || !ts4.isObjectLiteralExpression(obj)) return null;
|
|
5230
5588
|
let q = obj.parent;
|
|
5231
|
-
while (q && (
|
|
5232
|
-
return q &&
|
|
5589
|
+
while (q && (ts4.isParenthesizedExpression(q) || ts4.isAsExpression(q) || q.kind === ts4.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
5590
|
+
return q && ts4.isPropertyAssignment(q) ? keyName2(q.name) : null;
|
|
5233
5591
|
}
|
|
5234
5592
|
function asExpression2(src) {
|
|
5235
5593
|
const t = src.trim();
|
|
@@ -5370,15 +5728,15 @@ function collect(root, runner) {
|
|
|
5370
5728
|
projectList = e;
|
|
5371
5729
|
};
|
|
5372
5730
|
const visit = (node) => {
|
|
5373
|
-
if (
|
|
5731
|
+
if (ts4.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
|
|
5374
5732
|
sel.present = true;
|
|
5375
5733
|
sel.opaque = true;
|
|
5376
5734
|
}
|
|
5377
|
-
if (
|
|
5735
|
+
if (ts4.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts4.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
|
|
5378
5736
|
sel.present = true;
|
|
5379
5737
|
sel.opaque = true;
|
|
5380
5738
|
}
|
|
5381
|
-
if (
|
|
5739
|
+
if (ts4.isPropertyAssignment(node)) {
|
|
5382
5740
|
const k = keyName2(node.name);
|
|
5383
5741
|
if (runner === "jest") {
|
|
5384
5742
|
if (k === "testMatch") sel.include = take(k, node.initializer);
|
|
@@ -5408,7 +5766,7 @@ function collect(root, runner) {
|
|
|
5408
5766
|
}
|
|
5409
5767
|
}
|
|
5410
5768
|
}
|
|
5411
|
-
|
|
5769
|
+
ts4.forEachChild(node, visit);
|
|
5412
5770
|
};
|
|
5413
5771
|
visit(root);
|
|
5414
5772
|
if (projectList) sel.projects = readProjects(projectList, sel, runner);
|
|
@@ -5417,30 +5775,30 @@ function collect(root, runner) {
|
|
|
5417
5775
|
}
|
|
5418
5776
|
function unwrapConfigCall(e) {
|
|
5419
5777
|
let x = e;
|
|
5420
|
-
while (
|
|
5778
|
+
while (ts4.isParenthesizedExpression(x) || ts4.isAsExpression(x) || x.kind === ts4.SyntaxKind.SatisfiesExpression) {
|
|
5421
5779
|
x = x.expression;
|
|
5422
5780
|
}
|
|
5423
|
-
if (
|
|
5781
|
+
if (ts4.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
|
|
5424
5782
|
return x;
|
|
5425
5783
|
}
|
|
5426
5784
|
var JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
|
|
5427
5785
|
function spreadsIntoConfig(node, runner) {
|
|
5428
5786
|
const obj = node.parent;
|
|
5429
|
-
if (!
|
|
5430
|
-
const keys = obj.properties.map((p) =>
|
|
5787
|
+
if (!ts4.isObjectLiteralExpression(obj)) return false;
|
|
5788
|
+
const keys = obj.properties.map((p) => ts4.isPropertyAssignment(p) ? keyName2(p.name) : null);
|
|
5431
5789
|
if (runner === "vitest") {
|
|
5432
5790
|
if (keys.includes("test")) return true;
|
|
5433
5791
|
let q = obj.parent;
|
|
5434
|
-
while (q && (
|
|
5435
|
-
return !!q &&
|
|
5792
|
+
while (q && (ts4.isParenthesizedExpression(q) || ts4.isAsExpression(q) || q.kind === ts4.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
5793
|
+
return !!q && ts4.isPropertyAssignment(q) && keyName2(q.name) === "test";
|
|
5436
5794
|
}
|
|
5437
5795
|
return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
|
|
5438
5796
|
}
|
|
5439
5797
|
function extendsOf(el) {
|
|
5440
5798
|
for (const p of el.properties) {
|
|
5441
|
-
if (!
|
|
5442
|
-
if (p.initializer.kind ===
|
|
5443
|
-
if (p.initializer.kind ===
|
|
5799
|
+
if (!ts4.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
|
|
5800
|
+
if (p.initializer.kind === ts4.SyntaxKind.TrueKeyword) return true;
|
|
5801
|
+
if (p.initializer.kind === ts4.SyntaxKind.FalseKeyword) return null;
|
|
5444
5802
|
return "path";
|
|
5445
5803
|
}
|
|
5446
5804
|
return null;
|
|
@@ -5461,14 +5819,14 @@ function readProject(el, root, runner) {
|
|
|
5461
5819
|
return p;
|
|
5462
5820
|
}
|
|
5463
5821
|
function readProjects(e, root, runner) {
|
|
5464
|
-
if (!
|
|
5822
|
+
if (!ts4.isArrayLiteralExpression(e)) {
|
|
5465
5823
|
root.opaque = true;
|
|
5466
5824
|
return [];
|
|
5467
5825
|
}
|
|
5468
5826
|
const list2 = [];
|
|
5469
5827
|
for (const el of e.elements) {
|
|
5470
5828
|
const lit = unwrapConfigCall(el);
|
|
5471
|
-
if (
|
|
5829
|
+
if (ts4.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
|
|
5472
5830
|
else root.opaque = true;
|
|
5473
5831
|
}
|
|
5474
5832
|
return list2;
|
|
@@ -5476,14 +5834,14 @@ function readProjects(e, root, runner) {
|
|
|
5476
5834
|
function workspaceArray(sf) {
|
|
5477
5835
|
for (const st of sf.statements) {
|
|
5478
5836
|
let e;
|
|
5479
|
-
if (
|
|
5480
|
-
else if (
|
|
5837
|
+
if (ts4.isExportAssignment(st)) e = st.expression;
|
|
5838
|
+
else if (ts4.isExpressionStatement(st) && ts4.isBinaryExpression(st.expression) && /module\.exports/.test(st.expression.left.getText())) e = st.expression.right;
|
|
5481
5839
|
if (!e) continue;
|
|
5482
|
-
while (
|
|
5840
|
+
while (ts4.isParenthesizedExpression(e) || ts4.isAsExpression(e) || e.kind === ts4.SyntaxKind.SatisfiesExpression) {
|
|
5483
5841
|
e = e.expression;
|
|
5484
5842
|
}
|
|
5485
|
-
if (
|
|
5486
|
-
return
|
|
5843
|
+
if (ts4.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
|
|
5844
|
+
return ts4.isArrayLiteralExpression(e) ? e : null;
|
|
5487
5845
|
}
|
|
5488
5846
|
return null;
|
|
5489
5847
|
}
|
|
@@ -5491,7 +5849,7 @@ function parseSelection(src, runner, path = "") {
|
|
|
5491
5849
|
let sel = empty();
|
|
5492
5850
|
if (runner === "pytest") return pytestSelection(src);
|
|
5493
5851
|
try {
|
|
5494
|
-
const sf =
|
|
5852
|
+
const sf = ts4.createSourceFile("cfg.ts", asExpression2(src), ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
|
|
5495
5853
|
if (runner === "vitest" && isWorkspaceFile(path)) {
|
|
5496
5854
|
const arr = workspaceArray(sf);
|
|
5497
5855
|
if (arr) {
|
|
@@ -5598,21 +5956,21 @@ var TEST_DEFS = {
|
|
|
5598
5956
|
cs: /^\s*\[(?:Fact|Theory|Test|TestMethod|TestCase|DataTestMethod)\b/
|
|
5599
5957
|
};
|
|
5600
5958
|
function calleeName(expr) {
|
|
5601
|
-
if (
|
|
5602
|
-
if (
|
|
5959
|
+
if (ts5.isIdentifier(expr)) return expr.text;
|
|
5960
|
+
if (ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression)) {
|
|
5603
5961
|
return expr.expression.text;
|
|
5604
5962
|
}
|
|
5605
5963
|
return null;
|
|
5606
5964
|
}
|
|
5607
5965
|
var TABLE_METHOD = /^(?:each|for)$/;
|
|
5608
|
-
var isEachOf = (expr) =>
|
|
5609
|
-
var isDescribeEach = (expr) =>
|
|
5966
|
+
var isEachOf = (expr) => ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
|
|
5967
|
+
var isDescribeEach = (expr) => ts5.isPropertyAccessExpression(expr) && ts5.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
|
|
5610
5968
|
function loopRows(iterable) {
|
|
5611
|
-
if (
|
|
5969
|
+
if (ts5.isArrayLiteralExpression(iterable)) {
|
|
5612
5970
|
let n = 0;
|
|
5613
5971
|
let open = false;
|
|
5614
5972
|
for (const el of iterable.elements) {
|
|
5615
|
-
if (
|
|
5973
|
+
if (ts5.isSpreadElement(el)) open = true;
|
|
5616
5974
|
else n++;
|
|
5617
5975
|
}
|
|
5618
5976
|
return { n: Math.max(n, open ? 0 : 1), open };
|
|
@@ -5624,20 +5982,20 @@ function templateRows(node) {
|
|
|
5624
5982
|
return Math.max(rows, 1);
|
|
5625
5983
|
}
|
|
5626
5984
|
function hasSubstantiveBody(call) {
|
|
5627
|
-
const fn = call.arguments.find((a) =>
|
|
5985
|
+
const fn = call.arguments.find((a) => ts5.isArrowFunction(a) || ts5.isFunctionExpression(a));
|
|
5628
5986
|
if (!fn) return true;
|
|
5629
5987
|
const body = fn.body;
|
|
5630
|
-
const text =
|
|
5988
|
+
const text = ts5.isBlock(body) ? body.getText().slice(1, -1) : body.getText();
|
|
5631
5989
|
return text.split("\n").some((l) => isSignificantLine(l.trim(), "js"));
|
|
5632
5990
|
}
|
|
5633
5991
|
function eachRows(call) {
|
|
5634
5992
|
const arg = call.arguments[0];
|
|
5635
5993
|
if (!arg) return { n: 1, open: true };
|
|
5636
|
-
if (
|
|
5994
|
+
if (ts5.isArrayLiteralExpression(arg)) {
|
|
5637
5995
|
let n = 0;
|
|
5638
5996
|
let open = false;
|
|
5639
5997
|
for (const el of arg.elements) {
|
|
5640
|
-
if (
|
|
5998
|
+
if (ts5.isSpreadElement(el)) open = true;
|
|
5641
5999
|
else n++;
|
|
5642
6000
|
}
|
|
5643
6001
|
return { n: Math.max(n, open ? 0 : 1), open };
|
|
@@ -5652,35 +6010,35 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
|
|
|
5652
6010
|
for (const line of src.split("\n")) if (re.test(line)) n2++;
|
|
5653
6011
|
return { min: n2, open: false };
|
|
5654
6012
|
}
|
|
5655
|
-
const sf =
|
|
6013
|
+
const sf = ts5.createSourceFile("spec.ts", src, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
|
|
5656
6014
|
let n = 0;
|
|
5657
6015
|
let open = false;
|
|
5658
6016
|
const visit = (node, mult) => {
|
|
5659
|
-
if (
|
|
6017
|
+
if (ts5.isForOfStatement(node)) {
|
|
5660
6018
|
const r = loopRows(node.expression);
|
|
5661
6019
|
if (r.open) open = true;
|
|
5662
6020
|
visit(node.statement, mult * r.n);
|
|
5663
6021
|
return;
|
|
5664
6022
|
}
|
|
5665
|
-
if (
|
|
6023
|
+
if (ts5.isForStatement(node) || ts5.isForInStatement(node) || ts5.isWhileStatement(node) || ts5.isDoStatement(node)) {
|
|
5666
6024
|
open = true;
|
|
5667
6025
|
}
|
|
5668
|
-
if (
|
|
6026
|
+
if (ts5.isCallExpression(node)) {
|
|
5669
6027
|
const callee = node.expression;
|
|
5670
|
-
if (
|
|
6028
|
+
if (ts5.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
|
|
5671
6029
|
const r = loopRows(callee.expression);
|
|
5672
6030
|
if (r.open) open = true;
|
|
5673
6031
|
for (const a of node.arguments) visit(a, mult * r.n);
|
|
5674
6032
|
return;
|
|
5675
6033
|
}
|
|
5676
6034
|
const head = node.expression;
|
|
5677
|
-
if (
|
|
6035
|
+
if (ts5.isCallExpression(head) && isDescribeEach(head.expression)) {
|
|
5678
6036
|
const r = eachRows(head);
|
|
5679
6037
|
if (r.open) open = true;
|
|
5680
6038
|
for (const a of node.arguments) visit(a, mult * r.n);
|
|
5681
6039
|
return;
|
|
5682
6040
|
}
|
|
5683
|
-
if (
|
|
6041
|
+
if (ts5.isTaggedTemplateExpression(head) && isDescribeEach(head.tag)) {
|
|
5684
6042
|
for (const a of node.arguments) visit(a, mult * templateRows(head));
|
|
5685
6043
|
return;
|
|
5686
6044
|
}
|
|
@@ -5692,10 +6050,10 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
|
|
|
5692
6050
|
const name = calleeName(node.expression);
|
|
5693
6051
|
if ((name === "it" || name === "test") && (!substantiveOnly || hasSubstantiveBody(node))) n += mult;
|
|
5694
6052
|
}
|
|
5695
|
-
} else if (
|
|
6053
|
+
} else if (ts5.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
|
|
5696
6054
|
n += templateRows(node) * mult;
|
|
5697
6055
|
}
|
|
5698
|
-
|
|
6056
|
+
ts5.forEachChild(node, (c) => visit(c, mult));
|
|
5699
6057
|
};
|
|
5700
6058
|
visit(sf, 1);
|
|
5701
6059
|
return { min: n, open };
|
|
@@ -5997,7 +6355,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
5997
6355
|
}
|
|
5998
6356
|
|
|
5999
6357
|
// src/detectors/test-content-removal.ts
|
|
6000
|
-
import
|
|
6358
|
+
import ts6 from "typescript";
|
|
6001
6359
|
var RULE8 = "test-content-removal";
|
|
6002
6360
|
var MIN_REMOVED_LINES = 3;
|
|
6003
6361
|
var MIN_REMOVED_ROWS = 2;
|
|
@@ -6007,13 +6365,13 @@ function eachTables(src, path) {
|
|
|
6007
6365
|
let open = false;
|
|
6008
6366
|
if (langOf(path) !== "js" && langOf(path) !== null) return { rows, open };
|
|
6009
6367
|
try {
|
|
6010
|
-
const sf =
|
|
6368
|
+
const sf = ts6.createSourceFile("spec.ts", src, ts6.ScriptTarget.Latest, true, ts6.ScriptKind.TS);
|
|
6011
6369
|
const visit = (node) => {
|
|
6012
|
-
if (
|
|
6370
|
+
if (ts6.isCallExpression(node) && ts6.isPropertyAccessExpression(node.expression) && /^(?:each|for)$/.test(node.expression.name.text) && ts6.isIdentifier(node.expression.expression) && /^(?:it|test|describe)$/.test(node.expression.expression.text)) {
|
|
6013
6371
|
const arg = node.arguments[0];
|
|
6014
|
-
if (arg &&
|
|
6372
|
+
if (arg && ts6.isArrayLiteralExpression(arg)) {
|
|
6015
6373
|
for (const el of arg.elements) {
|
|
6016
|
-
if (
|
|
6374
|
+
if (ts6.isSpreadElement(el)) {
|
|
6017
6375
|
open = true;
|
|
6018
6376
|
continue;
|
|
6019
6377
|
}
|
|
@@ -6023,7 +6381,7 @@ function eachTables(src, path) {
|
|
|
6023
6381
|
}
|
|
6024
6382
|
} else open = true;
|
|
6025
6383
|
}
|
|
6026
|
-
|
|
6384
|
+
ts6.forEachChild(node, visit);
|
|
6027
6385
|
};
|
|
6028
6386
|
visit(sf);
|
|
6029
6387
|
} catch {
|
|
@@ -6201,6 +6559,214 @@ var snapshotOnlyRewrite = {
|
|
|
6201
6559
|
}
|
|
6202
6560
|
};
|
|
6203
6561
|
|
|
6562
|
+
// src/detectors/assertion-weakening.ts
|
|
6563
|
+
import ts7 from "typescript";
|
|
6564
|
+
var RULE11 = "assertion-weakening";
|
|
6565
|
+
var TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
6566
|
+
var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
|
|
6567
|
+
var EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
|
|
6568
|
+
var WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
|
|
6569
|
+
var compact = (s) => s.replace(/\s+/g, "");
|
|
6570
|
+
function literalTitle(node) {
|
|
6571
|
+
if (!node) return null;
|
|
6572
|
+
if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
6573
|
+
return null;
|
|
6574
|
+
}
|
|
6575
|
+
function scriptKindForPath(path) {
|
|
6576
|
+
const lower = path.toLowerCase();
|
|
6577
|
+
if (lower.endsWith(".tsx")) return ts7.ScriptKind.TSX;
|
|
6578
|
+
if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
|
|
6579
|
+
return ts7.ScriptKind.TS;
|
|
6580
|
+
}
|
|
6581
|
+
if (lower.endsWith(".jsx")) return ts7.ScriptKind.JSX;
|
|
6582
|
+
return ts7.ScriptKind.JS;
|
|
6583
|
+
}
|
|
6584
|
+
function staticFacts(node) {
|
|
6585
|
+
if (!node) return null;
|
|
6586
|
+
if (node.kind === ts7.SyntaxKind.TrueKeyword) return { defined: true, truthy: true };
|
|
6587
|
+
if (node.kind === ts7.SyntaxKind.FalseKeyword) return { defined: true, truthy: false };
|
|
6588
|
+
if (node.kind === ts7.SyntaxKind.NullKeyword) return { defined: true, truthy: false };
|
|
6589
|
+
if (ts7.isIdentifier(node) && node.text === "undefined") {
|
|
6590
|
+
return { defined: false, truthy: false };
|
|
6591
|
+
}
|
|
6592
|
+
if (ts7.isVoidExpression(node)) return { defined: false, truthy: false };
|
|
6593
|
+
if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) {
|
|
6594
|
+
return { defined: true, truthy: node.text.length > 0 };
|
|
6595
|
+
}
|
|
6596
|
+
if (ts7.isNumericLiteral(node)) {
|
|
6597
|
+
return { defined: true, truthy: Number(node.text) !== 0 };
|
|
6598
|
+
}
|
|
6599
|
+
if (ts7.isBigIntLiteral(node)) {
|
|
6600
|
+
return { defined: true, truthy: node.text.replace(/n$/i, "") !== "0" };
|
|
6601
|
+
}
|
|
6602
|
+
if (ts7.isPrefixUnaryExpression(node) && (node.operator === ts7.SyntaxKind.MinusToken || node.operator === ts7.SyntaxKind.PlusToken) && ts7.isNumericLiteral(node.operand)) {
|
|
6603
|
+
const value = Number(node.operand.text) * (node.operator === ts7.SyntaxKind.MinusToken ? -1 : 1);
|
|
6604
|
+
return { defined: true, truthy: value !== 0 };
|
|
6605
|
+
}
|
|
6606
|
+
if (ts7.isObjectLiteralExpression(node) || ts7.isArrayLiteralExpression(node) || ts7.isRegularExpressionLiteral(node) || ts7.isArrowFunction(node) || ts7.isFunctionExpression(node) || ts7.isClassExpression(node) || ts7.isNewExpression(node)) {
|
|
6607
|
+
return { defined: true, truthy: true };
|
|
6608
|
+
}
|
|
6609
|
+
return null;
|
|
6610
|
+
}
|
|
6611
|
+
function expectAssertion(node, sf) {
|
|
6612
|
+
if (!ts7.isPropertyAccessExpression(node.expression)) return null;
|
|
6613
|
+
const matcher = node.expression.name.text;
|
|
6614
|
+
let receiver = node.expression.expression;
|
|
6615
|
+
let polarity = "";
|
|
6616
|
+
if (ts7.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
|
|
6617
|
+
polarity = "not";
|
|
6618
|
+
receiver = receiver.expression;
|
|
6619
|
+
}
|
|
6620
|
+
if (!ts7.isCallExpression(receiver) || !ts7.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
|
|
6621
|
+
return null;
|
|
6622
|
+
}
|
|
6623
|
+
const subject = compact(receiver.arguments[0].getText(sf));
|
|
6624
|
+
const args = node.arguments.map((arg) => compact(arg.getText(sf)));
|
|
6625
|
+
return {
|
|
6626
|
+
subject,
|
|
6627
|
+
polarity,
|
|
6628
|
+
matcher,
|
|
6629
|
+
args,
|
|
6630
|
+
firstArgFacts: staticFacts(node.arguments[0]),
|
|
6631
|
+
canonical: [subject, polarity, matcher, ...args].join("|"),
|
|
6632
|
+
text: node.getText(sf).replace(/\s+/g, " ")
|
|
6633
|
+
};
|
|
6634
|
+
}
|
|
6635
|
+
function simpleCallName(node) {
|
|
6636
|
+
return ts7.isIdentifier(node.expression) ? node.expression.text : null;
|
|
6637
|
+
}
|
|
6638
|
+
function callbackFor(node) {
|
|
6639
|
+
const cb = node.arguments[1];
|
|
6640
|
+
return cb && (ts7.isArrowFunction(cb) || ts7.isFunctionExpression(cb)) ? cb : null;
|
|
6641
|
+
}
|
|
6642
|
+
function blocks(src, path) {
|
|
6643
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
6644
|
+
if (langOf(path) !== "js") return out2;
|
|
6645
|
+
try {
|
|
6646
|
+
const sf = ts7.createSourceFile(path, src, ts7.ScriptTarget.Latest, true, scriptKindForPath(path));
|
|
6647
|
+
const visit = (node, suites) => {
|
|
6648
|
+
if (ts7.isCallExpression(node)) {
|
|
6649
|
+
const call = simpleCallName(node);
|
|
6650
|
+
const title = literalTitle(node.arguments[0]);
|
|
6651
|
+
const cb = callbackFor(node);
|
|
6652
|
+
if (call && SUITE_CALLS.has(call) && title !== null && cb) {
|
|
6653
|
+
const nextSuites = [...suites, title];
|
|
6654
|
+
ts7.forEachChild(cb, (child) => visit(child, nextSuites));
|
|
6655
|
+
return;
|
|
6656
|
+
}
|
|
6657
|
+
if (call && TEST_CALLS.has(call) && title !== null && cb) {
|
|
6658
|
+
const assertions = [];
|
|
6659
|
+
const collect2 = (child) => {
|
|
6660
|
+
if (child !== cb && ts7.isCallExpression(child)) {
|
|
6661
|
+
const nestedCall = simpleCallName(child);
|
|
6662
|
+
if (nestedCall && (TEST_CALLS.has(nestedCall) || SUITE_CALLS.has(nestedCall))) {
|
|
6663
|
+
return;
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
if (ts7.isCallExpression(child)) {
|
|
6667
|
+
const a = expectAssertion(child, sf);
|
|
6668
|
+
if (a) assertions.push(a);
|
|
6669
|
+
}
|
|
6670
|
+
ts7.forEachChild(child, collect2);
|
|
6671
|
+
};
|
|
6672
|
+
collect2(cb);
|
|
6673
|
+
const identity2 = [...suites, title].join(" > ");
|
|
6674
|
+
const list2 = out2.get(identity2) ?? [];
|
|
6675
|
+
list2.push({ title, identity: identity2, assertions });
|
|
6676
|
+
out2.set(identity2, list2);
|
|
6677
|
+
return;
|
|
6678
|
+
}
|
|
6679
|
+
}
|
|
6680
|
+
ts7.forEachChild(node, (child) => visit(child, suites));
|
|
6681
|
+
};
|
|
6682
|
+
visit(sf, []);
|
|
6683
|
+
} catch {
|
|
6684
|
+
}
|
|
6685
|
+
return out2;
|
|
6686
|
+
}
|
|
6687
|
+
function pureRemoval(before, after) {
|
|
6688
|
+
if (after.length >= before.length) return null;
|
|
6689
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
6690
|
+
for (const a of before) remaining.set(a.canonical, (remaining.get(a.canonical) ?? 0) + 1);
|
|
6691
|
+
for (const a of after) {
|
|
6692
|
+
const n = remaining.get(a.canonical) ?? 0;
|
|
6693
|
+
if (n <= 0) return null;
|
|
6694
|
+
remaining.set(a.canonical, n - 1);
|
|
6695
|
+
}
|
|
6696
|
+
for (const a of before) {
|
|
6697
|
+
const n = remaining.get(a.canonical) ?? 0;
|
|
6698
|
+
if (n > 0) return a;
|
|
6699
|
+
}
|
|
6700
|
+
return null;
|
|
6701
|
+
}
|
|
6702
|
+
function factsProveWeakMatcher(oldA, newMatcher) {
|
|
6703
|
+
if (!EXACT_MATCHERS.has(oldA.matcher) || oldA.args.length !== 1) return false;
|
|
6704
|
+
const facts = oldA.firstArgFacts;
|
|
6705
|
+
if (!facts) return false;
|
|
6706
|
+
if (newMatcher === "toBeDefined") return facts.defined;
|
|
6707
|
+
if (newMatcher === "toBeTruthy") return facts.truthy === true;
|
|
6708
|
+
return false;
|
|
6709
|
+
}
|
|
6710
|
+
function directionalWeakening(before, after) {
|
|
6711
|
+
for (const oldA of before) {
|
|
6712
|
+
for (const newA of after) {
|
|
6713
|
+
if (oldA.subject !== newA.subject) continue;
|
|
6714
|
+
if (oldA.polarity !== "" || newA.polarity !== "") continue;
|
|
6715
|
+
if (WEAK_MATCHERS.has(newA.matcher) && newA.args.length === 0 && factsProveWeakMatcher(oldA, newA.matcher)) {
|
|
6716
|
+
return { before: oldA, after: newA, kind: "matcher" };
|
|
6717
|
+
}
|
|
6718
|
+
if (oldA.matcher === "toThrow" && oldA.args.length > 0 && newA.matcher === "toThrow" && newA.args.length === 0) {
|
|
6719
|
+
return { before: oldA, after: newA, kind: "throw-specificity" };
|
|
6720
|
+
}
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6723
|
+
return null;
|
|
6724
|
+
}
|
|
6725
|
+
function findingForBlock(path, before, after, policy) {
|
|
6726
|
+
const weakened = directionalWeakening(before.assertions, after.assertions);
|
|
6727
|
+
if (weakened) {
|
|
6728
|
+
const message = weakened.kind === "throw-specificity" ? `Assertion weakened in test "${before.identity}": exception-message specificity was removed from toThrow().` : `Assertion weakened in test "${before.identity}": ${weakened.before.matcher} was replaced by weaker ${weakened.after.matcher} on the same subject.`;
|
|
6729
|
+
return makeFinding(RULE11, policy, {
|
|
6730
|
+
file: path,
|
|
6731
|
+
message,
|
|
6732
|
+
evidence: `${weakened.before.text} -> ${weakened.after.text}`,
|
|
6733
|
+
remediation: "Keep the stronger expectation and fix the code under test. If the contract genuinely changed, review the test change explicitly; this rule is warning-only while its precision is measured."
|
|
6734
|
+
});
|
|
6735
|
+
}
|
|
6736
|
+
const removed = pureRemoval(before.assertions, after.assertions);
|
|
6737
|
+
if (removed) {
|
|
6738
|
+
return makeFinding(RULE11, policy, {
|
|
6739
|
+
file: path,
|
|
6740
|
+
message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
|
|
6741
|
+
evidence: removed.text,
|
|
6742
|
+
remediation: "Keep the assertion and fix the code under test. If the assertion is genuinely obsolete, review the test change explicitly; this rule is warning-only while its precision is measured."
|
|
6743
|
+
});
|
|
6744
|
+
}
|
|
6745
|
+
return null;
|
|
6746
|
+
}
|
|
6747
|
+
var assertionWeakening = {
|
|
6748
|
+
id: RULE11,
|
|
6749
|
+
surface: ["file"],
|
|
6750
|
+
certainty: "heuristic",
|
|
6751
|
+
run(changes, policy) {
|
|
6752
|
+
const out2 = [];
|
|
6753
|
+
for (const c of changes) {
|
|
6754
|
+
if (c.kind !== "file" || c.op !== "modify" && c.op !== "rename" || c.before == null || c.after == null || langOf(c.path) !== "js" || !isProtected(c.path, policy, "tests") || isProtected(c.path, policy, "snapshots")) {
|
|
6755
|
+
continue;
|
|
6756
|
+
}
|
|
6757
|
+
const beforeBlocks = blocks(c.before, c.path);
|
|
6758
|
+
const afterBlocks = blocks(c.after, c.path);
|
|
6759
|
+
for (const [identity2, oldList] of beforeBlocks) {
|
|
6760
|
+
const newList = afterBlocks.get(identity2);
|
|
6761
|
+
if (!newList || oldList.length !== 1 || newList.length !== 1) continue;
|
|
6762
|
+
const f = findingForBlock(c.path, oldList[0], newList[0], policy);
|
|
6763
|
+
if (f) out2.push(f);
|
|
6764
|
+
}
|
|
6765
|
+
}
|
|
6766
|
+
return out2;
|
|
6767
|
+
}
|
|
6768
|
+
};
|
|
6769
|
+
|
|
6204
6770
|
// src/detectors/index.ts
|
|
6205
6771
|
var allDetectors = [
|
|
6206
6772
|
noVerify,
|
|
@@ -6213,7 +6779,8 @@ var allDetectors = [
|
|
|
6213
6779
|
testDeletion,
|
|
6214
6780
|
testContentRemoval,
|
|
6215
6781
|
snapshotRewrite,
|
|
6216
|
-
snapshotOnlyRewrite
|
|
6782
|
+
snapshotOnlyRewrite,
|
|
6783
|
+
assertionWeakening
|
|
6217
6784
|
];
|
|
6218
6785
|
|
|
6219
6786
|
// src/engine.ts
|
|
@@ -6575,9 +7142,9 @@ function orderFindings(findings) {
|
|
|
6575
7142
|
return (a.line ?? 0) - (b.line ?? 0);
|
|
6576
7143
|
});
|
|
6577
7144
|
}
|
|
6578
|
-
function countsPhrase(
|
|
7145
|
+
function countsPhrase(blocks2, warns) {
|
|
6579
7146
|
const parts = [];
|
|
6580
|
-
if (
|
|
7147
|
+
if (blocks2) parts.push(`${blocks2} blocking`);
|
|
6581
7148
|
if (warns) parts.push(`${warns} warning${warns === 1 ? "" : "s"}`);
|
|
6582
7149
|
return parts.join(", ");
|
|
6583
7150
|
}
|
|
@@ -6589,7 +7156,7 @@ var SIGNOFF_NOTE = "A blocking finding clears only with a human sign-off. In CI
|
|
|
6589
7156
|
function renderText(input, opts) {
|
|
6590
7157
|
const { scanned, ignoredFiles } = input;
|
|
6591
7158
|
const findings = orderFindings(input.findings);
|
|
6592
|
-
const
|
|
7159
|
+
const blocks2 = findings.filter((f) => f.severity === "block");
|
|
6593
7160
|
const warns = findings.filter((f) => f.severity === "warn");
|
|
6594
7161
|
const c = opts.colour;
|
|
6595
7162
|
const out2 = [];
|
|
@@ -6597,8 +7164,8 @@ function renderText(input, opts) {
|
|
|
6597
7164
|
return `${paint("tamperward: clean", GREEN, c)} \u2014 no integrity findings (${scopePhrase(scanned, ignoredFiles)}).
|
|
6598
7165
|
`;
|
|
6599
7166
|
}
|
|
6600
|
-
const headColour =
|
|
6601
|
-
out2.push(paint(`tamperward: ${countsPhrase(
|
|
7167
|
+
const headColour = blocks2.length ? RED : YELLOW;
|
|
7168
|
+
out2.push(paint(`tamperward: ${countsPhrase(blocks2.length, warns.length)}`, BOLD + headColour, c));
|
|
6602
7169
|
out2.push(paint(`(${scopePhrase(scanned, ignoredFiles)})`, DIM, c));
|
|
6603
7170
|
out2.push("");
|
|
6604
7171
|
const LABEL = "sign-off".length;
|
|
@@ -6618,7 +7185,7 @@ function renderText(input, opts) {
|
|
|
6618
7185
|
if (f.signoff.required) row("sign-off", [stripControl(f.signoff.command)]);
|
|
6619
7186
|
out2.push("");
|
|
6620
7187
|
}
|
|
6621
|
-
if (
|
|
7188
|
+
if (blocks2.length > 0) {
|
|
6622
7189
|
out2.push(
|
|
6623
7190
|
wrap(SIGNOFF_NOTE, opts.width - 2).map((l) => paint(l, DIM, c)).join("\n")
|
|
6624
7191
|
);
|
|
@@ -6668,7 +7235,7 @@ function locationCell(f, env) {
|
|
|
6668
7235
|
}
|
|
6669
7236
|
function renderSummary(input, env = process.env) {
|
|
6670
7237
|
const findings = orderFindings(input.findings);
|
|
6671
|
-
const
|
|
7238
|
+
const blocks2 = findings.filter((f) => f.severity === "block");
|
|
6672
7239
|
const warns = findings.filter((f) => f.severity === "warn");
|
|
6673
7240
|
const scope = scopePhrase(input.scanned, input.ignoredFiles);
|
|
6674
7241
|
if (findings.length === 0) {
|
|
@@ -6678,7 +7245,7 @@ No integrity findings (${scope}).
|
|
|
6678
7245
|
`;
|
|
6679
7246
|
}
|
|
6680
7247
|
const out2 = [
|
|
6681
|
-
`## Tamperward: ${countsPhrase(
|
|
7248
|
+
`## Tamperward: ${countsPhrase(blocks2.length, warns.length)}`,
|
|
6682
7249
|
"",
|
|
6683
7250
|
`${scope}. Every finding below is also annotated on the diff in **Files changed**.`,
|
|
6684
7251
|
"",
|
|
@@ -6731,12 +7298,12 @@ function report(input) {
|
|
|
6731
7298
|
const { findings, scanned, ignoredFiles } = input;
|
|
6732
7299
|
const format = resolveFormat(input);
|
|
6733
7300
|
if (format === "json") {
|
|
6734
|
-
const
|
|
7301
|
+
const blocks2 = findings.filter((f) => f.severity === "block").length;
|
|
6735
7302
|
process.stdout.write(
|
|
6736
7303
|
JSON.stringify(
|
|
6737
7304
|
// `findings`, `scanned` and `ignoredFiles` keep their exact prior shape; `summary`
|
|
6738
7305
|
// is additive, so an existing consumer reading only the old keys is unaffected.
|
|
6739
|
-
{ findings, scanned, ignoredFiles, summary: { block:
|
|
7306
|
+
{ findings, scanned, ignoredFiles, summary: { block: blocks2, warn: findings.length - blocks2 } },
|
|
6740
7307
|
null,
|
|
6741
7308
|
2
|
|
6742
7309
|
) + "\n"
|
|
@@ -6833,10 +7400,10 @@ import { readFileSync as readFileSync10, appendFileSync as appendFileSync4, writ
|
|
|
6833
7400
|
import { createHash as createHash5 } from "node:crypto";
|
|
6834
7401
|
|
|
6835
7402
|
// src/adapters/claude/deny.ts
|
|
6836
|
-
function formatDenial(
|
|
6837
|
-
const lead =
|
|
7403
|
+
function formatDenial(blocks2) {
|
|
7404
|
+
const lead = blocks2[0];
|
|
6838
7405
|
const lines = ["Tamperward blocked this change \u2014 it weakens a protected safety net to pass checks.", ""];
|
|
6839
|
-
for (const f of
|
|
7406
|
+
for (const f of blocks2) {
|
|
6840
7407
|
const loc = f.file ? ` (${escapeControl(f.file)}${f.line ? `:${f.line}` : ""})` : "";
|
|
6841
7408
|
lines.push(` \u2022 ${f.rule}${loc}: ${f.message}`);
|
|
6842
7409
|
}
|
|
@@ -6845,7 +7412,7 @@ function formatDenial(blocks) {
|
|
|
6845
7412
|
lines.push(
|
|
6846
7413
|
"The other shortcuts are blocked too: skipping the hooks, editing the hook or CI workflow, lowering the coverage gate, and rewriting a protected file from the shell will each be denied."
|
|
6847
7414
|
);
|
|
6848
|
-
if (
|
|
7415
|
+
if (blocks2.some((b) => b.signoff.required)) {
|
|
6849
7416
|
lines.push(`If this change is genuinely correct, a human must sign off: ${lead.signoff.command}`);
|
|
6850
7417
|
}
|
|
6851
7418
|
return lines.join("\n") + "\n";
|
|
@@ -7263,7 +7830,7 @@ function runWatch(args) {
|
|
|
7263
7830
|
|
|
7264
7831
|
// src/detectors/fs-events.ts
|
|
7265
7832
|
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
|
|
7266
|
-
var
|
|
7833
|
+
var RULE12 = "transient-protected-mutation";
|
|
7267
7834
|
var MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
|
|
7268
7835
|
var MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
|
|
7269
7836
|
var DEFAULT_EVENT_READ_IO = {
|
|
@@ -7458,7 +8025,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
|
|
|
7458
8025
|
};
|
|
7459
8026
|
}
|
|
7460
8027
|
function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
7461
|
-
if (!isEnabled(
|
|
8028
|
+
if (!isEnabled(RULE12, policy)) return [];
|
|
7462
8029
|
const byPath = /* @__PURE__ */ new Map();
|
|
7463
8030
|
for (const e of events) {
|
|
7464
8031
|
if (!isProtected(e.path, policy)) continue;
|
|
@@ -7478,7 +8045,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
7478
8045
|
const modeChurn = modes.size > 1;
|
|
7479
8046
|
if (!contentChurn && !modeChurn) continue;
|
|
7480
8047
|
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 ")})`;
|
|
7481
|
-
const f = makeFinding(
|
|
8048
|
+
const f = makeFinding(RULE12, policy, {
|
|
7482
8049
|
file: path,
|
|
7483
8050
|
message: `Transient protected mutation: ${what}, leaving no net diff.`,
|
|
7484
8051
|
evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
|
|
@@ -7517,11 +8084,11 @@ function parseInput(raw) {
|
|
|
7517
8084
|
}
|
|
7518
8085
|
return parsed;
|
|
7519
8086
|
}
|
|
7520
|
-
function recordDenylog(
|
|
8087
|
+
function recordDenylog(blocks2) {
|
|
7521
8088
|
const log = process.env.TAMPERWARD_DENYLOG;
|
|
7522
8089
|
if (!log) return;
|
|
7523
8090
|
try {
|
|
7524
|
-
appendFileSync4(log,
|
|
8091
|
+
appendFileSync4(log, blocks2.map((b) => b.rule).join(",") + "\n");
|
|
7525
8092
|
} catch {
|
|
7526
8093
|
}
|
|
7527
8094
|
}
|
|
@@ -7540,10 +8107,10 @@ function failClosed(kind, detail) {
|
|
|
7540
8107
|
};
|
|
7541
8108
|
return verdict([f], kind);
|
|
7542
8109
|
}
|
|
7543
|
-
function verdict(
|
|
7544
|
-
if (
|
|
7545
|
-
recordDenylog(
|
|
7546
|
-
const reason = formatDenial(
|
|
8110
|
+
function verdict(blocks2, kind) {
|
|
8111
|
+
if (blocks2.length === 0) return { exitCode: 0, stdout: "" };
|
|
8112
|
+
recordDenylog(blocks2);
|
|
8113
|
+
const reason = formatDenial(blocks2);
|
|
7547
8114
|
const payload = kind === "PreToolUse" ? {
|
|
7548
8115
|
hookSpecificOutput: {
|
|
7549
8116
|
hookEventName: "PreToolUse",
|
|
@@ -7600,14 +8167,14 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
7600
8167
|
if (c.oldPath) seen.add(c.oldPath);
|
|
7601
8168
|
}
|
|
7602
8169
|
const changes = [];
|
|
7603
|
-
const
|
|
8170
|
+
const blocks2 = [];
|
|
7604
8171
|
const trusted = base ?? "HEAD";
|
|
7605
8172
|
const candidates = base ? [base, "HEAD", ""] : ["HEAD", ""];
|
|
7606
8173
|
for (const rel of [...d.changed, ...d.deleted]) {
|
|
7607
8174
|
if (seen.has(rel)) continue;
|
|
7608
8175
|
const unjudgeable = unjudgeableFinding(cwd, rel);
|
|
7609
8176
|
if (unjudgeable) {
|
|
7610
|
-
|
|
8177
|
+
blocks2.push(unjudgeable);
|
|
7611
8178
|
continue;
|
|
7612
8179
|
}
|
|
7613
8180
|
const after = fileOnDisk(rel, { cwd });
|
|
@@ -7627,7 +8194,7 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
7627
8194
|
}
|
|
7628
8195
|
}
|
|
7629
8196
|
if (before == null) {
|
|
7630
|
-
|
|
8197
|
+
blocks2.push({
|
|
7631
8198
|
rule: "hidden-drift",
|
|
7632
8199
|
severity: "block",
|
|
7633
8200
|
file: rel,
|
|
@@ -7660,12 +8227,12 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
7660
8227
|
if (seen.has(rel)) continue;
|
|
7661
8228
|
const unjudgeable = unjudgeableFinding(cwd, rel);
|
|
7662
8229
|
if (unjudgeable) {
|
|
7663
|
-
|
|
8230
|
+
blocks2.push(unjudgeable);
|
|
7664
8231
|
continue;
|
|
7665
8232
|
}
|
|
7666
8233
|
const after = fileOnDisk(rel, { cwd });
|
|
7667
8234
|
if (after == null) {
|
|
7668
|
-
|
|
8235
|
+
blocks2.push({
|
|
7669
8236
|
rule: "hidden-drift",
|
|
7670
8237
|
severity: "block",
|
|
7671
8238
|
file: rel,
|
|
@@ -7678,7 +8245,7 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
7678
8245
|
}
|
|
7679
8246
|
changes.push(...synthFileChange(rel, null, after));
|
|
7680
8247
|
}
|
|
7681
|
-
return { changes, blocks };
|
|
8248
|
+
return { changes, blocks: blocks2 };
|
|
7682
8249
|
}
|
|
7683
8250
|
function effectDriftBlocks(cwd, sessionId, policy) {
|
|
7684
8251
|
if (!sessionId || !isGitRepo(cwd)) return null;
|
|
@@ -7703,8 +8270,8 @@ function effectDriftBlocks(cwd, sessionId, policy) {
|
|
|
7703
8270
|
);
|
|
7704
8271
|
const gap = uncoveredDrift(cwd, policy, expected, current, scoped, base, loadTurnTree(cwd, sessionId));
|
|
7705
8272
|
const judged = [...scoped, ...gap.changes];
|
|
7706
|
-
const
|
|
7707
|
-
if (
|
|
8273
|
+
const blocks2 = evaluate(judged, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(gap.blocks, unjudgeableProtected(cwd, policy, judged));
|
|
8274
|
+
if (blocks2.length > 0) return blocks2;
|
|
7708
8275
|
savePtree(cwd, sessionId, current);
|
|
7709
8276
|
return null;
|
|
7710
8277
|
}
|
|
@@ -7736,9 +8303,9 @@ function preToolUseVerdict(input) {
|
|
|
7736
8303
|
const driftBlocks = effectDriftBlocks(cwd, input.session_id, policy);
|
|
7737
8304
|
if (driftBlocks) return verdict(driftBlocks, "PreToolUse");
|
|
7738
8305
|
const changes = changesFromClaudeHook(input, cwd);
|
|
7739
|
-
const
|
|
7740
|
-
if (
|
|
7741
|
-
return verdict(
|
|
8306
|
+
const blocks2 = evaluate(changes, policy, void 0, "tool-call", { cwd }).filter((f) => f.severity === "block");
|
|
8307
|
+
if (blocks2.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
|
|
8308
|
+
return verdict(blocks2, "PreToolUse");
|
|
7742
8309
|
} catch (e) {
|
|
7743
8310
|
return failClosed("PreToolUse", errText2(e));
|
|
7744
8311
|
}
|
|
@@ -7840,7 +8407,7 @@ function stopVerdict(input) {
|
|
|
7840
8407
|
if (input.stop_hook_active) return { exitCode: 0, stdout: "" };
|
|
7841
8408
|
const cwd = input.cwd ?? process.cwd();
|
|
7842
8409
|
if (!isGitRepo(cwd)) return { exitCode: 0, stdout: "" };
|
|
7843
|
-
let
|
|
8410
|
+
let blocks2;
|
|
7844
8411
|
let commitCursor = () => {
|
|
7845
8412
|
};
|
|
7846
8413
|
try {
|
|
@@ -7855,22 +8422,22 @@ function stopVerdict(input) {
|
|
|
7855
8422
|
changes = changes.concat(gap.changes);
|
|
7856
8423
|
hiddenBlocks = gap.blocks;
|
|
7857
8424
|
}
|
|
7858
|
-
|
|
8425
|
+
blocks2 = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks, unjudgeableProtected(cwd, policy, changes));
|
|
7859
8426
|
const transient = turnTransientBlocks(cwd, input.session_id, policy, changes);
|
|
7860
|
-
|
|
8427
|
+
blocks2 = blocks2.concat(transient.blocks);
|
|
7861
8428
|
commitCursor = transient.commit;
|
|
7862
|
-
if (
|
|
8429
|
+
if (blocks2.length === 0 && input.session_id && current) {
|
|
7863
8430
|
savePtree(cwd, input.session_id, current);
|
|
7864
8431
|
saveTurnTree(cwd, input.session_id, current);
|
|
7865
8432
|
}
|
|
7866
8433
|
} catch (e) {
|
|
7867
8434
|
return failClosed("Stop", errText2(e));
|
|
7868
8435
|
}
|
|
7869
|
-
if (
|
|
8436
|
+
if (blocks2.length === 0) {
|
|
7870
8437
|
advanceTurnBaseline(cwd, input.session_id);
|
|
7871
8438
|
commitCursor();
|
|
7872
8439
|
}
|
|
7873
|
-
return verdict(
|
|
8440
|
+
return verdict(blocks2, "Stop");
|
|
7874
8441
|
}
|
|
7875
8442
|
function emit(r) {
|
|
7876
8443
|
if (r.stdout) process.stdout.write(r.stdout);
|