progmune-runtime 3.7.43 → 3.7.46
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/CHANGELOG.md +271 -0
- package/dist/extract-ir-taint-structural.test.js +464 -3
- package/dist/extract-ir.js +1102 -75
- package/dist/sdk.js +1 -1
- package/package.json +1 -1
|
@@ -1401,7 +1401,9 @@ const WRITE_LAYER = [
|
|
|
1401
1401
|
"};",
|
|
1402
1402
|
"export class Renderer {",
|
|
1403
1403
|
' inst(n: string): string { return n + ".md"; }',
|
|
1404
|
-
|
|
1404
|
+
// 原名 stat —— **stat 在 fs sink 名单里**,调用点会被 sink 兜底直接标中,
|
|
1405
|
+
// 这条用例于是变成假通过(测的根本不是 helper 传播,R13)。改名后才测得准。
|
|
1406
|
+
' static toFile(n: string): string { return n + ".txt"; }',
|
|
1405
1407
|
"}",
|
|
1406
1408
|
'export function viaName({ name }: any): string { return name + ".md"; }',
|
|
1407
1409
|
].join("\n");
|
|
@@ -1410,8 +1412,8 @@ const WRITE_LAYER = [
|
|
|
1410
1412
|
expectMark: true, why: "宿主名可确证 ⇒ 走限定名 Owner.method" },
|
|
1411
1413
|
{ name: "正例·类实例方法", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("r.inst(k)", "const r = new Renderer();") },
|
|
1412
1414
|
expectMark: true, why: "宿主是变量,限定名对不上 ⇒ 靠方法名唯一 + 成员调用位" },
|
|
1413
|
-
{ name: "正例·静态类方法", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("Renderer.
|
|
1414
|
-
expectMark: true, why: "
|
|
1415
|
+
{ name: "正例·静态类方法", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("Renderer.toFile(k)") },
|
|
1416
|
+
expectMark: true, why: "与对象字面量同形,走限定名(方法名刻意避开 sink 名单,否则用例是假通过)" },
|
|
1415
1417
|
{ name: "正例·解构形参", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("viaName({ name: k })") },
|
|
1416
1418
|
expectMark: true, why: "形参名要从绑定模式里抽出来 —— getName() 给的是整个模式而不是 name" },
|
|
1417
1419
|
{ name: "负对照·对象字面量方法丢弃形参", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("Drop.fixed(k)") },
|
|
@@ -1437,3 +1439,462 @@ const WRITE_LAYER = [
|
|
|
1437
1439
|
}, 150000);
|
|
1438
1440
|
}
|
|
1439
1441
|
});
|
|
1442
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1443
|
+
// C4h(2026-09-20):三件都在「名录 ↔ 调用点」交界上的事
|
|
1444
|
+
//
|
|
1445
|
+
// ① 调用点常量代入 —— helper 的 return 走三元,本次调用喂的是常量 ⇒ 返回值
|
|
1446
|
+
// 其实是字面量。名录里的依赖集是**跨调用点**的并集,收不住这种个案
|
|
1447
|
+
// (探针实测:`viaFlag(k, true)` 误标)。按位置把常量代回去、解掉可判定的
|
|
1448
|
+
// 三元,判不出就退回原结论(保守侧)。
|
|
1449
|
+
// ② 同名成员多处 —— 旧规则要求方法名全项目唯一才登记 `.method(`。唯一性只是
|
|
1450
|
+
// 「不可能认错」的充分条件:同名两处**都被确证**时同样不可能认错。
|
|
1451
|
+
// 顺带修掉一处真实缺陷:旧代码在确证之前就把裸名塞进了成员名录。
|
|
1452
|
+
// ③ 属性承载的箭头 helper —— `const Util = { toPath: (n) => … }` 与
|
|
1453
|
+
// `Util.toPath = (n) => …`,这两种既不是方法声明也不是变量声明。
|
|
1454
|
+
//
|
|
1455
|
+
// 写法约定(与前几族同源):sink 实参里**不直接出现**污点变量 k。
|
|
1456
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1457
|
+
(0, vitest_1.describe)("C4h:名录之外的三处收口(调用点常量 / 同名成员 / 属性箭头)", () => {
|
|
1458
|
+
const emit = (relExpr, pre = "", imports = "", helpers = "") => ({
|
|
1459
|
+
"helpers.ts": helpers,
|
|
1460
|
+
"it.ts": [
|
|
1461
|
+
'import * as fs from "fs";',
|
|
1462
|
+
...(imports ? [`import { ${imports} } from "./helpers";`] : []),
|
|
1463
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1464
|
+
...(pre ? [` ${pre}`] : []),
|
|
1465
|
+
" Object.keys(doc).forEach((k) => {",
|
|
1466
|
+
` const rel = ${relExpr};`,
|
|
1467
|
+
' fs.writeFileSync(outDir + "/" + rel, "x");',
|
|
1468
|
+
" });",
|
|
1469
|
+
"}",
|
|
1470
|
+
].join("\n"),
|
|
1471
|
+
});
|
|
1472
|
+
// ── ① 调用点常量代入 ───────────────────────────────────────────────
|
|
1473
|
+
const H_CONST = [
|
|
1474
|
+
'export const viaFlag = (n: string, flag: boolean): string => (flag ? "fixed.md" : n + ".md");',
|
|
1475
|
+
// 两支都含形参 —— 收精度不能收到这儿
|
|
1476
|
+
"export const eitherWay = (n: string, flag: boolean): string => (flag ? n : n + '.md');",
|
|
1477
|
+
"export const pickMode = (n: string, mode: string): string => (mode === 'raw' ? n : 'fixed.md');",
|
|
1478
|
+
// 两跳:折叠要能穿过 helper 之间的嵌套调用
|
|
1479
|
+
'export const wrapFlag = (n: string, flag: boolean): string => viaFlag(n, flag);',
|
|
1480
|
+
].join("\n");
|
|
1481
|
+
// ── ② 同名成员多处 ─────────────────────────────────────────────────
|
|
1482
|
+
const H_TWICE_OK = [
|
|
1483
|
+
"export class Uploader { toPath(n: string): string { return n + '.md'; } }",
|
|
1484
|
+
"export class Downloader { toPath(n: string): string { return n + '.tmp'; } }",
|
|
1485
|
+
].join("\n");
|
|
1486
|
+
const H_TWICE_DROP = [
|
|
1487
|
+
"export class DropA { still(n: string): string { return 'fixed.md'; } }",
|
|
1488
|
+
"export class DropB { still(n: string): string { return 'other.md'; } }",
|
|
1489
|
+
].join("\n");
|
|
1490
|
+
// 一处引用了模块级**非字面量**对象 ⇒ 判据③不成立 ⇒ 整个同名组都不许放行。
|
|
1491
|
+
// 刻意不含 sink —— 否则会被跨函数 sink 形参映射标中,归因就不干净了(R13)。
|
|
1492
|
+
const H_TWICE_BAD = [
|
|
1493
|
+
"export class Uploader { toPath(n: string): string { return n + '.md'; } }",
|
|
1494
|
+
"export const dict: Record<string, string> = {};",
|
|
1495
|
+
"export class Weird { toPath(n: string): string { return dict[n]; } }",
|
|
1496
|
+
].join("\n");
|
|
1497
|
+
const H_SOLO = [
|
|
1498
|
+
"export class Solo { toPath(n: string): string { return n + '.md'; } }",
|
|
1499
|
+
].join("\n");
|
|
1500
|
+
// ── ③ 属性承载的箭头 ───────────────────────────────────────────────
|
|
1501
|
+
const H_PROP = [
|
|
1502
|
+
"export const Util2 = {",
|
|
1503
|
+
" toPath: (n: string): string => n + '.md',",
|
|
1504
|
+
"};",
|
|
1505
|
+
"export const NS = {",
|
|
1506
|
+
" path: {",
|
|
1507
|
+
" toPath: (n: string): string => n + '.md',",
|
|
1508
|
+
" },",
|
|
1509
|
+
"};",
|
|
1510
|
+
"export const Patched: any = {};",
|
|
1511
|
+
"Patched.toPath = (n: string): string => n + '.md';",
|
|
1512
|
+
"export const DropProp = {",
|
|
1513
|
+
" fixed: (n: string): string => 'fixed.md',",
|
|
1514
|
+
"};",
|
|
1515
|
+
].join("\n");
|
|
1516
|
+
// ── 缺陷回归门:非塑形方法的成员调用位不能算塑形 ────────────────────
|
|
1517
|
+
const H_NONSHAPER = [
|
|
1518
|
+
"export const registry: Record<string, string> = {};",
|
|
1519
|
+
"export class Reg {",
|
|
1520
|
+
" lookup(n: string): string { return registry[n]; }",
|
|
1521
|
+
"}",
|
|
1522
|
+
].join("\n");
|
|
1523
|
+
const H_SHAPER_LOOKUP = [
|
|
1524
|
+
"export class RegSafe {",
|
|
1525
|
+
" lookup(n: string): string { return n + '.md'; }",
|
|
1526
|
+
"}",
|
|
1527
|
+
].join("\n");
|
|
1528
|
+
const cases = [
|
|
1529
|
+
// ① —— 精度侧
|
|
1530
|
+
{ name: "正例·① 走真流出分支", fn: "emit", files: emit("viaFlag(k, false)", "", "viaFlag", H_CONST),
|
|
1531
|
+
expectMark: true, why: "flag=false ⇒ 走的是含 n 的那支" },
|
|
1532
|
+
{ name: "正例·① 两支都含形参", fn: "emit", files: emit("eitherWay(k, true)", "", "eitherWay", H_CONST),
|
|
1533
|
+
expectMark: true, why: "收精度不许收过头 —— 选中的分支里仍有 n" },
|
|
1534
|
+
{ name: "正例·① 字符串比较命中", fn: "emit", files: emit('pickMode(k, "raw")', "", "pickMode", H_CONST),
|
|
1535
|
+
expectMark: true, why: "mode === 'raw' 命中 ⇒ 返回 n" },
|
|
1536
|
+
{ name: "正例·① 第二参是变量(判不出就照标)", fn: "emit", files: emit("viaFlag(k, dyn)", "const dyn = Math.random() > 0;", "viaFlag", H_CONST),
|
|
1537
|
+
expectMark: true, why: "保守侧:实参不是常量 ⇒ 一律退回名录结论,不许假装看得懂" },
|
|
1538
|
+
{ name: "负对照·① 常量把分支钉死成字面量", fn: "emit", files: emit("viaFlag(k, true)", "", "viaFlag", H_CONST),
|
|
1539
|
+
expectMark: false, why: "本次调用的返回值就是 'fixed.md' —— 名录级依赖集收不住这种个案;正对照 = ① 走真流出分支" },
|
|
1540
|
+
{ name: "负对照·① 字符串比较没命中", fn: "emit", files: emit('pickMode(k, "cooked")', "", "pickMode", H_CONST),
|
|
1541
|
+
expectMark: false, why: "mode 不是 'raw' ⇒ 走常量支;正对照 = ① 字符串比较命中" },
|
|
1542
|
+
{ name: "负对照·① 嵌套在 path.join 里", fn: "emit", files: emit('path.join("out", viaFlag(k, true))', "", "viaFlag", H_CONST),
|
|
1543
|
+
expectMark: false, why: "整条 rhs 含 k,但那条支路不流出 —— 折叠要能作用在嵌套的内层" },
|
|
1544
|
+
{ name: "负对照·① 两跳 + 常量", fn: "emit", files: emit("wrapFlag(k, true)", "", "wrapFlag", H_CONST),
|
|
1545
|
+
expectMark: false, why: "wrapFlag(n, flag) = viaFlag(n, flag),折叠要穿过一层 helper" },
|
|
1546
|
+
{ name: "正对照·① 两跳走流出分支", fn: "emit", files: emit("wrapFlag(k, false)", "", "wrapFlag", H_CONST),
|
|
1547
|
+
expectMark: true, why: "与上一条镜像:常量改变量 ⇒ 立刻转红,证明归因在这条机制上" },
|
|
1548
|
+
// ② —— 召回侧
|
|
1549
|
+
{ name: "正例·② 同名成员两处均塑形", fn: "emit", files: emit("u.toPath(k)", "const u = new Uploader();", "Uploader", H_TWICE_OK),
|
|
1550
|
+
expectMark: true, why: "两处都被确证 ⇒ `.toPath(` 不可能落到别的实现上(唯一性换成全组确证)" },
|
|
1551
|
+
{ name: "正例·② 另一处同名也被识别", fn: "emit", files: emit("d.toPath(k)", "const d = new Downloader();", "Downloader", H_TWICE_OK),
|
|
1552
|
+
expectMark: true, why: "合并是对称的:同组的另一个宿主同样要接得上" },
|
|
1553
|
+
{ name: "正例·② 单处同名的老对照组", fn: "emit", files: emit("s.toPath(k)", "const s = new Solo();", "Solo", H_SOLO),
|
|
1554
|
+
expectMark: true, why: "全组确证是旧唯一性规则的上位,老行为必须还在" },
|
|
1555
|
+
{ name: "负对照·② 同名两处都丢弃形参", fn: "emit", files: emit("a.still(k)", "const a = new DropA();", "DropA", H_TWICE_DROP),
|
|
1556
|
+
expectMark: false, why: "合并后的依赖集按位取并集 —— 两处都丢 ⇒ 仍为空,精度不许丢" },
|
|
1557
|
+
{ name: "负对照·② 同名一处非塑形", fn: "emit", files: emit("u.toPath(k)", "const u = new Uploader();", "Uploader", H_TWICE_BAD),
|
|
1558
|
+
expectMark: false, why: "整组不放行(R11 的不变量:必须在每一种实现上都成立);正对照 = ② 同名成员两处均塑形" },
|
|
1559
|
+
// ③ —— 召回侧
|
|
1560
|
+
{ name: "正例·③ 属性箭头 helper", fn: "emit", files: emit("Util2.toPath(k)", "", "Util2", H_PROP),
|
|
1561
|
+
expectMark: true, why: "`{ toPath: (n) => … }` 既不是方法声明也不是变量声明 —— 旧名录整片漏" },
|
|
1562
|
+
{ name: "正例·③ 嵌套对象的属性箭头", fn: "emit", files: emit("NS.path.toPath(k)", "", "NS", H_PROP),
|
|
1563
|
+
expectMark: true, why: "限定名要能沿 owner 链一路拼到最外层变量" },
|
|
1564
|
+
{ name: "正例·③ 属性赋值箭头", fn: "emit", files: emit("Patched.toPath(k)", "", "Patched", H_PROP),
|
|
1565
|
+
expectMark: true, why: "`Obj.method = (n) => …` 这种后挂写法同属一族" },
|
|
1566
|
+
{ name: "负对照·③ 属性箭头丢弃形参", fn: "emit", files: emit("DropProp.fixed(k)", "", "DropProp", H_PROP),
|
|
1567
|
+
expectMark: false, why: "召回放宽到新载体,C4f/C4h-① 的收窄要跟得上;正对照 = ③ 属性箭头 helper" },
|
|
1568
|
+
{ name: "负对照·③ 属性箭头喂字面量", fn: "emit", files: emit('Util2.toPath("lit")', "", "Util2", H_PROP),
|
|
1569
|
+
expectMark: false, why: "无污点流入" },
|
|
1570
|
+
// 缺陷回归门(旧行为:确证之前就把裸名登记进成员名录)
|
|
1571
|
+
{ name: "负对照·非塑形方法的成员调用位不算塑形", fn: "emit", files: emit("r.lookup(k)", "const r = new Reg();", "Reg", H_NONSHAPER),
|
|
1572
|
+
expectMark: false, why: "lookup 引用了模块级非字面量对象 ⇒ 三条判据不成立 ⇒ 成员调用位不许进名录" },
|
|
1573
|
+
{ name: "正对照·同一个名字换成塑形版", fn: "emit", files: emit("r.lookup(k)", "const r = new RegSafe();", "RegSafe", H_SHAPER_LOOKUP),
|
|
1574
|
+
expectMark: true, why: "与上一条镜像 ⇒ 不标是因为判据,不是因为这条路压根走不通" },
|
|
1575
|
+
];
|
|
1576
|
+
for (const c of cases) {
|
|
1577
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
1578
|
+
const dir = makeProject(c.files);
|
|
1579
|
+
try {
|
|
1580
|
+
const marks = marksFor(dir, c.fn);
|
|
1581
|
+
if (c.expectMark)
|
|
1582
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
1583
|
+
else
|
|
1584
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
1585
|
+
}
|
|
1586
|
+
finally {
|
|
1587
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1588
|
+
}
|
|
1589
|
+
}, 150000);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
// C4i(2026-09-21):六个缺口 —— 三个是精度、三个是召回载体
|
|
1593
|
+
// 精度 ① 短路运算符(|| / && / ?? 与三元同形,只是判据换成左操作数的真假)
|
|
1594
|
+
// ② 模板串内嵌的三元(${} 内部是独立表达式,折叠要能进去)
|
|
1595
|
+
// 召回 ③ 柯里化高阶 helper ④ namespace 内的函数声明
|
|
1596
|
+
// ⑤ rest 形参 ⑥ IIFE 定义 / 计算属性名
|
|
1597
|
+
// 每组都配「判不出 ⇒ 退回保守」与「helper 真丢弃 ⇒ 仍不标」两条镜像(R14/R15)。
|
|
1598
|
+
(0, vitest_1.describe)("C4i:helper 的又一批载体与又一批可判定分支", () => {
|
|
1599
|
+
const emit = (relExpr, pre = "", imports = "", helpers = "") => ({
|
|
1600
|
+
"helpers.ts": helpers,
|
|
1601
|
+
"it.ts": [
|
|
1602
|
+
'import * as fs from "fs";',
|
|
1603
|
+
...(imports ? [`import { ${imports} } from "./helpers";`] : []),
|
|
1604
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1605
|
+
...(pre ? [` ${pre}`] : []),
|
|
1606
|
+
" Object.keys(doc).forEach((k) => {",
|
|
1607
|
+
` const rel = ${relExpr};`,
|
|
1608
|
+
' fs.writeFileSync(outDir + "/" + rel, "x");',
|
|
1609
|
+
" });",
|
|
1610
|
+
"}",
|
|
1611
|
+
].join("\n"),
|
|
1612
|
+
});
|
|
1613
|
+
// ── ① 短路 + ② 模板串 ─────────────────────────────────────────────
|
|
1614
|
+
const H_FOLD = [
|
|
1615
|
+
'export const viaOr = (n: string, s: any): string => s || n + ".md";',
|
|
1616
|
+
'export const viaAnd = (n: string, s: any): string => s && n + ".md";',
|
|
1617
|
+
'export const viaNull = (n: string, s: any): string => s ?? n + ".md";',
|
|
1618
|
+
'export const viaTpl = (n: string, f: boolean): string => `${f ? "fixed" : n}.md`;',
|
|
1619
|
+
].join("\n");
|
|
1620
|
+
// ── ③ 柯里化 ─────────────────────────────────────────────────────
|
|
1621
|
+
const H_CURRY = [
|
|
1622
|
+
'export const withExt = (ext: string) => (n: string): string => n + ext;',
|
|
1623
|
+
'export const dropExt = (_ext: string) => (_n: string): string => "fixed.md";',
|
|
1624
|
+
'export function curriedFn(ext: string) { return (n: string): string => n + ext; }',
|
|
1625
|
+
].join("\n");
|
|
1626
|
+
// ── ④ namespace ──────────────────────────────────────────────────
|
|
1627
|
+
const H_NS = [
|
|
1628
|
+
'import * as fs from "fs";',
|
|
1629
|
+
"export namespace P {",
|
|
1630
|
+
" export function toPath(n: string): string { return n + '.md'; }",
|
|
1631
|
+
" export function dropPath(_n: string): string { return 'fixed.md'; }",
|
|
1632
|
+
"}",
|
|
1633
|
+
"export namespace L {",
|
|
1634
|
+
" export function leakPath(n: string): string { fs.readFileSync(n); return 'x'; }",
|
|
1635
|
+
"}",
|
|
1636
|
+
].join("\n");
|
|
1637
|
+
// ── ⑤ rest 形参 ──────────────────────────────────────────────────
|
|
1638
|
+
const H_REST = [
|
|
1639
|
+
'export function joinAll(...parts: string[]): string { return parts.join("/"); }',
|
|
1640
|
+
'export function fixedAll(..._parts: string[]): string { return "fixed.md"; }',
|
|
1641
|
+
].join("\n");
|
|
1642
|
+
// ── ⑥ IIFE 定义 ──────────────────────────────────────────────────
|
|
1643
|
+
const H_IIFE = [
|
|
1644
|
+
'import * as fs from "fs";',
|
|
1645
|
+
'export const toPathIife = (() => (n: string): string => n + ".md")();',
|
|
1646
|
+
'export const dropIife = (() => (_n: string): string => "fixed.md")();',
|
|
1647
|
+
'export const leakIife = (() => (n: string): string => { fs.readFileSync(n); return "x"; })();',
|
|
1648
|
+
].join("\n");
|
|
1649
|
+
// ── ⑦ 计算属性名(只有 KEY 是模块级字面量常量才解得出来)────────────
|
|
1650
|
+
const H_COMPUTED = [
|
|
1651
|
+
'const KEY = "toPath";',
|
|
1652
|
+
'const DROP = "toFile";',
|
|
1653
|
+
"export const Box: any = { [KEY]: (n: string): string => n + '.md' };",
|
|
1654
|
+
"export const DropBox: any = { [DROP]: (_n: string): string => 'fixed.md' };",
|
|
1655
|
+
].join("\n");
|
|
1656
|
+
// 刻意**单独一个文件**:成员调用位 `.toPath(` 的匹配与宿主无关(C4g 的保守策略),
|
|
1657
|
+
// 把它和上面那个可解的 Box 放进同一个文件,会被 Box 登记的成员名顺带命中 ——
|
|
1658
|
+
// 那条期望就得改成 mark,测的也就不是「算不出」了(2026-09-21 实测踩到)。
|
|
1659
|
+
const H_DYN = [
|
|
1660
|
+
"const RANDOM = String(Math.random());",
|
|
1661
|
+
"export const DynBox: any = { [RANDOM]: (n: string): string => n + '.md' };",
|
|
1662
|
+
].join("\n");
|
|
1663
|
+
const cases = [
|
|
1664
|
+
// ① 短路 —— 精度侧
|
|
1665
|
+
{ name: "正例·① ||左为假值 ⇒ 取右支", fn: "emit", files: emit('viaOr(k, "")', "", "viaOr", H_FOLD),
|
|
1666
|
+
expectMark: true, why: "空串 falsy ⇒ 真的会走到含 n 的那支" },
|
|
1667
|
+
{ name: "负对照·① ||左为真值 ⇒ 取左支", fn: "emit", files: emit('viaOr(k, "fixed.md")', "", "viaOr", H_FOLD),
|
|
1668
|
+
expectMark: false, why: "返回值就是左值常量;正对照 = 上一条" },
|
|
1669
|
+
{ name: "正例·① &&左为真值 ⇒ 取右支", fn: "emit", files: emit('viaAnd(k, "pre")', "", "viaAnd", H_FOLD),
|
|
1670
|
+
expectMark: true, why: "truthy ⇒ 走到形参那支" },
|
|
1671
|
+
{ name: "负对照·① &&左为假值 ⇒ 取左支", fn: "emit", files: emit('viaAnd(k, "")', "", "viaAnd", H_FOLD),
|
|
1672
|
+
expectMark: false, why: "短路在左值就结束了;正对照 = 上一条" },
|
|
1673
|
+
{ name: "负对照·① ??左非 null ⇒ 取左支", fn: "emit", files: emit('viaNull(k, "fixed.md")', "", "viaNull", H_FOLD),
|
|
1674
|
+
expectMark: false, why: "?? 的判据是「非空」而不是「真」,两种语义不能混" },
|
|
1675
|
+
{ name: "正例·① ??左为 null ⇒ 取右支", fn: "emit", files: emit("viaNull(k, null)", "", "viaNull", H_FOLD),
|
|
1676
|
+
expectMark: true, why: "与上一条镜像 ⇒ 归因确实落在 ?? 的判定上" },
|
|
1677
|
+
{ name: "保守侧·① 实参是变量 ⇒ 判不出就照标", fn: "emit", files: emit("viaOr(k, dyn)", "const dyn = Math.random() > 0;", "viaOr", H_FOLD),
|
|
1678
|
+
expectMark: true, why: "认不出就不收精度 —— 这条守的是保守侧的底线" },
|
|
1679
|
+
// ② 模板串 —— 精度侧
|
|
1680
|
+
{ name: "正例·② 模板串里走流出分支", fn: "emit", files: emit("viaTpl(k, false)", "", "viaTpl", H_FOLD),
|
|
1681
|
+
expectMark: true, why: "f=false ⇒ ${} 里取 n" },
|
|
1682
|
+
{ name: "负对照·② 模板串里走常量分支", fn: "emit", files: emit("viaTpl(k, true)", "", "viaTpl", H_FOLD),
|
|
1683
|
+
expectMark: false, why: "折叠必须能进 ${} 内部;正对照 = 上一条" },
|
|
1684
|
+
{ name: "保守侧·② 模板串的 flag 是变量", fn: "emit", files: emit("viaTpl(k, dyn)", "const dyn = Math.random() > 0;", "viaTpl", H_FOLD),
|
|
1685
|
+
expectMark: true, why: "判不出 ⇒ 退回保守" },
|
|
1686
|
+
// ③ 柯里化 —— 召回侧
|
|
1687
|
+
{ name: "正例·③ 柯里化·污点在第二跳", fn: "emit", files: emit('withExt(".md")(k)', "", "withExt", H_CURRY),
|
|
1688
|
+
expectMark: true, why: "真正塑形的是被返回出来的内层函数" },
|
|
1689
|
+
{ name: "正例·③ 柯里化·污点在第一跳", fn: "emit", files: emit('withExt(k)("name")', "", "withExt", H_CURRY),
|
|
1690
|
+
expectMark: true, why: "外层实参被闭包捕获,同样是流进去的" },
|
|
1691
|
+
{ name: "正例·③ 柯里化·函数声明写法", fn: "emit", files: emit('curriedFn(".md")(k)', "", "curriedFn", H_CURRY),
|
|
1692
|
+
expectMark: true, why: "return 箭头不只出现在箭头 helper 上" },
|
|
1693
|
+
{ name: "负对照·③ 柯里化·内层丢弃形参", fn: "emit", files: emit('dropExt(".md")(k)', "", "dropExt", H_CURRY),
|
|
1694
|
+
expectMark: false, why: "召回放宽到新载体,收窄要跟得上;正对照 = ③ 污点在第二跳" },
|
|
1695
|
+
{ name: "负对照·③ 柯里化·两边都是常量", fn: "emit", files: emit('withExt(".md")("fixed")', "", "withExt", H_CURRY),
|
|
1696
|
+
expectMark: false, why: "根本没有污点进来" },
|
|
1697
|
+
// ④ namespace —— 召回侧
|
|
1698
|
+
{ name: "正例·④ namespace 内的 helper", fn: "emit", files: emit("P.toPath(k)", "", "P", H_NS),
|
|
1699
|
+
expectMark: true, why: "旧收集只走 sf.getFunctions() ⇒ namespace 里整片漏" },
|
|
1700
|
+
{ name: "负对照·④ namespace 内丢弃形参", fn: "emit", files: emit("P.dropPath(k)", "", "P", H_NS),
|
|
1701
|
+
expectMark: false, why: "限定名也要走依赖分析;正对照 = 上一条" },
|
|
1702
|
+
{ name: "负对照·④ namespace 内有 sink", fn: "emit", files: emit("L.leakPath(k)", "", "L", H_NS),
|
|
1703
|
+
expectMark: false, why: "判据③照旧生效 —— 载体换了不等于放水" },
|
|
1704
|
+
// ⑤ rest —— 召回侧
|
|
1705
|
+
{ name: "正例·⑤ rest 形参吃掉剩余实参", fn: "emit", files: emit('joinAll("out", k)', "", "joinAll", H_REST),
|
|
1706
|
+
expectMark: true, why: "rest 位是**一批**实参,按位截断会把第二位当成越界抹掉" },
|
|
1707
|
+
{ name: "负对照·⑤ rest 但丢弃", fn: "emit", files: emit('fixedAll("out", k)', "", "fixedAll", H_REST),
|
|
1708
|
+
expectMark: false, why: "召回放宽后精度不许丢;正对照 = 上一条" },
|
|
1709
|
+
// ⑥ IIFE —— 召回侧
|
|
1710
|
+
{ name: "正例·⑥ IIFE 定义 helper", fn: "emit", files: emit("toPathIife(k)", "", "toPathIife", H_IIFE),
|
|
1711
|
+
expectMark: true, why: "初值是**调用**不是箭头 —— 真正的 helper 在返回值里" },
|
|
1712
|
+
{ name: "负对照·⑥ IIFE 内丢弃形参", fn: "emit", files: emit("dropIife(k)", "", "dropIife", H_IIFE),
|
|
1713
|
+
expectMark: false, why: "收窄要跟得上;正对照 = 上一条" },
|
|
1714
|
+
{ name: "负对照·⑥ IIFE 体内有 sink", fn: "emit", files: emit("leakIife(k)", "", "leakIife", H_IIFE),
|
|
1715
|
+
expectMark: false, why: "declText 取的是整条 IIFE ⇒ sink 检查仍覆盖得到" },
|
|
1716
|
+
// ⑦ 计算属性名 —— 召回侧
|
|
1717
|
+
{ name: "正例·⑦ 计算属性名可解", fn: "emit", files: emit("Box.toPath(k)", "", "Box", H_COMPUTED),
|
|
1718
|
+
expectMark: true, why: "KEY 是模块级字面量常量 ⇒ 名字解得出来" },
|
|
1719
|
+
{ name: "负对照·⑦ 计算属性名里的 helper 丢弃形参", fn: "emit", files: emit("DropBox.toFile(k)", "", "DropBox", H_COMPUTED),
|
|
1720
|
+
expectMark: false, why: "收窄要跟得上;正对照 = 上一条" },
|
|
1721
|
+
{ name: "负对照·⑦ 属性名算不出来", fn: "emit", files: emit("DynBox.toPath(k)", "", "DynBox", H_DYN),
|
|
1722
|
+
expectMark: false, why: "`String(Math.random())` 不是常量 ⇒ 不登记(认不出就不传播)" },
|
|
1723
|
+
];
|
|
1724
|
+
for (const c of cases) {
|
|
1725
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
1726
|
+
const dir = makeProject(c.files);
|
|
1727
|
+
try {
|
|
1728
|
+
const marks = marksFor(dir, c.fn);
|
|
1729
|
+
if (c.expectMark)
|
|
1730
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
1731
|
+
else
|
|
1732
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
1733
|
+
}
|
|
1734
|
+
finally {
|
|
1735
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1736
|
+
}
|
|
1737
|
+
}, 150000);
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
// C4j(2026-09-21):再一批载体 + 一条判据误杀 + 一处**既有**缺陷的回归门
|
|
1741
|
+
// 召回 ① 运算符关键字(typeof)不再让整条 helper 落选
|
|
1742
|
+
// ② getter 返回出来的箭头(类 / 对象字面量)
|
|
1743
|
+
// ③ as / satisfies / 类型断言包住箭头
|
|
1744
|
+
// ④ 柯里化多跳(≥3 跳)+ 首跳实参是污点
|
|
1745
|
+
// ⑤ 工厂形态:返回的对象字面量里再取方法
|
|
1746
|
+
// ⑥ export default 匿名箭头(名字只能来自导入方)
|
|
1747
|
+
// 回归 ⑦ 成员组必须**记全**:同名三处里只要有一处判据不成立,成员调用位就不许放行
|
|
1748
|
+
// (C4h-② 的"全组确证"漏掉了"进不了组"的实现 —— 探针 B4/B6 实测挖到)
|
|
1749
|
+
// 每条真缺口都配镜像负对照(R14/R15)。
|
|
1750
|
+
(0, vitest_1.describe)("C4j:载体收口的最后一批 + 成员组完整性回归", () => {
|
|
1751
|
+
const emit = (relExpr, pre = "", imports = "", helpers = "") => ({
|
|
1752
|
+
"helpers.ts": helpers,
|
|
1753
|
+
"it.ts": [
|
|
1754
|
+
'import * as fs from "fs";',
|
|
1755
|
+
...(imports ? [`import { ${imports} } from "./helpers";`] : []),
|
|
1756
|
+
"export function emit(doc: Record<string, any>, outDir: string, mode: string) {",
|
|
1757
|
+
...(pre ? [` ${pre}`] : []),
|
|
1758
|
+
" Object.keys(doc).forEach((k) => {",
|
|
1759
|
+
` const rel = ${relExpr};`,
|
|
1760
|
+
' fs.writeFileSync(outDir + "/" + rel, "x");',
|
|
1761
|
+
" });",
|
|
1762
|
+
"}",
|
|
1763
|
+
].join("\n"),
|
|
1764
|
+
});
|
|
1765
|
+
/** 默认导出形态:helpers.ts 里 default 导出,it.ts 里真的 import 它 */
|
|
1766
|
+
const emitDef = (relExpr, importLine, helpers) => ({
|
|
1767
|
+
"helpers.ts": helpers,
|
|
1768
|
+
"it.ts": [
|
|
1769
|
+
'import * as fs from "fs";',
|
|
1770
|
+
importLine,
|
|
1771
|
+
"export function emit(doc: Record<string, any>, outDir: string, mode: string) {",
|
|
1772
|
+
" Object.keys(doc).forEach((k) => {",
|
|
1773
|
+
` const rel = ${relExpr};`,
|
|
1774
|
+
' fs.writeFileSync(outDir + "/" + rel, "x");',
|
|
1775
|
+
" });",
|
|
1776
|
+
"}",
|
|
1777
|
+
].join("\n"),
|
|
1778
|
+
});
|
|
1779
|
+
// ── ① 关键字判据 ─────────────────────────────────────────────────
|
|
1780
|
+
const H_KW = [
|
|
1781
|
+
'export const viaKw = (n: string, m: string): any => typeof m === "string" || n + ".md";',
|
|
1782
|
+
'export const viaKwDrop = (n: string, m: string): any => typeof m === "string" || "fixed.md";',
|
|
1783
|
+
].join("\n");
|
|
1784
|
+
// ── ② getter 返回箭头(含同名三处的组成员完整性)─────────────────
|
|
1785
|
+
const H_GET = [
|
|
1786
|
+
"import * as fs from 'fs';",
|
|
1787
|
+
// 同一个文件里三个同名 getter,覆盖三类命运:
|
|
1788
|
+
// 无形参(判据①不收)/ 好 helper(确证)/ 体内有 sink(判据③不收)
|
|
1789
|
+
'export const ONoParam: any = { get mk() { return (): string => "fixed.md"; } };',
|
|
1790
|
+
'export const OSideCall: any = { get mk() { return (n: string): string => { __audit(n); return n + ".md"; }; } };',
|
|
1791
|
+
'export const OWithSink: any = { get mk() { return (n: string): void => { fs.writeFileSync(n, ""); }; } };',
|
|
1792
|
+
].join("\n");
|
|
1793
|
+
const H_GET_CLS = [
|
|
1794
|
+
'export class CfgMk { get mk(): (n: string) => string { return (n: string): string => n + ".md"; } }',
|
|
1795
|
+
].join("\n");
|
|
1796
|
+
const H_GET_DROP = [
|
|
1797
|
+
'export const OGet2: any = { get mk() { return (_n: string): string => "fixed.md"; } };',
|
|
1798
|
+
].join("\n");
|
|
1799
|
+
// ── ③ 断言解包 ───────────────────────────────────────────────────
|
|
1800
|
+
const H_ASSERT = [
|
|
1801
|
+
'export const toPathAs = ((n: string): string => n + ".md") as (n: string) => string;',
|
|
1802
|
+
'export const toPathSat = ((n: string): string => n + ".md") satisfies (n: string) => string;',
|
|
1803
|
+
'export const ObjAs: any = { toPath: ((n: string): string => n + ".md") as any };',
|
|
1804
|
+
'export const notFn = (("x") as any);',
|
|
1805
|
+
].join("\n");
|
|
1806
|
+
// ── ④ 柯里化多跳 ─────────────────────────────────────────────────
|
|
1807
|
+
const H_LVL = [
|
|
1808
|
+
'export const lvl3 = (a: string) => (b: string) => (n: string): string => n + a + b;',
|
|
1809
|
+
'export const lvl4 = (a: string) => (b: string) => (c: string) => (n: string): string => n + a + b + c;',
|
|
1810
|
+
'export const dropLast = (a: string) => (b: string) => (_n: string): string => "fixed.md";',
|
|
1811
|
+
].join("\n");
|
|
1812
|
+
// ── ⑤ 工厂形态 ───────────────────────────────────────────────────
|
|
1813
|
+
const H_FACT = [
|
|
1814
|
+
"import * as fs from 'fs';",
|
|
1815
|
+
'export const factory = (ext: string) => ({ toPath: (n: string): string => n + ext });',
|
|
1816
|
+
'export const factoryDrop = (_ext: string) => ({ toPath: (_n: string): string => "fixed.md" });',
|
|
1817
|
+
'export const factorySink = (ext: string) => ({ toPath: (n: string): string => { fs.readFileSync(n); return ext; } });',
|
|
1818
|
+
].join("\n");
|
|
1819
|
+
// 一个工厂返回含**两个**方法的对象:一个塑形、一个丢弃。
|
|
1820
|
+
// 这守的是「一个名字 → 多条链,按成员名选链」——选错就等于张冠李戴。
|
|
1821
|
+
const H_MIX = [
|
|
1822
|
+
'export const makeMixed = (ext: string) => ({',
|
|
1823
|
+
' mix: (n: string): string => n + ext,',
|
|
1824
|
+
' skip: (_n: string): string => "fixed.md",',
|
|
1825
|
+
'});',
|
|
1826
|
+
].join("\n");
|
|
1827
|
+
// ── ⑥ 默认导出 ───────────────────────────────────────────────────
|
|
1828
|
+
const H_DEF = 'export default (n: string): string => n + ".md";';
|
|
1829
|
+
const H_DEF_DROP = 'export default (_n: string): string => "fixed.md";';
|
|
1830
|
+
const H_DEF_NOTFN = 'export default "fixed.md";';
|
|
1831
|
+
const cases = [
|
|
1832
|
+
// ① 关键字
|
|
1833
|
+
{ name: "正例·① 含 typeof 的 helper 要能进名录", fn: "emit", files: emit("viaKw(k, mode)", "", "viaKw", H_KW),
|
|
1834
|
+
expectMark: true, why: "旧代码把 typeof 当成未知自由标识符 ⇒ 整条判据不通过 ⇒ 漏报" },
|
|
1835
|
+
{ name: "负对照·① 同形但返回值是常量", fn: "emit", files: emit("viaKwDrop(k, mode)", "", "viaKwDrop", H_KW),
|
|
1836
|
+
expectMark: false, why: "关键字放行不等于放水:依赖分析照旧;正对照 = 上一条" },
|
|
1837
|
+
// ② getter
|
|
1838
|
+
{ name: "正例·② 类 getter 返回的箭头", fn: "emit", files: emit("new CfgMk().mk(k)", "", "CfgMk", H_GET_CLS),
|
|
1839
|
+
expectMark: true, why: "getter 不是方法声明(没有形参)⇒ 旧收集整族漏" },
|
|
1840
|
+
{ name: "正例·② 对象 getter 返回的箭头", fn: "emit", files: emit("OSideCall.mk(k)", "", "OSideCall", H_GET),
|
|
1841
|
+
expectMark: true, why: "靠限定名 OSideCall.mk 接上(裸名 mk 因组里有未确证的实现而不放行)" },
|
|
1842
|
+
{ name: "回归·⑦ 同名 getter 有一处无形参 ⇒ 成员位不许放行", fn: "emit", files: emit("ONoParam.mk(k)", "", "ONoParam", H_GET),
|
|
1843
|
+
expectMark: false, why: "组必须记全:进不了组的实现(判据①不成立)也算「不是全组确证」" },
|
|
1844
|
+
{ name: "回归·⑦ 同名 getter 有一处体内有 sink ⇒ 成员位不许放行", fn: "emit", files: emit("OWithSink.mk(k)", "", "OWithSink", H_GET),
|
|
1845
|
+
expectMark: false, why: "同上,判据③不成立的那一处同样要进组" },
|
|
1846
|
+
{ name: "负对照·② getter 返回的箭头丢弃形参", fn: "emit", files: emit("OGet2.mk(k)", "", "OGet2", H_GET_DROP),
|
|
1847
|
+
expectMark: false, why: "载体换了,收窄照旧;正对照 = ② 对象 getter" },
|
|
1848
|
+
// ③ 断言解包
|
|
1849
|
+
{ name: "正例·③ as 断言包住箭头", fn: "emit", files: emit("toPathAs(k)", "", "toPathAs", H_ASSERT),
|
|
1850
|
+
expectMark: true, why: "初值是 AsExpression,旧代码只认「初值就是箭头」" },
|
|
1851
|
+
{ name: "正例·③ satisfies 断言包住箭头", fn: "emit", files: emit("toPathSat(k)", "", "toPathSat", H_ASSERT),
|
|
1852
|
+
expectMark: true, why: "同族另一种写法" },
|
|
1853
|
+
{ name: "正例·③ 成员位也要解断言", fn: "emit", files: emit("ObjAs.toPath(k)", "", "ObjAs", H_ASSERT),
|
|
1854
|
+
expectMark: true, why: "属性位的初值同样被断言包着" },
|
|
1855
|
+
{ name: "负对照·③ as 包住的不是函数", fn: "emit", files: emit("notFn(k)", "", "notFn", H_ASSERT),
|
|
1856
|
+
expectMark: false, why: "解断言不等于把任意 as 表达式当 helper" },
|
|
1857
|
+
// ④ 柯里化多跳
|
|
1858
|
+
{ name: "正例·④ 三跳柯里化", fn: "emit", files: emit('lvl3("a")("b")(k)', "", "lvl3", H_LVL),
|
|
1859
|
+
expectMark: true, why: "旧代码只吃两跳 ⇒ 三跳整条不展" },
|
|
1860
|
+
{ name: "正例·④ 四跳柯里化", fn: "emit", files: emit('lvl4("a")("b")("c")(k)', "", "lvl4", H_LVL),
|
|
1861
|
+
expectMark: true, why: "链式消费各跳实参,跳数不设上限(爬取有界 4 层)" },
|
|
1862
|
+
{ name: "正例·④ 三跳·污点在第一跳", fn: "emit", files: emit('lvl3(k)("a")("b")', "", "lvl3", H_LVL),
|
|
1863
|
+
expectMark: true, why: "首跳实参被闭包一路捕获" },
|
|
1864
|
+
{ name: "负对照·④ 三跳·最后一跳丢弃形参", fn: "emit", files: emit('dropLast("a")("b")(k)', "", "dropLast", H_LVL),
|
|
1865
|
+
expectMark: false, why: "多跳之下依赖分析仍要生效;正对照 = ④ 三跳" },
|
|
1866
|
+
// ⑤ 工厂形态
|
|
1867
|
+
{ name: "正例·⑤ 返回对象再取方法", fn: "emit", files: emit('factory(".md").toPath(k)', "", "factory", H_FACT),
|
|
1868
|
+
expectMark: true, why: "真正塑形的是返回出来的那个成员方法" },
|
|
1869
|
+
{ name: "负对照·⑤ 工厂里方法丢弃形参", fn: "emit", files: emit('factoryDrop(".md").toPath(k)', "", "factoryDrop", H_FACT),
|
|
1870
|
+
expectMark: false, why: "镜像" },
|
|
1871
|
+
{ name: "负对照·⑤ 工厂里方法体内有 sink", fn: "emit", files: emit('factorySink(".md").toPath(k)', "", "factorySink", H_FACT),
|
|
1872
|
+
expectMark: false, why: "判据③在链的最内层照旧生效" },
|
|
1873
|
+
{ name: "正例·⑤ 工厂返回多个方法·取塑形那个", fn: "emit", files: emit('makeMixed(".md").mix(k)', "", "makeMixed", H_MIX),
|
|
1874
|
+
expectMark: true, why: "一个工厂名对多条链,按**成员名**选链" },
|
|
1875
|
+
{ name: "负对照·⑤ 工厂返回多个方法·取丢弃那个", fn: "emit", files: emit('makeMixed(".md").skip(k)', "", "makeMixed", H_MIX),
|
|
1876
|
+
expectMark: false, why: "同名工厂的另条链返回常量 ⇒ 不许张冠李戴;正对照 = 上一条" },
|
|
1877
|
+
// ⑥ 默认导出
|
|
1878
|
+
{ name: "正例·⑥ 匿名默认导出(名字来自导入方)", fn: "emit", files: emitDef("toPath(k)", 'import toPath from "./helpers";', H_DEF),
|
|
1879
|
+
expectMark: true, why: "匿名 default 没有名字可登记 ⇒ 只能用导入它的文件里起的本地名" },
|
|
1880
|
+
{ name: "负对照·⑥ 匿名默认导出·丢弃形参", fn: "emit", files: emitDef("toPath(k)", 'import toPath from "./helpers";', H_DEF_DROP),
|
|
1881
|
+
expectMark: false, why: "镜像" },
|
|
1882
|
+
{ name: "负对照·⑥ 默认导出的不是函数", fn: "emit", files: emitDef("toPath(k)", 'import toPath from "./helpers";', H_DEF_NOTFN),
|
|
1883
|
+
expectMark: false, why: "边界:不许乱收" },
|
|
1884
|
+
];
|
|
1885
|
+
for (const c of cases) {
|
|
1886
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
1887
|
+
const dir = makeProject(c.files);
|
|
1888
|
+
try {
|
|
1889
|
+
const marks = marksFor(dir, c.fn);
|
|
1890
|
+
if (c.expectMark)
|
|
1891
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
1892
|
+
else
|
|
1893
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
1894
|
+
}
|
|
1895
|
+
finally {
|
|
1896
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1897
|
+
}
|
|
1898
|
+
}, 150000);
|
|
1899
|
+
}
|
|
1900
|
+
});
|