progmune-runtime 3.7.45 → 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 +103 -0
- package/dist/extract-ir-taint-structural.test.js +161 -0
- package/dist/extract-ir.js +343 -82
- package/dist/sdk.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.46] — 2026-09-21
|
|
4
|
+
|
|
5
|
+
### C4j:载体收口的最后一批 + 一处既有缺陷的回归门
|
|
6
|
+
|
|
7
|
+
C4i 的备忘留了四条候选。按 R17 先写探针复现,同时塞进七条「没人提过但看着差不多」的
|
|
8
|
+
形状 —— 三轮探针:24 条覆盖面 → 17 条**防修过头**的边界 → 6 条给默认导出补真 import。
|
|
9
|
+
真缺口六条;另有一条**不是这轮引入**的缺陷被探针顺手挖出来(见下)。
|
|
10
|
+
|
|
11
|
+
| 形状 | 旧行为 | 机制 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `typeof m === "string" \|\| n + ".md"` | 整条 helper 落选 | `typeof` 被当成**未知自由标识符** ⇒ 判据不通过 |
|
|
14
|
+
| `((n) => …) as Fn` / `satisfies` / `<Fn>…` | 不收 | 初值是**断言表达式**不是箭头;成员位同理 |
|
|
15
|
+
| `get mk() { return (n) => … }` | 不收 | getter 没有形参,`getFunctions()` 取不到 |
|
|
16
|
+
| `lvl3("a")("b")(k)` 三/四跳 | 不展 | 旧机制只吃**两跳** |
|
|
17
|
+
| `factory(".md").toPath(k)` | 不展 | 真正塑形的是**返回对象里的成员方法** |
|
|
18
|
+
| `export default (n) => …` | 收不到 | 匿名默认导出**没有名字**可登记 |
|
|
19
|
+
|
|
20
|
+
**精度侧一条:运算符关键字。** `typeof` / `void` / `in` / `instanceof` 是运算符不是自由
|
|
21
|
+
标识符,出现在 return 表达式里不该让整条 helper 落选。**只放行运算符** —— `this` /
|
|
22
|
+
`super` 不放行,它们指向宿主状态,放行等于给「返回值依赖 this」的假塑形开口子。
|
|
23
|
+
|
|
24
|
+
**召回侧五条。**
|
|
25
|
+
|
|
26
|
+
- **断言解包**:`unwrapFns` 解 `as` / `satisfies` / `<T>` 类型断言,**变量位、属性位、
|
|
27
|
+
调用点 callee、返回位**都要用 —— `get mk() { return ((n) => …) as Fn }` 就是「返回位
|
|
28
|
+
没解」漏出来的。
|
|
29
|
+
- **getter 返回的箭头**:getter 自己的形参表是空的,判据要落在**它返回出来的那一层**,
|
|
30
|
+
与柯里化同一条路。
|
|
31
|
+
- **柯里化推广成「跳链」**:`CurrySpec` 从写死的「外层 + 一层内层」改成
|
|
32
|
+
`{ outerParams, hops: CurryHop[], allParams, rets }`。`lvl3 = (a) => (b) => (n) => n+a+b`
|
|
33
|
+
⇒ `outerParams=[a]`、`hops=[call[b], call[n]]`;工厂形态 ⇒ `hops=[member toPath [n]]`。
|
|
34
|
+
调用点从「两层实参代入」改成**逐跳消费**,跳数不设死上限(爬取有界 4 层),首跳实参
|
|
35
|
+
同样被闭包捕获(所以 `lvl3(k)("a")("b")` 与 `lvl3("a")("b")(k)` 都要对)。
|
|
36
|
+
- **工厂形态 = 一个名字多条链**:按**成员名**选链(`makeMixed(".md").mix(k)` 走 mix 那条),
|
|
37
|
+
脏链(返回常量 / 体内有 sink)单独剔掉,不许污染同名的另一条。
|
|
38
|
+
- **匿名默认导出**:没名字 ⇒ 用**导入方**起的本地名发布(`import toPath from "./helpers"`)。
|
|
39
|
+
|
|
40
|
+
### 探针挖出的一处既有缺陷(C4h-② 的洞)
|
|
41
|
+
|
|
42
|
+
C4h 把「同名成员唯一才登记」换成「全组确证」,但只把**通过判据**的实现算进组:判据不
|
|
43
|
+
成立的实现没进组 ⇒ 组看起来「全组确证」⇒ 成员调用位放行 ⇒ 误标。
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
同名三处 getter:无形参的(判据①不成立)/ 正常塑形 / 体内有 sink 的(判据③不成立)
|
|
47
|
+
后两处进组、第一处没进 ⇒ 组"干净" ⇒ .mk( 放行 ⇒ ONoParam.mk(k) 被误标
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
修法 `noteUnaccepted`:**失败的实现也记进组**(记 key + group,不进候选)。组一脏,整组
|
|
51
|
+
不放行。教训:**「全组」的分母必须包含被判据拒掉的那些**,否则「全组确证」会退化成
|
|
52
|
+
「通过者全体确证」—— 已升为方法学规则 **R19**。
|
|
53
|
+
|
|
54
|
+
### 又一条「探针期望写错」(第二次)
|
|
55
|
+
|
|
56
|
+
- **F9a**:探针忘了写 import。匿名默认导出的名字只能来自导入它的文件,两文件之间没有
|
|
57
|
+
import 就无从登记 —— 期望本身不可达。补上真 import 后通过。
|
|
58
|
+
- **F9f**:期望 unmarked、实测 mark。查清后是**期望写错**不是误报:`(0, (n) => n + ".md")(k)`
|
|
59
|
+
里那个**内联匿名箭头自己就是塑形函数**,与默认导出无关 —— 把 `helpers.ts` 删掉照样标。
|
|
60
|
+
|
|
61
|
+
⇒ 两条都不是机制错,是**预测错**。写期望前先问:这条标/不标,到底由**哪一个机制**决定。
|
|
62
|
+
|
|
63
|
+
### 三条「看着像缺口、这轮故意不修」的边界
|
|
64
|
+
|
|
65
|
+
探针里留着(F5 / F7 / A6 / F12),都退回保守侧:`switch` 分支与 `if` 早返回的返回值取
|
|
66
|
+
**并集**(常量分支和污点分支混在一起,判不出);`x != null` 这类**比较式**左值不折;
|
|
67
|
+
标识符位「先声明后赋值」(`let f; f = (n) => …`)不收。这三条不是漏,是「判不出就退回
|
|
68
|
+
保守」的固有代价 —— 要收得让调用点折叠长出手臂(按分支路径分别代入),属另一轮的量级。
|
|
69
|
+
|
|
70
|
+
### 验收
|
|
71
|
+
|
|
72
|
+
| 门 | 结果 |
|
|
73
|
+
|---|---|
|
|
74
|
+
| taintpath 闸门 | **160/160**(新增 K 族 22 条:12 mark / 9 no-taint / 1 suppressed) |
|
|
75
|
+
| fr-007 真实语料 | pre 5 / post 0 **维持** |
|
|
76
|
+
| fr-016 真实语料 | pre 7 / post 0 **维持** |
|
|
77
|
+
| 定向用例 | 提取器定向用例 **227 passed / 6 个测试文件**(其中本文件新增 C4j 组 23 条:12 正 11 负,含两条回归·⑦) |
|
|
78
|
+
| TS 盲测 | 111 项目 / perFunction 口径 **3201 条**:**LOST 0 / ADDED 18**,全部来自新增的 K 族 —— 其中 12 条 `path_traversal` 是本该标的,6 条 `data_integrity`(写文件无审计)属新项目的无关类别;基线那 3183 条**零漂移零丢失** |
|
|
79
|
+
| tsc | 零错误 |
|
|
80
|
+
|
|
81
|
+
### 反向验证七刀(归因)
|
|
82
|
+
|
|
83
|
+
每刀只把一个机制退回**旧行为**(R18:不许 `return null` 逼停):
|
|
84
|
+
|
|
85
|
+
| 刀 | 改动 | 转红 |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| 1 | 关掉运算符关键字放行 | emitKw |
|
|
88
|
+
| 2 | 摘掉 getter 收集 | emitGetterCls / emitGetterObj |
|
|
89
|
+
| 3 | 断言解包退回只脱括号 | emitAs / emitAsSat / emitAsMember |
|
|
90
|
+
| 4 | 柯里化退回两跳语义 | emitCurry3 / emitCurry4 / emitCurryFirst |
|
|
91
|
+
| 5 | 工厂形态不登记 | emitFactory / emitFactoryMix |
|
|
92
|
+
| 6 | 摘掉匿名默认导出收集 | emitDefault |
|
|
93
|
+
| 7 | 成员组退回「判据失败不进组」 | 定向用例 回归·⑦ 两条 |
|
|
94
|
+
|
|
95
|
+
闸门其余 148 条、定向用例 C4j 组其余 21 条全程不动 ⇒ 七处互不相干,归因干净。
|
|
96
|
+
|
|
97
|
+
### 实现坑三条
|
|
98
|
+
|
|
99
|
+
1. **解包要解到位**:`returnedValueOf` 也得解断言 —— `get mk() { return ((n) => …) as Fn }`
|
|
100
|
+
漏的就是这一处("返回位没解")。
|
|
101
|
+
2. **一条名字多条链时,脏链要单独剔掉**:`makeMixed` 的 `skip`(返回常量)不许把 `mix`
|
|
102
|
+
那条链带下水,否则「张冠李戴」在链层面复现。
|
|
103
|
+
3. **`expandCurried` 的实参位置**:首跳实参落在外层形参的第一个,不是内层 —— 从「两层
|
|
104
|
+
实参代入」改成「逐跳消费」时最容易写反。
|
|
105
|
+
|
|
3
106
|
## [3.7.45] — 2026-09-21
|
|
4
107
|
|
|
5
108
|
### C4i:又一批可判定分支 + 又一批 helper 载体
|
|
@@ -1737,3 +1737,164 @@ const WRITE_LAYER = [
|
|
|
1737
1737
|
}, 150000);
|
|
1738
1738
|
}
|
|
1739
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
|
+
});
|
package/dist/extract-ir.js
CHANGED
|
@@ -669,6 +669,21 @@ const VALUE_STRING_METHODS = new Set([
|
|
|
669
669
|
const SHAPER_ALLOWED_FREE_IDENTS = new Set([
|
|
670
670
|
"path", "sep", "undefined", "null", "true", "false", "String", "Number",
|
|
671
671
|
]);
|
|
672
|
+
/**
|
|
673
|
+
* C4j:**运算符关键字**——本身不携带数据,出现在 return 表达式里不该让整条 helper 落选。
|
|
674
|
+
*
|
|
675
|
+
* 缺陷实证(2026-09-21 探针 G1/G2):`(n, m) => typeof m === "string" || n + ".md"`
|
|
676
|
+
* 整条判据不通过 —— 单趟标识符扫描把 `typeof` 当成"未知的自由标识符"给否了。
|
|
677
|
+
* 后果不是"少收一点精度",而是**整个 helper 不进名录** ⇒ 调用点拿不到任何证据 ⇒ 漏报。
|
|
678
|
+
* 差别只在 typeof:G2 同形去掉 typeof 后 MARKED,G1 不标。
|
|
679
|
+
*
|
|
680
|
+
* `this` / `super` 刻意**不放行**:它们是接收者,可能携带数据(`this.prefix + n`),
|
|
681
|
+
* 属于另一个话题(要不要把接收者纳入证据),不能靠"关键字"这条口子捎带进来。
|
|
682
|
+
*/
|
|
683
|
+
const SHAPER_ALLOWED_KEYWORDS = new Set([
|
|
684
|
+
"typeof", "void", "delete", "in", "instanceof", "keyof", "as", "satisfies",
|
|
685
|
+
"is", "asserts", "readonly", "infer", "new", "await", "yield", "of",
|
|
686
|
+
]);
|
|
672
687
|
/** 允许出现在调用位的值转换函数(不改变路径语义) */
|
|
673
688
|
const VALUE_CONVERTER_CALLS = new Set(["String", "Number", "Boolean"]);
|
|
674
689
|
/**
|
|
@@ -713,6 +728,8 @@ function isPureShaperExpr(expr, params, shapers, extraIdents) {
|
|
|
713
728
|
// 自由标识符:必须是自己的形参、语言级常量白名单,或**已确证的字面量常量 / path 别名**
|
|
714
729
|
if (params.has(n) || SHAPER_ALLOWED_FREE_IDENTS.has(n))
|
|
715
730
|
continue;
|
|
731
|
+
if (SHAPER_ALLOWED_KEYWORDS.has(n))
|
|
732
|
+
continue; // C4j:运算符关键字不携带数据
|
|
716
733
|
if (extraIdents && extraIdents.has(n))
|
|
717
734
|
continue;
|
|
718
735
|
return false;
|
|
@@ -891,11 +908,35 @@ function moduleQualifier(fn) {
|
|
|
891
908
|
}
|
|
892
909
|
return null;
|
|
893
910
|
}
|
|
894
|
-
/**
|
|
895
|
-
|
|
911
|
+
/**
|
|
912
|
+
* 剥掉包在**函数体外面的**那几层壳,露出真正的箭头 / 函数表达式:
|
|
913
|
+
* 括号 `((n) => …)`、类型断言 `((n) => …) as Fn`、`satisfies Fn`、`<Fn>(…)`。
|
|
914
|
+
*
|
|
915
|
+
* C4j 补后三种。实测(探针 F3b/F3c):`export const toPath = ((n) => n + ".md")
|
|
916
|
+
* satisfies Fn` 与 `{ toPath: ((n) => n + ".md") as any }` 都不传播 ——
|
|
917
|
+
* 旧代码只认「初值就是箭头」,断言把它们包了一层,整族漏。
|
|
918
|
+
*/
|
|
919
|
+
function unwrapFns(n) {
|
|
896
920
|
let cur = n;
|
|
897
|
-
|
|
898
|
-
|
|
921
|
+
for (let i = 0; i < 6 && cur; i++) {
|
|
922
|
+
if (ts_morph_1.Node.isParenthesizedExpression(cur)) {
|
|
923
|
+
cur = cur.getExpression();
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (ts_morph_1.Node.isAsExpression(cur)) {
|
|
927
|
+
cur = cur.getExpression();
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
if (ts_morph_1.Node.isSatisfiesExpression(cur)) {
|
|
931
|
+
cur = cur.getExpression();
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
if (ts_morph_1.Node.isTypeAssertion(cur)) {
|
|
935
|
+
cur = cur.getExpression();
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
break;
|
|
939
|
+
}
|
|
899
940
|
return cur;
|
|
900
941
|
}
|
|
901
942
|
/**
|
|
@@ -906,23 +947,23 @@ function unparen(n) {
|
|
|
906
947
|
function functionLikeOf(init) {
|
|
907
948
|
// 实测:`(() => (n) => n + ".md")()` 里被括号包住的**不止**内层箭头 ——
|
|
908
949
|
// 连被调用的 callee 都是 ParenthesizedExpression。两处都得脱。
|
|
909
|
-
const base =
|
|
950
|
+
const base = unwrapFns(init);
|
|
910
951
|
if (!base)
|
|
911
952
|
return null;
|
|
912
953
|
if (ts_morph_1.Node.isArrowFunction(base) || ts_morph_1.Node.isFunctionExpression(base))
|
|
913
954
|
return base;
|
|
914
955
|
if (ts_morph_1.Node.isCallExpression(base)) {
|
|
915
|
-
const callee =
|
|
956
|
+
const callee = unwrapFns(base.getExpression());
|
|
916
957
|
if (!ts_morph_1.Node.isArrowFunction(callee) && !ts_morph_1.Node.isFunctionExpression(callee))
|
|
917
958
|
return null;
|
|
918
959
|
const b = callee.getBody();
|
|
919
960
|
if (!ts_morph_1.Node.isBlock(b)) {
|
|
920
961
|
// 简洁体:`(() => (n) => n + ".md")()` —— 括号包着也算(unparen)
|
|
921
|
-
const e =
|
|
962
|
+
const e = unwrapFns(b);
|
|
922
963
|
return e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)) ? e : null;
|
|
923
964
|
}
|
|
924
965
|
for (const rs of b.getDescendantsOfKind(ts.SyntaxKind.ReturnStatement)) {
|
|
925
|
-
const e =
|
|
966
|
+
const e = unwrapFns(rs.getExpression());
|
|
926
967
|
if (e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)))
|
|
927
968
|
return e;
|
|
928
969
|
}
|
|
@@ -975,51 +1016,135 @@ function paramIndexOf(si, argIndex) {
|
|
|
975
1016
|
return si.restAt >= 0 && argIndex >= si.restAt ? si.restAt : argIndex;
|
|
976
1017
|
}
|
|
977
1018
|
/**
|
|
978
|
-
* C4i:柯里化 helper ——
|
|
979
|
-
* (`
|
|
980
|
-
*
|
|
981
|
-
*
|
|
1019
|
+
* C4i/C4j:柯里化 helper —— 函数体【返回】另一个函数
|
|
1020
|
+
* (`(ext) => (n) => n + ext`、`(a) => (b) => (n) => n + a + b`)。
|
|
1021
|
+
* 被调用的不是最外层,而是它一路返回出来的最内层;真正塑形在那里,
|
|
1022
|
+
* 外层只负责捕获自己那一批实参。返回的不是函数(含返回对象字面量的工厂形态)
|
|
1023
|
+
* 一律返回 null,交给 currySpecsOf 的另一条路。
|
|
982
1024
|
*/
|
|
983
|
-
function
|
|
984
|
-
const
|
|
985
|
-
if (!
|
|
986
|
-
return null;
|
|
987
|
-
const innerParams = inner.getParameters().flatMap(paramNamesOf).filter(Boolean);
|
|
988
|
-
if (innerParams.length === 0)
|
|
989
|
-
return null;
|
|
990
|
-
const innerBody = inner.getBody();
|
|
991
|
-
if (!innerBody)
|
|
1025
|
+
function curryChainOf(outerParams, body) {
|
|
1026
|
+
const ret = returnedValueOf(body);
|
|
1027
|
+
if (!ret)
|
|
992
1028
|
return null;
|
|
993
|
-
|
|
994
|
-
if (rets.length === 0)
|
|
1029
|
+
if (!ts_morph_1.Node.isArrowFunction(ret) && !ts_morph_1.Node.isFunctionExpression(ret))
|
|
995
1030
|
return null;
|
|
996
|
-
|
|
1031
|
+
const hops = [];
|
|
1032
|
+
let cur = ret;
|
|
1033
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
1034
|
+
if (!cur)
|
|
1035
|
+
break;
|
|
1036
|
+
if (!ts_morph_1.Node.isArrowFunction(cur) && !ts_morph_1.Node.isFunctionExpression(cur))
|
|
1037
|
+
return null;
|
|
1038
|
+
const fbody = cur.getBody();
|
|
1039
|
+
if (!fbody)
|
|
1040
|
+
return null;
|
|
1041
|
+
const params = cur.getParameters().flatMap(paramNamesOf).filter(Boolean);
|
|
1042
|
+
hops.push({ kind: "call", name: "", params });
|
|
1043
|
+
const next = returnedFunctionOf(fbody);
|
|
1044
|
+
if (!next) {
|
|
1045
|
+
const rets = returnExprsOf(fbody);
|
|
1046
|
+
if (rets.length === 0)
|
|
1047
|
+
return null;
|
|
1048
|
+
// 内层无形参 ⇒ 没有「经内层实参进来」的污点,外层按普通 helper 处理
|
|
1049
|
+
if (hops[hops.length - 1].params.length === 0)
|
|
1050
|
+
return null;
|
|
1051
|
+
const allParams = new Set(outerParams);
|
|
1052
|
+
for (const h of hops)
|
|
1053
|
+
for (const p of h.params)
|
|
1054
|
+
allParams.add(p);
|
|
1055
|
+
return { outerParams, hops, allParams: [...allParams], rets };
|
|
1056
|
+
}
|
|
1057
|
+
cur = next;
|
|
1058
|
+
}
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* C4j:一个函数体对应的全部调用链。
|
|
1063
|
+
* - 「返回函数」形态 ⇒ 0 或 1 条(沿返回值爬到最内层);
|
|
1064
|
+
* - 「返回对象字面量」形态 ⇒ 每个**函数属性**各一条(工厂 / builder 的常态,
|
|
1065
|
+
* `factory(".md").toPath(k)` 里的 `toPath` 才是塑形那一层)。
|
|
1066
|
+
* 属性名算不出(计算属性名而常量不可见)或无形参 / 无返回的,跳过该条。
|
|
1067
|
+
*/
|
|
1068
|
+
function currySpecsOf(outerParams, body) {
|
|
1069
|
+
const ret = returnedValueOf(body);
|
|
1070
|
+
if (!ret)
|
|
1071
|
+
return [];
|
|
1072
|
+
if (!ts_morph_1.Node.isObjectLiteralExpression(ret)) {
|
|
1073
|
+
const one = curryChainOf(outerParams, body);
|
|
1074
|
+
return one ? [one] : [];
|
|
1075
|
+
}
|
|
1076
|
+
const specs = [];
|
|
1077
|
+
for (const prop of ret.getProperties()) {
|
|
1078
|
+
let name = null;
|
|
1079
|
+
let fn;
|
|
1080
|
+
if (ts_morph_1.Node.isPropertyAssignment(prop)) {
|
|
1081
|
+
name = prop.getName().replace(/^["'`]|["'`]$/g, "");
|
|
1082
|
+
const inner = unwrapFns(prop.getInitializer());
|
|
1083
|
+
if (inner && (ts_morph_1.Node.isArrowFunction(inner) || ts_morph_1.Node.isFunctionExpression(inner))) {
|
|
1084
|
+
fn = inner;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
else if (ts_morph_1.Node.isMethodDeclaration(prop)) {
|
|
1088
|
+
name = prop.getName();
|
|
1089
|
+
fn = prop;
|
|
1090
|
+
}
|
|
1091
|
+
if (!fn || !name || !/^[A-Za-z_$][\w$]*$/.test(name))
|
|
1092
|
+
continue;
|
|
1093
|
+
const fbody = fn.getBody();
|
|
1094
|
+
if (!fbody)
|
|
1095
|
+
continue; // 重载签名 / declare
|
|
1096
|
+
const params = fn.getParameters().flatMap(paramNamesOf).filter(Boolean);
|
|
1097
|
+
if (params.length === 0)
|
|
1098
|
+
continue; // 判据①
|
|
1099
|
+
const rets = returnExprsOf(fbody);
|
|
1100
|
+
if (rets.length === 0)
|
|
1101
|
+
continue;
|
|
1102
|
+
specs.push({
|
|
1103
|
+
outerParams,
|
|
1104
|
+
hops: [{ kind: "member", name, params }],
|
|
1105
|
+
allParams: [...outerParams, ...params],
|
|
1106
|
+
rets,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
return specs;
|
|
1110
|
+
}
|
|
1111
|
+
/** 函数体返回出来的那个值(简洁体本身 / return 语句的表达式,都先解断言与括号) */
|
|
1112
|
+
function returnedValueOf(body) {
|
|
1113
|
+
if (!ts_morph_1.Node.isBlock(body))
|
|
1114
|
+
return unwrapFns(body);
|
|
1115
|
+
for (const rs of body.getDescendantsOfKind(ts.SyntaxKind.ReturnStatement)) {
|
|
1116
|
+
const e = unwrapFns(rs.getExpression());
|
|
1117
|
+
if (e)
|
|
1118
|
+
return e;
|
|
1119
|
+
}
|
|
1120
|
+
return undefined;
|
|
997
1121
|
}
|
|
998
1122
|
/** 函数体返回出来的那个箭头 / 函数表达式(没有则 null) */
|
|
999
1123
|
function returnedFunctionOf(body) {
|
|
1000
1124
|
if (!ts_morph_1.Node.isBlock(body)) {
|
|
1001
1125
|
// 箭头简洁体本身就是要返回的函数:`(ext) => (n) => n + ext`(可能被括号包住)
|
|
1002
|
-
const e =
|
|
1126
|
+
const e = unwrapFns(body);
|
|
1003
1127
|
return e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)) ? e : null;
|
|
1004
1128
|
}
|
|
1005
1129
|
for (const rs of body.getDescendantsOfKind(ts.SyntaxKind.ReturnStatement)) {
|
|
1006
|
-
const e =
|
|
1130
|
+
const e = unwrapFns(rs.getExpression());
|
|
1007
1131
|
if (e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)))
|
|
1008
1132
|
return e;
|
|
1009
1133
|
}
|
|
1010
1134
|
return null;
|
|
1011
1135
|
}
|
|
1012
1136
|
/**
|
|
1013
|
-
* C4i
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1137
|
+
* C4i/C4j:柯里化与工厂形态的调用点展开。`withExt(".md")(k)`、`lvl4(a)(b)(c)(k)`、
|
|
1138
|
+
* `factory(".md").toPath(k)` 里真正塑形的是链的**最后一跳** —— 这里按各跳的实参把
|
|
1139
|
+
* 最内层 return 表达式代入后原地摊开,之后的污点检测直接看摊开后的串:
|
|
1140
|
+
* `withExt(".md")(k)` ⇒ `k + ".md"`;`factory(".md").toPath(k)` ⇒ `k + ".md"`。
|
|
1141
|
+
* 丢形参的最后一跳(`(_n) => "fixed.md"`)摊开后是常量 ⇒ 自然不传播。
|
|
1017
1142
|
*/
|
|
1018
1143
|
function expandCurried(text, curried) {
|
|
1019
1144
|
if (curried.size === 0)
|
|
1020
1145
|
return text;
|
|
1021
1146
|
let cur = text;
|
|
1022
|
-
for (let round = 0; round <
|
|
1147
|
+
for (let round = 0; round < 4; round++) {
|
|
1023
1148
|
const next = expandCurriedOnce(cur, curried);
|
|
1024
1149
|
if (next === null)
|
|
1025
1150
|
break;
|
|
@@ -1027,9 +1152,9 @@ function expandCurried(text, curried) {
|
|
|
1027
1152
|
}
|
|
1028
1153
|
return cur;
|
|
1029
1154
|
}
|
|
1030
|
-
/**
|
|
1155
|
+
/** 摊开**一处**柯里化 / 工厂形态调用;没有可摊的返回 null(供调用方收敛) */
|
|
1031
1156
|
function expandCurriedOnce(text, curried) {
|
|
1032
|
-
for (const [name,
|
|
1157
|
+
for (const [name, specs] of curried) {
|
|
1033
1158
|
const re = new RegExp(`(?:^|[^\\w$.])${escapeRe(name)}\\s*\\(`, "g");
|
|
1034
1159
|
let m;
|
|
1035
1160
|
while ((m = re.exec(text)) !== null) {
|
|
@@ -1037,32 +1162,61 @@ function expandCurriedOnce(text, curried) {
|
|
|
1037
1162
|
const close1 = matchingParen(text, open1);
|
|
1038
1163
|
if (close1 < 0)
|
|
1039
1164
|
continue;
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1165
|
+
const args0 = splitTopLevelArgs(text.slice(open1 + 1, close1));
|
|
1166
|
+
for (const cs of specs) {
|
|
1167
|
+
const map = new Map();
|
|
1168
|
+
args0.forEach((a, i) => {
|
|
1169
|
+
const p = cs.outerParams[i];
|
|
1170
|
+
if (p !== undefined)
|
|
1171
|
+
map.set(p, stripOuterParens(a.trim()));
|
|
1172
|
+
});
|
|
1173
|
+
// 沿链一路吃掉各跳:普通跳是紧跟的 `(`,成员跳是 `.name(`
|
|
1174
|
+
let cursor = close1 + 1;
|
|
1175
|
+
let ok = true;
|
|
1176
|
+
for (const hop of cs.hops) {
|
|
1177
|
+
let j = cursor;
|
|
1178
|
+
while (j < text.length && /\s/.test(text[j]))
|
|
1179
|
+
j++;
|
|
1180
|
+
if (hop.kind === "member") {
|
|
1181
|
+
if (text[j] === "?")
|
|
1182
|
+
j++; // 可选链 `?.toPath(`
|
|
1183
|
+
if (text[j] !== ".") {
|
|
1184
|
+
ok = false;
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
j++;
|
|
1188
|
+
const nm = /^[A-Za-z_$][\w$]*/.exec(text.slice(j));
|
|
1189
|
+
if (!nm || nm[0] !== hop.name) {
|
|
1190
|
+
ok = false;
|
|
1191
|
+
break;
|
|
1192
|
+
}
|
|
1193
|
+
j += nm[0].length;
|
|
1194
|
+
while (j < text.length && /\s/.test(text[j]))
|
|
1195
|
+
j++;
|
|
1196
|
+
}
|
|
1197
|
+
if (text[j] !== "(") {
|
|
1198
|
+
ok = false;
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
const cl = matchingParen(text, j);
|
|
1202
|
+
if (cl < 0) {
|
|
1203
|
+
ok = false;
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
splitTopLevelArgs(text.slice(j + 1, cl)).forEach((a, i) => {
|
|
1207
|
+
const p = hop.params[i];
|
|
1208
|
+
if (p !== undefined && !map.has(p))
|
|
1209
|
+
map.set(p, stripOuterParens(a.trim()));
|
|
1210
|
+
});
|
|
1211
|
+
cursor = cl + 1;
|
|
1212
|
+
}
|
|
1213
|
+
if (!ok)
|
|
1214
|
+
continue;
|
|
1215
|
+
const inner = cs.rets.length === 1 ? cs.rets[0] : cs.rets.join(" + ");
|
|
1216
|
+
const expanded = foldDecidableTernary(substituteParams(inner, map));
|
|
1217
|
+
const nameStart = m.index + (m[0].startsWith(name) ? 0 : 1);
|
|
1218
|
+
return text.slice(0, nameStart) + expanded + text.slice(cursor);
|
|
1219
|
+
}
|
|
1066
1220
|
}
|
|
1067
1221
|
}
|
|
1068
1222
|
return null;
|
|
@@ -1589,6 +1743,34 @@ function returnExprsOf(body) {
|
|
|
1589
1743
|
}
|
|
1590
1744
|
return rets;
|
|
1591
1745
|
}
|
|
1746
|
+
/**
|
|
1747
|
+
* C4j:`export default (n) => n + ".md"` 这种**匿名**默认导出没有名字可登记,
|
|
1748
|
+
* 能用的名字只有「别的文件 import 它时起的本地名」。用 ts-morph 的模块解析找到
|
|
1749
|
+
* 真正指向该文件的默认导入(解析不出的模块一律跳过 —— 认不出就不收)。
|
|
1750
|
+
*/
|
|
1751
|
+
function defaultImportLocalNames(project, target) {
|
|
1752
|
+
const out = new Set();
|
|
1753
|
+
const targetPath = target.getFilePath();
|
|
1754
|
+
for (const sf of project.getSourceFiles()) {
|
|
1755
|
+
if (sf.getFilePath() === targetPath)
|
|
1756
|
+
continue;
|
|
1757
|
+
for (const imp of sf.getImportDeclarations()) {
|
|
1758
|
+
let resolved;
|
|
1759
|
+
try {
|
|
1760
|
+
resolved = imp.getModuleSpecifierSourceFile() ?? undefined;
|
|
1761
|
+
}
|
|
1762
|
+
catch {
|
|
1763
|
+
continue; // 解析不了(路径写错 / 别名没配)—— 跳过
|
|
1764
|
+
}
|
|
1765
|
+
if (!resolved || resolved.getFilePath() !== targetPath)
|
|
1766
|
+
continue;
|
|
1767
|
+
const d = imp.getDefaultImport();
|
|
1768
|
+
if (d)
|
|
1769
|
+
out.add(d.getText());
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return out;
|
|
1773
|
+
}
|
|
1592
1774
|
/** 扫描全项目,收集「纯塑形」helper 的名字 + 返回值依赖(有界两轮不动点) */
|
|
1593
1775
|
function pureShaperFunctionNames(project) {
|
|
1594
1776
|
const sinkRe = tsSinkCallRegex();
|
|
@@ -1597,21 +1779,37 @@ function pureShaperFunctionNames(project) {
|
|
|
1597
1779
|
const groups = new Map();
|
|
1598
1780
|
// 模块级字面量常量(值一并留着:C4i 的计算属性名 `[KEY]` 要靠它解)
|
|
1599
1781
|
const constTexts = moduleConstTexts(project);
|
|
1782
|
+
/**
|
|
1783
|
+
* 把一个**没通过判据**的实现记进它所属的成员组。
|
|
1784
|
+
*
|
|
1785
|
+
* C4j 修(探针 B4/B6 实测):C4h-② 的"全组确证"只在**进了组**的候选之间成立,
|
|
1786
|
+
* 而三条判据不成立的实现在 consider() 里就 return 了 —— 它压根不在组里,于是
|
|
1787
|
+
* 「同名三处,一处无形参、一处体内有 sink、一处是好 helper」会被当成"全组都确证",
|
|
1788
|
+
* 成员调用位被放行 ⇒ 误标。组必须记全:**存在过但没确证**也要算进"不是全组确证"。
|
|
1789
|
+
*/
|
|
1790
|
+
const noteUnaccepted = (key, group) => {
|
|
1791
|
+
if (group === null)
|
|
1792
|
+
return;
|
|
1793
|
+
const list = groups.get(group) ?? [];
|
|
1794
|
+
if (!list.includes(key))
|
|
1795
|
+
list.push(key);
|
|
1796
|
+
groups.set(group, list);
|
|
1797
|
+
};
|
|
1600
1798
|
/** 登记一个候选:三条判据 —— 有形参 / 有返回值 / 体内无 sink */
|
|
1601
1799
|
const consider = (key, name, group, paramNodes, restAt, curry, declText, bodyNode) => {
|
|
1602
1800
|
const paramList = paramNodes.filter(Boolean);
|
|
1603
1801
|
const params = new Set(paramList);
|
|
1604
1802
|
if (params.size === 0)
|
|
1605
|
-
return;
|
|
1803
|
+
return noteUnaccepted(key, group); // 判据①
|
|
1606
1804
|
sinkRe.lastIndex = 0;
|
|
1607
1805
|
if (sinkRe.test(declText))
|
|
1608
|
-
return; // ②
|
|
1806
|
+
return noteUnaccepted(key, group); // ②
|
|
1609
1807
|
// 用 AST 取 return 表达式,而不是正则 —— 模板字面量里的 `${}` 会
|
|
1610
1808
|
// 让「遇到花括号就停」的朴素正则在 `path.join(a,b) + \`.${c}\`` 上截断。
|
|
1611
1809
|
const rets = returnExprsOf(bodyNode);
|
|
1612
|
-
//
|
|
1613
|
-
if (rets.length === 0 && curry ===
|
|
1614
|
-
return; //
|
|
1810
|
+
// 柯里化 / 工厂形态:return 出来的是函数或装着函数的对象 ⇒ 判据①看的是最内层
|
|
1811
|
+
if (rets.length === 0 && curry.length === 0)
|
|
1812
|
+
return noteUnaccepted(key, group); // ③
|
|
1615
1813
|
let pub = name;
|
|
1616
1814
|
if (pub !== null) {
|
|
1617
1815
|
if (takenNames.has(pub))
|
|
@@ -1646,7 +1844,7 @@ function pureShaperFunctionNames(project) {
|
|
|
1646
1844
|
const ps = fn.getParameters();
|
|
1647
1845
|
const paramNodes = ps.flatMap(paramNamesOf);
|
|
1648
1846
|
const restAt = restIndexOf(ps);
|
|
1649
|
-
const curry =
|
|
1847
|
+
const curry = currySpecsOf(paramNodes, body);
|
|
1650
1848
|
const owner = moduleQualifier(fn);
|
|
1651
1849
|
// namespace 内却认不出宿主链 ⇒ 不收(宁可漏,不可张冠李戴)
|
|
1652
1850
|
if (!ts_morph_1.Node.isSourceFile(parent) && owner === null)
|
|
@@ -1674,7 +1872,7 @@ function pureShaperFunctionNames(project) {
|
|
|
1674
1872
|
continue;
|
|
1675
1873
|
const ps = fnLike.getParameters();
|
|
1676
1874
|
const paramNodes = ps.flatMap(paramNamesOf);
|
|
1677
|
-
consider(`var:${vd.getName()}`, vd.getName(), null, paramNodes, restIndexOf(ps),
|
|
1875
|
+
consider(`var:${vd.getName()}`, vd.getName(), null, paramNodes, restIndexOf(ps), currySpecsOf(paramNodes, body), init.getText(), // IIFE 时含整条调用 ⇒ sink 检查仍覆盖得到
|
|
1678
1876
|
body);
|
|
1679
1877
|
}
|
|
1680
1878
|
}
|
|
@@ -1702,35 +1900,40 @@ function pureShaperFunctionNames(project) {
|
|
|
1702
1900
|
continue; // 重载签名 / declare
|
|
1703
1901
|
const ps = md.getParameters();
|
|
1704
1902
|
const pns = ps.flatMap(paramNamesOf);
|
|
1705
|
-
considerMember(bare, ownerQualifier(md, constTexts), pns, restIndexOf(ps),
|
|
1903
|
+
considerMember(bare, ownerQualifier(md, constTexts), pns, restIndexOf(ps), currySpecsOf(pns, body), md.getText(), body);
|
|
1706
1904
|
}
|
|
1707
1905
|
// (d) 属性承载的箭头 / 函数表达式 —— 现代工程里 Objects-as-namespace 的常态。
|
|
1708
1906
|
// ownerQualifier 从父链走(对象字面量 → 属性赋值 → 变量声明),嵌套也认。
|
|
1907
|
+
// C4j:初值外面可能还包着 as / satisfies(`{ toPath: ((n) => …) as any }`)。
|
|
1709
1908
|
for (const pa of sf.getDescendantsOfKind(ts.SyntaxKind.PropertyAssignment)) {
|
|
1710
|
-
const
|
|
1909
|
+
const rawInit = pa.getInitializer();
|
|
1910
|
+
if (!rawInit)
|
|
1911
|
+
continue;
|
|
1912
|
+
const init = unwrapFns(rawInit);
|
|
1711
1913
|
if (!init)
|
|
1712
1914
|
continue;
|
|
1713
1915
|
if (!ts_morph_1.Node.isArrowFunction(init) && !ts_morph_1.Node.isFunctionExpression(init))
|
|
1714
1916
|
continue;
|
|
1715
|
-
const
|
|
1917
|
+
const fn = init;
|
|
1918
|
+
const body = fn.getBody();
|
|
1716
1919
|
if (!body)
|
|
1717
1920
|
continue;
|
|
1718
1921
|
const bare = propertyNameOf(pa, constTexts);
|
|
1719
1922
|
if (!bare || !/^[A-Za-z_$][\w$]*$/.test(bare))
|
|
1720
1923
|
continue;
|
|
1721
|
-
const ps =
|
|
1924
|
+
const ps = fn.getParameters();
|
|
1722
1925
|
const pns = ps.flatMap(paramNamesOf);
|
|
1723
|
-
considerMember(bare, ownerQualifier(pa, constTexts), pns, restIndexOf(ps),
|
|
1926
|
+
considerMember(bare, ownerQualifier(pa, constTexts), pns, restIndexOf(ps), currySpecsOf(pns, body), rawInit.getText(), body);
|
|
1724
1927
|
}
|
|
1725
1928
|
// (e) 后挂上去的属性箭头 `Util.toPath = (n) => …`(monkey patch / 渐进导出)
|
|
1726
1929
|
for (const be of sf.getDescendantsOfKind(ts.SyntaxKind.BinaryExpression)) {
|
|
1727
1930
|
if (be.getOperatorToken().getKind() !== ts.SyntaxKind.EqualsToken)
|
|
1728
1931
|
continue;
|
|
1729
1932
|
const left = be.getLeft();
|
|
1730
|
-
const right = be.getRight();
|
|
1933
|
+
const right = unwrapFns(be.getRight());
|
|
1731
1934
|
if (!ts_morph_1.Node.isPropertyAccessExpression(left))
|
|
1732
1935
|
continue;
|
|
1733
|
-
if (!ts_morph_1.Node.isArrowFunction(right) && !ts_morph_1.Node.isFunctionExpression(right))
|
|
1936
|
+
if (!right || (!ts_morph_1.Node.isArrowFunction(right) && !ts_morph_1.Node.isFunctionExpression(right)))
|
|
1734
1937
|
continue;
|
|
1735
1938
|
const body = right.getBody();
|
|
1736
1939
|
if (!body)
|
|
@@ -1742,7 +1945,52 @@ function pureShaperFunctionNames(project) {
|
|
|
1742
1945
|
const bare = qname.split(".").pop();
|
|
1743
1946
|
const ps = right.getParameters();
|
|
1744
1947
|
const pns = ps.flatMap(paramNamesOf);
|
|
1745
|
-
considerMember(bare, qname.slice(0, -(bare.length + 1)), pns, restIndexOf(ps),
|
|
1948
|
+
considerMember(bare, qname.slice(0, -(bare.length + 1)), pns, restIndexOf(ps), currySpecsOf(pns, body), right.getText(), body);
|
|
1949
|
+
}
|
|
1950
|
+
// (f) C4j:getter 返回出来的箭头 —— `get mk() { return (n) => n + ".md"; }`。
|
|
1951
|
+
// 它既不是 MethodDeclaration(没有形参)也不是 PropertyAssignment(值是 getter),
|
|
1952
|
+
// 上一轮三种载体都收不到它,整族漏(探针 B1/B2)。真正塑形的是**返回出来的箭头**,
|
|
1953
|
+
// 所以判据看的是内层:无形参不收(B4)、体内有 sink 不收(B6)。
|
|
1954
|
+
// 注意这里**不**走 currySpecOf —— getter 的读取本身不消耗实参,调用点是
|
|
1955
|
+
// `o.mk(k)` 一次调用,不是 `mk()(k)` 两跳。
|
|
1956
|
+
for (const ga of sf.getDescendantsOfKind(ts.SyntaxKind.GetAccessor)) {
|
|
1957
|
+
const body = ga.getBody();
|
|
1958
|
+
if (!body)
|
|
1959
|
+
continue;
|
|
1960
|
+
const inner = returnedFunctionOf(body);
|
|
1961
|
+
if (!inner)
|
|
1962
|
+
continue; // getter 直接返回拼接(无形参)⇒ 判据①本就不成立
|
|
1963
|
+
const innerBody = inner.getBody();
|
|
1964
|
+
if (!innerBody)
|
|
1965
|
+
continue;
|
|
1966
|
+
const bare = propertyNameOf(ga, constTexts);
|
|
1967
|
+
if (!bare || !/^[A-Za-z_$][\w$]*$/.test(bare))
|
|
1968
|
+
continue;
|
|
1969
|
+
const ps = inner.getParameters();
|
|
1970
|
+
const pns = ps.flatMap(paramNamesOf);
|
|
1971
|
+
considerMember(bare, ownerQualifier(ga, constTexts), pns, restIndexOf(ps), currySpecsOf(pns, innerBody), ga.getText(), innerBody);
|
|
1972
|
+
}
|
|
1973
|
+
// (g) C4j:`export default (n) => …` —— 匿名默认导出。它既不是函数声明(没有名字)
|
|
1974
|
+
// 也不是变量声明,上一轮整族漏(探针 F9)。名字只能用**导入它的文件**里起的
|
|
1975
|
+
// 本地名;多个文件用不同名字导入 ⇒ 每个名字各登记一条(takenNames 仍然先到先得)。
|
|
1976
|
+
for (const ea of sf.getDescendantsOfKind(ts.SyntaxKind.ExportAssignment)) {
|
|
1977
|
+
if (ea.isExportEquals())
|
|
1978
|
+
continue;
|
|
1979
|
+
const fn = unwrapFns(ea.getExpression());
|
|
1980
|
+
if (!fn || (!ts_morph_1.Node.isArrowFunction(fn) && !ts_morph_1.Node.isFunctionExpression(fn)))
|
|
1981
|
+
continue;
|
|
1982
|
+
const fnBody = fn.getBody();
|
|
1983
|
+
if (!fnBody)
|
|
1984
|
+
continue;
|
|
1985
|
+
const localNames = defaultImportLocalNames(project, sf);
|
|
1986
|
+
if (localNames.size === 0)
|
|
1987
|
+
continue; // 没人默认导入 ⇒ 名字无从谈起
|
|
1988
|
+
const ps = fn.getParameters();
|
|
1989
|
+
const pns = ps.flatMap(paramNamesOf);
|
|
1990
|
+
const curry = currySpecsOf(pns, fnBody);
|
|
1991
|
+
for (const ln of localNames) {
|
|
1992
|
+
consider(`def:${sf.getFilePath()}:${ln}`, ln, null, pns, restIndexOf(ps), curry, ea.getText(), fnBody);
|
|
1993
|
+
}
|
|
1746
1994
|
}
|
|
1747
1995
|
}
|
|
1748
1996
|
// 形参之外允许出现的自由标识符 = 模块级字面量常量 ∪ 确证的 path 模块别名
|
|
@@ -1763,18 +2011,23 @@ function pureShaperFunctionNames(project) {
|
|
|
1763
2011
|
names.add(name);
|
|
1764
2012
|
info.set(name, si);
|
|
1765
2013
|
};
|
|
2014
|
+
/** C4j:确证通过的链(工厂可能有几条,脏的那条会被剔掉,见下) */
|
|
2015
|
+
const pureCurried = new Map();
|
|
1766
2016
|
for (let round = 0; round < 2; round++) {
|
|
1767
2017
|
let added = false;
|
|
1768
2018
|
for (const c of cands) {
|
|
1769
2019
|
if (acceptedKeys.has(c.key))
|
|
1770
2020
|
continue;
|
|
1771
2021
|
let si;
|
|
1772
|
-
if (c.curry
|
|
1773
|
-
//
|
|
1774
|
-
|
|
1775
|
-
|
|
2022
|
+
if (c.curry.length > 0) {
|
|
2023
|
+
// 柯里化 / 工厂形态:判据落在**最内层**的 return 上,形参集合是本链各层并起来。
|
|
2024
|
+
// 每个 spec 各自过判据:工厂返回多个方法时,只把**确证过的那些**收进名录,
|
|
2025
|
+
// 脏的那条不发 spec ⇒ 调用点不会摊开它 ⇒ 退化为漏报而不是误报(保守侧)。
|
|
2026
|
+
const pure = c.curry.filter((spec) => spec.rets.every((e) => isPureShaperExpr(e, new Set([...c.params, ...spec.allParams]), names, extraIdents)));
|
|
2027
|
+
if (pure.length === 0)
|
|
1776
2028
|
continue;
|
|
1777
2029
|
acceptedKeys.add(c.key);
|
|
2030
|
+
pureCurried.set(c.key, pure);
|
|
1778
2031
|
// 外层名照发:闭包会捕获外层实参(`const g = withExt(k)` 也是污的)。
|
|
1779
2032
|
// rets 留空 ⇒ 调用点折叠自动退回「全部依赖」(保守侧)。
|
|
1780
2033
|
si = { params: c.paramList, deps: new Set(c.paramList), rets: [], restAt: c.restAt };
|
|
@@ -1798,14 +2051,22 @@ function pureShaperFunctionNames(project) {
|
|
|
1798
2051
|
if (!added)
|
|
1799
2052
|
break;
|
|
1800
2053
|
}
|
|
1801
|
-
// C4i
|
|
2054
|
+
// C4i/C4j:柯里化与工厂名录。只有**发布了名字**的才登记 —— 名字被别处同名定义占用时
|
|
1802
2055
|
// (pub 降为 null),`withExt(a)(b)` 里的 withExt 可能指的不是这个 helper,
|
|
1803
2056
|
// 展开它会张冠李戴 ⇒ 宁可不展(保守侧,退化为漏报而非误报)。
|
|
2057
|
+
// 一个名字可以对多条链(工厂返回含多个方法的对象)。
|
|
1804
2058
|
const curried = new Map();
|
|
1805
2059
|
for (const c of cands) {
|
|
1806
|
-
if (c.
|
|
1807
|
-
|
|
1808
|
-
|
|
2060
|
+
if (c.name === null || !acceptedKeys.has(c.key))
|
|
2061
|
+
continue;
|
|
2062
|
+
const pure = pureCurried.get(c.key);
|
|
2063
|
+
if (!pure || pure.length === 0)
|
|
2064
|
+
continue;
|
|
2065
|
+
const list = curried.get(c.name) ?? [];
|
|
2066
|
+
for (const spec of pure)
|
|
2067
|
+
if (!list.includes(spec))
|
|
2068
|
+
list.push(spec);
|
|
2069
|
+
curried.set(c.name, list);
|
|
1809
2070
|
}
|
|
1810
2071
|
// C4f:依赖分析有次序依赖 —— `wrap(n) { return discard(n); }` 若在 discard 之前
|
|
1811
2072
|
// 被接受,那一刻 info 里还没有 discard ⇒ deps 偏宽(偏宽 = 继续传播 = 保守侧,
|
package/dist/sdk.js
CHANGED
|
@@ -20,7 +20,7 @@ const risk_model_1 = require("./risk-model");
|
|
|
20
20
|
const protocol_knowledge_1 = require("./protocol-knowledge");
|
|
21
21
|
const evidence_repository_1 = require("./evidence-repository");
|
|
22
22
|
/** Runtime version — stable public identifier. Internal layers evolve underneath. */
|
|
23
|
-
exports.RUNTIME_VERSION = "3.7.
|
|
23
|
+
exports.RUNTIME_VERSION = "3.7.46";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.46",
|
|
4
4
|
"description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|