tamperward 2.17.0 → 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 +7 -0
- package/dist/cli/index.js +479 -121
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -513,6 +513,13 @@ Seventeen rules are specified and sixteen ship (see the table in
|
|
|
513
513
|
(`ts-any-cast`, `ts-any-launder`, `lint-suppression`), pipeline protection
|
|
514
514
|
(`ci-tampering`, `hook-tampering`, `no-verify`), and the effect/outcome layers
|
|
515
515
|
(`transient-protected-mutation`, `pristine-verification`, plus the `run` envelope).
|
|
516
|
+
`test-skip` keeps the established regex coverage for diff-only inputs and non-JS
|
|
517
|
+
ecosystems, while full parse-clean JS/TS BEFORE/AFTER changes also use a conservative
|
|
518
|
+
TypeScript AST path for multiline member chains, statically-computable properties,
|
|
519
|
+
known test-runner aliases and node:test-style option objects. Resolution is lexical
|
|
520
|
+
(symbol-based), dynamic computed properties are not guessed, statically false shorthand
|
|
521
|
+
options are clean, and trivia-free BEFORE/AFTER semantic identity prevents formatting-only
|
|
522
|
+
rewrites from re-blocking an already-existing skip/focus.
|
|
516
523
|
`assertion-weakening` ships only the AST-proven one-way subset — statically
|
|
517
524
|
proven exact/structural values weakened to truthy/defined, positive exception
|
|
518
525
|
specificity removed, or a pure assertion removed from the same suite-qualified
|
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 {
|
|
@@ -6202,7 +6560,7 @@ var snapshotOnlyRewrite = {
|
|
|
6202
6560
|
};
|
|
6203
6561
|
|
|
6204
6562
|
// src/detectors/assertion-weakening.ts
|
|
6205
|
-
import
|
|
6563
|
+
import ts7 from "typescript";
|
|
6206
6564
|
var RULE11 = "assertion-weakening";
|
|
6207
6565
|
var TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
6208
6566
|
var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
|
|
@@ -6211,55 +6569,55 @@ var WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
|
|
|
6211
6569
|
var compact = (s) => s.replace(/\s+/g, "");
|
|
6212
6570
|
function literalTitle(node) {
|
|
6213
6571
|
if (!node) return null;
|
|
6214
|
-
if (
|
|
6572
|
+
if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
6215
6573
|
return null;
|
|
6216
6574
|
}
|
|
6217
6575
|
function scriptKindForPath(path) {
|
|
6218
6576
|
const lower = path.toLowerCase();
|
|
6219
|
-
if (lower.endsWith(".tsx")) return
|
|
6577
|
+
if (lower.endsWith(".tsx")) return ts7.ScriptKind.TSX;
|
|
6220
6578
|
if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
|
|
6221
|
-
return
|
|
6579
|
+
return ts7.ScriptKind.TS;
|
|
6222
6580
|
}
|
|
6223
|
-
if (lower.endsWith(".jsx")) return
|
|
6224
|
-
return
|
|
6581
|
+
if (lower.endsWith(".jsx")) return ts7.ScriptKind.JSX;
|
|
6582
|
+
return ts7.ScriptKind.JS;
|
|
6225
6583
|
}
|
|
6226
6584
|
function staticFacts(node) {
|
|
6227
6585
|
if (!node) return null;
|
|
6228
|
-
if (node.kind ===
|
|
6229
|
-
if (node.kind ===
|
|
6230
|
-
if (node.kind ===
|
|
6231
|
-
if (
|
|
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") {
|
|
6232
6590
|
return { defined: false, truthy: false };
|
|
6233
6591
|
}
|
|
6234
|
-
if (
|
|
6235
|
-
if (
|
|
6592
|
+
if (ts7.isVoidExpression(node)) return { defined: false, truthy: false };
|
|
6593
|
+
if (ts7.isStringLiteral(node) || ts7.isNoSubstitutionTemplateLiteral(node)) {
|
|
6236
6594
|
return { defined: true, truthy: node.text.length > 0 };
|
|
6237
6595
|
}
|
|
6238
|
-
if (
|
|
6596
|
+
if (ts7.isNumericLiteral(node)) {
|
|
6239
6597
|
return { defined: true, truthy: Number(node.text) !== 0 };
|
|
6240
6598
|
}
|
|
6241
|
-
if (
|
|
6599
|
+
if (ts7.isBigIntLiteral(node)) {
|
|
6242
6600
|
return { defined: true, truthy: node.text.replace(/n$/i, "") !== "0" };
|
|
6243
6601
|
}
|
|
6244
|
-
if (
|
|
6245
|
-
const value = Number(node.operand.text) * (node.operator ===
|
|
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);
|
|
6246
6604
|
return { defined: true, truthy: value !== 0 };
|
|
6247
6605
|
}
|
|
6248
|
-
if (
|
|
6606
|
+
if (ts7.isObjectLiteralExpression(node) || ts7.isArrayLiteralExpression(node) || ts7.isRegularExpressionLiteral(node) || ts7.isArrowFunction(node) || ts7.isFunctionExpression(node) || ts7.isClassExpression(node) || ts7.isNewExpression(node)) {
|
|
6249
6607
|
return { defined: true, truthy: true };
|
|
6250
6608
|
}
|
|
6251
6609
|
return null;
|
|
6252
6610
|
}
|
|
6253
6611
|
function expectAssertion(node, sf) {
|
|
6254
|
-
if (!
|
|
6612
|
+
if (!ts7.isPropertyAccessExpression(node.expression)) return null;
|
|
6255
6613
|
const matcher = node.expression.name.text;
|
|
6256
6614
|
let receiver = node.expression.expression;
|
|
6257
6615
|
let polarity = "";
|
|
6258
|
-
if (
|
|
6616
|
+
if (ts7.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
|
|
6259
6617
|
polarity = "not";
|
|
6260
6618
|
receiver = receiver.expression;
|
|
6261
6619
|
}
|
|
6262
|
-
if (!
|
|
6620
|
+
if (!ts7.isCallExpression(receiver) || !ts7.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
|
|
6263
6621
|
return null;
|
|
6264
6622
|
}
|
|
6265
6623
|
const subject = compact(receiver.arguments[0].getText(sf));
|
|
@@ -6275,41 +6633,41 @@ function expectAssertion(node, sf) {
|
|
|
6275
6633
|
};
|
|
6276
6634
|
}
|
|
6277
6635
|
function simpleCallName(node) {
|
|
6278
|
-
return
|
|
6636
|
+
return ts7.isIdentifier(node.expression) ? node.expression.text : null;
|
|
6279
6637
|
}
|
|
6280
6638
|
function callbackFor(node) {
|
|
6281
6639
|
const cb = node.arguments[1];
|
|
6282
|
-
return cb && (
|
|
6640
|
+
return cb && (ts7.isArrowFunction(cb) || ts7.isFunctionExpression(cb)) ? cb : null;
|
|
6283
6641
|
}
|
|
6284
6642
|
function blocks(src, path) {
|
|
6285
6643
|
const out2 = /* @__PURE__ */ new Map();
|
|
6286
6644
|
if (langOf(path) !== "js") return out2;
|
|
6287
6645
|
try {
|
|
6288
|
-
const sf =
|
|
6646
|
+
const sf = ts7.createSourceFile(path, src, ts7.ScriptTarget.Latest, true, scriptKindForPath(path));
|
|
6289
6647
|
const visit = (node, suites) => {
|
|
6290
|
-
if (
|
|
6648
|
+
if (ts7.isCallExpression(node)) {
|
|
6291
6649
|
const call = simpleCallName(node);
|
|
6292
6650
|
const title = literalTitle(node.arguments[0]);
|
|
6293
6651
|
const cb = callbackFor(node);
|
|
6294
6652
|
if (call && SUITE_CALLS.has(call) && title !== null && cb) {
|
|
6295
6653
|
const nextSuites = [...suites, title];
|
|
6296
|
-
|
|
6654
|
+
ts7.forEachChild(cb, (child) => visit(child, nextSuites));
|
|
6297
6655
|
return;
|
|
6298
6656
|
}
|
|
6299
6657
|
if (call && TEST_CALLS.has(call) && title !== null && cb) {
|
|
6300
6658
|
const assertions = [];
|
|
6301
6659
|
const collect2 = (child) => {
|
|
6302
|
-
if (child !== cb &&
|
|
6660
|
+
if (child !== cb && ts7.isCallExpression(child)) {
|
|
6303
6661
|
const nestedCall = simpleCallName(child);
|
|
6304
6662
|
if (nestedCall && (TEST_CALLS.has(nestedCall) || SUITE_CALLS.has(nestedCall))) {
|
|
6305
6663
|
return;
|
|
6306
6664
|
}
|
|
6307
6665
|
}
|
|
6308
|
-
if (
|
|
6666
|
+
if (ts7.isCallExpression(child)) {
|
|
6309
6667
|
const a = expectAssertion(child, sf);
|
|
6310
6668
|
if (a) assertions.push(a);
|
|
6311
6669
|
}
|
|
6312
|
-
|
|
6670
|
+
ts7.forEachChild(child, collect2);
|
|
6313
6671
|
};
|
|
6314
6672
|
collect2(cb);
|
|
6315
6673
|
const identity2 = [...suites, title].join(" > ");
|
|
@@ -6319,7 +6677,7 @@ function blocks(src, path) {
|
|
|
6319
6677
|
return;
|
|
6320
6678
|
}
|
|
6321
6679
|
}
|
|
6322
|
-
|
|
6680
|
+
ts7.forEachChild(node, (child) => visit(child, suites));
|
|
6323
6681
|
};
|
|
6324
6682
|
visit(sf, []);
|
|
6325
6683
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.17.
|
|
3
|
+
"version": "2.17.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",
|