progmune-runtime 3.7.42 → 3.7.45
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 +232 -0
- package/dist/extract-ir-taint-structural.test.js +357 -0
- package/dist/extract-ir.js +912 -42
- package/dist/sdk.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,237 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.45] — 2026-09-21
|
|
4
|
+
|
|
5
|
+
### C4i:又一批可判定分支 + 又一批 helper 载体
|
|
6
|
+
|
|
7
|
+
备忘里挂着两条(getter/computed 属性 helper、常量代入扩展到短路与模板串)。按 R17
|
|
8
|
+
先写探针复现,跑完 19 个形状:**备忘只覆盖了一半**,真缺口有六个,其中柯里化与 rest
|
|
9
|
+
两条备忘里根本没有。
|
|
10
|
+
|
|
11
|
+
| 形状 | 探针实测 | 结论 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| 短路 `\|\|` / `&&` / `??` | `viaOr(k, "fixed.md")` 误标 | 真误报(精度侧) |
|
|
14
|
+
| 模板串内嵌三元 | `viaTpl(k, true)` 误标 | 真误报(精度侧) |
|
|
15
|
+
| 柯里化 `withExt(".md")(k)` | 不标 | 真漏报(备忘未列) |
|
|
16
|
+
| namespace 内函数声明 | `P.toPath(k)` 不标 | 真漏报 |
|
|
17
|
+
| rest 形参 `...parts` | `joinAll("out", k)` 不标 | 真漏报(备忘未列) |
|
|
18
|
+
| IIFE 定义 / 计算属性名 | `toPathIife(k)`、`Box.toPath(k)` 不标 | 真漏报 |
|
|
19
|
+
| getter(无形参) | 不标 | **不是缺口** —— 无形参 ⇒ 判据①本就不成立 |
|
|
20
|
+
|
|
21
|
+
**精度侧:可判定分支的扩展。** 三元之外,短路运算符与三元同形,只是判据从 cond 换成
|
|
22
|
+
**左操作数的真假**(`\|\|` 左真取左、`&&` 左真取右、`??` 左非空取左)。模板串的
|
|
23
|
+
`${...}` 是独立表达式,折叠要能进去 —— 现代代码里 `` `${flag ? "fixed" : n}.md` `` 比
|
|
24
|
+
裸三元更常见。判不出一律退回原串。
|
|
25
|
+
|
|
26
|
+
**召回侧:四个新载体。**
|
|
27
|
+
|
|
28
|
+
- **柯里化**:`withExt(ext)(n)` 真正塑形的是被返回出来的内层函数。收集时识别「return
|
|
29
|
+
箭头」,判据落在内层;调用点把 `NAME(a)(b)` 按两层实参代入后**原地摊开**
|
|
30
|
+
(⇒ `k + ".md"`),之后的污点检测直接看摊开后的串。
|
|
31
|
+
- **namespace**:旧收集走 `sf.getFunctions()`,实测取不到 namespace 里的函数 ⇒ 改遍历
|
|
32
|
+
声明节点,只收顶层 / namespace 链上的(函数体内嵌套的作用域与调用点不同,不收)。
|
|
33
|
+
- **rest 形参**:它吃掉的是**一批**实参,不是一位。原先按位对齐会把第二位当成「越界」
|
|
34
|
+
抹掉 ⇒ 漏报。现在 rest 位吃掉末尾所有位,且常量代入时跳过 rest 位。
|
|
35
|
+
- **IIFE / 计算属性名**:`(() => (n) => …)()` 的初值是调用不是箭头;`{ [KEY]: (n) => … }`
|
|
36
|
+
的属性名只有在 KEY 是**模块级字面量常量**时才解 —— 解不出就不登记。
|
|
37
|
+
|
|
38
|
+
### 又一条「用例自己写错了」
|
|
39
|
+
|
|
40
|
+
初版把「属性名算不出来」的 `DynBox` 和同名可解的 `Box` 放在同一个项目里,期望
|
|
41
|
+
unmarked 却实测 mark。原因不是机制错:成员调用位 `.name(` 的匹配**本就与宿主无关**
|
|
42
|
+
(C4g 的保守策略),`Box.toPath` 登记之后 `DynBox.toPath` 必然被顺带命中。教训:写负
|
|
43
|
+
对照前先确认「不标」到底是因为哪一个机制不成立 —— 这条已写进语料注释。
|
|
44
|
+
|
|
45
|
+
### 验收
|
|
46
|
+
|
|
47
|
+
| 门 | 结果 |
|
|
48
|
+
|---|---|
|
|
49
|
+
| taintpath 闸门 | **138/138**(新增 J 族 26 条:13 mark / 12 no-taint / 1 suppressed) |
|
|
50
|
+
| fr-007 真实语料 | pre 5 / post 0 **维持** |
|
|
51
|
+
| fr-016 真实语料 | pre 7 / post 0 **维持** |
|
|
52
|
+
| 定向用例 | 提取器定向用例 **204 passed / 6 个测试文件**(其中本文件新增 C4i 组 26 条:11 正 15 负) |
|
|
53
|
+
| TS 盲测 | 110 项目 / perFunction 口径 **3183 条**:**LOST 0 / ADDED 18**,全部来自新增的 J 族 —— 其中 12 条 `path_traversal` 是本该标的,6 条 `data_integrity`(写文件无审计)属新项目的无关类别;基线那 3165 条**零漂移零丢失** |
|
|
54
|
+
| tsc | 零错误 |
|
|
55
|
+
|
|
56
|
+
### 反向验证七刀
|
|
57
|
+
|
|
58
|
+
| 刀 | 改动 | 转红 |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| 1 | `topLevelLogical` 恒返回 null(关短路折叠) | `emitOrTakeLeft` / `emitAndTakeLeft` / `emitNullishTakeLeft`(no-taint 转误标) |
|
|
61
|
+
| 2 | `foldTemplateSubsts` 恒返回原串 | `emitTplConst`(转误标) |
|
|
62
|
+
| 3 | `currySpecOf` 恒返回 null(柯里化不登记) | `emitCurrySecond` / `emitCurryFirst` / `emitCurryFn`(转漏报) |
|
|
63
|
+
| 4 | `moduleQualifier` 恒返回 null(namespace 不发限定名) | `emitNsPath`(转漏报) |
|
|
64
|
+
| 5 | `paramIndexOf` 退回按位(rest 位截断) | `emitRestJoin`(转漏报) |
|
|
65
|
+
| 6 | 不解 IIFE(`functionLikeOf` 的 CallExpression 分支) | `emitIifePath`(转漏报) |
|
|
66
|
+
| 7 | 不解计算属性名(`propertyNameOf` 的 computed 分支) | `emitComputed`(转漏报) |
|
|
67
|
+
|
|
68
|
+
七刀各自只动自己那一组,没有交叉。第 2 刀和第 5 刀第一次都**打偏了**(详见下),
|
|
69
|
+
打偏的刀会连累无关用例 —— 已升为方法学规则 **R18**。
|
|
70
|
+
|
|
71
|
+
### 实现坑三条
|
|
72
|
+
|
|
73
|
+
1. **`substituteParams` 把模板串整段跳过了** —— 折叠逻辑写对了也没用,`${flag ? …}`
|
|
74
|
+
里的 flag 根本没被代进去。模板串要按 `${}` 逐段递归代入,字面量部分照抄。
|
|
75
|
+
2. **ts-morph 的两层包装**:`namespace P { … }` 里函数的父节点是 **ModuleBlock**
|
|
76
|
+
(ModuleDeclaration 在上一层);`(() => (n) => …)()` 里被括号包住的**不止**内层
|
|
77
|
+
箭头,连被调用的 callee 都是 `ParenthesizedExpression`。两处都得脱。
|
|
78
|
+
3. **模板串里写注释会提前终结模板串**(生成器里 `${}`、反引号都要转义)—— 报错信息
|
|
79
|
+
指向的是文件末尾,很容易误判成别处语法错。
|
|
80
|
+
|
|
81
|
+
### 反向验证的两刀打偏(R18)
|
|
82
|
+
|
|
83
|
+
反向验证不是"随便让一个函数失效"就行,退回必须是**旧行为**:
|
|
84
|
+
|
|
85
|
+
- 第 5 刀第一次写成 `paramIndexOf` 返回 `null` —— rest 位不是"退回按位",而是整个
|
|
86
|
+
失去对应形参 ⇒ 所有 helper 的实参被抹光 ⇒ **40 条漏报**。正确写法是 `return argIndex`。
|
|
87
|
+
- 第 2 刀第一次写成 `foldTemplateSubsts` 返回 `null` —— 调用方靠 `tpl !== s` 判断
|
|
88
|
+
"这一层有没有做事",返回 null 会被当成"已折叠" ⇒ 连带 11 条无关用例转红。
|
|
89
|
+
|
|
90
|
+
⇒ R18:**反向验证的刀要退回旧行为,不能随便 return null**;刀的转红清单必须落在
|
|
91
|
+
自己那一组,一旦出现跨组转红,先怀疑刀打偏了,而不是"机制有耦合"。
|
|
92
|
+
|
|
93
|
+
## [3.7.44] — 2026-09-20
|
|
94
|
+
|
|
95
|
+
### C4h:名录之外的三处收口
|
|
96
|
+
|
|
97
|
+
helper 名录这轮的起点是三个备忘候选,动工前先写探针复现(R12),结果只有两个是真
|
|
98
|
+
缺口,外加一个**探针顺手挖出来的既有缺陷**:
|
|
99
|
+
|
|
100
|
+
| 候选 | 探针实测 | 结论 |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| 调用点常量实参代入 | `viaFlag(k, true)` 误标 | 真误报,本轮修(精度侧) |
|
|
103
|
+
| 同名方法出现两次 | `u.toPath(k)` 不标 | 真漏报,本轮修(召回侧) |
|
|
104
|
+
| 属性承载的箭头 helper | `Util.toPath(k)` 不标 | 真漏报,本轮修(召回侧) |
|
|
105
|
+
| ——(非备忘项) | 方法名唯一但判据不成立时,成员调用位照样进名录 | **既有缺陷**,`r.lookup(k)` 误标 |
|
|
106
|
+
|
|
107
|
+
**① 调用点常量代入。** 名录里的依赖集是**跨调用点**的并集 —— 对所有实参形状都成立
|
|
108
|
+
的最宽结论,收不住个案:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const viaFlag = (n: string, flag: boolean): string => (flag ? "fixed.md" : n + ".md");
|
|
112
|
+
const rel = viaFlag(k, true); // 本次调用的返回值就是字面量 —— n 位进了函数也出不来
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
按形参位把**字面量实参**代回 return 表达式,试着解掉 cond 可判定的三元,再看还有
|
|
116
|
+
哪些形参活着;判不出(实参不是常量、cond 代入后算不出真假)就一律退回名录结论 ——
|
|
117
|
+
保守侧,认不出就不收精度。
|
|
118
|
+
|
|
119
|
+
**② 同名成员多处。** 旧规则要求方法名全项目唯一才登记 `.method(` 成员调用位。唯一性
|
|
120
|
+
只是「不可能认错」的**充分条件**,不是必要条件:同名两处都被确证为纯塑形时,同样
|
|
121
|
+
不可能认错。换成「全组确证」之后依赖集按形参位取并集(任一实现在某位会流出,那位
|
|
122
|
+
就算会流出)。
|
|
123
|
+
|
|
124
|
+
同一处顺带补上一个坑:候选登记改成 `key / name / group` 三段,**确证之后**才发布到
|
|
125
|
+
成员名录 —— 旧代码在确证之前就把裸名登记进去,于是
|
|
126
|
+
`class Reg { lookup(n) { return registry[n]; } }`(registry 是模块级非字面量对象 ⇒
|
|
127
|
+
判据③不成立)之后的 `r.lookup(k)` 被当成塑形 ⇒ 误标。
|
|
128
|
+
|
|
129
|
+
**③ 属性承载的箭头。** `const Util = { toPath: (n) => … }` 与 `Util.toPath = (n) => …`
|
|
130
|
+
既不是方法声明也不是变量声明,旧名录一片都收不到。现在与方法同表登记(限定名 +
|
|
131
|
+
同组确证发布裸名)。
|
|
132
|
+
|
|
133
|
+
### R13 第二次的变种:假通过也会发生在**定向用例**里
|
|
134
|
+
|
|
135
|
+
C4g 组的静态类方法用例原本叫 `Renderer.stat(k)` —— `stat` 就在 fs sink 名单里,调用点
|
|
136
|
+
被 sink 兜底直接标中,**那条用例一直在假通过**(删掉整段方法收集它照样绿)。改名
|
|
137
|
+
`toFile` 后才真的在测 helper 传播。R16 原只约束盲测语料命名,本轮起同样适用于定向
|
|
138
|
+
用例:写 fixture 前先 grep `TS_FILE_SINK_NAMES`。
|
|
139
|
+
|
|
140
|
+
### 验收
|
|
141
|
+
|
|
142
|
+
| 门 | 结果 |
|
|
143
|
+
|---|---|
|
|
144
|
+
| taintpath 闸门 | **112/112**(新增 I 族 20 条:11 mark / 8 no-taint / 1 suppressed) |
|
|
145
|
+
| fr-007 真实语料 | pre 5 / post 0 **维持** |
|
|
146
|
+
| fr-016 真实语料 | pre 7 / post 0 **维持** |
|
|
147
|
+
| 定向用例 | 提取器定向用例 **178 passed / 6 个测试文件**(其中本文件 102 passed,新增 C4h 组 21 条:12 正 9 负) |
|
|
148
|
+
| TS 盲测 | 109 项目 / perFunction 口径 **3165 条**:**LOST 0 / ADDED 11**,全部是新增 I 族里那 11 条本该标的 path_traversal;基线那 3152 条**零漂移零丢失**(另 2 条 `data_integrity` 属新项目的无关类别) |
|
|
149
|
+
| tsc | 零错误 |
|
|
150
|
+
|
|
151
|
+
### 反向验证四刀
|
|
152
|
+
|
|
153
|
+
| 刀 | 改动 | 转红 |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| 1 | 关掉调用点折叠(`callSiteKeepFlags` 退回名录 deps) | `emitFlagTrue` / `emitPickMiss` / `emitFlagNested` / `emitWrapFlagTrue`(4 条由 no-taint 转误标) |
|
|
156
|
+
| 2 | 同名合并退回「全项目唯一」 | `emitSameNameA` / `emitSameNameB`(转漏报) |
|
|
157
|
+
| 3 | 摘掉属性箭头收集((d)(e) 两个分支) | `emitPropArrow` / `emitNestedPropArrow` / `emitPatchedArrow`(转漏报) |
|
|
158
|
+
| 4 | 成员名退回「确证之前就登记」 | `emitNonShaperMember` / `emitSameNameRejected`(转误标 —— 那个缺陷就是这么被探针逮到的) |
|
|
159
|
+
|
|
160
|
+
四刀各自只动自己那一组,没有交叉:归因是干净的。
|
|
161
|
+
|
|
162
|
+
### 实现坑两条
|
|
163
|
+
|
|
164
|
+
1. **整条括号会让 cond 切不干净**:return 表达式常被写成 `=> (flag ? a : b)`,在第一
|
|
165
|
+
个顶层 `?` 处切开得到的前缀 `((true)` 括号不配对 ⇒ 判不出真假 ⇒ 折叠静默失效。
|
|
166
|
+
要先脱掉包住整串的括号(`stripOuterParens`)。
|
|
167
|
+
2. **代入时别给字面量加括号**:`(true)` 会让 `maskNonDependentArgsOnce` 把实参串误判
|
|
168
|
+
成嵌套调用(它的判据是「实参里还有 `(`」),于是内层 helper 永远轮不到被处理 ——
|
|
169
|
+
两跳那条(`wrapFlag(k, true)`)就卡在这儿。
|
|
170
|
+
|
|
171
|
+
## [3.7.43] — 2026-09-20
|
|
172
|
+
|
|
173
|
+
### C4g:helper 换个载体(对象字面量方法 / 类方法 / 解构形参)
|
|
174
|
+
|
|
175
|
+
helper 不一定是函数,也可能是方法:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
export const Util = { toPath(n: string): string { return n + ".md"; } }; // 对象字面量方法
|
|
179
|
+
export class Renderer { inst(n: string): string { return n + ".md"; } } // 类实例方法
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
C4e 时刻意没做这一路(按裸名放行会让别的对象上的同名方法被误认成塑形,R11)。
|
|
183
|
+
这一轮用**限定名 + 唯一性**解决:
|
|
184
|
+
|
|
185
|
+
- 宿主名能确定时登记 `Owner.method`,调用点 `Util.toPath(k)` 直接对上;
|
|
186
|
+
- 实例方法的主要写法是 `r.inst(k)`(宿主是变量,限定名对不上),所以该方法名在
|
|
187
|
+
**全项目唯一**时额外允许 `.inst(` 这种成员调用位匹配 —— 唯一性是 R11 的替代品,
|
|
188
|
+
不存在同名方法就不可能张冠李戴。同名方法出现两次时一律不登记,宁可漏报。
|
|
189
|
+
|
|
190
|
+
另补**解构形参**:`function f({ name }) {…}` 的 `getName()` 给的是整个绑定模式
|
|
191
|
+
而不是 `name`,要从 BindingElement 里逐个抽出来,否则这类 helper 一个都认不出。
|
|
192
|
+
|
|
193
|
+
R12 第四次生效:备忘里列的「依赖分析深层形态」(三元返回、默认参、多 return)
|
|
194
|
+
探针实测**本来就是通的**,真漏的只有解构形参。
|
|
195
|
+
|
|
196
|
+
### 语料踩到的坑:helper 名字不能撞 sink 名单
|
|
197
|
+
|
|
198
|
+
H 族第一版把静态方法写成 `Renderer.stat(k)`,闸门全绿;反向验证摘掉方法收集之后
|
|
199
|
+
它**仍然标**。一查:`stat` 就在 `TS_FILE_SINK_NAMES` 里,调用点被 sink 兜底直接
|
|
200
|
+
命中,与 helper 传播毫无关系 —— 实测 `Other.stat(k)`(接收者根本没定义)、以及类里
|
|
201
|
+
压根没有 `stat` 方法,照样标。
|
|
202
|
+
|
|
203
|
+
这是 R13 那条旁路的第二次出现,而且更阴:它让正例「标对了」,但标对的原因是另一
|
|
204
|
+
回事。改名为 `toFile` 之后归因才干净。已登记为 **R16**。
|
|
205
|
+
|
|
206
|
+
### 验收
|
|
207
|
+
|
|
208
|
+
| 门 | 结果 |
|
|
209
|
+
|---|---|
|
|
210
|
+
| taintpath 闸门 | **92/92**(新增 H 族 11 条:7 mark / 3 no-taint / 1 suppressed) |
|
|
211
|
+
| fr-007 真实语料 | pre 5 / post 0 **维持** |
|
|
212
|
+
| fr-016 真实语料 | pre 7 / post 0 **维持** —— 放宽召回后守卫侧仍无新增误报 |
|
|
213
|
+
| 定向用例 | **171 passed / 7 个测试文件**(新增 C4g 组 7 条:4 正 3 负) |
|
|
214
|
+
| TS 盲测 | 108 项目,path_traversal **LOST 0 / ADDED 27**(H 7 + E 9 + F 6 + G 4 + D 1),基线那 104 个项目**零漂移零丢失** |
|
|
215
|
+
| tsc | 零错误 |
|
|
216
|
+
|
|
217
|
+
### 反向验证四刀
|
|
218
|
+
|
|
219
|
+
| 刀 | 改动 | 转红 |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| 1 | 摘掉方法候选收集 | 3 条方法正例 |
|
|
222
|
+
| 2 | 只摘掉成员调用位 `.method(` | emitClassInstance(宿主是变量那条) |
|
|
223
|
+
| 3 | 解构形参名提取退回整体文本 | emitDestructured |
|
|
224
|
+
| 4 | 关掉 C4f 收窄 | G 族 6 条 + H 族 `emitObjMethodDrop` |
|
|
225
|
+
|
|
226
|
+
刀 4 顺带证明 C4f 的收窄对新载体同样生效 —— `Drop.fixed(k)` 不是空转对照。
|
|
227
|
+
|
|
228
|
+
### 刻意不做:常量实参代入
|
|
229
|
+
|
|
230
|
+
`viaTernaryDrop(n, flag) { return flag ? "fixed.md" : n + ".md"; }` 在
|
|
231
|
+
`viaTernaryDrop(k, true)` 下走的是常量分支、形参不流出,现在仍会被判污染。要收这条
|
|
232
|
+
得在调用点做常量传播(把实参字面量代入 return 再重算依赖),是另一个量级的改动,
|
|
233
|
+
且代入出错的代价是漏报。登记为已知缺口。
|
|
234
|
+
|
|
3
235
|
## [3.7.42] — 2026-09-20
|
|
4
236
|
|
|
5
237
|
### C4f:名字级传播的精度收窄(形参-实参位置对齐 + 返回值依赖分析)
|
|
@@ -1380,3 +1380,360 @@ const WRITE_LAYER = [
|
|
|
1380
1380
|
}, 150000);
|
|
1381
1381
|
}
|
|
1382
1382
|
});
|
|
1383
|
+
(0, vitest_1.describe)("C4g:helper 换个载体(对象字面量方法 / 类方法 / 解构形参)也要能传播", () => {
|
|
1384
|
+
const emit = (relExpr, pre = "") => [
|
|
1385
|
+
'import * as fs from "fs";',
|
|
1386
|
+
'import { Util, Drop, Renderer, viaName } from "./helpers";',
|
|
1387
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1388
|
+
...(pre ? [` ${pre}`] : []),
|
|
1389
|
+
" Object.keys(doc).forEach((k) => {",
|
|
1390
|
+
` const rel = ${relExpr};`,
|
|
1391
|
+
' fs.writeFileSync(outDir + "/" + rel, "x");',
|
|
1392
|
+
" });",
|
|
1393
|
+
"}",
|
|
1394
|
+
].join("\n");
|
|
1395
|
+
const HELPERS = [
|
|
1396
|
+
"export const Util = {",
|
|
1397
|
+
' toPath(n: string): string { return n + ".md"; },',
|
|
1398
|
+
"};",
|
|
1399
|
+
"export const Drop = {",
|
|
1400
|
+
' fixed(n: string): string { return "fixed.md"; },',
|
|
1401
|
+
"};",
|
|
1402
|
+
"export class Renderer {",
|
|
1403
|
+
' inst(n: string): string { return n + ".md"; }',
|
|
1404
|
+
// 原名 stat —— **stat 在 fs sink 名单里**,调用点会被 sink 兜底直接标中,
|
|
1405
|
+
// 这条用例于是变成假通过(测的根本不是 helper 传播,R13)。改名后才测得准。
|
|
1406
|
+
' static toFile(n: string): string { return n + ".txt"; }',
|
|
1407
|
+
"}",
|
|
1408
|
+
'export function viaName({ name }: any): string { return name + ".md"; }',
|
|
1409
|
+
].join("\n");
|
|
1410
|
+
const cases = [
|
|
1411
|
+
{ name: "正例·对象字面量方法", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("Util.toPath(k)") },
|
|
1412
|
+
expectMark: true, why: "宿主名可确证 ⇒ 走限定名 Owner.method" },
|
|
1413
|
+
{ name: "正例·类实例方法", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("r.inst(k)", "const r = new Renderer();") },
|
|
1414
|
+
expectMark: true, why: "宿主是变量,限定名对不上 ⇒ 靠方法名唯一 + 成员调用位" },
|
|
1415
|
+
{ name: "正例·静态类方法", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("Renderer.toFile(k)") },
|
|
1416
|
+
expectMark: true, why: "与对象字面量同形,走限定名(方法名刻意避开 sink 名单,否则用例是假通过)" },
|
|
1417
|
+
{ name: "正例·解构形参", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("viaName({ name: k })") },
|
|
1418
|
+
expectMark: true, why: "形参名要从绑定模式里抽出来 —— getName() 给的是整个模式而不是 name" },
|
|
1419
|
+
{ name: "负对照·对象字面量方法丢弃形参", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit("Drop.fixed(k)") },
|
|
1420
|
+
expectMark: false, why: "召回放宽到方法之后,C4f 的收窄也要跟得上新载体;正对照 = 正例·对象字面量方法" },
|
|
1421
|
+
{ name: "负对照·解构形参喂字面量", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit('viaName({ name: "lit" })') },
|
|
1422
|
+
expectMark: false, why: "无污点流入;正对照 = 正例·解构形参" },
|
|
1423
|
+
{ name: "负对照·方法是真塑形但实参是字面量", fn: "emit", files: { "helpers.ts": HELPERS, "it.ts": emit('Util.toPath("static")') },
|
|
1424
|
+
expectMark: false, why: "传播得靠实参带污点" },
|
|
1425
|
+
];
|
|
1426
|
+
for (const c of cases) {
|
|
1427
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
1428
|
+
const dir = makeProject(c.files);
|
|
1429
|
+
try {
|
|
1430
|
+
const marks = marksFor(dir, c.fn);
|
|
1431
|
+
if (c.expectMark)
|
|
1432
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
1433
|
+
else
|
|
1434
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
1435
|
+
}
|
|
1436
|
+
finally {
|
|
1437
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1438
|
+
}
|
|
1439
|
+
}, 150000);
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1443
|
+
// C4h(2026-09-20):三件都在「名录 ↔ 调用点」交界上的事
|
|
1444
|
+
//
|
|
1445
|
+
// ① 调用点常量代入 —— helper 的 return 走三元,本次调用喂的是常量 ⇒ 返回值
|
|
1446
|
+
// 其实是字面量。名录里的依赖集是**跨调用点**的并集,收不住这种个案
|
|
1447
|
+
// (探针实测:`viaFlag(k, true)` 误标)。按位置把常量代回去、解掉可判定的
|
|
1448
|
+
// 三元,判不出就退回原结论(保守侧)。
|
|
1449
|
+
// ② 同名成员多处 —— 旧规则要求方法名全项目唯一才登记 `.method(`。唯一性只是
|
|
1450
|
+
// 「不可能认错」的充分条件:同名两处**都被确证**时同样不可能认错。
|
|
1451
|
+
// 顺带修掉一处真实缺陷:旧代码在确证之前就把裸名塞进了成员名录。
|
|
1452
|
+
// ③ 属性承载的箭头 helper —— `const Util = { toPath: (n) => … }` 与
|
|
1453
|
+
// `Util.toPath = (n) => …`,这两种既不是方法声明也不是变量声明。
|
|
1454
|
+
//
|
|
1455
|
+
// 写法约定(与前几族同源):sink 实参里**不直接出现**污点变量 k。
|
|
1456
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
1457
|
+
(0, vitest_1.describe)("C4h:名录之外的三处收口(调用点常量 / 同名成员 / 属性箭头)", () => {
|
|
1458
|
+
const emit = (relExpr, pre = "", imports = "", helpers = "") => ({
|
|
1459
|
+
"helpers.ts": helpers,
|
|
1460
|
+
"it.ts": [
|
|
1461
|
+
'import * as fs from "fs";',
|
|
1462
|
+
...(imports ? [`import { ${imports} } from "./helpers";`] : []),
|
|
1463
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1464
|
+
...(pre ? [` ${pre}`] : []),
|
|
1465
|
+
" Object.keys(doc).forEach((k) => {",
|
|
1466
|
+
` const rel = ${relExpr};`,
|
|
1467
|
+
' fs.writeFileSync(outDir + "/" + rel, "x");',
|
|
1468
|
+
" });",
|
|
1469
|
+
"}",
|
|
1470
|
+
].join("\n"),
|
|
1471
|
+
});
|
|
1472
|
+
// ── ① 调用点常量代入 ───────────────────────────────────────────────
|
|
1473
|
+
const H_CONST = [
|
|
1474
|
+
'export const viaFlag = (n: string, flag: boolean): string => (flag ? "fixed.md" : n + ".md");',
|
|
1475
|
+
// 两支都含形参 —— 收精度不能收到这儿
|
|
1476
|
+
"export const eitherWay = (n: string, flag: boolean): string => (flag ? n : n + '.md');",
|
|
1477
|
+
"export const pickMode = (n: string, mode: string): string => (mode === 'raw' ? n : 'fixed.md');",
|
|
1478
|
+
// 两跳:折叠要能穿过 helper 之间的嵌套调用
|
|
1479
|
+
'export const wrapFlag = (n: string, flag: boolean): string => viaFlag(n, flag);',
|
|
1480
|
+
].join("\n");
|
|
1481
|
+
// ── ② 同名成员多处 ─────────────────────────────────────────────────
|
|
1482
|
+
const H_TWICE_OK = [
|
|
1483
|
+
"export class Uploader { toPath(n: string): string { return n + '.md'; } }",
|
|
1484
|
+
"export class Downloader { toPath(n: string): string { return n + '.tmp'; } }",
|
|
1485
|
+
].join("\n");
|
|
1486
|
+
const H_TWICE_DROP = [
|
|
1487
|
+
"export class DropA { still(n: string): string { return 'fixed.md'; } }",
|
|
1488
|
+
"export class DropB { still(n: string): string { return 'other.md'; } }",
|
|
1489
|
+
].join("\n");
|
|
1490
|
+
// 一处引用了模块级**非字面量**对象 ⇒ 判据③不成立 ⇒ 整个同名组都不许放行。
|
|
1491
|
+
// 刻意不含 sink —— 否则会被跨函数 sink 形参映射标中,归因就不干净了(R13)。
|
|
1492
|
+
const H_TWICE_BAD = [
|
|
1493
|
+
"export class Uploader { toPath(n: string): string { return n + '.md'; } }",
|
|
1494
|
+
"export const dict: Record<string, string> = {};",
|
|
1495
|
+
"export class Weird { toPath(n: string): string { return dict[n]; } }",
|
|
1496
|
+
].join("\n");
|
|
1497
|
+
const H_SOLO = [
|
|
1498
|
+
"export class Solo { toPath(n: string): string { return n + '.md'; } }",
|
|
1499
|
+
].join("\n");
|
|
1500
|
+
// ── ③ 属性承载的箭头 ───────────────────────────────────────────────
|
|
1501
|
+
const H_PROP = [
|
|
1502
|
+
"export const Util2 = {",
|
|
1503
|
+
" toPath: (n: string): string => n + '.md',",
|
|
1504
|
+
"};",
|
|
1505
|
+
"export const NS = {",
|
|
1506
|
+
" path: {",
|
|
1507
|
+
" toPath: (n: string): string => n + '.md',",
|
|
1508
|
+
" },",
|
|
1509
|
+
"};",
|
|
1510
|
+
"export const Patched: any = {};",
|
|
1511
|
+
"Patched.toPath = (n: string): string => n + '.md';",
|
|
1512
|
+
"export const DropProp = {",
|
|
1513
|
+
" fixed: (n: string): string => 'fixed.md',",
|
|
1514
|
+
"};",
|
|
1515
|
+
].join("\n");
|
|
1516
|
+
// ── 缺陷回归门:非塑形方法的成员调用位不能算塑形 ────────────────────
|
|
1517
|
+
const H_NONSHAPER = [
|
|
1518
|
+
"export const registry: Record<string, string> = {};",
|
|
1519
|
+
"export class Reg {",
|
|
1520
|
+
" lookup(n: string): string { return registry[n]; }",
|
|
1521
|
+
"}",
|
|
1522
|
+
].join("\n");
|
|
1523
|
+
const H_SHAPER_LOOKUP = [
|
|
1524
|
+
"export class RegSafe {",
|
|
1525
|
+
" lookup(n: string): string { return n + '.md'; }",
|
|
1526
|
+
"}",
|
|
1527
|
+
].join("\n");
|
|
1528
|
+
const cases = [
|
|
1529
|
+
// ① —— 精度侧
|
|
1530
|
+
{ name: "正例·① 走真流出分支", fn: "emit", files: emit("viaFlag(k, false)", "", "viaFlag", H_CONST),
|
|
1531
|
+
expectMark: true, why: "flag=false ⇒ 走的是含 n 的那支" },
|
|
1532
|
+
{ name: "正例·① 两支都含形参", fn: "emit", files: emit("eitherWay(k, true)", "", "eitherWay", H_CONST),
|
|
1533
|
+
expectMark: true, why: "收精度不许收过头 —— 选中的分支里仍有 n" },
|
|
1534
|
+
{ name: "正例·① 字符串比较命中", fn: "emit", files: emit('pickMode(k, "raw")', "", "pickMode", H_CONST),
|
|
1535
|
+
expectMark: true, why: "mode === 'raw' 命中 ⇒ 返回 n" },
|
|
1536
|
+
{ name: "正例·① 第二参是变量(判不出就照标)", fn: "emit", files: emit("viaFlag(k, dyn)", "const dyn = Math.random() > 0;", "viaFlag", H_CONST),
|
|
1537
|
+
expectMark: true, why: "保守侧:实参不是常量 ⇒ 一律退回名录结论,不许假装看得懂" },
|
|
1538
|
+
{ name: "负对照·① 常量把分支钉死成字面量", fn: "emit", files: emit("viaFlag(k, true)", "", "viaFlag", H_CONST),
|
|
1539
|
+
expectMark: false, why: "本次调用的返回值就是 'fixed.md' —— 名录级依赖集收不住这种个案;正对照 = ① 走真流出分支" },
|
|
1540
|
+
{ name: "负对照·① 字符串比较没命中", fn: "emit", files: emit('pickMode(k, "cooked")', "", "pickMode", H_CONST),
|
|
1541
|
+
expectMark: false, why: "mode 不是 'raw' ⇒ 走常量支;正对照 = ① 字符串比较命中" },
|
|
1542
|
+
{ name: "负对照·① 嵌套在 path.join 里", fn: "emit", files: emit('path.join("out", viaFlag(k, true))', "", "viaFlag", H_CONST),
|
|
1543
|
+
expectMark: false, why: "整条 rhs 含 k,但那条支路不流出 —— 折叠要能作用在嵌套的内层" },
|
|
1544
|
+
{ name: "负对照·① 两跳 + 常量", fn: "emit", files: emit("wrapFlag(k, true)", "", "wrapFlag", H_CONST),
|
|
1545
|
+
expectMark: false, why: "wrapFlag(n, flag) = viaFlag(n, flag),折叠要穿过一层 helper" },
|
|
1546
|
+
{ name: "正对照·① 两跳走流出分支", fn: "emit", files: emit("wrapFlag(k, false)", "", "wrapFlag", H_CONST),
|
|
1547
|
+
expectMark: true, why: "与上一条镜像:常量改变量 ⇒ 立刻转红,证明归因在这条机制上" },
|
|
1548
|
+
// ② —— 召回侧
|
|
1549
|
+
{ name: "正例·② 同名成员两处均塑形", fn: "emit", files: emit("u.toPath(k)", "const u = new Uploader();", "Uploader", H_TWICE_OK),
|
|
1550
|
+
expectMark: true, why: "两处都被确证 ⇒ `.toPath(` 不可能落到别的实现上(唯一性换成全组确证)" },
|
|
1551
|
+
{ name: "正例·② 另一处同名也被识别", fn: "emit", files: emit("d.toPath(k)", "const d = new Downloader();", "Downloader", H_TWICE_OK),
|
|
1552
|
+
expectMark: true, why: "合并是对称的:同组的另一个宿主同样要接得上" },
|
|
1553
|
+
{ name: "正例·② 单处同名的老对照组", fn: "emit", files: emit("s.toPath(k)", "const s = new Solo();", "Solo", H_SOLO),
|
|
1554
|
+
expectMark: true, why: "全组确证是旧唯一性规则的上位,老行为必须还在" },
|
|
1555
|
+
{ name: "负对照·② 同名两处都丢弃形参", fn: "emit", files: emit("a.still(k)", "const a = new DropA();", "DropA", H_TWICE_DROP),
|
|
1556
|
+
expectMark: false, why: "合并后的依赖集按位取并集 —— 两处都丢 ⇒ 仍为空,精度不许丢" },
|
|
1557
|
+
{ name: "负对照·② 同名一处非塑形", fn: "emit", files: emit("u.toPath(k)", "const u = new Uploader();", "Uploader", H_TWICE_BAD),
|
|
1558
|
+
expectMark: false, why: "整组不放行(R11 的不变量:必须在每一种实现上都成立);正对照 = ② 同名成员两处均塑形" },
|
|
1559
|
+
// ③ —— 召回侧
|
|
1560
|
+
{ name: "正例·③ 属性箭头 helper", fn: "emit", files: emit("Util2.toPath(k)", "", "Util2", H_PROP),
|
|
1561
|
+
expectMark: true, why: "`{ toPath: (n) => … }` 既不是方法声明也不是变量声明 —— 旧名录整片漏" },
|
|
1562
|
+
{ name: "正例·③ 嵌套对象的属性箭头", fn: "emit", files: emit("NS.path.toPath(k)", "", "NS", H_PROP),
|
|
1563
|
+
expectMark: true, why: "限定名要能沿 owner 链一路拼到最外层变量" },
|
|
1564
|
+
{ name: "正例·③ 属性赋值箭头", fn: "emit", files: emit("Patched.toPath(k)", "", "Patched", H_PROP),
|
|
1565
|
+
expectMark: true, why: "`Obj.method = (n) => …` 这种后挂写法同属一族" },
|
|
1566
|
+
{ name: "负对照·③ 属性箭头丢弃形参", fn: "emit", files: emit("DropProp.fixed(k)", "", "DropProp", H_PROP),
|
|
1567
|
+
expectMark: false, why: "召回放宽到新载体,C4f/C4h-① 的收窄要跟得上;正对照 = ③ 属性箭头 helper" },
|
|
1568
|
+
{ name: "负对照·③ 属性箭头喂字面量", fn: "emit", files: emit('Util2.toPath("lit")', "", "Util2", H_PROP),
|
|
1569
|
+
expectMark: false, why: "无污点流入" },
|
|
1570
|
+
// 缺陷回归门(旧行为:确证之前就把裸名登记进成员名录)
|
|
1571
|
+
{ name: "负对照·非塑形方法的成员调用位不算塑形", fn: "emit", files: emit("r.lookup(k)", "const r = new Reg();", "Reg", H_NONSHAPER),
|
|
1572
|
+
expectMark: false, why: "lookup 引用了模块级非字面量对象 ⇒ 三条判据不成立 ⇒ 成员调用位不许进名录" },
|
|
1573
|
+
{ name: "正对照·同一个名字换成塑形版", fn: "emit", files: emit("r.lookup(k)", "const r = new RegSafe();", "RegSafe", H_SHAPER_LOOKUP),
|
|
1574
|
+
expectMark: true, why: "与上一条镜像 ⇒ 不标是因为判据,不是因为这条路压根走不通" },
|
|
1575
|
+
];
|
|
1576
|
+
for (const c of cases) {
|
|
1577
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
1578
|
+
const dir = makeProject(c.files);
|
|
1579
|
+
try {
|
|
1580
|
+
const marks = marksFor(dir, c.fn);
|
|
1581
|
+
if (c.expectMark)
|
|
1582
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
1583
|
+
else
|
|
1584
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
1585
|
+
}
|
|
1586
|
+
finally {
|
|
1587
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1588
|
+
}
|
|
1589
|
+
}, 150000);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
// C4i(2026-09-21):六个缺口 —— 三个是精度、三个是召回载体
|
|
1593
|
+
// 精度 ① 短路运算符(|| / && / ?? 与三元同形,只是判据换成左操作数的真假)
|
|
1594
|
+
// ② 模板串内嵌的三元(${} 内部是独立表达式,折叠要能进去)
|
|
1595
|
+
// 召回 ③ 柯里化高阶 helper ④ namespace 内的函数声明
|
|
1596
|
+
// ⑤ rest 形参 ⑥ IIFE 定义 / 计算属性名
|
|
1597
|
+
// 每组都配「判不出 ⇒ 退回保守」与「helper 真丢弃 ⇒ 仍不标」两条镜像(R14/R15)。
|
|
1598
|
+
(0, vitest_1.describe)("C4i:helper 的又一批载体与又一批可判定分支", () => {
|
|
1599
|
+
const emit = (relExpr, pre = "", imports = "", helpers = "") => ({
|
|
1600
|
+
"helpers.ts": helpers,
|
|
1601
|
+
"it.ts": [
|
|
1602
|
+
'import * as fs from "fs";',
|
|
1603
|
+
...(imports ? [`import { ${imports} } from "./helpers";`] : []),
|
|
1604
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1605
|
+
...(pre ? [` ${pre}`] : []),
|
|
1606
|
+
" Object.keys(doc).forEach((k) => {",
|
|
1607
|
+
` const rel = ${relExpr};`,
|
|
1608
|
+
' fs.writeFileSync(outDir + "/" + rel, "x");',
|
|
1609
|
+
" });",
|
|
1610
|
+
"}",
|
|
1611
|
+
].join("\n"),
|
|
1612
|
+
});
|
|
1613
|
+
// ── ① 短路 + ② 模板串 ─────────────────────────────────────────────
|
|
1614
|
+
const H_FOLD = [
|
|
1615
|
+
'export const viaOr = (n: string, s: any): string => s || n + ".md";',
|
|
1616
|
+
'export const viaAnd = (n: string, s: any): string => s && n + ".md";',
|
|
1617
|
+
'export const viaNull = (n: string, s: any): string => s ?? n + ".md";',
|
|
1618
|
+
'export const viaTpl = (n: string, f: boolean): string => `${f ? "fixed" : n}.md`;',
|
|
1619
|
+
].join("\n");
|
|
1620
|
+
// ── ③ 柯里化 ─────────────────────────────────────────────────────
|
|
1621
|
+
const H_CURRY = [
|
|
1622
|
+
'export const withExt = (ext: string) => (n: string): string => n + ext;',
|
|
1623
|
+
'export const dropExt = (_ext: string) => (_n: string): string => "fixed.md";',
|
|
1624
|
+
'export function curriedFn(ext: string) { return (n: string): string => n + ext; }',
|
|
1625
|
+
].join("\n");
|
|
1626
|
+
// ── ④ namespace ──────────────────────────────────────────────────
|
|
1627
|
+
const H_NS = [
|
|
1628
|
+
'import * as fs from "fs";',
|
|
1629
|
+
"export namespace P {",
|
|
1630
|
+
" export function toPath(n: string): string { return n + '.md'; }",
|
|
1631
|
+
" export function dropPath(_n: string): string { return 'fixed.md'; }",
|
|
1632
|
+
"}",
|
|
1633
|
+
"export namespace L {",
|
|
1634
|
+
" export function leakPath(n: string): string { fs.readFileSync(n); return 'x'; }",
|
|
1635
|
+
"}",
|
|
1636
|
+
].join("\n");
|
|
1637
|
+
// ── ⑤ rest 形参 ──────────────────────────────────────────────────
|
|
1638
|
+
const H_REST = [
|
|
1639
|
+
'export function joinAll(...parts: string[]): string { return parts.join("/"); }',
|
|
1640
|
+
'export function fixedAll(..._parts: string[]): string { return "fixed.md"; }',
|
|
1641
|
+
].join("\n");
|
|
1642
|
+
// ── ⑥ IIFE 定义 ──────────────────────────────────────────────────
|
|
1643
|
+
const H_IIFE = [
|
|
1644
|
+
'import * as fs from "fs";',
|
|
1645
|
+
'export const toPathIife = (() => (n: string): string => n + ".md")();',
|
|
1646
|
+
'export const dropIife = (() => (_n: string): string => "fixed.md")();',
|
|
1647
|
+
'export const leakIife = (() => (n: string): string => { fs.readFileSync(n); return "x"; })();',
|
|
1648
|
+
].join("\n");
|
|
1649
|
+
// ── ⑦ 计算属性名(只有 KEY 是模块级字面量常量才解得出来)────────────
|
|
1650
|
+
const H_COMPUTED = [
|
|
1651
|
+
'const KEY = "toPath";',
|
|
1652
|
+
'const DROP = "toFile";',
|
|
1653
|
+
"export const Box: any = { [KEY]: (n: string): string => n + '.md' };",
|
|
1654
|
+
"export const DropBox: any = { [DROP]: (_n: string): string => 'fixed.md' };",
|
|
1655
|
+
].join("\n");
|
|
1656
|
+
// 刻意**单独一个文件**:成员调用位 `.toPath(` 的匹配与宿主无关(C4g 的保守策略),
|
|
1657
|
+
// 把它和上面那个可解的 Box 放进同一个文件,会被 Box 登记的成员名顺带命中 ——
|
|
1658
|
+
// 那条期望就得改成 mark,测的也就不是「算不出」了(2026-09-21 实测踩到)。
|
|
1659
|
+
const H_DYN = [
|
|
1660
|
+
"const RANDOM = String(Math.random());",
|
|
1661
|
+
"export const DynBox: any = { [RANDOM]: (n: string): string => n + '.md' };",
|
|
1662
|
+
].join("\n");
|
|
1663
|
+
const cases = [
|
|
1664
|
+
// ① 短路 —— 精度侧
|
|
1665
|
+
{ name: "正例·① ||左为假值 ⇒ 取右支", fn: "emit", files: emit('viaOr(k, "")', "", "viaOr", H_FOLD),
|
|
1666
|
+
expectMark: true, why: "空串 falsy ⇒ 真的会走到含 n 的那支" },
|
|
1667
|
+
{ name: "负对照·① ||左为真值 ⇒ 取左支", fn: "emit", files: emit('viaOr(k, "fixed.md")', "", "viaOr", H_FOLD),
|
|
1668
|
+
expectMark: false, why: "返回值就是左值常量;正对照 = 上一条" },
|
|
1669
|
+
{ name: "正例·① &&左为真值 ⇒ 取右支", fn: "emit", files: emit('viaAnd(k, "pre")', "", "viaAnd", H_FOLD),
|
|
1670
|
+
expectMark: true, why: "truthy ⇒ 走到形参那支" },
|
|
1671
|
+
{ name: "负对照·① &&左为假值 ⇒ 取左支", fn: "emit", files: emit('viaAnd(k, "")', "", "viaAnd", H_FOLD),
|
|
1672
|
+
expectMark: false, why: "短路在左值就结束了;正对照 = 上一条" },
|
|
1673
|
+
{ name: "负对照·① ??左非 null ⇒ 取左支", fn: "emit", files: emit('viaNull(k, "fixed.md")', "", "viaNull", H_FOLD),
|
|
1674
|
+
expectMark: false, why: "?? 的判据是「非空」而不是「真」,两种语义不能混" },
|
|
1675
|
+
{ name: "正例·① ??左为 null ⇒ 取右支", fn: "emit", files: emit("viaNull(k, null)", "", "viaNull", H_FOLD),
|
|
1676
|
+
expectMark: true, why: "与上一条镜像 ⇒ 归因确实落在 ?? 的判定上" },
|
|
1677
|
+
{ name: "保守侧·① 实参是变量 ⇒ 判不出就照标", fn: "emit", files: emit("viaOr(k, dyn)", "const dyn = Math.random() > 0;", "viaOr", H_FOLD),
|
|
1678
|
+
expectMark: true, why: "认不出就不收精度 —— 这条守的是保守侧的底线" },
|
|
1679
|
+
// ② 模板串 —— 精度侧
|
|
1680
|
+
{ name: "正例·② 模板串里走流出分支", fn: "emit", files: emit("viaTpl(k, false)", "", "viaTpl", H_FOLD),
|
|
1681
|
+
expectMark: true, why: "f=false ⇒ ${} 里取 n" },
|
|
1682
|
+
{ name: "负对照·② 模板串里走常量分支", fn: "emit", files: emit("viaTpl(k, true)", "", "viaTpl", H_FOLD),
|
|
1683
|
+
expectMark: false, why: "折叠必须能进 ${} 内部;正对照 = 上一条" },
|
|
1684
|
+
{ name: "保守侧·② 模板串的 flag 是变量", fn: "emit", files: emit("viaTpl(k, dyn)", "const dyn = Math.random() > 0;", "viaTpl", H_FOLD),
|
|
1685
|
+
expectMark: true, why: "判不出 ⇒ 退回保守" },
|
|
1686
|
+
// ③ 柯里化 —— 召回侧
|
|
1687
|
+
{ name: "正例·③ 柯里化·污点在第二跳", fn: "emit", files: emit('withExt(".md")(k)', "", "withExt", H_CURRY),
|
|
1688
|
+
expectMark: true, why: "真正塑形的是被返回出来的内层函数" },
|
|
1689
|
+
{ name: "正例·③ 柯里化·污点在第一跳", fn: "emit", files: emit('withExt(k)("name")', "", "withExt", H_CURRY),
|
|
1690
|
+
expectMark: true, why: "外层实参被闭包捕获,同样是流进去的" },
|
|
1691
|
+
{ name: "正例·③ 柯里化·函数声明写法", fn: "emit", files: emit('curriedFn(".md")(k)', "", "curriedFn", H_CURRY),
|
|
1692
|
+
expectMark: true, why: "return 箭头不只出现在箭头 helper 上" },
|
|
1693
|
+
{ name: "负对照·③ 柯里化·内层丢弃形参", fn: "emit", files: emit('dropExt(".md")(k)', "", "dropExt", H_CURRY),
|
|
1694
|
+
expectMark: false, why: "召回放宽到新载体,收窄要跟得上;正对照 = ③ 污点在第二跳" },
|
|
1695
|
+
{ name: "负对照·③ 柯里化·两边都是常量", fn: "emit", files: emit('withExt(".md")("fixed")', "", "withExt", H_CURRY),
|
|
1696
|
+
expectMark: false, why: "根本没有污点进来" },
|
|
1697
|
+
// ④ namespace —— 召回侧
|
|
1698
|
+
{ name: "正例·④ namespace 内的 helper", fn: "emit", files: emit("P.toPath(k)", "", "P", H_NS),
|
|
1699
|
+
expectMark: true, why: "旧收集只走 sf.getFunctions() ⇒ namespace 里整片漏" },
|
|
1700
|
+
{ name: "负对照·④ namespace 内丢弃形参", fn: "emit", files: emit("P.dropPath(k)", "", "P", H_NS),
|
|
1701
|
+
expectMark: false, why: "限定名也要走依赖分析;正对照 = 上一条" },
|
|
1702
|
+
{ name: "负对照·④ namespace 内有 sink", fn: "emit", files: emit("L.leakPath(k)", "", "L", H_NS),
|
|
1703
|
+
expectMark: false, why: "判据③照旧生效 —— 载体换了不等于放水" },
|
|
1704
|
+
// ⑤ rest —— 召回侧
|
|
1705
|
+
{ name: "正例·⑤ rest 形参吃掉剩余实参", fn: "emit", files: emit('joinAll("out", k)', "", "joinAll", H_REST),
|
|
1706
|
+
expectMark: true, why: "rest 位是**一批**实参,按位截断会把第二位当成越界抹掉" },
|
|
1707
|
+
{ name: "负对照·⑤ rest 但丢弃", fn: "emit", files: emit('fixedAll("out", k)', "", "fixedAll", H_REST),
|
|
1708
|
+
expectMark: false, why: "召回放宽后精度不许丢;正对照 = 上一条" },
|
|
1709
|
+
// ⑥ IIFE —— 召回侧
|
|
1710
|
+
{ name: "正例·⑥ IIFE 定义 helper", fn: "emit", files: emit("toPathIife(k)", "", "toPathIife", H_IIFE),
|
|
1711
|
+
expectMark: true, why: "初值是**调用**不是箭头 —— 真正的 helper 在返回值里" },
|
|
1712
|
+
{ name: "负对照·⑥ IIFE 内丢弃形参", fn: "emit", files: emit("dropIife(k)", "", "dropIife", H_IIFE),
|
|
1713
|
+
expectMark: false, why: "收窄要跟得上;正对照 = 上一条" },
|
|
1714
|
+
{ name: "负对照·⑥ IIFE 体内有 sink", fn: "emit", files: emit("leakIife(k)", "", "leakIife", H_IIFE),
|
|
1715
|
+
expectMark: false, why: "declText 取的是整条 IIFE ⇒ sink 检查仍覆盖得到" },
|
|
1716
|
+
// ⑦ 计算属性名 —— 召回侧
|
|
1717
|
+
{ name: "正例·⑦ 计算属性名可解", fn: "emit", files: emit("Box.toPath(k)", "", "Box", H_COMPUTED),
|
|
1718
|
+
expectMark: true, why: "KEY 是模块级字面量常量 ⇒ 名字解得出来" },
|
|
1719
|
+
{ name: "负对照·⑦ 计算属性名里的 helper 丢弃形参", fn: "emit", files: emit("DropBox.toFile(k)", "", "DropBox", H_COMPUTED),
|
|
1720
|
+
expectMark: false, why: "收窄要跟得上;正对照 = 上一条" },
|
|
1721
|
+
{ name: "负对照·⑦ 属性名算不出来", fn: "emit", files: emit("DynBox.toPath(k)", "", "DynBox", H_DYN),
|
|
1722
|
+
expectMark: false, why: "`String(Math.random())` 不是常量 ⇒ 不登记(认不出就不传播)" },
|
|
1723
|
+
];
|
|
1724
|
+
for (const c of cases) {
|
|
1725
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
1726
|
+
const dir = makeProject(c.files);
|
|
1727
|
+
try {
|
|
1728
|
+
const marks = marksFor(dir, c.fn);
|
|
1729
|
+
if (c.expectMark)
|
|
1730
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
1731
|
+
else
|
|
1732
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
1733
|
+
}
|
|
1734
|
+
finally {
|
|
1735
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1736
|
+
}
|
|
1737
|
+
}, 150000);
|
|
1738
|
+
}
|
|
1739
|
+
});
|