progmune-runtime 3.7.45 → 3.7.47
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 +190 -0
- package/dist/extract-ir-taint-structural.test.js +378 -0
- package/dist/extract-ir.js +359 -83
- package/dist/sdk.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,195 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.47] — 2026-09-21
|
|
4
|
+
|
|
5
|
+
### 类方法的真实调用必须进 IR(状态机在 OO 代码上没有输入)
|
|
6
|
+
|
|
7
|
+
**一句话**:主 IR 循环的类方法分支**只调了 `computeMarkerCalls`(产 `__progmune_*` 语义
|
|
8
|
+
标记),从来没调过 `extractDirectCalls`(产真实调用名)**;而函数声明 / 箭头 / 包装箭头
|
|
9
|
+
三个分支都是两行齐全。后果是类方法的 `calls` 里永远只有标记、没有真实调用 ⇒ **协议状态
|
|
10
|
+
机在 OO 代码上拿不到输入**。
|
|
11
|
+
|
|
12
|
+
这不是新引入的回归。Claude 补充的历史事实:修 A1 时类方法提取循环里写死的就是
|
|
13
|
+
`calls: []`,`extractDirectCalls` 只对函数声明和箭头函数调用过,类方法从未接上。A1 只是
|
|
14
|
+
给方法条目挂标记时动过这个数组,基础的调用提取从来没补。**自该功能诞生之日起就空转**,
|
|
15
|
+
taintpath 十一族语料全是函数载体,从来没有东西逼它现形。
|
|
16
|
+
|
|
17
|
+
**顺带解释了三件悬案**:真实语料 `protocol=0`、引擎自扫「0 误报」、NestJS 语料过分干净
|
|
18
|
+
—— 裁判一直没拿到证据。
|
|
19
|
+
|
|
20
|
+
**修复**:类方法分支补上 `extractDirectCalls(m, mText)`(签名形参加 `MethodDeclaration`,
|
|
21
|
+
遍历逻辑无需改动),顺序与既有三个载体一致:先真实调用,再追加语义标记。
|
|
22
|
+
|
|
23
|
+
| 探针形态(18 条) | 修复前 | 修复后 |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| 类方法(同步 / async / this 成员 / 装饰器 / 构造函数注入) | 全空 | ✓ 全部提出 |
|
|
26
|
+
| static / private / 抽象类实现方法 / `this.a.b()` 链式 / 计算属性名 | 全空 | ✓ 全部提出 |
|
|
27
|
+
| 函数声明 / 箭头(基线对照) | ✓ | ✓ 不变 |
|
|
28
|
+
|
|
29
|
+
**三条不修**(探针确认后明确登记,避免下一轮重踩):
|
|
30
|
+
|
|
31
|
+
1. **getter / setter / 构造函数体 / 对象字面量方法 / 类表达式** —— 这五种不是「calls 空」,
|
|
32
|
+
而是**条目压根不在 IR 里**(载体未收集)。属另一类缺陷,风险量级不同(新增条目 = 新增
|
|
33
|
+
检测面),不能与本轮混在一起,否则反向验证无法归因。
|
|
34
|
+
2. **嵌套箭头回调里的调用** —— `extractDirectCalls` 遇嵌套函数即 `traversal.skip()`。实测
|
|
35
|
+
函数声明 / 箭头 / 类方法三者口径**完全一致**(都只提得到 `setTimeout`),是既有设计。
|
|
36
|
+
3. **计算属性名的名字解算** —— `C.[K]` 名字不可解是另一回事,不影响 calls。
|
|
37
|
+
|
|
38
|
+
### FP 观测池:把「裁判拿到证据」量化出来
|
|
39
|
+
|
|
40
|
+
修复后重扫(`fp-pool-scan.ts`,4 切片 / 197 文件):
|
|
41
|
+
|
|
42
|
+
| 读数 | 修复前 | 修复后 |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| 空 calls 函数 | 140 / 197(**71%**) | 23 / 197(**12%**) |
|
|
45
|
+
| 其中类方法空转 | 126 | 9 |
|
|
46
|
+
| NestJS 切片 | 59 / 61 空 | 3 / 61 空 |
|
|
47
|
+
| Express 切片 | 76 / 113 空 | 15 / 113 空 |
|
|
48
|
+
|
|
49
|
+
**代价也量出来了**:项目级 safeguard 19 → 21,但 **perFunction 层面新增 18 条** —— 类方法
|
|
50
|
+
第一次有了 calls,规则第一次看见这些序列。逐条判定:**FP 15 / 弱真 3 / 真漏洞 0**。四条
|
|
51
|
+
规则侧校准线索已登记在 `fp-pool.json`(迁移与种子脚本应豁免、TypeORM query builder 链被
|
|
52
|
+
Input Validation 误判、内部工具方法不应被要求 Authorization、`validate` 已存在却仍报缺校验
|
|
53
|
+
⇒ 是词表不认 class-validator)。
|
|
54
|
+
|
|
55
|
+
上一轮 7 条人工判定里那条「无法判定」的 `UserController.login`(Token Security)**本轮变
|
|
56
|
+
成有效票**:有了 calls 才看得出它是标准 `jwt.sign` 签发 ⇒ FP。
|
|
57
|
+
|
|
58
|
+
### 验收
|
|
59
|
+
|
|
60
|
+
| 门 | 结果 |
|
|
61
|
+
|---|---|
|
|
62
|
+
| taintpath 闸门 | **160/160** |
|
|
63
|
+
| fr-007 / fr-016 | pre 5→post 0 / pre 7→post 0 **均维持** |
|
|
64
|
+
| 定向用例 | **241 passed / 6 文件**(新增 E1 组 14 条:10 正 2 正对照 2 负对照) |
|
|
65
|
+
| TS 盲测 | 111 项目 / perFunction **3201 条**:**LOST 0 / ADDED 0**,汇总完全一致 |
|
|
66
|
+
| 反向验证 | 三刀,残留 0(见下) |
|
|
67
|
+
| tsc | 零错误 |
|
|
68
|
+
|
|
69
|
+
**反向验证三刀**(`/tmp/rev_e1.py(原名 rev_c4k.py)`):
|
|
70
|
+
|
|
71
|
+
| 刀 | 退回的旧行为 | 转红 |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| ① 类方法只留标记 | `const mCalls = computeMarkerCalls(...)` | **12 条**:正例 ①–⑩ + 负对照 ①② |
|
|
74
|
+
| ② 类方法不追加标记 | 摘掉 `mCalls.push(...computeMarkerCalls(...))` | 0 条(marker 由**调用方侧**补上) |
|
|
75
|
+
| ③ 同上(闸门视角) | 同 ② | 闸门仍 160/160 |
|
|
76
|
+
|
|
77
|
+
刀①归因**干净**:转红的 12 条全在类方法组,正对照 ⑪⑫(函数声明 / 箭头)仍绿。
|
|
78
|
+
刀②③顺带证明两件事:marker 链路有调用方侧兜底(不是单点);**taintpath 语料里根本没有
|
|
79
|
+
类方法 ⇒ 闸门对本轮是空过**,真正的验收在 fp-pool 与定向用例。
|
|
80
|
+
|
|
81
|
+
### 方法学
|
|
82
|
+
|
|
83
|
+
- **R22 第三次应验**:负对照·② 一开始写错期望(以为类方法体内有 sink 就会有 marker),
|
|
84
|
+
跑探针才确认 —— marker 只在**调用方把污点传进来**时才注入到被调用方。写 expect 之前
|
|
85
|
+
先把判据抄进探针注释,这条规矩已经是第三次救我。
|
|
86
|
+
- **R20 的适用边界**(Claude 指出):那三问(pre 有无校验 / 污点根是否在切片内 / fix 是否
|
|
87
|
+
新增校验)只适用于**修复回归语料**,**别误用到 FP 观测池**——池的标准是「真实 + HTTP
|
|
88
|
+
入口 + 文件操作」,不要求 CVE。两边管的方向不同(召回 vs 精度),标准本来就不同。
|
|
89
|
+
|
|
90
|
+
## [3.7.46] — 2026-09-21
|
|
91
|
+
|
|
92
|
+
### C4j:载体收口的最后一批 + 一处既有缺陷的回归门
|
|
93
|
+
|
|
94
|
+
C4i 的备忘留了四条候选。按 R17 先写探针复现,同时塞进七条「没人提过但看着差不多」的
|
|
95
|
+
形状 —— 三轮探针:24 条覆盖面 → 17 条**防修过头**的边界 → 6 条给默认导出补真 import。
|
|
96
|
+
真缺口六条;另有一条**不是这轮引入**的缺陷被探针顺手挖出来(见下)。
|
|
97
|
+
|
|
98
|
+
| 形状 | 旧行为 | 机制 |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `typeof m === "string" \|\| n + ".md"` | 整条 helper 落选 | `typeof` 被当成**未知自由标识符** ⇒ 判据不通过 |
|
|
101
|
+
| `((n) => …) as Fn` / `satisfies` / `<Fn>…` | 不收 | 初值是**断言表达式**不是箭头;成员位同理 |
|
|
102
|
+
| `get mk() { return (n) => … }` | 不收 | getter 没有形参,`getFunctions()` 取不到 |
|
|
103
|
+
| `lvl3("a")("b")(k)` 三/四跳 | 不展 | 旧机制只吃**两跳** |
|
|
104
|
+
| `factory(".md").toPath(k)` | 不展 | 真正塑形的是**返回对象里的成员方法** |
|
|
105
|
+
| `export default (n) => …` | 收不到 | 匿名默认导出**没有名字**可登记 |
|
|
106
|
+
|
|
107
|
+
**精度侧一条:运算符关键字。** `typeof` / `void` / `in` / `instanceof` 是运算符不是自由
|
|
108
|
+
标识符,出现在 return 表达式里不该让整条 helper 落选。**只放行运算符** —— `this` /
|
|
109
|
+
`super` 不放行,它们指向宿主状态,放行等于给「返回值依赖 this」的假塑形开口子。
|
|
110
|
+
|
|
111
|
+
**召回侧五条。**
|
|
112
|
+
|
|
113
|
+
- **断言解包**:`unwrapFns` 解 `as` / `satisfies` / `<T>` 类型断言,**变量位、属性位、
|
|
114
|
+
调用点 callee、返回位**都要用 —— `get mk() { return ((n) => …) as Fn }` 就是「返回位
|
|
115
|
+
没解」漏出来的。
|
|
116
|
+
- **getter 返回的箭头**:getter 自己的形参表是空的,判据要落在**它返回出来的那一层**,
|
|
117
|
+
与柯里化同一条路。
|
|
118
|
+
- **柯里化推广成「跳链」**:`CurrySpec` 从写死的「外层 + 一层内层」改成
|
|
119
|
+
`{ outerParams, hops: CurryHop[], allParams, rets }`。`lvl3 = (a) => (b) => (n) => n+a+b`
|
|
120
|
+
⇒ `outerParams=[a]`、`hops=[call[b], call[n]]`;工厂形态 ⇒ `hops=[member toPath [n]]`。
|
|
121
|
+
调用点从「两层实参代入」改成**逐跳消费**,跳数不设死上限(爬取有界 4 层),首跳实参
|
|
122
|
+
同样被闭包捕获(所以 `lvl3(k)("a")("b")` 与 `lvl3("a")("b")(k)` 都要对)。
|
|
123
|
+
- **工厂形态 = 一个名字多条链**:按**成员名**选链(`makeMixed(".md").mix(k)` 走 mix 那条),
|
|
124
|
+
脏链(返回常量 / 体内有 sink)单独剔掉,不许污染同名的另一条。
|
|
125
|
+
- **匿名默认导出**:没名字 ⇒ 用**导入方**起的本地名发布(`import toPath from "./helpers"`)。
|
|
126
|
+
|
|
127
|
+
### 探针挖出的一处既有缺陷(C4h-② 的洞)
|
|
128
|
+
|
|
129
|
+
C4h 把「同名成员唯一才登记」换成「全组确证」,但只把**通过判据**的实现算进组:判据不
|
|
130
|
+
成立的实现没进组 ⇒ 组看起来「全组确证」⇒ 成员调用位放行 ⇒ 误标。
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
同名三处 getter:无形参的(判据①不成立)/ 正常塑形 / 体内有 sink 的(判据③不成立)
|
|
134
|
+
后两处进组、第一处没进 ⇒ 组"干净" ⇒ .mk( 放行 ⇒ ONoParam.mk(k) 被误标
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
修法 `noteUnaccepted`:**失败的实现也记进组**(记 key + group,不进候选)。组一脏,整组
|
|
138
|
+
不放行。教训:**「全组」的分母必须包含被判据拒掉的那些**,否则「全组确证」会退化成
|
|
139
|
+
「通过者全体确证」—— 已升为方法学规则 **R19**。
|
|
140
|
+
|
|
141
|
+
### 又一条「探针期望写错」(第二次)
|
|
142
|
+
|
|
143
|
+
- **F9a**:探针忘了写 import。匿名默认导出的名字只能来自导入它的文件,两文件之间没有
|
|
144
|
+
import 就无从登记 —— 期望本身不可达。补上真 import 后通过。
|
|
145
|
+
- **F9f**:期望 unmarked、实测 mark。查清后是**期望写错**不是误报:`(0, (n) => n + ".md")(k)`
|
|
146
|
+
里那个**内联匿名箭头自己就是塑形函数**,与默认导出无关 —— 把 `helpers.ts` 删掉照样标。
|
|
147
|
+
|
|
148
|
+
⇒ 两条都不是机制错,是**预测错**。写期望前先问:这条标/不标,到底由**哪一个机制**决定。
|
|
149
|
+
|
|
150
|
+
### 三条「看着像缺口、这轮故意不修」的边界
|
|
151
|
+
|
|
152
|
+
探针里留着(F5 / F7 / A6 / F12),都退回保守侧:`switch` 分支与 `if` 早返回的返回值取
|
|
153
|
+
**并集**(常量分支和污点分支混在一起,判不出);`x != null` 这类**比较式**左值不折;
|
|
154
|
+
标识符位「先声明后赋值」(`let f; f = (n) => …`)不收。这三条不是漏,是「判不出就退回
|
|
155
|
+
保守」的固有代价 —— 要收得让调用点折叠长出手臂(按分支路径分别代入),属另一轮的量级。
|
|
156
|
+
|
|
157
|
+
### 验收
|
|
158
|
+
|
|
159
|
+
| 门 | 结果 |
|
|
160
|
+
|---|---|
|
|
161
|
+
| taintpath 闸门 | **160/160**(新增 K 族 22 条:12 mark / 9 no-taint / 1 suppressed) |
|
|
162
|
+
| fr-007 真实语料 | pre 5 / post 0 **维持** |
|
|
163
|
+
| fr-016 真实语料 | pre 7 / post 0 **维持** |
|
|
164
|
+
| 定向用例 | 提取器定向用例 **227 passed / 6 个测试文件**(其中本文件新增 C4j 组 23 条:12 正 11 负,含两条回归·⑦) |
|
|
165
|
+
| TS 盲测 | 111 项目 / perFunction 口径 **3201 条**:**LOST 0 / ADDED 18**,全部来自新增的 K 族 —— 其中 12 条 `path_traversal` 是本该标的,6 条 `data_integrity`(写文件无审计)属新项目的无关类别;基线那 3183 条**零漂移零丢失** |
|
|
166
|
+
| tsc | 零错误 |
|
|
167
|
+
|
|
168
|
+
### 反向验证七刀(归因)
|
|
169
|
+
|
|
170
|
+
每刀只把一个机制退回**旧行为**(R18:不许 `return null` 逼停):
|
|
171
|
+
|
|
172
|
+
| 刀 | 改动 | 转红 |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| 1 | 关掉运算符关键字放行 | emitKw |
|
|
175
|
+
| 2 | 摘掉 getter 收集 | emitGetterCls / emitGetterObj |
|
|
176
|
+
| 3 | 断言解包退回只脱括号 | emitAs / emitAsSat / emitAsMember |
|
|
177
|
+
| 4 | 柯里化退回两跳语义 | emitCurry3 / emitCurry4 / emitCurryFirst |
|
|
178
|
+
| 5 | 工厂形态不登记 | emitFactory / emitFactoryMix |
|
|
179
|
+
| 6 | 摘掉匿名默认导出收集 | emitDefault |
|
|
180
|
+
| 7 | 成员组退回「判据失败不进组」 | 定向用例 回归·⑦ 两条 |
|
|
181
|
+
|
|
182
|
+
闸门其余 148 条、定向用例 C4j 组其余 21 条全程不动 ⇒ 七处互不相干,归因干净。
|
|
183
|
+
|
|
184
|
+
### 实现坑三条
|
|
185
|
+
|
|
186
|
+
1. **解包要解到位**:`returnedValueOf` 也得解断言 —— `get mk() { return ((n) => …) as Fn }`
|
|
187
|
+
漏的就是这一处("返回位没解")。
|
|
188
|
+
2. **一条名字多条链时,脏链要单独剔掉**:`makeMixed` 的 `skip`(返回常量)不许把 `mix`
|
|
189
|
+
那条链带下水,否则「张冠李戴」在链层面复现。
|
|
190
|
+
3. **`expandCurried` 的实参位置**:首跳实参落在外层形参的第一个,不是内层 —— 从「两层
|
|
191
|
+
实参代入」改成「逐跳消费」时最容易写反。
|
|
192
|
+
|
|
3
193
|
## [3.7.45] — 2026-09-21
|
|
4
194
|
|
|
5
195
|
### C4i:又一批可判定分支 + 又一批 helper 载体
|
|
@@ -1737,3 +1737,381 @@ 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
|
+
});
|
|
1901
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1902
|
+
// E1:类方法的**真实调用**必须进 IR(2026-09-21)
|
|
1903
|
+
//
|
|
1904
|
+
// 命名说明:C4k 已被占用(容器承载轴「属性写入 ⇒ 容器传播」,实测收益上限 0,
|
|
1905
|
+
// 判定不修,见设计稿 §十七)。本组是**提取器主干**,另开 E(Extractor)系列。
|
|
1906
|
+
//
|
|
1907
|
+
// 背景(Claude 补充的历史事实):主 IR 循环的类方法分支自该功能诞生之日起
|
|
1908
|
+
// 只调 computeMarkerCalls(产 __progmune_* 语义标记),从未调 extractDirectCalls
|
|
1909
|
+
// (产真实调用名);函数声明 / 箭头 / 包装箭头三个分支都是两行齐全。
|
|
1910
|
+
// 结果:类方法的 calls 里永远只有标记、没有真实调用 ⇒ **状态机在 OO 代码上
|
|
1911
|
+
// 拿不到输入**。FP 观测池实测:NestJS 切片 61 个函数里 59 个 calls 为空(97%),
|
|
1912
|
+
// 池合计 197 个里 140 个空。此前未暴露是因为 taintpath 十一族语料全是函数载体。
|
|
1913
|
+
//
|
|
1914
|
+
// 判据:类方法条目的 calls 必须同时含「真实调用名」与「语义标记」——
|
|
1915
|
+
// 本轮是**补**真实调用,不是拿它替换标记(负对照·②守这条)。
|
|
1916
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1917
|
+
(0, vitest_1.describe)("E1:类方法的真实调用必须进 IR(此前只有标记,状态机拿不到输入)", () => {
|
|
1918
|
+
const cases = [
|
|
1919
|
+
// ── 正例:此前全空的五种已知形态 ──────────────────────────
|
|
1920
|
+
{
|
|
1921
|
+
name: "正例·① 类方法(同步)",
|
|
1922
|
+
fn: "C.m",
|
|
1923
|
+
files: {
|
|
1924
|
+
"a.ts": `
|
|
1925
|
+
function helper() { return 1; }
|
|
1926
|
+
export class C { m() { helper(); } }
|
|
1927
|
+
`,
|
|
1928
|
+
},
|
|
1929
|
+
want: ["helper"],
|
|
1930
|
+
why: "最基础的类方法;此前 calls 为空",
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
name: "正例·② 类方法(async / await)",
|
|
1934
|
+
fn: "C.m",
|
|
1935
|
+
files: {
|
|
1936
|
+
"a.ts": `
|
|
1937
|
+
function helper() { return 1; }
|
|
1938
|
+
export class C { async m() { await helper(); } }
|
|
1939
|
+
`,
|
|
1940
|
+
},
|
|
1941
|
+
want: ["helper"],
|
|
1942
|
+
why: "async 方法体同样是 Block,此前一样空",
|
|
1943
|
+
},
|
|
1944
|
+
{
|
|
1945
|
+
name: "正例·③ 类方法经 this 成员调用",
|
|
1946
|
+
fn: "C.m",
|
|
1947
|
+
files: {
|
|
1948
|
+
"a.ts": `
|
|
1949
|
+
export class C { private repo: any; m() { this.repo.save(); } }
|
|
1950
|
+
`,
|
|
1951
|
+
},
|
|
1952
|
+
want: ["save"],
|
|
1953
|
+
why: "NestJS/TypeORM 的实际写法;属性访问取末端名",
|
|
1954
|
+
},
|
|
1955
|
+
{
|
|
1956
|
+
name: "正例·④ 类方法带装饰器",
|
|
1957
|
+
fn: "C.m",
|
|
1958
|
+
files: {
|
|
1959
|
+
"a.ts": `
|
|
1960
|
+
function helper() { return 1; }
|
|
1961
|
+
function Get(): any { return () => {}; }
|
|
1962
|
+
export class C { @Get() m() { helper(); } }
|
|
1963
|
+
`,
|
|
1964
|
+
},
|
|
1965
|
+
want: ["helper"],
|
|
1966
|
+
why: "装饰器不得影响方法体遍历",
|
|
1967
|
+
},
|
|
1968
|
+
{
|
|
1969
|
+
name: "正例·⑤ 构造函数注入(方法用注入进来的依赖)",
|
|
1970
|
+
fn: "C.m",
|
|
1971
|
+
files: {
|
|
1972
|
+
"a.ts": `
|
|
1973
|
+
export class C { constructor(private repo: any) {} m() { this.repo.save(); } }
|
|
1974
|
+
`,
|
|
1975
|
+
},
|
|
1976
|
+
want: ["save"],
|
|
1977
|
+
why: "DI 形态;探针里五种已知失效形态之一",
|
|
1978
|
+
},
|
|
1979
|
+
// ── 正例:顺手测到的、没人提过的形状 ──────────────────────
|
|
1980
|
+
{
|
|
1981
|
+
name: "正例·⑥ static 方法",
|
|
1982
|
+
fn: "C.m",
|
|
1983
|
+
files: {
|
|
1984
|
+
"a.ts": `
|
|
1985
|
+
function helper() { return 1; }
|
|
1986
|
+
export class C { static m() { helper(); } }
|
|
1987
|
+
`,
|
|
1988
|
+
},
|
|
1989
|
+
want: ["helper"],
|
|
1990
|
+
why: "static 也在 getMethods() 内,同属本轮",
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
name: "正例·⑦ private 方法",
|
|
1994
|
+
fn: "C.m",
|
|
1995
|
+
files: {
|
|
1996
|
+
"a.ts": `
|
|
1997
|
+
function helper() { return 1; }
|
|
1998
|
+
export class C { private m() { helper(); } }
|
|
1999
|
+
`,
|
|
2000
|
+
},
|
|
2001
|
+
want: ["helper"],
|
|
2002
|
+
why: "可见性修饰符不得影响提取",
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
name: "正例·⑧ 抽象类里的实现方法",
|
|
2006
|
+
fn: "C.m",
|
|
2007
|
+
files: {
|
|
2008
|
+
"a.ts": `
|
|
2009
|
+
function helper() { return 1; }
|
|
2010
|
+
export abstract class C { abstract a(): void; m() { helper(); } }
|
|
2011
|
+
`,
|
|
2012
|
+
},
|
|
2013
|
+
want: ["helper"],
|
|
2014
|
+
why: "abstract 无体,但有体的方法照旧要提",
|
|
2015
|
+
},
|
|
2016
|
+
{
|
|
2017
|
+
name: "正例·⑨ 链式调用 this.a.b()",
|
|
2018
|
+
fn: "C.m",
|
|
2019
|
+
files: {
|
|
2020
|
+
"a.ts": `
|
|
2021
|
+
export class C { a: any; m() { this.a.b(); } }
|
|
2022
|
+
`,
|
|
2023
|
+
},
|
|
2024
|
+
want: ["b"],
|
|
2025
|
+
why: "末端名即被调用的方法名",
|
|
2026
|
+
},
|
|
2027
|
+
{
|
|
2028
|
+
name: "正例·⑩ 计算属性名方法",
|
|
2029
|
+
fn: "C.[K]",
|
|
2030
|
+
files: {
|
|
2031
|
+
"a.ts": `
|
|
2032
|
+
function helper() { return 1; }
|
|
2033
|
+
const K = "m";
|
|
2034
|
+
export class C { [K]() { helper(); } }
|
|
2035
|
+
`,
|
|
2036
|
+
},
|
|
2037
|
+
want: ["helper"],
|
|
2038
|
+
why: "名字解不解得出是另一回事,calls 不该受名字影响",
|
|
2039
|
+
},
|
|
2040
|
+
// ── 正对照:既有载体不得被误伤 ────────────────────────────
|
|
2041
|
+
{
|
|
2042
|
+
name: "正对照·⑪ 函数声明(基线)",
|
|
2043
|
+
fn: "plain",
|
|
2044
|
+
files: {
|
|
2045
|
+
"a.ts": `
|
|
2046
|
+
function helper() { return 1; }
|
|
2047
|
+
export function plain() { helper(); }
|
|
2048
|
+
`,
|
|
2049
|
+
},
|
|
2050
|
+
want: ["helper"],
|
|
2051
|
+
why: "本轮只补类方法,函数声明分支不得回归",
|
|
2052
|
+
},
|
|
2053
|
+
{
|
|
2054
|
+
name: "正对照·⑫ 箭头函数(基线)",
|
|
2055
|
+
fn: "arrow",
|
|
2056
|
+
files: {
|
|
2057
|
+
"a.ts": `
|
|
2058
|
+
function helper() { return 1; }
|
|
2059
|
+
export const arrow = () => { helper(); };
|
|
2060
|
+
`,
|
|
2061
|
+
},
|
|
2062
|
+
want: ["helper"],
|
|
2063
|
+
why: "同上;箭头分支不得回归",
|
|
2064
|
+
},
|
|
2065
|
+
// ── 负对照:防修过头 ─────────────────────────────────────
|
|
2066
|
+
{
|
|
2067
|
+
name: "负对照·① 嵌套箭头回调里的调用仍不提取",
|
|
2068
|
+
fn: "C.m",
|
|
2069
|
+
files: {
|
|
2070
|
+
"a.ts": `
|
|
2071
|
+
function helper() { return 1; }
|
|
2072
|
+
export class C { m() { setTimeout(() => { helper(); }, 0); } }
|
|
2073
|
+
`,
|
|
2074
|
+
},
|
|
2075
|
+
want: ["setTimeout"],
|
|
2076
|
+
dontWant: ["helper"],
|
|
2077
|
+
why: "既有设计:extractDirectCalls 遇嵌套函数即 skip。已实测函数声明/箭头/类方法三者口径一致 ⇒ 本轮不动",
|
|
2078
|
+
},
|
|
2079
|
+
{
|
|
2080
|
+
name: "负对照·② 真实调用不得挤掉语义标记",
|
|
2081
|
+
fn: "Uploader.read",
|
|
2082
|
+
files: {
|
|
2083
|
+
"handler.ts": `
|
|
2084
|
+
import * as fs from "fs";
|
|
2085
|
+
export class Uploader {
|
|
2086
|
+
read(filePath: string) {
|
|
2087
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
export function dispatchTool(args: any) {
|
|
2091
|
+
const target = params.arguments.file_path;
|
|
2092
|
+
return new Uploader().read(target);
|
|
2093
|
+
}
|
|
2094
|
+
`,
|
|
2095
|
+
},
|
|
2096
|
+
want: ["readFileSync", PATH_MARK],
|
|
2097
|
+
why: "本轮是**补**真实调用,标记必须仍在——否则污点轴会整体倒退。" +
|
|
2098
|
+
"(写这条用例时先跑探针确认判据:marker 只在**调用方把污点传进来**时才注入到" +
|
|
2099
|
+
"被调用方,无调用方的类方法本就无 marker —— 探针 ①/④ 一致、②/③ 一致)",
|
|
2100
|
+
},
|
|
2101
|
+
];
|
|
2102
|
+
for (const c of cases) {
|
|
2103
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
2104
|
+
const dir = makeProject(c.files);
|
|
2105
|
+
try {
|
|
2106
|
+
const calls = marksFor(dir, c.fn);
|
|
2107
|
+
for (const w of c.want)
|
|
2108
|
+
(0, vitest_1.expect)(calls).toContain(w);
|
|
2109
|
+
for (const d of c.dontWant ?? [])
|
|
2110
|
+
(0, vitest_1.expect)(calls).not.toContain(d);
|
|
2111
|
+
}
|
|
2112
|
+
finally {
|
|
2113
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
2114
|
+
}
|
|
2115
|
+
}, 150000);
|
|
2116
|
+
}
|
|
2117
|
+
});
|
package/dist/extract-ir.js
CHANGED
|
@@ -338,6 +338,18 @@ function manualResolveModule(moduleSpecifier, sourceFilePath, projectRoot, origi
|
|
|
338
338
|
}
|
|
339
339
|
return null;
|
|
340
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* 提取函数体内**真实出现的调用名**(区别于 computeMarkerCalls 产出的语义标记)。
|
|
343
|
+
*
|
|
344
|
+
* 2026-09-21(类方法空转修复):形参类型补 MethodDeclaration。
|
|
345
|
+
* 此前本函数只对函数声明 / 箭头 / 包装箭头三个载体调用过;主 IR 循环里的
|
|
346
|
+
* **类方法分支从来没调过它**,只调了 computeMarkerCalls ⇒ 类方法的 calls 里
|
|
347
|
+
* 永远只有标记、没有真实调用 ⇒ 状态机在 OO 代码上拿不到输入(FP 观测池实测:
|
|
348
|
+
* NestJS 切片 61 个函数里 59 个 calls 为空)。该空转自类方法提取功能诞生即存在,
|
|
349
|
+
* 因 taintpath 十一族语料全是函数载体而从未现形。
|
|
350
|
+
* MethodDeclaration 与 FunctionDeclaration 一样具备 getBody() / getText(),
|
|
351
|
+
* 遍历逻辑无需改动。
|
|
352
|
+
*/
|
|
341
353
|
function extractDirectCalls(func, preText) {
|
|
342
354
|
const body = func.getBody();
|
|
343
355
|
if (!body)
|
|
@@ -669,6 +681,21 @@ const VALUE_STRING_METHODS = new Set([
|
|
|
669
681
|
const SHAPER_ALLOWED_FREE_IDENTS = new Set([
|
|
670
682
|
"path", "sep", "undefined", "null", "true", "false", "String", "Number",
|
|
671
683
|
]);
|
|
684
|
+
/**
|
|
685
|
+
* C4j:**运算符关键字**——本身不携带数据,出现在 return 表达式里不该让整条 helper 落选。
|
|
686
|
+
*
|
|
687
|
+
* 缺陷实证(2026-09-21 探针 G1/G2):`(n, m) => typeof m === "string" || n + ".md"`
|
|
688
|
+
* 整条判据不通过 —— 单趟标识符扫描把 `typeof` 当成"未知的自由标识符"给否了。
|
|
689
|
+
* 后果不是"少收一点精度",而是**整个 helper 不进名录** ⇒ 调用点拿不到任何证据 ⇒ 漏报。
|
|
690
|
+
* 差别只在 typeof:G2 同形去掉 typeof 后 MARKED,G1 不标。
|
|
691
|
+
*
|
|
692
|
+
* `this` / `super` 刻意**不放行**:它们是接收者,可能携带数据(`this.prefix + n`),
|
|
693
|
+
* 属于另一个话题(要不要把接收者纳入证据),不能靠"关键字"这条口子捎带进来。
|
|
694
|
+
*/
|
|
695
|
+
const SHAPER_ALLOWED_KEYWORDS = new Set([
|
|
696
|
+
"typeof", "void", "delete", "in", "instanceof", "keyof", "as", "satisfies",
|
|
697
|
+
"is", "asserts", "readonly", "infer", "new", "await", "yield", "of",
|
|
698
|
+
]);
|
|
672
699
|
/** 允许出现在调用位的值转换函数(不改变路径语义) */
|
|
673
700
|
const VALUE_CONVERTER_CALLS = new Set(["String", "Number", "Boolean"]);
|
|
674
701
|
/**
|
|
@@ -713,6 +740,8 @@ function isPureShaperExpr(expr, params, shapers, extraIdents) {
|
|
|
713
740
|
// 自由标识符:必须是自己的形参、语言级常量白名单,或**已确证的字面量常量 / path 别名**
|
|
714
741
|
if (params.has(n) || SHAPER_ALLOWED_FREE_IDENTS.has(n))
|
|
715
742
|
continue;
|
|
743
|
+
if (SHAPER_ALLOWED_KEYWORDS.has(n))
|
|
744
|
+
continue; // C4j:运算符关键字不携带数据
|
|
716
745
|
if (extraIdents && extraIdents.has(n))
|
|
717
746
|
continue;
|
|
718
747
|
return false;
|
|
@@ -891,11 +920,35 @@ function moduleQualifier(fn) {
|
|
|
891
920
|
}
|
|
892
921
|
return null;
|
|
893
922
|
}
|
|
894
|
-
/**
|
|
895
|
-
|
|
923
|
+
/**
|
|
924
|
+
* 剥掉包在**函数体外面的**那几层壳,露出真正的箭头 / 函数表达式:
|
|
925
|
+
* 括号 `((n) => …)`、类型断言 `((n) => …) as Fn`、`satisfies Fn`、`<Fn>(…)`。
|
|
926
|
+
*
|
|
927
|
+
* C4j 补后三种。实测(探针 F3b/F3c):`export const toPath = ((n) => n + ".md")
|
|
928
|
+
* satisfies Fn` 与 `{ toPath: ((n) => n + ".md") as any }` 都不传播 ——
|
|
929
|
+
* 旧代码只认「初值就是箭头」,断言把它们包了一层,整族漏。
|
|
930
|
+
*/
|
|
931
|
+
function unwrapFns(n) {
|
|
896
932
|
let cur = n;
|
|
897
|
-
|
|
898
|
-
|
|
933
|
+
for (let i = 0; i < 6 && cur; i++) {
|
|
934
|
+
if (ts_morph_1.Node.isParenthesizedExpression(cur)) {
|
|
935
|
+
cur = cur.getExpression();
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (ts_morph_1.Node.isAsExpression(cur)) {
|
|
939
|
+
cur = cur.getExpression();
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (ts_morph_1.Node.isSatisfiesExpression(cur)) {
|
|
943
|
+
cur = cur.getExpression();
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (ts_morph_1.Node.isTypeAssertion(cur)) {
|
|
947
|
+
cur = cur.getExpression();
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
899
952
|
return cur;
|
|
900
953
|
}
|
|
901
954
|
/**
|
|
@@ -906,23 +959,23 @@ function unparen(n) {
|
|
|
906
959
|
function functionLikeOf(init) {
|
|
907
960
|
// 实测:`(() => (n) => n + ".md")()` 里被括号包住的**不止**内层箭头 ——
|
|
908
961
|
// 连被调用的 callee 都是 ParenthesizedExpression。两处都得脱。
|
|
909
|
-
const base =
|
|
962
|
+
const base = unwrapFns(init);
|
|
910
963
|
if (!base)
|
|
911
964
|
return null;
|
|
912
965
|
if (ts_morph_1.Node.isArrowFunction(base) || ts_morph_1.Node.isFunctionExpression(base))
|
|
913
966
|
return base;
|
|
914
967
|
if (ts_morph_1.Node.isCallExpression(base)) {
|
|
915
|
-
const callee =
|
|
968
|
+
const callee = unwrapFns(base.getExpression());
|
|
916
969
|
if (!ts_morph_1.Node.isArrowFunction(callee) && !ts_morph_1.Node.isFunctionExpression(callee))
|
|
917
970
|
return null;
|
|
918
971
|
const b = callee.getBody();
|
|
919
972
|
if (!ts_morph_1.Node.isBlock(b)) {
|
|
920
973
|
// 简洁体:`(() => (n) => n + ".md")()` —— 括号包着也算(unparen)
|
|
921
|
-
const e =
|
|
974
|
+
const e = unwrapFns(b);
|
|
922
975
|
return e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)) ? e : null;
|
|
923
976
|
}
|
|
924
977
|
for (const rs of b.getDescendantsOfKind(ts.SyntaxKind.ReturnStatement)) {
|
|
925
|
-
const e =
|
|
978
|
+
const e = unwrapFns(rs.getExpression());
|
|
926
979
|
if (e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)))
|
|
927
980
|
return e;
|
|
928
981
|
}
|
|
@@ -975,51 +1028,135 @@ function paramIndexOf(si, argIndex) {
|
|
|
975
1028
|
return si.restAt >= 0 && argIndex >= si.restAt ? si.restAt : argIndex;
|
|
976
1029
|
}
|
|
977
1030
|
/**
|
|
978
|
-
* C4i:柯里化 helper ——
|
|
979
|
-
* (`
|
|
980
|
-
*
|
|
981
|
-
*
|
|
1031
|
+
* C4i/C4j:柯里化 helper —— 函数体【返回】另一个函数
|
|
1032
|
+
* (`(ext) => (n) => n + ext`、`(a) => (b) => (n) => n + a + b`)。
|
|
1033
|
+
* 被调用的不是最外层,而是它一路返回出来的最内层;真正塑形在那里,
|
|
1034
|
+
* 外层只负责捕获自己那一批实参。返回的不是函数(含返回对象字面量的工厂形态)
|
|
1035
|
+
* 一律返回 null,交给 currySpecsOf 的另一条路。
|
|
982
1036
|
*/
|
|
983
|
-
function
|
|
984
|
-
const
|
|
985
|
-
if (!
|
|
1037
|
+
function curryChainOf(outerParams, body) {
|
|
1038
|
+
const ret = returnedValueOf(body);
|
|
1039
|
+
if (!ret)
|
|
986
1040
|
return null;
|
|
987
|
-
|
|
988
|
-
if (innerParams.length === 0)
|
|
1041
|
+
if (!ts_morph_1.Node.isArrowFunction(ret) && !ts_morph_1.Node.isFunctionExpression(ret))
|
|
989
1042
|
return null;
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1043
|
+
const hops = [];
|
|
1044
|
+
let cur = ret;
|
|
1045
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
1046
|
+
if (!cur)
|
|
1047
|
+
break;
|
|
1048
|
+
if (!ts_morph_1.Node.isArrowFunction(cur) && !ts_morph_1.Node.isFunctionExpression(cur))
|
|
1049
|
+
return null;
|
|
1050
|
+
const fbody = cur.getBody();
|
|
1051
|
+
if (!fbody)
|
|
1052
|
+
return null;
|
|
1053
|
+
const params = cur.getParameters().flatMap(paramNamesOf).filter(Boolean);
|
|
1054
|
+
hops.push({ kind: "call", name: "", params });
|
|
1055
|
+
const next = returnedFunctionOf(fbody);
|
|
1056
|
+
if (!next) {
|
|
1057
|
+
const rets = returnExprsOf(fbody);
|
|
1058
|
+
if (rets.length === 0)
|
|
1059
|
+
return null;
|
|
1060
|
+
// 内层无形参 ⇒ 没有「经内层实参进来」的污点,外层按普通 helper 处理
|
|
1061
|
+
if (hops[hops.length - 1].params.length === 0)
|
|
1062
|
+
return null;
|
|
1063
|
+
const allParams = new Set(outerParams);
|
|
1064
|
+
for (const h of hops)
|
|
1065
|
+
for (const p of h.params)
|
|
1066
|
+
allParams.add(p);
|
|
1067
|
+
return { outerParams, hops, allParams: [...allParams], rets };
|
|
1068
|
+
}
|
|
1069
|
+
cur = next;
|
|
1070
|
+
}
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* C4j:一个函数体对应的全部调用链。
|
|
1075
|
+
* - 「返回函数」形态 ⇒ 0 或 1 条(沿返回值爬到最内层);
|
|
1076
|
+
* - 「返回对象字面量」形态 ⇒ 每个**函数属性**各一条(工厂 / builder 的常态,
|
|
1077
|
+
* `factory(".md").toPath(k)` 里的 `toPath` 才是塑形那一层)。
|
|
1078
|
+
* 属性名算不出(计算属性名而常量不可见)或无形参 / 无返回的,跳过该条。
|
|
1079
|
+
*/
|
|
1080
|
+
function currySpecsOf(outerParams, body) {
|
|
1081
|
+
const ret = returnedValueOf(body);
|
|
1082
|
+
if (!ret)
|
|
1083
|
+
return [];
|
|
1084
|
+
if (!ts_morph_1.Node.isObjectLiteralExpression(ret)) {
|
|
1085
|
+
const one = curryChainOf(outerParams, body);
|
|
1086
|
+
return one ? [one] : [];
|
|
1087
|
+
}
|
|
1088
|
+
const specs = [];
|
|
1089
|
+
for (const prop of ret.getProperties()) {
|
|
1090
|
+
let name = null;
|
|
1091
|
+
let fn;
|
|
1092
|
+
if (ts_morph_1.Node.isPropertyAssignment(prop)) {
|
|
1093
|
+
name = prop.getName().replace(/^["'`]|["'`]$/g, "");
|
|
1094
|
+
const inner = unwrapFns(prop.getInitializer());
|
|
1095
|
+
if (inner && (ts_morph_1.Node.isArrowFunction(inner) || ts_morph_1.Node.isFunctionExpression(inner))) {
|
|
1096
|
+
fn = inner;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
else if (ts_morph_1.Node.isMethodDeclaration(prop)) {
|
|
1100
|
+
name = prop.getName();
|
|
1101
|
+
fn = prop;
|
|
1102
|
+
}
|
|
1103
|
+
if (!fn || !name || !/^[A-Za-z_$][\w$]*$/.test(name))
|
|
1104
|
+
continue;
|
|
1105
|
+
const fbody = fn.getBody();
|
|
1106
|
+
if (!fbody)
|
|
1107
|
+
continue; // 重载签名 / declare
|
|
1108
|
+
const params = fn.getParameters().flatMap(paramNamesOf).filter(Boolean);
|
|
1109
|
+
if (params.length === 0)
|
|
1110
|
+
continue; // 判据①
|
|
1111
|
+
const rets = returnExprsOf(fbody);
|
|
1112
|
+
if (rets.length === 0)
|
|
1113
|
+
continue;
|
|
1114
|
+
specs.push({
|
|
1115
|
+
outerParams,
|
|
1116
|
+
hops: [{ kind: "member", name, params }],
|
|
1117
|
+
allParams: [...outerParams, ...params],
|
|
1118
|
+
rets,
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
return specs;
|
|
1122
|
+
}
|
|
1123
|
+
/** 函数体返回出来的那个值(简洁体本身 / return 语句的表达式,都先解断言与括号) */
|
|
1124
|
+
function returnedValueOf(body) {
|
|
1125
|
+
if (!ts_morph_1.Node.isBlock(body))
|
|
1126
|
+
return unwrapFns(body);
|
|
1127
|
+
for (const rs of body.getDescendantsOfKind(ts.SyntaxKind.ReturnStatement)) {
|
|
1128
|
+
const e = unwrapFns(rs.getExpression());
|
|
1129
|
+
if (e)
|
|
1130
|
+
return e;
|
|
1131
|
+
}
|
|
1132
|
+
return undefined;
|
|
997
1133
|
}
|
|
998
1134
|
/** 函数体返回出来的那个箭头 / 函数表达式(没有则 null) */
|
|
999
1135
|
function returnedFunctionOf(body) {
|
|
1000
1136
|
if (!ts_morph_1.Node.isBlock(body)) {
|
|
1001
1137
|
// 箭头简洁体本身就是要返回的函数:`(ext) => (n) => n + ext`(可能被括号包住)
|
|
1002
|
-
const e =
|
|
1138
|
+
const e = unwrapFns(body);
|
|
1003
1139
|
return e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)) ? e : null;
|
|
1004
1140
|
}
|
|
1005
1141
|
for (const rs of body.getDescendantsOfKind(ts.SyntaxKind.ReturnStatement)) {
|
|
1006
|
-
const e =
|
|
1142
|
+
const e = unwrapFns(rs.getExpression());
|
|
1007
1143
|
if (e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)))
|
|
1008
1144
|
return e;
|
|
1009
1145
|
}
|
|
1010
1146
|
return null;
|
|
1011
1147
|
}
|
|
1012
1148
|
/**
|
|
1013
|
-
* C4i
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1149
|
+
* C4i/C4j:柯里化与工厂形态的调用点展开。`withExt(".md")(k)`、`lvl4(a)(b)(c)(k)`、
|
|
1150
|
+
* `factory(".md").toPath(k)` 里真正塑形的是链的**最后一跳** —— 这里按各跳的实参把
|
|
1151
|
+
* 最内层 return 表达式代入后原地摊开,之后的污点检测直接看摊开后的串:
|
|
1152
|
+
* `withExt(".md")(k)` ⇒ `k + ".md"`;`factory(".md").toPath(k)` ⇒ `k + ".md"`。
|
|
1153
|
+
* 丢形参的最后一跳(`(_n) => "fixed.md"`)摊开后是常量 ⇒ 自然不传播。
|
|
1017
1154
|
*/
|
|
1018
1155
|
function expandCurried(text, curried) {
|
|
1019
1156
|
if (curried.size === 0)
|
|
1020
1157
|
return text;
|
|
1021
1158
|
let cur = text;
|
|
1022
|
-
for (let round = 0; round <
|
|
1159
|
+
for (let round = 0; round < 4; round++) {
|
|
1023
1160
|
const next = expandCurriedOnce(cur, curried);
|
|
1024
1161
|
if (next === null)
|
|
1025
1162
|
break;
|
|
@@ -1027,9 +1164,9 @@ function expandCurried(text, curried) {
|
|
|
1027
1164
|
}
|
|
1028
1165
|
return cur;
|
|
1029
1166
|
}
|
|
1030
|
-
/**
|
|
1167
|
+
/** 摊开**一处**柯里化 / 工厂形态调用;没有可摊的返回 null(供调用方收敛) */
|
|
1031
1168
|
function expandCurriedOnce(text, curried) {
|
|
1032
|
-
for (const [name,
|
|
1169
|
+
for (const [name, specs] of curried) {
|
|
1033
1170
|
const re = new RegExp(`(?:^|[^\\w$.])${escapeRe(name)}\\s*\\(`, "g");
|
|
1034
1171
|
let m;
|
|
1035
1172
|
while ((m = re.exec(text)) !== null) {
|
|
@@ -1037,32 +1174,61 @@ function expandCurriedOnce(text, curried) {
|
|
|
1037
1174
|
const close1 = matchingParen(text, open1);
|
|
1038
1175
|
if (close1 < 0)
|
|
1039
1176
|
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
|
-
|
|
1177
|
+
const args0 = splitTopLevelArgs(text.slice(open1 + 1, close1));
|
|
1178
|
+
for (const cs of specs) {
|
|
1179
|
+
const map = new Map();
|
|
1180
|
+
args0.forEach((a, i) => {
|
|
1181
|
+
const p = cs.outerParams[i];
|
|
1182
|
+
if (p !== undefined)
|
|
1183
|
+
map.set(p, stripOuterParens(a.trim()));
|
|
1184
|
+
});
|
|
1185
|
+
// 沿链一路吃掉各跳:普通跳是紧跟的 `(`,成员跳是 `.name(`
|
|
1186
|
+
let cursor = close1 + 1;
|
|
1187
|
+
let ok = true;
|
|
1188
|
+
for (const hop of cs.hops) {
|
|
1189
|
+
let j = cursor;
|
|
1190
|
+
while (j < text.length && /\s/.test(text[j]))
|
|
1191
|
+
j++;
|
|
1192
|
+
if (hop.kind === "member") {
|
|
1193
|
+
if (text[j] === "?")
|
|
1194
|
+
j++; // 可选链 `?.toPath(`
|
|
1195
|
+
if (text[j] !== ".") {
|
|
1196
|
+
ok = false;
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
j++;
|
|
1200
|
+
const nm = /^[A-Za-z_$][\w$]*/.exec(text.slice(j));
|
|
1201
|
+
if (!nm || nm[0] !== hop.name) {
|
|
1202
|
+
ok = false;
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
j += nm[0].length;
|
|
1206
|
+
while (j < text.length && /\s/.test(text[j]))
|
|
1207
|
+
j++;
|
|
1208
|
+
}
|
|
1209
|
+
if (text[j] !== "(") {
|
|
1210
|
+
ok = false;
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
const cl = matchingParen(text, j);
|
|
1214
|
+
if (cl < 0) {
|
|
1215
|
+
ok = false;
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
splitTopLevelArgs(text.slice(j + 1, cl)).forEach((a, i) => {
|
|
1219
|
+
const p = hop.params[i];
|
|
1220
|
+
if (p !== undefined && !map.has(p))
|
|
1221
|
+
map.set(p, stripOuterParens(a.trim()));
|
|
1222
|
+
});
|
|
1223
|
+
cursor = cl + 1;
|
|
1224
|
+
}
|
|
1225
|
+
if (!ok)
|
|
1226
|
+
continue;
|
|
1227
|
+
const inner = cs.rets.length === 1 ? cs.rets[0] : cs.rets.join(" + ");
|
|
1228
|
+
const expanded = foldDecidableTernary(substituteParams(inner, map));
|
|
1229
|
+
const nameStart = m.index + (m[0].startsWith(name) ? 0 : 1);
|
|
1230
|
+
return text.slice(0, nameStart) + expanded + text.slice(cursor);
|
|
1231
|
+
}
|
|
1066
1232
|
}
|
|
1067
1233
|
}
|
|
1068
1234
|
return null;
|
|
@@ -1589,6 +1755,34 @@ function returnExprsOf(body) {
|
|
|
1589
1755
|
}
|
|
1590
1756
|
return rets;
|
|
1591
1757
|
}
|
|
1758
|
+
/**
|
|
1759
|
+
* C4j:`export default (n) => n + ".md"` 这种**匿名**默认导出没有名字可登记,
|
|
1760
|
+
* 能用的名字只有「别的文件 import 它时起的本地名」。用 ts-morph 的模块解析找到
|
|
1761
|
+
* 真正指向该文件的默认导入(解析不出的模块一律跳过 —— 认不出就不收)。
|
|
1762
|
+
*/
|
|
1763
|
+
function defaultImportLocalNames(project, target) {
|
|
1764
|
+
const out = new Set();
|
|
1765
|
+
const targetPath = target.getFilePath();
|
|
1766
|
+
for (const sf of project.getSourceFiles()) {
|
|
1767
|
+
if (sf.getFilePath() === targetPath)
|
|
1768
|
+
continue;
|
|
1769
|
+
for (const imp of sf.getImportDeclarations()) {
|
|
1770
|
+
let resolved;
|
|
1771
|
+
try {
|
|
1772
|
+
resolved = imp.getModuleSpecifierSourceFile() ?? undefined;
|
|
1773
|
+
}
|
|
1774
|
+
catch {
|
|
1775
|
+
continue; // 解析不了(路径写错 / 别名没配)—— 跳过
|
|
1776
|
+
}
|
|
1777
|
+
if (!resolved || resolved.getFilePath() !== targetPath)
|
|
1778
|
+
continue;
|
|
1779
|
+
const d = imp.getDefaultImport();
|
|
1780
|
+
if (d)
|
|
1781
|
+
out.add(d.getText());
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
return out;
|
|
1785
|
+
}
|
|
1592
1786
|
/** 扫描全项目,收集「纯塑形」helper 的名字 + 返回值依赖(有界两轮不动点) */
|
|
1593
1787
|
function pureShaperFunctionNames(project) {
|
|
1594
1788
|
const sinkRe = tsSinkCallRegex();
|
|
@@ -1597,21 +1791,37 @@ function pureShaperFunctionNames(project) {
|
|
|
1597
1791
|
const groups = new Map();
|
|
1598
1792
|
// 模块级字面量常量(值一并留着:C4i 的计算属性名 `[KEY]` 要靠它解)
|
|
1599
1793
|
const constTexts = moduleConstTexts(project);
|
|
1794
|
+
/**
|
|
1795
|
+
* 把一个**没通过判据**的实现记进它所属的成员组。
|
|
1796
|
+
*
|
|
1797
|
+
* C4j 修(探针 B4/B6 实测):C4h-② 的"全组确证"只在**进了组**的候选之间成立,
|
|
1798
|
+
* 而三条判据不成立的实现在 consider() 里就 return 了 —— 它压根不在组里,于是
|
|
1799
|
+
* 「同名三处,一处无形参、一处体内有 sink、一处是好 helper」会被当成"全组都确证",
|
|
1800
|
+
* 成员调用位被放行 ⇒ 误标。组必须记全:**存在过但没确证**也要算进"不是全组确证"。
|
|
1801
|
+
*/
|
|
1802
|
+
const noteUnaccepted = (key, group) => {
|
|
1803
|
+
if (group === null)
|
|
1804
|
+
return;
|
|
1805
|
+
const list = groups.get(group) ?? [];
|
|
1806
|
+
if (!list.includes(key))
|
|
1807
|
+
list.push(key);
|
|
1808
|
+
groups.set(group, list);
|
|
1809
|
+
};
|
|
1600
1810
|
/** 登记一个候选:三条判据 —— 有形参 / 有返回值 / 体内无 sink */
|
|
1601
1811
|
const consider = (key, name, group, paramNodes, restAt, curry, declText, bodyNode) => {
|
|
1602
1812
|
const paramList = paramNodes.filter(Boolean);
|
|
1603
1813
|
const params = new Set(paramList);
|
|
1604
1814
|
if (params.size === 0)
|
|
1605
|
-
return;
|
|
1815
|
+
return noteUnaccepted(key, group); // 判据①
|
|
1606
1816
|
sinkRe.lastIndex = 0;
|
|
1607
1817
|
if (sinkRe.test(declText))
|
|
1608
|
-
return; // ②
|
|
1818
|
+
return noteUnaccepted(key, group); // ②
|
|
1609
1819
|
// 用 AST 取 return 表达式,而不是正则 —— 模板字面量里的 `${}` 会
|
|
1610
1820
|
// 让「遇到花括号就停」的朴素正则在 `path.join(a,b) + \`.${c}\`` 上截断。
|
|
1611
1821
|
const rets = returnExprsOf(bodyNode);
|
|
1612
|
-
//
|
|
1613
|
-
if (rets.length === 0 && curry ===
|
|
1614
|
-
return; //
|
|
1822
|
+
// 柯里化 / 工厂形态:return 出来的是函数或装着函数的对象 ⇒ 判据①看的是最内层
|
|
1823
|
+
if (rets.length === 0 && curry.length === 0)
|
|
1824
|
+
return noteUnaccepted(key, group); // ③
|
|
1615
1825
|
let pub = name;
|
|
1616
1826
|
if (pub !== null) {
|
|
1617
1827
|
if (takenNames.has(pub))
|
|
@@ -1646,7 +1856,7 @@ function pureShaperFunctionNames(project) {
|
|
|
1646
1856
|
const ps = fn.getParameters();
|
|
1647
1857
|
const paramNodes = ps.flatMap(paramNamesOf);
|
|
1648
1858
|
const restAt = restIndexOf(ps);
|
|
1649
|
-
const curry =
|
|
1859
|
+
const curry = currySpecsOf(paramNodes, body);
|
|
1650
1860
|
const owner = moduleQualifier(fn);
|
|
1651
1861
|
// namespace 内却认不出宿主链 ⇒ 不收(宁可漏,不可张冠李戴)
|
|
1652
1862
|
if (!ts_morph_1.Node.isSourceFile(parent) && owner === null)
|
|
@@ -1674,7 +1884,7 @@ function pureShaperFunctionNames(project) {
|
|
|
1674
1884
|
continue;
|
|
1675
1885
|
const ps = fnLike.getParameters();
|
|
1676
1886
|
const paramNodes = ps.flatMap(paramNamesOf);
|
|
1677
|
-
consider(`var:${vd.getName()}`, vd.getName(), null, paramNodes, restIndexOf(ps),
|
|
1887
|
+
consider(`var:${vd.getName()}`, vd.getName(), null, paramNodes, restIndexOf(ps), currySpecsOf(paramNodes, body), init.getText(), // IIFE 时含整条调用 ⇒ sink 检查仍覆盖得到
|
|
1678
1888
|
body);
|
|
1679
1889
|
}
|
|
1680
1890
|
}
|
|
@@ -1702,35 +1912,40 @@ function pureShaperFunctionNames(project) {
|
|
|
1702
1912
|
continue; // 重载签名 / declare
|
|
1703
1913
|
const ps = md.getParameters();
|
|
1704
1914
|
const pns = ps.flatMap(paramNamesOf);
|
|
1705
|
-
considerMember(bare, ownerQualifier(md, constTexts), pns, restIndexOf(ps),
|
|
1915
|
+
considerMember(bare, ownerQualifier(md, constTexts), pns, restIndexOf(ps), currySpecsOf(pns, body), md.getText(), body);
|
|
1706
1916
|
}
|
|
1707
1917
|
// (d) 属性承载的箭头 / 函数表达式 —— 现代工程里 Objects-as-namespace 的常态。
|
|
1708
1918
|
// ownerQualifier 从父链走(对象字面量 → 属性赋值 → 变量声明),嵌套也认。
|
|
1919
|
+
// C4j:初值外面可能还包着 as / satisfies(`{ toPath: ((n) => …) as any }`)。
|
|
1709
1920
|
for (const pa of sf.getDescendantsOfKind(ts.SyntaxKind.PropertyAssignment)) {
|
|
1710
|
-
const
|
|
1921
|
+
const rawInit = pa.getInitializer();
|
|
1922
|
+
if (!rawInit)
|
|
1923
|
+
continue;
|
|
1924
|
+
const init = unwrapFns(rawInit);
|
|
1711
1925
|
if (!init)
|
|
1712
1926
|
continue;
|
|
1713
1927
|
if (!ts_morph_1.Node.isArrowFunction(init) && !ts_morph_1.Node.isFunctionExpression(init))
|
|
1714
1928
|
continue;
|
|
1715
|
-
const
|
|
1929
|
+
const fn = init;
|
|
1930
|
+
const body = fn.getBody();
|
|
1716
1931
|
if (!body)
|
|
1717
1932
|
continue;
|
|
1718
1933
|
const bare = propertyNameOf(pa, constTexts);
|
|
1719
1934
|
if (!bare || !/^[A-Za-z_$][\w$]*$/.test(bare))
|
|
1720
1935
|
continue;
|
|
1721
|
-
const ps =
|
|
1936
|
+
const ps = fn.getParameters();
|
|
1722
1937
|
const pns = ps.flatMap(paramNamesOf);
|
|
1723
|
-
considerMember(bare, ownerQualifier(pa, constTexts), pns, restIndexOf(ps),
|
|
1938
|
+
considerMember(bare, ownerQualifier(pa, constTexts), pns, restIndexOf(ps), currySpecsOf(pns, body), rawInit.getText(), body);
|
|
1724
1939
|
}
|
|
1725
1940
|
// (e) 后挂上去的属性箭头 `Util.toPath = (n) => …`(monkey patch / 渐进导出)
|
|
1726
1941
|
for (const be of sf.getDescendantsOfKind(ts.SyntaxKind.BinaryExpression)) {
|
|
1727
1942
|
if (be.getOperatorToken().getKind() !== ts.SyntaxKind.EqualsToken)
|
|
1728
1943
|
continue;
|
|
1729
1944
|
const left = be.getLeft();
|
|
1730
|
-
const right = be.getRight();
|
|
1945
|
+
const right = unwrapFns(be.getRight());
|
|
1731
1946
|
if (!ts_morph_1.Node.isPropertyAccessExpression(left))
|
|
1732
1947
|
continue;
|
|
1733
|
-
if (!ts_morph_1.Node.isArrowFunction(right) && !ts_morph_1.Node.isFunctionExpression(right))
|
|
1948
|
+
if (!right || (!ts_morph_1.Node.isArrowFunction(right) && !ts_morph_1.Node.isFunctionExpression(right)))
|
|
1734
1949
|
continue;
|
|
1735
1950
|
const body = right.getBody();
|
|
1736
1951
|
if (!body)
|
|
@@ -1742,7 +1957,52 @@ function pureShaperFunctionNames(project) {
|
|
|
1742
1957
|
const bare = qname.split(".").pop();
|
|
1743
1958
|
const ps = right.getParameters();
|
|
1744
1959
|
const pns = ps.flatMap(paramNamesOf);
|
|
1745
|
-
considerMember(bare, qname.slice(0, -(bare.length + 1)), pns, restIndexOf(ps),
|
|
1960
|
+
considerMember(bare, qname.slice(0, -(bare.length + 1)), pns, restIndexOf(ps), currySpecsOf(pns, body), right.getText(), body);
|
|
1961
|
+
}
|
|
1962
|
+
// (f) C4j:getter 返回出来的箭头 —— `get mk() { return (n) => n + ".md"; }`。
|
|
1963
|
+
// 它既不是 MethodDeclaration(没有形参)也不是 PropertyAssignment(值是 getter),
|
|
1964
|
+
// 上一轮三种载体都收不到它,整族漏(探针 B1/B2)。真正塑形的是**返回出来的箭头**,
|
|
1965
|
+
// 所以判据看的是内层:无形参不收(B4)、体内有 sink 不收(B6)。
|
|
1966
|
+
// 注意这里**不**走 currySpecOf —— getter 的读取本身不消耗实参,调用点是
|
|
1967
|
+
// `o.mk(k)` 一次调用,不是 `mk()(k)` 两跳。
|
|
1968
|
+
for (const ga of sf.getDescendantsOfKind(ts.SyntaxKind.GetAccessor)) {
|
|
1969
|
+
const body = ga.getBody();
|
|
1970
|
+
if (!body)
|
|
1971
|
+
continue;
|
|
1972
|
+
const inner = returnedFunctionOf(body);
|
|
1973
|
+
if (!inner)
|
|
1974
|
+
continue; // getter 直接返回拼接(无形参)⇒ 判据①本就不成立
|
|
1975
|
+
const innerBody = inner.getBody();
|
|
1976
|
+
if (!innerBody)
|
|
1977
|
+
continue;
|
|
1978
|
+
const bare = propertyNameOf(ga, constTexts);
|
|
1979
|
+
if (!bare || !/^[A-Za-z_$][\w$]*$/.test(bare))
|
|
1980
|
+
continue;
|
|
1981
|
+
const ps = inner.getParameters();
|
|
1982
|
+
const pns = ps.flatMap(paramNamesOf);
|
|
1983
|
+
considerMember(bare, ownerQualifier(ga, constTexts), pns, restIndexOf(ps), currySpecsOf(pns, innerBody), ga.getText(), innerBody);
|
|
1984
|
+
}
|
|
1985
|
+
// (g) C4j:`export default (n) => …` —— 匿名默认导出。它既不是函数声明(没有名字)
|
|
1986
|
+
// 也不是变量声明,上一轮整族漏(探针 F9)。名字只能用**导入它的文件**里起的
|
|
1987
|
+
// 本地名;多个文件用不同名字导入 ⇒ 每个名字各登记一条(takenNames 仍然先到先得)。
|
|
1988
|
+
for (const ea of sf.getDescendantsOfKind(ts.SyntaxKind.ExportAssignment)) {
|
|
1989
|
+
if (ea.isExportEquals())
|
|
1990
|
+
continue;
|
|
1991
|
+
const fn = unwrapFns(ea.getExpression());
|
|
1992
|
+
if (!fn || (!ts_morph_1.Node.isArrowFunction(fn) && !ts_morph_1.Node.isFunctionExpression(fn)))
|
|
1993
|
+
continue;
|
|
1994
|
+
const fnBody = fn.getBody();
|
|
1995
|
+
if (!fnBody)
|
|
1996
|
+
continue;
|
|
1997
|
+
const localNames = defaultImportLocalNames(project, sf);
|
|
1998
|
+
if (localNames.size === 0)
|
|
1999
|
+
continue; // 没人默认导入 ⇒ 名字无从谈起
|
|
2000
|
+
const ps = fn.getParameters();
|
|
2001
|
+
const pns = ps.flatMap(paramNamesOf);
|
|
2002
|
+
const curry = currySpecsOf(pns, fnBody);
|
|
2003
|
+
for (const ln of localNames) {
|
|
2004
|
+
consider(`def:${sf.getFilePath()}:${ln}`, ln, null, pns, restIndexOf(ps), curry, ea.getText(), fnBody);
|
|
2005
|
+
}
|
|
1746
2006
|
}
|
|
1747
2007
|
}
|
|
1748
2008
|
// 形参之外允许出现的自由标识符 = 模块级字面量常量 ∪ 确证的 path 模块别名
|
|
@@ -1763,18 +2023,23 @@ function pureShaperFunctionNames(project) {
|
|
|
1763
2023
|
names.add(name);
|
|
1764
2024
|
info.set(name, si);
|
|
1765
2025
|
};
|
|
2026
|
+
/** C4j:确证通过的链(工厂可能有几条,脏的那条会被剔掉,见下) */
|
|
2027
|
+
const pureCurried = new Map();
|
|
1766
2028
|
for (let round = 0; round < 2; round++) {
|
|
1767
2029
|
let added = false;
|
|
1768
2030
|
for (const c of cands) {
|
|
1769
2031
|
if (acceptedKeys.has(c.key))
|
|
1770
2032
|
continue;
|
|
1771
2033
|
let si;
|
|
1772
|
-
if (c.curry
|
|
1773
|
-
//
|
|
1774
|
-
|
|
1775
|
-
|
|
2034
|
+
if (c.curry.length > 0) {
|
|
2035
|
+
// 柯里化 / 工厂形态:判据落在**最内层**的 return 上,形参集合是本链各层并起来。
|
|
2036
|
+
// 每个 spec 各自过判据:工厂返回多个方法时,只把**确证过的那些**收进名录,
|
|
2037
|
+
// 脏的那条不发 spec ⇒ 调用点不会摊开它 ⇒ 退化为漏报而不是误报(保守侧)。
|
|
2038
|
+
const pure = c.curry.filter((spec) => spec.rets.every((e) => isPureShaperExpr(e, new Set([...c.params, ...spec.allParams]), names, extraIdents)));
|
|
2039
|
+
if (pure.length === 0)
|
|
1776
2040
|
continue;
|
|
1777
2041
|
acceptedKeys.add(c.key);
|
|
2042
|
+
pureCurried.set(c.key, pure);
|
|
1778
2043
|
// 外层名照发:闭包会捕获外层实参(`const g = withExt(k)` 也是污的)。
|
|
1779
2044
|
// rets 留空 ⇒ 调用点折叠自动退回「全部依赖」(保守侧)。
|
|
1780
2045
|
si = { params: c.paramList, deps: new Set(c.paramList), rets: [], restAt: c.restAt };
|
|
@@ -1798,14 +2063,22 @@ function pureShaperFunctionNames(project) {
|
|
|
1798
2063
|
if (!added)
|
|
1799
2064
|
break;
|
|
1800
2065
|
}
|
|
1801
|
-
// C4i
|
|
2066
|
+
// C4i/C4j:柯里化与工厂名录。只有**发布了名字**的才登记 —— 名字被别处同名定义占用时
|
|
1802
2067
|
// (pub 降为 null),`withExt(a)(b)` 里的 withExt 可能指的不是这个 helper,
|
|
1803
2068
|
// 展开它会张冠李戴 ⇒ 宁可不展(保守侧,退化为漏报而非误报)。
|
|
2069
|
+
// 一个名字可以对多条链(工厂返回含多个方法的对象)。
|
|
1804
2070
|
const curried = new Map();
|
|
1805
2071
|
for (const c of cands) {
|
|
1806
|
-
if (c.
|
|
1807
|
-
|
|
1808
|
-
|
|
2072
|
+
if (c.name === null || !acceptedKeys.has(c.key))
|
|
2073
|
+
continue;
|
|
2074
|
+
const pure = pureCurried.get(c.key);
|
|
2075
|
+
if (!pure || pure.length === 0)
|
|
2076
|
+
continue;
|
|
2077
|
+
const list = curried.get(c.name) ?? [];
|
|
2078
|
+
for (const spec of pure)
|
|
2079
|
+
if (!list.includes(spec))
|
|
2080
|
+
list.push(spec);
|
|
2081
|
+
curried.set(c.name, list);
|
|
1809
2082
|
}
|
|
1810
2083
|
// C4f:依赖分析有次序依赖 —— `wrap(n) { return discard(n); }` 若在 discard 之前
|
|
1811
2084
|
// 被接受,那一刻 info 里还没有 discard ⇒ deps 偏宽(偏宽 = 继续传播 = 保守侧,
|
|
@@ -2600,7 +2873,10 @@ function _extractSingleProject(absRoot, tsconfigPath) {
|
|
|
2600
2873
|
continue;
|
|
2601
2874
|
const mParams = m.getParameters();
|
|
2602
2875
|
const mText = m.getText();
|
|
2603
|
-
|
|
2876
|
+
// 2026-09-21:补上真实调用提取(此前只有 computeMarkerCalls,见 extractDirectCalls 注释)。
|
|
2877
|
+
// 顺序与函数声明 / 箭头两个分支保持一致:先真实调用,再追加语义标记。
|
|
2878
|
+
const mCalls = extractDirectCalls(m, mText);
|
|
2879
|
+
mCalls.push(...computeMarkerCalls(mText, mParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns, shapers));
|
|
2604
2880
|
funcs.push({
|
|
2605
2881
|
name: `${cn}.${mn}`,
|
|
2606
2882
|
params: mParams.map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
|
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.47";
|
|
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.47",
|
|
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/",
|