progmune-runtime 3.7.37 → 3.7.41
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 +249 -0
- package/dist/extract-ir-taint-guard.test.js +53 -0
- package/dist/extract-ir-taint-structural.test.js +874 -3
- package/dist/extract-ir.js +441 -27
- package/dist/sdk.js +1 -1
- package/package.json +1 -1
|
@@ -401,7 +401,10 @@ export function seedFlow() {
|
|
|
401
401
|
why: "正对照 = readJoin(把 \"index.html\" 换成 req.params.name 必须报出)",
|
|
402
402
|
},
|
|
403
403
|
{
|
|
404
|
-
|
|
404
|
+
// 2026-09-20 由负对照翻正:C4b 上线后,helper 的函数体可证明为纯塑形
|
|
405
|
+
// (无调用、自由标识符只有形参 name)⇒ 污点穿过它。此前这条是登记在册的
|
|
406
|
+
// 缺口 C4b,翻正说明缺口闭合,不是断言写错。
|
|
407
|
+
name: "helper 可证明为纯塑形 ⇒ 传播(C4b,3.7.39)",
|
|
405
408
|
fn: "readUnknownHelper",
|
|
406
409
|
code: `function buildPath(name: string): string {
|
|
407
410
|
return "/data/" + name;
|
|
@@ -410,8 +413,8 @@ export function readUnknownHelper(req: any) {
|
|
|
410
413
|
const target = buildPath(req.params.name);
|
|
411
414
|
return fs.readFileSync(target, "utf-8");
|
|
412
415
|
}`,
|
|
413
|
-
expectMark:
|
|
414
|
-
why: "
|
|
416
|
+
expectMark: true,
|
|
417
|
+
why: "判据三条全中(有形参 / 有带表达式的 return / 体内无 sink / 自由标识符只有形参);负对照 = readSanitized",
|
|
415
418
|
},
|
|
416
419
|
{
|
|
417
420
|
name: "负对照:净化函数不传播",
|
|
@@ -443,3 +446,871 @@ export function readSanitized(req: any) {
|
|
|
443
446
|
}, 150000);
|
|
444
447
|
}
|
|
445
448
|
});
|
|
449
|
+
// ═══════════════════════════════════════════════════════════════
|
|
450
|
+
// fr-016 两处断点(2026-09-19,3.7.38)
|
|
451
|
+
//
|
|
452
|
+
// 真实修复语料 fr-016(Redocly/redocly-cli split 命令)在连续三轮票据
|
|
453
|
+
// (C4 塑形传播 → G2 调用点抑制 → fr-016)之后仍然 pre 0 / post 0。
|
|
454
|
+
// 逐点拆开才发现流量断在**两个独立缺陷**上,两侧对称:
|
|
455
|
+
// ① 来源侧:污点根表里没有「外部文档」。文档本体由上游 parseYaml 解析好后
|
|
456
|
+
// 作为形参传入(`channels: Record<string, any>`),函数体内唯一可见的入口
|
|
457
|
+
// 就是 `for (const channelName of Object.keys(channels))` 这次枚举。
|
|
458
|
+
// ② sink 侧:落盘必经 `writeToFileByExtension → writeYaml → fs.writeFileSync`
|
|
459
|
+
// 两层自有封装,而 methodSinkParamMap 只做「形参 → 本函数体内 fs sink」
|
|
460
|
+
// 的一跳登记,整层 wrapper 从未入表。
|
|
461
|
+
// 只补一侧语料纹丝不动——这也是为什么要把它们放在同一个 describe 里。
|
|
462
|
+
//
|
|
463
|
+
// 方法学提醒(R6):每条负向断言都必须配正对照。本组把「记」
|
|
464
|
+
// 写在 why 里,避免后人把漏报当成通过。
|
|
465
|
+
// ═══════════════════════════════════════════════════════════════
|
|
466
|
+
/** 真实工程的落盘封装层:写文件的是最里面一跳,外层只是转发 */
|
|
467
|
+
const WRITE_LAYER = [
|
|
468
|
+
'import * as fs from "fs";',
|
|
469
|
+
"export function writeYaml(data: any, filename: string) {",
|
|
470
|
+
' fs.writeFileSync(filename, "x");',
|
|
471
|
+
"}",
|
|
472
|
+
"export function writeToFileByExtension(data: unknown, filePath: string) {",
|
|
473
|
+
" writeYaml(data, filePath);",
|
|
474
|
+
"}",
|
|
475
|
+
].join("\n");
|
|
476
|
+
(0, vitest_1.describe)("fr-016:文档解析产物根 + sink 形参继承(两侧对称,缺一不可)", () => {
|
|
477
|
+
const cases = [
|
|
478
|
+
{
|
|
479
|
+
name: "正例·fr-016 形态(两侧都在位)",
|
|
480
|
+
fn: "iterate",
|
|
481
|
+
files: {
|
|
482
|
+
"misc.ts": WRITE_LAYER,
|
|
483
|
+
"it.ts": [
|
|
484
|
+
'import * as path from "path";',
|
|
485
|
+
'import { writeToFileByExtension } from "./misc";',
|
|
486
|
+
"export function iterate(channels: Record<string, any>, outDir: string) {",
|
|
487
|
+
" for (const n of Object.keys(channels)) {",
|
|
488
|
+
' const f = `${outDir}/${n}.yaml`;',
|
|
489
|
+
" writeToFileByExtension(channels[n], f);",
|
|
490
|
+
" }",
|
|
491
|
+
"}",
|
|
492
|
+
].join("\n"),
|
|
493
|
+
},
|
|
494
|
+
expectMark: true,
|
|
495
|
+
why: "Object.keys 枚举(根)+ 模板字面量(C4)+ wrapper 继承 sink 位(新)—— 三环齐全才动",
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "负对照·键列表是字面量",
|
|
499
|
+
fn: "iterate",
|
|
500
|
+
files: {
|
|
501
|
+
"misc.ts": WRITE_LAYER,
|
|
502
|
+
"it.ts": [
|
|
503
|
+
'import { writeToFileByExtension } from "./misc";',
|
|
504
|
+
"export function iterate(outDir: string) {",
|
|
505
|
+
' const names = ["a", "b", "c"];',
|
|
506
|
+
" for (const n of names) {",
|
|
507
|
+
' const f = `${outDir}/${n}.yaml`;',
|
|
508
|
+
" writeToFileByExtension({}, f);",
|
|
509
|
+
" }",
|
|
510
|
+
"}",
|
|
511
|
+
].join("\n"),
|
|
512
|
+
},
|
|
513
|
+
expectMark: false,
|
|
514
|
+
why: "名字由代码写死,不是运行时数据结构的产物 —— 根必须认得出「数据来源」这一点",
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "正例·Object.entries 解构绑定",
|
|
518
|
+
fn: "iterate",
|
|
519
|
+
files: {
|
|
520
|
+
"misc.ts": WRITE_LAYER,
|
|
521
|
+
"it.ts": [
|
|
522
|
+
'import { writeToFileByExtension } from "./misc";',
|
|
523
|
+
"export function iterate(doc: Record<string, any>, outDir: string) {",
|
|
524
|
+
" for (const [name, value] of Object.entries(doc)) {",
|
|
525
|
+
' const f = `${outDir}/${name}.yaml`;',
|
|
526
|
+
" writeToFileByExtension(value, f);",
|
|
527
|
+
" }",
|
|
528
|
+
"}",
|
|
529
|
+
].join("\n"),
|
|
530
|
+
},
|
|
531
|
+
expectMark: true,
|
|
532
|
+
why: "for-of 的解构绑定也要收进污点集合,不能只认单个标识符",
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: "正例·JSON.parse 产物进 sink",
|
|
536
|
+
fn: "emit",
|
|
537
|
+
files: {
|
|
538
|
+
"it.ts": [
|
|
539
|
+
'import * as fs from "fs";',
|
|
540
|
+
"export function emit(text: string, outDir: string) {",
|
|
541
|
+
" const cfg = JSON.parse(text);",
|
|
542
|
+
' fs.writeFileSync(outDir + "/" + cfg.name, "x");',
|
|
543
|
+
"}",
|
|
544
|
+
].join("\n"),
|
|
545
|
+
},
|
|
546
|
+
expectMark: true,
|
|
547
|
+
why: "外部文档解析产物:值的内容由被解析输入决定,这是「传输面」而不是变量名猜测",
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "负对照·常量配置不经解析",
|
|
551
|
+
fn: "emit",
|
|
552
|
+
files: {
|
|
553
|
+
"it.ts": [
|
|
554
|
+
'import * as fs from "fs";',
|
|
555
|
+
'const DEFAULT = { name: "out" };',
|
|
556
|
+
"export function emit(outDir: string) {",
|
|
557
|
+
" const cfg = DEFAULT;",
|
|
558
|
+
' fs.writeFileSync(outDir + "/" + cfg.name, "x");',
|
|
559
|
+
"}",
|
|
560
|
+
].join("\n"),
|
|
561
|
+
},
|
|
562
|
+
expectMark: false,
|
|
563
|
+
why: "同形状但没有解析调用 —— 证明上一个用例不是「见到 cfg 就标记」",
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: "负对照·helper 不写文件 ⇒ 不继承 sink",
|
|
567
|
+
fn: "iterate",
|
|
568
|
+
files: {
|
|
569
|
+
"misc.ts": [
|
|
570
|
+
"export function touchPath(p: string) { return p.length; }",
|
|
571
|
+
].join("\n"),
|
|
572
|
+
"it.ts": [
|
|
573
|
+
'import { touchPath } from "./misc";',
|
|
574
|
+
"export function iterate(doc: Record<string, any>, outDir: string) {",
|
|
575
|
+
" for (const n of Object.keys(doc)) {",
|
|
576
|
+
' const f = `${outDir}/${n}.yaml`;',
|
|
577
|
+
" touchPath(f);",
|
|
578
|
+
" }",
|
|
579
|
+
"}",
|
|
580
|
+
].join("\n"),
|
|
581
|
+
},
|
|
582
|
+
expectMark: false,
|
|
583
|
+
why: "sink 形参继承必须真的落到 fs sink —— 只是被传进某个 helper 不构成继承",
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
name: "负对照·形参落在非 sink 位 ⇒ 不继承",
|
|
587
|
+
fn: "iterate",
|
|
588
|
+
files: {
|
|
589
|
+
"misc.ts": [
|
|
590
|
+
'import * as fs from "fs";',
|
|
591
|
+
"export function writeYaml(data: any, filename: string) {",
|
|
592
|
+
' fs.writeFileSync(filename, "x");',
|
|
593
|
+
"}",
|
|
594
|
+
"export function writeFrom(name: string, data: unknown) {",
|
|
595
|
+
' writeYaml(data, "/fixed/out.yaml");',
|
|
596
|
+
"}",
|
|
597
|
+
].join("\n"),
|
|
598
|
+
"it.ts": [
|
|
599
|
+
'import { writeFrom } from "./misc";',
|
|
600
|
+
"export function iterate(doc: Record<string, any>) {",
|
|
601
|
+
" for (const n of Object.keys(doc)) {",
|
|
602
|
+
" writeFrom(n, {});",
|
|
603
|
+
" }",
|
|
604
|
+
"}",
|
|
605
|
+
].join("\n"),
|
|
606
|
+
},
|
|
607
|
+
expectMark: false,
|
|
608
|
+
why: "writeFrom 的 name 被传进 writeYaml 的【非 sink 位】(第 0 位)⇒ 不该继承第 1 位的 sink",
|
|
609
|
+
},
|
|
610
|
+
];
|
|
611
|
+
for (const c of cases) {
|
|
612
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
613
|
+
const dir = makeProject(c.files);
|
|
614
|
+
try {
|
|
615
|
+
const marks = marksFor(dir, c.fn);
|
|
616
|
+
if (c.expectMark)
|
|
617
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
618
|
+
else
|
|
619
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
623
|
+
}
|
|
624
|
+
}, 150000);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
// ═══════════════════════════════════════════════════════════════
|
|
628
|
+
// C4b / C4c(2026-09-20,3.7.39)
|
|
629
|
+
//
|
|
630
|
+
// C4b:项目自有的「纯塑形」helper 此前不传播污点 —— fr-016 因此少两条真阳性:
|
|
631
|
+
// const filename = getFileNamePath(dir, componentName, ext); // 污点断在这一跳
|
|
632
|
+
// writeToFileByExtension(data, filename);
|
|
633
|
+
// // getFileNamePath(a, b, c) { return path.join(a, b) + `.${c}`; }
|
|
634
|
+
// C4 的塑形词表是 node:path + String 原型方法的【固定清单】,不可能覆盖每个
|
|
635
|
+
// 项目自己的封装。修法不是放宽词表(那是打地鼠),而是**按函数体证明它只做
|
|
636
|
+
// 塑形**:有形参、有带表达式的 return、体内无文件 sink、return 里所有调用都是
|
|
637
|
+
// 塑形调用且所有自由标识符都是形参 —— 三条全中才传播,认不出就留着漏报。
|
|
638
|
+
//
|
|
639
|
+
// C4c:`const ks = Object.keys(o); for (const k of ks)` —— 被迭代对象已经是
|
|
640
|
+
// 【被污染的变量】,不再是根表达式。枚举绑定此前只按根匹配,于是这种
|
|
641
|
+
// 「先收集、再迭代」的写法一根都收不到。
|
|
642
|
+
//
|
|
643
|
+
// 方向提醒:传播是**放宽召回**,与 G2 的抑制方向相反,所以判据要严。
|
|
644
|
+
// 每条负向断言都配正对照(R6)—— 否则「不标记」可能是没跑通而不是判对了。
|
|
645
|
+
// ═══════════════════════════════════════════════════════════════
|
|
646
|
+
(0, vitest_1.describe)("C4b:项目自有纯塑形 helper 的传播", () => {
|
|
647
|
+
const cases = [
|
|
648
|
+
{
|
|
649
|
+
name: "正例·helper 只做塑形",
|
|
650
|
+
fn: "emit",
|
|
651
|
+
files: {
|
|
652
|
+
"misc.ts": [
|
|
653
|
+
'import * as path from "path";',
|
|
654
|
+
"export function getFileNamePath(dir: string, name: string, ext: string) {",
|
|
655
|
+
' return path.join(dir, name) + `.${ext}`;',
|
|
656
|
+
"}",
|
|
657
|
+
].join("\n"),
|
|
658
|
+
"it.ts": [
|
|
659
|
+
'import * as fs from "fs";',
|
|
660
|
+
'import { getFileNamePath } from "./misc";',
|
|
661
|
+
"export function emit(doc: Record<string, any>, outDir: string, ext: string) {",
|
|
662
|
+
" for (const name of Object.keys(doc)) {",
|
|
663
|
+
" const filename = getFileNamePath(outDir, name, ext);",
|
|
664
|
+
' fs.writeFileSync(filename, "x");',
|
|
665
|
+
" }",
|
|
666
|
+
"}",
|
|
667
|
+
].join("\n"),
|
|
668
|
+
},
|
|
669
|
+
expectMark: true,
|
|
670
|
+
why: "path.join(dir,name) + 模板串:调用与自由标识符全在白名单内 ⇒ 认定为纯塑形,污点穿过它",
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: "负对照·同形状但实参是字面量",
|
|
674
|
+
fn: "emit",
|
|
675
|
+
files: {
|
|
676
|
+
"misc.ts": [
|
|
677
|
+
'import * as path from "path";',
|
|
678
|
+
"export function getFileNamePath(dir: string, name: string, ext: string) {",
|
|
679
|
+
' return path.join(dir, name) + `.${ext}`;',
|
|
680
|
+
"}",
|
|
681
|
+
].join("\n"),
|
|
682
|
+
"it.ts": [
|
|
683
|
+
'import * as fs from "fs";',
|
|
684
|
+
'import { getFileNamePath } from "./misc";',
|
|
685
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
686
|
+
" const names = Object.keys(doc);",
|
|
687
|
+
" if (names.length === 0) return;",
|
|
688
|
+
' const filename = getFileNamePath(outDir, "fixed", "yaml");',
|
|
689
|
+
' fs.writeFileSync(filename, "x");',
|
|
690
|
+
"}",
|
|
691
|
+
].join("\n"),
|
|
692
|
+
},
|
|
693
|
+
expectMark: false,
|
|
694
|
+
why: "根在(names)、helper 也在(证明门是开的),但传进去的是字面量 —— 不是「见到 helper 就标记」",
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
name: "正例·helper 的 return 引用模块级字面量常量(原判据③缺口,C4e 闭合)",
|
|
698
|
+
fn: "emit",
|
|
699
|
+
files: {
|
|
700
|
+
"misc.ts": [
|
|
701
|
+
'const FIXED = "/safe";',
|
|
702
|
+
"export function getFileNamePath(dir: string, name: string) {",
|
|
703
|
+
" return FIXED + name;",
|
|
704
|
+
"}",
|
|
705
|
+
].join("\n"),
|
|
706
|
+
"it.ts": [
|
|
707
|
+
'import * as fs from "fs";',
|
|
708
|
+
'import { getFileNamePath } from "./misc";',
|
|
709
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
710
|
+
" for (const name of Object.keys(doc)) {",
|
|
711
|
+
" const filename = getFileNamePath(outDir, name);",
|
|
712
|
+
' fs.writeFileSync(filename, "x");',
|
|
713
|
+
" }",
|
|
714
|
+
"}",
|
|
715
|
+
].join("\n"),
|
|
716
|
+
},
|
|
717
|
+
expectMark: true,
|
|
718
|
+
why: "原判据③的【保守缺口】,2026-09-20 C4e 闭合:初值是【字面量】的模块级常量放行 —— 出处可确证。注意这里放任必须有界:见下一条「非字面量模块常量」",
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
// 上一条把按钮松了一格,这条负责钉死松到哪一格为止:常量初值若是【任意表达式】,
|
|
722
|
+
// 就退回到「看不见 ⇒ 拦死」。否则 helper 能把别处的污点藏在一个模块常量后面
|
|
723
|
+
// (跨函数常量摘要一旦放行非字面量,等于给任意数据流发了通行证)。
|
|
724
|
+
name: "负对照·helper 的 return 引用非字面量模块常量",
|
|
725
|
+
fn: "emit",
|
|
726
|
+
files: {
|
|
727
|
+
"misc.ts": [
|
|
728
|
+
'import * as path from "path";',
|
|
729
|
+
'const ROOT = path.resolve(process.cwd(), "runs");',
|
|
730
|
+
"export function getFileNamePath(dir: string, name: string) {",
|
|
731
|
+
" return ROOT + name;",
|
|
732
|
+
"}",
|
|
733
|
+
].join("\n"),
|
|
734
|
+
"it.ts": [
|
|
735
|
+
'import * as fs from "fs";',
|
|
736
|
+
'import { getFileNamePath } from "./misc";',
|
|
737
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
738
|
+
" for (const name of Object.keys(doc)) {",
|
|
739
|
+
" const filename = getFileNamePath(outDir, name);",
|
|
740
|
+
' fs.writeFileSync(filename, "x");',
|
|
741
|
+
" }",
|
|
742
|
+
"}",
|
|
743
|
+
].join("\n"),
|
|
744
|
+
},
|
|
745
|
+
expectMark: false,
|
|
746
|
+
why: "ROOT 的初值是表达式不是字面量 ⇒ 仍按判据③拦死;正对照 = 上一条(同名 fixtures,只有常量初值不同)",
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
// 本条锁的是 C4b 的【收窄决定】:字符过滤不算「纯塑形」的证据。
|
|
750
|
+
// C4 的内联规则里 `.replace` 是算塑形的,但 helper 形式看不见实参窗口,
|
|
751
|
+
// 判据必须更严 —— `.replace(/[^a-z0-9]/gi, "")` 就是净化函数的标准写法,
|
|
752
|
+
// 把它判成「确定不净化」正是 C4 用白名单避开的那类错误。
|
|
753
|
+
name: "负对照·helper 体内是字符过滤 ⇒ 不算纯塑形",
|
|
754
|
+
fn: "emit",
|
|
755
|
+
files: {
|
|
756
|
+
"misc.ts": [
|
|
757
|
+
"export function toSlug(p: string) {",
|
|
758
|
+
' return p.replace(/[^a-z0-9]/gi, "").toLowerCase();',
|
|
759
|
+
"}",
|
|
760
|
+
].join("\n"),
|
|
761
|
+
"it.ts": [
|
|
762
|
+
'import * as fs from "fs";',
|
|
763
|
+
'import { toSlug } from "./misc";',
|
|
764
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
765
|
+
" for (const name of Object.keys(doc)) {",
|
|
766
|
+
" const filename = toSlug(name);",
|
|
767
|
+
' fs.writeFileSync(outDir + "/" + filename, "x");',
|
|
768
|
+
" }",
|
|
769
|
+
].join("\n"),
|
|
770
|
+
},
|
|
771
|
+
expectMark: false,
|
|
772
|
+
why: "字符过滤是净化函数的常见实现,不能作为「只做塑形」的证据;正对照 = 上一条 path.join 形态必须报出",
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
name: "负对照·helper 体内有文件 sink ⇒ 不是纯塑形",
|
|
776
|
+
fn: "emit",
|
|
777
|
+
files: {
|
|
778
|
+
"misc.ts": [
|
|
779
|
+
'import * as fs from "fs";',
|
|
780
|
+
'import * as path from "path";',
|
|
781
|
+
"export function getFileNamePath(dir: string, name: string) {",
|
|
782
|
+
' fs.mkdirSync(dir, { recursive: true });',
|
|
783
|
+
" return path.join(dir, name);",
|
|
784
|
+
"}",
|
|
785
|
+
].join("\n"),
|
|
786
|
+
"it.ts": [
|
|
787
|
+
'import * as fs from "fs";',
|
|
788
|
+
'import { getFileNamePath } from "./misc";',
|
|
789
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
790
|
+
" for (const name of Object.keys(doc)) {",
|
|
791
|
+
" const filename = getFileNamePath(outDir, name);",
|
|
792
|
+
' fs.writeFileSync(filename, "x");',
|
|
793
|
+
" }",
|
|
794
|
+
"}",
|
|
795
|
+
].join("\n"),
|
|
796
|
+
},
|
|
797
|
+
expectMark: false,
|
|
798
|
+
why: "判据②:含 sink 说明它不只是塑形。副作用型 helper 不参与传播(避免把副作用函数当管道)",
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: "正例·helper 调 helper(两跳,验不动点第二轮)",
|
|
802
|
+
fn: "emit",
|
|
803
|
+
files: {
|
|
804
|
+
"misc.ts": [
|
|
805
|
+
'import * as path from "path";',
|
|
806
|
+
"export function withExt(p: string, ext: string) {",
|
|
807
|
+
' return p + `.${ext}`;',
|
|
808
|
+
"}",
|
|
809
|
+
"export function getFileNamePath(dir: string, name: string, ext: string) {",
|
|
810
|
+
" return withExt(path.join(dir, name), ext);",
|
|
811
|
+
"}",
|
|
812
|
+
].join("\n"),
|
|
813
|
+
"it.ts": [
|
|
814
|
+
'import * as fs from "fs";',
|
|
815
|
+
'import { getFileNamePath } from "./misc";',
|
|
816
|
+
"export function emit(doc: Record<string, any>, outDir: string, ext: string) {",
|
|
817
|
+
" for (const name of Object.keys(doc)) {",
|
|
818
|
+
" const filename = getFileNamePath(outDir, name, ext);",
|
|
819
|
+
' fs.writeFileSync(filename, "x");',
|
|
820
|
+
" }",
|
|
821
|
+
"}",
|
|
822
|
+
].join("\n"),
|
|
823
|
+
},
|
|
824
|
+
expectMark: true,
|
|
825
|
+
why: "withExt 先被认定(它只用到 p 与 ext),第二轮才轮到 getFileNamePath —— 有界两轮,不追环",
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
name: "负对照·helper 没有 return ⇒ 认不出",
|
|
829
|
+
fn: "emit",
|
|
830
|
+
files: {
|
|
831
|
+
"misc.ts": [
|
|
832
|
+
'import * as path from "path";',
|
|
833
|
+
"export function getFileNamePath(dir: string, name: string): void {",
|
|
834
|
+
" path.join(dir, name);",
|
|
835
|
+
"}",
|
|
836
|
+
].join("\n"),
|
|
837
|
+
"it.ts": [
|
|
838
|
+
'import * as fs from "fs";',
|
|
839
|
+
'import { getFileNamePath } from "./misc";',
|
|
840
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
841
|
+
" for (const name of Object.keys(doc)) {",
|
|
842
|
+
" const filename = path.join(outDir, name);",
|
|
843
|
+
" getFileNamePath(outDir, name);",
|
|
844
|
+
' fs.writeFileSync(filename, "x");',
|
|
845
|
+
" }",
|
|
846
|
+
"}",
|
|
847
|
+
].join("\n"),
|
|
848
|
+
},
|
|
849
|
+
expectMark: true,
|
|
850
|
+
why: "正对照:helper 认不出(无 return)不影响本函数自己的 path.join 传播 —— 门本身是开的",
|
|
851
|
+
},
|
|
852
|
+
];
|
|
853
|
+
for (const c of cases) {
|
|
854
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
855
|
+
const dir = makeProject(c.files);
|
|
856
|
+
try {
|
|
857
|
+
const marks = marksFor(dir, c.fn);
|
|
858
|
+
if (c.expectMark)
|
|
859
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
860
|
+
else
|
|
861
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
862
|
+
}
|
|
863
|
+
finally {
|
|
864
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
865
|
+
}
|
|
866
|
+
}, 150000);
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
(0, vitest_1.describe)("C4c:迭代已被污染的聚合", () => {
|
|
870
|
+
const cases = [
|
|
871
|
+
{
|
|
872
|
+
name: "正例·先收集再迭代",
|
|
873
|
+
fn: "emit",
|
|
874
|
+
files: {
|
|
875
|
+
"it.ts": [
|
|
876
|
+
'import * as fs from "fs";',
|
|
877
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
878
|
+
" const ks = Object.keys(doc);",
|
|
879
|
+
" for (const k of ks) {",
|
|
880
|
+
' fs.writeFileSync(outDir + "/" + k, "x");',
|
|
881
|
+
" }",
|
|
882
|
+
"}",
|
|
883
|
+
].join("\n"),
|
|
884
|
+
},
|
|
885
|
+
expectMark: true,
|
|
886
|
+
why: "被迭代对象 ks 是【已被污染的变量】而非根表达式 —— 此前枚举绑定只按根匹配,这种写法一根都收不到",
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
name: "正对照·直接迭代根(已有能力不退化)",
|
|
890
|
+
fn: "emit",
|
|
891
|
+
files: {
|
|
892
|
+
"it.ts": [
|
|
893
|
+
'import * as fs from "fs";',
|
|
894
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
895
|
+
" for (const k of Object.keys(doc)) {",
|
|
896
|
+
' fs.writeFileSync(outDir + "/" + k, "x");',
|
|
897
|
+
" }",
|
|
898
|
+
"}",
|
|
899
|
+
].join("\n"),
|
|
900
|
+
},
|
|
901
|
+
expectMark: true,
|
|
902
|
+
why: "fr-016 原始形态,确认补 C4c 没有把它改坏",
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
name: "负对照·聚合本身无污点",
|
|
906
|
+
fn: "emit",
|
|
907
|
+
files: {
|
|
908
|
+
"it.ts": [
|
|
909
|
+
'import * as fs from "fs";',
|
|
910
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
911
|
+
" const all = Object.keys(doc);",
|
|
912
|
+
" if (all.length === 0) return;",
|
|
913
|
+
' const ks = ["a", "b"];',
|
|
914
|
+
" for (const k of ks) {",
|
|
915
|
+
' fs.writeFileSync(outDir + "/" + k, "x");',
|
|
916
|
+
" }",
|
|
917
|
+
"}",
|
|
918
|
+
].join("\n"),
|
|
919
|
+
},
|
|
920
|
+
expectMark: false,
|
|
921
|
+
why: "根在(all 已被污染)、sink 也在,但迭代的是字面量数组 —— 证明绑定看的是【被迭代对象是否污】,不是「函数里有污点就标」",
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
name: "正例·Object.values 收集后迭代",
|
|
925
|
+
fn: "emit",
|
|
926
|
+
files: {
|
|
927
|
+
"it.ts": [
|
|
928
|
+
'import * as fs from "fs";',
|
|
929
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
930
|
+
" const vs = Object.values(doc);",
|
|
931
|
+
" for (const v of vs) {",
|
|
932
|
+
' fs.writeFileSync(outDir + "/" + v.name, "x");',
|
|
933
|
+
" }",
|
|
934
|
+
"}",
|
|
935
|
+
].join("\n"),
|
|
936
|
+
},
|
|
937
|
+
expectMark: true,
|
|
938
|
+
why: "values 与 keys 同族,成员取值(v.name)也要能带出去",
|
|
939
|
+
},
|
|
940
|
+
];
|
|
941
|
+
for (const c of cases) {
|
|
942
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
943
|
+
const dir = makeProject(c.files);
|
|
944
|
+
try {
|
|
945
|
+
const marks = marksFor(dir, c.fn);
|
|
946
|
+
if (c.expectMark)
|
|
947
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
948
|
+
else
|
|
949
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
950
|
+
}
|
|
951
|
+
finally {
|
|
952
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
953
|
+
}
|
|
954
|
+
}, 150000);
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
(0, vitest_1.describe)("C4d:高阶枚举方法的回调形参必须绑定", () => {
|
|
958
|
+
const cases = [
|
|
959
|
+
{
|
|
960
|
+
name: "正例·Object.keys(doc).forEach",
|
|
961
|
+
fn: "emit",
|
|
962
|
+
files: {
|
|
963
|
+
"it.ts": [
|
|
964
|
+
'import * as fs from "fs";',
|
|
965
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
966
|
+
" Object.keys(doc).forEach((k) => {",
|
|
967
|
+
' fs.writeFileSync(outDir + "/" + k, "x");',
|
|
968
|
+
" });",
|
|
969
|
+
"}",
|
|
970
|
+
].join("\n"),
|
|
971
|
+
},
|
|
972
|
+
expectMark: true,
|
|
973
|
+
why: "回调形参 = 被枚举到的元素,与 for-of 语义等价 —— 此前只绑 for-of/for-in,这种写法整片漏",
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
name: "正例·map 回调 + entries 解构形参",
|
|
977
|
+
fn: "emit",
|
|
978
|
+
files: {
|
|
979
|
+
"it.ts": [
|
|
980
|
+
'import * as fs from "fs";',
|
|
981
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
982
|
+
" Object.entries(doc).map(([k, v]) => {",
|
|
983
|
+
' fs.writeFileSync(outDir + "/" + k, String(v));',
|
|
984
|
+
" return k;",
|
|
985
|
+
" });",
|
|
986
|
+
"}",
|
|
987
|
+
].join("\n"),
|
|
988
|
+
},
|
|
989
|
+
expectMark: true,
|
|
990
|
+
why: "[k, v] 解构的两个名字都是元素,都要绑(只绑一个会让 entries 形态半残)",
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
name: "正例·先收集再 forEach(C4c × C4d)",
|
|
994
|
+
fn: "emit",
|
|
995
|
+
files: {
|
|
996
|
+
"it.ts": [
|
|
997
|
+
'import * as fs from "fs";',
|
|
998
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
999
|
+
" const ks = Object.keys(doc);",
|
|
1000
|
+
" ks.forEach((k) => {",
|
|
1001
|
+
' fs.writeFileSync(outDir + "/" + k, "x");',
|
|
1002
|
+
" });",
|
|
1003
|
+
"}",
|
|
1004
|
+
].join("\n"),
|
|
1005
|
+
},
|
|
1006
|
+
expectMark: true,
|
|
1007
|
+
why: "接收者是被污染的变量而非根表达式 —— 必须走不动点的变量形态,只按根匹配收不到",
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
name: "正例·async 回调(Promise.all + map)",
|
|
1011
|
+
fn: "emit",
|
|
1012
|
+
files: {
|
|
1013
|
+
"it.ts": [
|
|
1014
|
+
'import * as fs from "fs";',
|
|
1015
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1016
|
+
" const vs = Object.values(doc);",
|
|
1017
|
+
" Promise.all(vs.map(async (v) => {",
|
|
1018
|
+
' fs.writeFileSync(outDir + "/" + v.name, "x");',
|
|
1019
|
+
" }));",
|
|
1020
|
+
"}",
|
|
1021
|
+
].join("\n"),
|
|
1022
|
+
},
|
|
1023
|
+
expectMark: true,
|
|
1024
|
+
why: "并发写法里形参前有个 async —— 不能被它挡住",
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
name: "负对照·只用索引(第二个形参)",
|
|
1028
|
+
fn: "emit",
|
|
1029
|
+
files: {
|
|
1030
|
+
"it.ts": [
|
|
1031
|
+
'import * as fs from "fs";',
|
|
1032
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1033
|
+
" Object.keys(doc).forEach((k, i) => {",
|
|
1034
|
+
' fs.writeFileSync(outDir + "/" + i, "x");',
|
|
1035
|
+
" });",
|
|
1036
|
+
"}",
|
|
1037
|
+
].join("\n"),
|
|
1038
|
+
},
|
|
1039
|
+
expectMark: false,
|
|
1040
|
+
why: "第二个形参是索引不是元素 —— 绑了它就是误报;本条钉死「只绑第一个形参」",
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
name: "负对照·字面量数组回调",
|
|
1044
|
+
fn: "emit",
|
|
1045
|
+
files: {
|
|
1046
|
+
"it.ts": [
|
|
1047
|
+
'import * as fs from "fs";',
|
|
1048
|
+
"export function emit(outDir: string) {",
|
|
1049
|
+
' ["a", "b"].forEach((k) => {',
|
|
1050
|
+
' fs.writeFileSync(outDir + "/" + k, "x");',
|
|
1051
|
+
" });",
|
|
1052
|
+
"}",
|
|
1053
|
+
].join("\n"),
|
|
1054
|
+
},
|
|
1055
|
+
expectMark: false,
|
|
1056
|
+
why: "无污点流入;正对照是同形状的 Object.keys(doc).forEach —— 证明不是「见到回调就标」",
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
name: "负对照·被枚举的是干净变量",
|
|
1060
|
+
fn: "emit",
|
|
1061
|
+
files: {
|
|
1062
|
+
"it.ts": [
|
|
1063
|
+
'import * as fs from "fs";',
|
|
1064
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1065
|
+
" const all = Object.keys(doc);",
|
|
1066
|
+
" if (all.length === 0) return;",
|
|
1067
|
+
' const ks = ["a", "b"];',
|
|
1068
|
+
" ks.forEach((k) => {",
|
|
1069
|
+
' fs.writeFileSync(outDir + "/" + k, "x");',
|
|
1070
|
+
" });",
|
|
1071
|
+
"}",
|
|
1072
|
+
].join("\n"),
|
|
1073
|
+
},
|
|
1074
|
+
expectMark: false,
|
|
1075
|
+
why: "根在、回调也在,但接收者不污 —— 证明绑定看的是【接收者是否污】,不是「函数里有污点就标」",
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
name: "正例·无括号单参箭头",
|
|
1079
|
+
fn: "emit",
|
|
1080
|
+
files: {
|
|
1081
|
+
"it.ts": [
|
|
1082
|
+
'import * as fs from "fs";',
|
|
1083
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1084
|
+
" Object.keys(doc).forEach(k => {",
|
|
1085
|
+
' fs.writeFileSync(outDir + "/" + k, "x");',
|
|
1086
|
+
" });",
|
|
1087
|
+
"}",
|
|
1088
|
+
].join("\n"),
|
|
1089
|
+
},
|
|
1090
|
+
expectMark: true,
|
|
1091
|
+
why: "TS/JS 里单参数箭头常常不写括号 —— 初版正则只认 `(k) =>`,这种写法漏",
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
name: "正例·ES5 function 回调",
|
|
1095
|
+
fn: "emit",
|
|
1096
|
+
files: {
|
|
1097
|
+
"it.ts": [
|
|
1098
|
+
'import * as fs from "fs";',
|
|
1099
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1100
|
+
" Object.keys(doc).forEach(function (k) {",
|
|
1101
|
+
' fs.writeFileSync(outDir + "/" + k, "x");',
|
|
1102
|
+
" });",
|
|
1103
|
+
"}",
|
|
1104
|
+
].join("\n"),
|
|
1105
|
+
},
|
|
1106
|
+
expectMark: true,
|
|
1107
|
+
why: "不是箭头函数,但形参同样是被枚举到的元素 —— 形态不同不该区别对待",
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
name: "正例·成员链接收者 + 形参带类型标注",
|
|
1111
|
+
fn: "emit",
|
|
1112
|
+
files: {
|
|
1113
|
+
"it.ts": [
|
|
1114
|
+
'import * as fs from "fs";',
|
|
1115
|
+
"export function emit(raw: string, outDir: string) {",
|
|
1116
|
+
" const doc = JSON.parse(raw);",
|
|
1117
|
+
" doc.sections.forEach((s: any) => {",
|
|
1118
|
+
' fs.writeFileSync(outDir + "/" + s.name, "x");',
|
|
1119
|
+
" });",
|
|
1120
|
+
"}",
|
|
1121
|
+
].join("\n"),
|
|
1122
|
+
},
|
|
1123
|
+
expectMark: true,
|
|
1124
|
+
why: "两处都得通:接收者隔着一层成员(doc.sections)、形参带 TS 类型标注(`: any` 曾经让整环不匹配)",
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
name: "负对照·回调是函数引用",
|
|
1128
|
+
fn: "emit",
|
|
1129
|
+
files: {
|
|
1130
|
+
"it.ts": [
|
|
1131
|
+
'import * as fs from "fs";',
|
|
1132
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1133
|
+
' const handleOne = outDir + "/fixed";',
|
|
1134
|
+
" Object.keys(doc).forEach(handleOne);",
|
|
1135
|
+
' fs.writeFileSync(handleOne + "/" + "index.md", "x");',
|
|
1136
|
+
"}",
|
|
1137
|
+
].join("\n"),
|
|
1138
|
+
},
|
|
1139
|
+
expectMark: false,
|
|
1140
|
+
why: "回调写成函数引用时,那个名字是【别人的函数名】不是元素 —— 初版把它当形参绑进污点集合,同名局部变量随即被误标(真实误报,本条锁住修复)",
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
name: "负对照·干净对象的成员链枚举",
|
|
1144
|
+
fn: "emit",
|
|
1145
|
+
files: {
|
|
1146
|
+
"it.ts": [
|
|
1147
|
+
'import * as fs from "fs";',
|
|
1148
|
+
"export function emit(outDir: string, cfg: { sections: Array<{ name: string }> }) {",
|
|
1149
|
+
" cfg.sections.forEach((s) => {",
|
|
1150
|
+
' fs.writeFileSync(outDir + "/" + s.name, "x");',
|
|
1151
|
+
" });",
|
|
1152
|
+
"}",
|
|
1153
|
+
].join("\n"),
|
|
1154
|
+
},
|
|
1155
|
+
expectMark: false,
|
|
1156
|
+
why: "放开了成员链接收者,就要跟着锁住「链的根不污时不标」 —— 正对照是同形状的 emitMemberChain 形态",
|
|
1157
|
+
},
|
|
1158
|
+
];
|
|
1159
|
+
for (const c of cases) {
|
|
1160
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
1161
|
+
const dir = makeProject(c.files);
|
|
1162
|
+
try {
|
|
1163
|
+
const marks = marksFor(dir, c.fn);
|
|
1164
|
+
if (c.expectMark)
|
|
1165
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
1166
|
+
else
|
|
1167
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
1168
|
+
}
|
|
1169
|
+
finally {
|
|
1170
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1171
|
+
}
|
|
1172
|
+
}, 150000);
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
(0, vitest_1.describe)("C4e:helper 的现代写法(箭头 / 函数表达式 / 模块常量)必须照样传播", () => {
|
|
1176
|
+
// 隔离写法:sink 实参里**不出现**污点变量本身,污点只能经 helper 的返回值流进去。
|
|
1177
|
+
// 否则会命中「sink 实参含污点名」的兜底判定,正负对照一次性全绿 —— 测的就不是传播了。
|
|
1178
|
+
const emit = (relExpr, extraImport = "") => [
|
|
1179
|
+
'import * as fs from "fs";',
|
|
1180
|
+
extraImport,
|
|
1181
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
1182
|
+
" Object.keys(doc).forEach((k) => {",
|
|
1183
|
+
` const rel = ${relExpr};`,
|
|
1184
|
+
' fs.writeFileSync(outDir + "/" + rel, "x");',
|
|
1185
|
+
" });",
|
|
1186
|
+
"}",
|
|
1187
|
+
]
|
|
1188
|
+
.filter((l) => l !== "")
|
|
1189
|
+
.join("\n");
|
|
1190
|
+
const cases = [
|
|
1191
|
+
{
|
|
1192
|
+
name: "正例·箭头 helper(简洁体)",
|
|
1193
|
+
fn: "emit",
|
|
1194
|
+
files: {
|
|
1195
|
+
"helpers.ts": [`export const withExtArrow = (name: string): string => name + ".md";`].join("\n"),
|
|
1196
|
+
"it.ts": emit("withExtArrow(k)", 'import { withExtArrow } from "./helpers";'),
|
|
1197
|
+
},
|
|
1198
|
+
expectMark: true,
|
|
1199
|
+
why: "`export const f = (x) => …` 是现代 TS 里 helper 的主力写法,而收集只走 getFunctions() ⇒ 整片漏",
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
name: "正例·箭头 helper(块体 + return)",
|
|
1203
|
+
fn: "emit",
|
|
1204
|
+
files: {
|
|
1205
|
+
"helpers.ts": [
|
|
1206
|
+
"export const withExtBlock = (name: string): string => {",
|
|
1207
|
+
' return name + ".md";',
|
|
1208
|
+
"};",
|
|
1209
|
+
].join("\n"),
|
|
1210
|
+
"it.ts": emit("withExtBlock(k)", 'import { withExtBlock } from "./helpers";'),
|
|
1211
|
+
},
|
|
1212
|
+
expectMark: true,
|
|
1213
|
+
why: "块体箭头没有「简洁返回值」,必须从 return 语句取 —— 与简洁体必须走同一条通路",
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
name: "正例·函数表达式 helper",
|
|
1217
|
+
fn: "emit",
|
|
1218
|
+
files: {
|
|
1219
|
+
"helpers.ts": [`export const withExtExpr = function (name: string): string { return name + ".md"; };`].join("\n"),
|
|
1220
|
+
"it.ts": emit("withExtExpr(k)", 'import { withExtExpr } from "./helpers";'),
|
|
1221
|
+
},
|
|
1222
|
+
expectMark: true,
|
|
1223
|
+
why: "函数表达式同样不是函数声明,getFunctions() 收不到 —— 与箭头同因",
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
name: "正例·return 引用模块级字面量常量",
|
|
1227
|
+
fn: "emit",
|
|
1228
|
+
files: {
|
|
1229
|
+
"helpers.ts": [
|
|
1230
|
+
'const EXT_DOC = ".doc";',
|
|
1231
|
+
"export function withConst(name: string): string { return name + EXT_DOC; }",
|
|
1232
|
+
].join("\n"),
|
|
1233
|
+
"it.ts": emit("withConst(k)", 'import { withConst } from "./helpers";'),
|
|
1234
|
+
},
|
|
1235
|
+
expectMark: true,
|
|
1236
|
+
why: "原 C4b 判据③的已知缺口:自由标识符一律拦死。现只放行【初值是字面量】的模块级常量 —— 出处可确证",
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
name: "正例·path 模块的局部别名",
|
|
1240
|
+
fn: "emit",
|
|
1241
|
+
files: {
|
|
1242
|
+
"helpers.ts": [
|
|
1243
|
+
'import * as p from "path";',
|
|
1244
|
+
'export const joinOut = (name: string): string => p.join("out", name);',
|
|
1245
|
+
].join("\n"),
|
|
1246
|
+
"it.ts": emit("joinOut(k)", 'import { joinOut } from "./helpers";'),
|
|
1247
|
+
},
|
|
1248
|
+
expectMark: true,
|
|
1249
|
+
why: "白名单里写死 'path' 一个名字不够 —— `import * as p` / `import nodePath from 'node:path'` 都要能认",
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
name: "正例·箭头两跳",
|
|
1253
|
+
fn: "emit",
|
|
1254
|
+
files: {
|
|
1255
|
+
"helpers.ts": [
|
|
1256
|
+
'export const innerArrow = (n: string): string => n + ".md";',
|
|
1257
|
+
"export const wrapTwice = (n: string): string => innerArrow(n);",
|
|
1258
|
+
].join("\n"),
|
|
1259
|
+
"it.ts": emit("wrapTwice(k)", 'import { wrapTwice } from "./helpers";'),
|
|
1260
|
+
},
|
|
1261
|
+
expectMark: true,
|
|
1262
|
+
why: "箭头之间互为调用方,要靠有界两轮不动点接上 —— 第一轮认不出,第二轮才有机会",
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
name: "负对照·helper 调用项目内不存在的函数",
|
|
1266
|
+
fn: "emit",
|
|
1267
|
+
files: {
|
|
1268
|
+
"helpers.ts": ["export const viaExternal = (name: string): string => lookupUnknown(name);"].join("\n"),
|
|
1269
|
+
"it.ts": emit("viaExternal(k)", 'import { viaExternal } from "./helpers";'),
|
|
1270
|
+
},
|
|
1271
|
+
expectMark: false,
|
|
1272
|
+
why: "认不出的一律站在精度一侧 —— 放宽到箭头之后这条仍必须不传播;正对照 = 正例·箭头 helper",
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
name: "负对照·helper 体内有 sink(箭头形态)",
|
|
1276
|
+
fn: "emit",
|
|
1277
|
+
files: {
|
|
1278
|
+
"helpers.ts": [
|
|
1279
|
+
'import * as fsx from "fs";',
|
|
1280
|
+
"export const sneakyArrow = (name: string): string => {",
|
|
1281
|
+
" fsx.existsSync(name);",
|
|
1282
|
+
' return name + ".md";',
|
|
1283
|
+
"};",
|
|
1284
|
+
].join("\n"),
|
|
1285
|
+
"it.ts": emit("sneakyArrow(k)", 'import { sneakyArrow } from "./helpers";'),
|
|
1286
|
+
},
|
|
1287
|
+
expectMark: false,
|
|
1288
|
+
why: "判据②对箭头同样成立 —— 体内有 IO 就不得假定它只是塑形",
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
name: "负对照·helper 真塑形但实参是字面量",
|
|
1292
|
+
fn: "emit",
|
|
1293
|
+
files: {
|
|
1294
|
+
"helpers.ts": [`export const withExtArrow = (name: string): string => name + ".md";`].join("\n"),
|
|
1295
|
+
"it.ts": emit('withExtArrow("static")', 'import { withExtArrow } from "./helpers";'),
|
|
1296
|
+
},
|
|
1297
|
+
expectMark: false,
|
|
1298
|
+
why: "传播得靠实参带污点,不能因为 helper 长得像塑形就一路放行",
|
|
1299
|
+
},
|
|
1300
|
+
];
|
|
1301
|
+
for (const c of cases) {
|
|
1302
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
1303
|
+
const dir = makeProject(c.files);
|
|
1304
|
+
try {
|
|
1305
|
+
const marks = marksFor(dir, c.fn);
|
|
1306
|
+
if (c.expectMark)
|
|
1307
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
1308
|
+
else
|
|
1309
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
1310
|
+
}
|
|
1311
|
+
finally {
|
|
1312
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1313
|
+
}
|
|
1314
|
+
}, 150000);
|
|
1315
|
+
}
|
|
1316
|
+
});
|