progmune-runtime 3.7.43 → 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 +168 -0
- package/dist/extract-ir-taint-structural.test.js +303 -3
- package/dist/extract-ir.js +838 -72
- package/dist/sdk.js +1 -1
- package/package.json +1 -1
package/dist/extract-ir.js
CHANGED
|
@@ -719,9 +719,16 @@ function isPureShaperExpr(expr, params, shapers, extraIdents) {
|
|
|
719
719
|
}
|
|
720
720
|
return true;
|
|
721
721
|
}
|
|
722
|
-
/**
|
|
723
|
-
|
|
724
|
-
|
|
722
|
+
/**
|
|
723
|
+
* 收集模块级(top-level)**字面量**常量:名字 → 字面量文本(引号已脱)。
|
|
724
|
+
*
|
|
725
|
+
* 两个用途:
|
|
726
|
+
* ① helper 的 return 引用它们仍是纯塑形(C4e);
|
|
727
|
+
* ② C4i:`{ [KEY]: (n) => … }` 这种计算属性名,只有 KEY 是字面量常量才解得
|
|
728
|
+
* 出来 —— 解不出一律不登记(与「看不见就不传播」同一条政策)。
|
|
729
|
+
*/
|
|
730
|
+
function moduleConstTexts(project) {
|
|
731
|
+
const out = new Map();
|
|
725
732
|
for (const sf of project.getSourceFiles()) {
|
|
726
733
|
for (const vd of sf.getVariableDeclarations()) {
|
|
727
734
|
// 只要模块作用域:VariableStatement 的父节点必须是 SourceFile
|
|
@@ -739,7 +746,7 @@ function moduleLiteralConstNames(project) {
|
|
|
739
746
|
init.getKind() === ts.SyntaxKind.TrueKeyword ||
|
|
740
747
|
init.getKind() === ts.SyntaxKind.FalseKeyword;
|
|
741
748
|
if (isLiteral)
|
|
742
|
-
out.
|
|
749
|
+
out.set(vd.getName(), init.getText().replace(/^["'`]|["'`]$/g, ""));
|
|
743
750
|
}
|
|
744
751
|
}
|
|
745
752
|
return out;
|
|
@@ -788,12 +795,32 @@ function paramNamesOf(p) {
|
|
|
788
795
|
}
|
|
789
796
|
return out;
|
|
790
797
|
}
|
|
798
|
+
/**
|
|
799
|
+
* 属性名:普通标识符 / 字符串字面量直接取;**计算属性名** `[KEY]` 只有在 KEY 是
|
|
800
|
+
* 模块级字面量常量时才解(C4i)。解不出返回 null —— 认不出的一律不登记。
|
|
801
|
+
*/
|
|
802
|
+
function propertyNameOf(pa, constTexts) {
|
|
803
|
+
const nn = pa.getNameNode();
|
|
804
|
+
if (ts_morph_1.Node.isIdentifier(nn))
|
|
805
|
+
return nn.getText();
|
|
806
|
+
if (ts_morph_1.Node.isStringLiteral(nn) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(nn)) {
|
|
807
|
+
return nn.getText().replace(/^["'`]|["'`]$/g, "");
|
|
808
|
+
}
|
|
809
|
+
if (ts_morph_1.Node.isComputedPropertyName(nn)) {
|
|
810
|
+
const e = nn.getExpression();
|
|
811
|
+
// `const KEY = "toPath"; … { [KEY]: (n) => … }` —— KEY 的值必须看得见;
|
|
812
|
+
// `[key()]` 这种算不出来的,宁可整个不收(探针 K2 守的就是这条)。
|
|
813
|
+
if (ts_morph_1.Node.isIdentifier(e))
|
|
814
|
+
return constTexts.get(e.getText()) ?? null;
|
|
815
|
+
}
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
791
818
|
/**
|
|
792
819
|
* 方法声明的宿主限定名:`const Util = { toPath(){} }` ⇒ "Util"、
|
|
793
820
|
* `const Cfg = { inner: { toPath(){} } }` ⇒ "Cfg.inner"、`class Paths {…}` ⇒ "Paths"。
|
|
794
821
|
* 取不出来就返回 null —— 认不出宿主的宁可不登记(保守侧)。
|
|
795
822
|
*/
|
|
796
|
-
function ownerQualifier(md) {
|
|
823
|
+
function ownerQualifier(md, constTexts = new Map()) {
|
|
797
824
|
const parent = md.getParent();
|
|
798
825
|
if (!parent)
|
|
799
826
|
return null;
|
|
@@ -808,10 +835,9 @@ function ownerQualifier(md) {
|
|
|
808
835
|
if (!up)
|
|
809
836
|
return null;
|
|
810
837
|
if (ts_morph_1.Node.isPropertyAssignment(up)) {
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
: up.getChildAtIndex(0).getText().replace(/^["'`]|["'`]$/g, "");
|
|
838
|
+
const keyName = propertyNameOf(up, constTexts);
|
|
839
|
+
if (keyName === null)
|
|
840
|
+
return null;
|
|
815
841
|
parts.unshift(keyName);
|
|
816
842
|
cur = up.getParent();
|
|
817
843
|
if (!cur)
|
|
@@ -826,6 +852,83 @@ function ownerQualifier(md) {
|
|
|
826
852
|
}
|
|
827
853
|
return null;
|
|
828
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* C4i:函数声明的命名空间限定名 —— `namespace P { export function toPath(){} }` ⇒ "P"
|
|
857
|
+
* (嵌套 namespace 拼成 "A.B")。只认**纯 namespace 链**:函数套函数那种 helper
|
|
858
|
+
* 作用域与调用点不同,按同一名字发布会张冠李戴,一律返回 null 交给调用方跳过。
|
|
859
|
+
*/
|
|
860
|
+
function moduleQualifier(fn) {
|
|
861
|
+
const parts = [];
|
|
862
|
+
let cur = fn;
|
|
863
|
+
for (let guard = 0; guard < 6; guard++) {
|
|
864
|
+
const up = cur.getParent();
|
|
865
|
+
if (!up)
|
|
866
|
+
return null;
|
|
867
|
+
// 注意(实测):`namespace P { export function f(){} }` 里函数的父节点是
|
|
868
|
+
// **ModuleBlock**(namespace 的体),ModuleDeclaration 在它的上一层。
|
|
869
|
+
if (ts_morph_1.Node.isModuleBlock(up)) {
|
|
870
|
+
const decl = up.getParent();
|
|
871
|
+
if (!decl || !ts_morph_1.Node.isModuleDeclaration(decl))
|
|
872
|
+
return null;
|
|
873
|
+
const n = decl.getName();
|
|
874
|
+
if (!n)
|
|
875
|
+
return null;
|
|
876
|
+
parts.unshift(n);
|
|
877
|
+
cur = decl;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (ts_morph_1.Node.isModuleDeclaration(up)) {
|
|
881
|
+
const n = up.getName();
|
|
882
|
+
if (!n)
|
|
883
|
+
return null;
|
|
884
|
+
parts.unshift(n);
|
|
885
|
+
cur = up;
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (ts_morph_1.Node.isSourceFile(up))
|
|
889
|
+
return parts.length > 0 ? parts.join(".") : null;
|
|
890
|
+
return null; // 函数体内的嵌套声明:作用域不同,不收
|
|
891
|
+
}
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
/** 脱掉包住表达式的括号(可能多层)—— `((n) => n + ".md")` ⇒ 箭头本身 */
|
|
895
|
+
function unparen(n) {
|
|
896
|
+
let cur = n;
|
|
897
|
+
while (cur && ts_morph_1.Node.isParenthesizedExpression(cur))
|
|
898
|
+
cur = cur.getExpression();
|
|
899
|
+
return cur;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* C4i:变量初值承载的 helper 体。除箭头 / 函数表达式外,再解一层 IIFE ——
|
|
903
|
+
* `const toPath = (() => (n) => n + ".md")();` 的初值是**调用**不是箭头,
|
|
904
|
+
* 真正的 helper 是被 IIFE 返回出来的那个内层箭头(旧代码整条族漏)。
|
|
905
|
+
*/
|
|
906
|
+
function functionLikeOf(init) {
|
|
907
|
+
// 实测:`(() => (n) => n + ".md")()` 里被括号包住的**不止**内层箭头 ——
|
|
908
|
+
// 连被调用的 callee 都是 ParenthesizedExpression。两处都得脱。
|
|
909
|
+
const base = unparen(init);
|
|
910
|
+
if (!base)
|
|
911
|
+
return null;
|
|
912
|
+
if (ts_morph_1.Node.isArrowFunction(base) || ts_morph_1.Node.isFunctionExpression(base))
|
|
913
|
+
return base;
|
|
914
|
+
if (ts_morph_1.Node.isCallExpression(base)) {
|
|
915
|
+
const callee = unparen(base.getExpression());
|
|
916
|
+
if (!ts_morph_1.Node.isArrowFunction(callee) && !ts_morph_1.Node.isFunctionExpression(callee))
|
|
917
|
+
return null;
|
|
918
|
+
const b = callee.getBody();
|
|
919
|
+
if (!ts_morph_1.Node.isBlock(b)) {
|
|
920
|
+
// 简洁体:`(() => (n) => n + ".md")()` —— 括号包着也算(unparen)
|
|
921
|
+
const e = unparen(b);
|
|
922
|
+
return e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)) ? e : null;
|
|
923
|
+
}
|
|
924
|
+
for (const rs of b.getDescendantsOfKind(ts.SyntaxKind.ReturnStatement)) {
|
|
925
|
+
const e = unparen(rs.getExpression());
|
|
926
|
+
if (e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)))
|
|
927
|
+
return e;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
829
932
|
/** 按顶层逗号切分实参串(跳过字符串与已配平的括号,后者由调用方保证不含嵌套) */
|
|
830
933
|
function splitTopLevelArgs(argText) {
|
|
831
934
|
const out = [];
|
|
@@ -860,6 +963,110 @@ function splitTopLevelArgs(argText) {
|
|
|
860
963
|
out.push(cur);
|
|
861
964
|
return out;
|
|
862
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* C4i:形参表里 rest 形参(`...parts`)的位置;没有返回 -1。
|
|
968
|
+
* rest 形参对应的是【一批】实参,不是一位 —— 位置对齐必须让它吃掉末尾所有位。
|
|
969
|
+
*/
|
|
970
|
+
function restIndexOf(ps) {
|
|
971
|
+
return ps.findIndex((p) => p.isRestParameter());
|
|
972
|
+
}
|
|
973
|
+
/** 实参位 ⇒ 形参位(rest 形参吃掉从它开始的剩余位) */
|
|
974
|
+
function paramIndexOf(si, argIndex) {
|
|
975
|
+
return si.restAt >= 0 && argIndex >= si.restAt ? si.restAt : argIndex;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* C4i:柯里化 helper —— 函数体【返回】另一个箭头 / 函数表达式
|
|
979
|
+
* (`const withExt = (ext) => (n) => n + ext`)。这时被调用的不是这一层,
|
|
980
|
+
* 而是它返回出来的内层函数;真正塑形的地方在内层,外层只负责捕获第一批实参。
|
|
981
|
+
* 内层无形参 ⇒ 不存在「经内层实参进来」的污点 ⇒ 返回 null(外层按普通 helper 走)。
|
|
982
|
+
*/
|
|
983
|
+
function currySpecOf(outerParams, body) {
|
|
984
|
+
const inner = returnedFunctionOf(body);
|
|
985
|
+
if (!inner)
|
|
986
|
+
return null;
|
|
987
|
+
const innerParams = inner.getParameters().flatMap(paramNamesOf).filter(Boolean);
|
|
988
|
+
if (innerParams.length === 0)
|
|
989
|
+
return null;
|
|
990
|
+
const innerBody = inner.getBody();
|
|
991
|
+
if (!innerBody)
|
|
992
|
+
return null;
|
|
993
|
+
const rets = returnExprsOf(innerBody);
|
|
994
|
+
if (rets.length === 0)
|
|
995
|
+
return null;
|
|
996
|
+
return { outerParams, innerParams, rets };
|
|
997
|
+
}
|
|
998
|
+
/** 函数体返回出来的那个箭头 / 函数表达式(没有则 null) */
|
|
999
|
+
function returnedFunctionOf(body) {
|
|
1000
|
+
if (!ts_morph_1.Node.isBlock(body)) {
|
|
1001
|
+
// 箭头简洁体本身就是要返回的函数:`(ext) => (n) => n + ext`(可能被括号包住)
|
|
1002
|
+
const e = unparen(body);
|
|
1003
|
+
return e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)) ? e : null;
|
|
1004
|
+
}
|
|
1005
|
+
for (const rs of body.getDescendantsOfKind(ts.SyntaxKind.ReturnStatement)) {
|
|
1006
|
+
const e = unparen(rs.getExpression());
|
|
1007
|
+
if (e && (ts_morph_1.Node.isArrowFunction(e) || ts_morph_1.Node.isFunctionExpression(e)))
|
|
1008
|
+
return e;
|
|
1009
|
+
}
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* C4i:柯里化 helper 的调用点展开。`withExt(".md")(k)` 这种两跳调用,真正塑形的是
|
|
1014
|
+
* 内层函数 —— 这里把【内层 return 表达式】按两层实参代入后原地摊开,之后的污点检测
|
|
1015
|
+
* 直接看摊开后的串:`withExt(".md")(k)` ⇒ `k + ".md"`,`withExt(k)("name")` ⇒
|
|
1016
|
+
* `"name" + k`。丢形参的内层(`(_n) => "fixed.md"`)摊开后是常量 ⇒ 自然不传播。
|
|
1017
|
+
*/
|
|
1018
|
+
function expandCurried(text, curried) {
|
|
1019
|
+
if (curried.size === 0)
|
|
1020
|
+
return text;
|
|
1021
|
+
let cur = text;
|
|
1022
|
+
for (let round = 0; round < 3; round++) {
|
|
1023
|
+
const next = expandCurriedOnce(cur, curried);
|
|
1024
|
+
if (next === null)
|
|
1025
|
+
break;
|
|
1026
|
+
cur = next;
|
|
1027
|
+
}
|
|
1028
|
+
return cur;
|
|
1029
|
+
}
|
|
1030
|
+
/** 摊开**一处**柯里化调用;没有可摊的返回 null(供调用方收敛) */
|
|
1031
|
+
function expandCurriedOnce(text, curried) {
|
|
1032
|
+
for (const [name, cs] of curried) {
|
|
1033
|
+
const re = new RegExp(`(?:^|[^\\w$.])${escapeRe(name)}\\s*\\(`, "g");
|
|
1034
|
+
let m;
|
|
1035
|
+
while ((m = re.exec(text)) !== null) {
|
|
1036
|
+
const open1 = m.index + m[0].length - 1;
|
|
1037
|
+
const close1 = matchingParen(text, open1);
|
|
1038
|
+
if (close1 < 0)
|
|
1039
|
+
continue;
|
|
1040
|
+
// 第二跳:紧跟其后的那个 `(`
|
|
1041
|
+
let j = close1 + 1;
|
|
1042
|
+
while (j < text.length && /\s/.test(text[j]))
|
|
1043
|
+
j++;
|
|
1044
|
+
if (text[j] !== "(")
|
|
1045
|
+
continue;
|
|
1046
|
+
const close2 = matchingParen(text, j);
|
|
1047
|
+
if (close2 < 0)
|
|
1048
|
+
continue;
|
|
1049
|
+
const args1 = splitTopLevelArgs(text.slice(open1 + 1, close1));
|
|
1050
|
+
const args2 = splitTopLevelArgs(text.slice(j + 1, close2));
|
|
1051
|
+
const map = new Map();
|
|
1052
|
+
args1.forEach((a, i) => {
|
|
1053
|
+
const p = cs.outerParams[i];
|
|
1054
|
+
if (p !== undefined)
|
|
1055
|
+
map.set(p, stripOuterParens(a.trim()));
|
|
1056
|
+
});
|
|
1057
|
+
args2.forEach((a, i) => {
|
|
1058
|
+
const p = cs.innerParams[i];
|
|
1059
|
+
if (p !== undefined && !map.has(p))
|
|
1060
|
+
map.set(p, stripOuterParens(a.trim()));
|
|
1061
|
+
});
|
|
1062
|
+
const inner = cs.rets.length === 1 ? cs.rets[0] : cs.rets.join(" + ");
|
|
1063
|
+
const expanded = foldDecidableTernary(substituteParams(inner, map));
|
|
1064
|
+
const nameStart = m.index + (m[0].startsWith(name) ? 0 : 1);
|
|
1065
|
+
return text.slice(0, nameStart) + expanded + text.slice(close2 + 1);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
863
1070
|
/**
|
|
864
1071
|
* C4f:把 helper 调用里【返回值不依赖的那些实参】抹掉。
|
|
865
1072
|
*
|
|
@@ -916,7 +1123,13 @@ function maskNonDependentArgsOnce(text, info, memberNames) {
|
|
|
916
1123
|
if (/[([{]/.test(argText))
|
|
917
1124
|
continue; // 还嵌着别的调用 —— 下一轮再处理
|
|
918
1125
|
const args = splitTopLevelArgs(argText);
|
|
919
|
-
|
|
1126
|
+
// C4h-①:按【本次调用】重算一下哪些位置会流出去 —— 名录里的 deps 是对所有
|
|
1127
|
+
// 实参形状都成立的最宽结论,本次若喂了常量,可能一个都流不出来。
|
|
1128
|
+
const flags = callSiteKeepFlags(si, args, info, memberNames);
|
|
1129
|
+
const kept = args.filter((_, i) => {
|
|
1130
|
+
const pi = paramIndexOf(si, i); // C4i:rest 形参吃掉剩余位
|
|
1131
|
+
return si.params[pi] !== undefined && flags[pi] === true;
|
|
1132
|
+
});
|
|
920
1133
|
if (kept.length === args.length)
|
|
921
1134
|
continue; // 每个形参都被依赖 ⇒ 无需抹
|
|
922
1135
|
const replacement = kept.length > 0 ? `(${kept.join(" + ")})` : `""`;
|
|
@@ -926,7 +1139,431 @@ function maskNonDependentArgsOnce(text, info, memberNames) {
|
|
|
926
1139
|
return null;
|
|
927
1140
|
}
|
|
928
1141
|
/**
|
|
929
|
-
*
|
|
1142
|
+
* C4h-①(2026-09-20):调用点专用的「哪些实参会流到返回值里」。
|
|
1143
|
+
*
|
|
1144
|
+
* 名录里的 deps 是**跨调用点**的并集 —— 对所有实参形状都成立的最宽结论。
|
|
1145
|
+
* 但本次调用喂的可能是常量:
|
|
1146
|
+
*
|
|
1147
|
+
* viaFlag(n, flag) { return flag ? "fixed.md" : n + ".md"; }
|
|
1148
|
+
* viaFlag(k, true) ⇒ 返回值就是字面量 —— n 位虽然进了函数也出不来。
|
|
1149
|
+
*
|
|
1150
|
+
* 于是按位置把**字面量实参**代进 return 表达式,试着解掉 cond 可判定的三元,
|
|
1151
|
+
* 再看还有哪些形参活着。两种情形一律退回名录的 deps(保守侧,宁可不收精度):
|
|
1152
|
+
* - 实参一个常量都没有;
|
|
1153
|
+
* - cond 代入后判不出真假 —— 认不出就不收精度。
|
|
1154
|
+
*/
|
|
1155
|
+
function callSiteKeepFlags(si, args, info, memberNames) {
|
|
1156
|
+
const fallback = si.params.map((p) => si.deps.has(p));
|
|
1157
|
+
if (si.rets.length === 0)
|
|
1158
|
+
return fallback;
|
|
1159
|
+
const litArgs = new Map();
|
|
1160
|
+
args.forEach((a, i) => {
|
|
1161
|
+
// C4i:rest 形参拿到的是【一批】实参,不等于任意一个 —— 不代,否则
|
|
1162
|
+
// `parts.join("/")` 会被代成 `"out".join("/"` 从而误判「不依赖 parts」
|
|
1163
|
+
if (si.restAt >= 0 && i >= si.restAt)
|
|
1164
|
+
return;
|
|
1165
|
+
const p = si.params[i];
|
|
1166
|
+
if (p === undefined)
|
|
1167
|
+
return;
|
|
1168
|
+
const t = stripOuterParens(a.trim());
|
|
1169
|
+
// 只代【字面量】—— 变量名进来不等于知道它的值
|
|
1170
|
+
if (litKind(t) !== null)
|
|
1171
|
+
litArgs.set(p, t);
|
|
1172
|
+
});
|
|
1173
|
+
if (litArgs.size === 0)
|
|
1174
|
+
return fallback;
|
|
1175
|
+
const subbed = si.rets.map((r) => foldDecidableTernary(substituteParams(r, litArgs)));
|
|
1176
|
+
const remaining = si.params.filter((p) => p !== undefined && !litArgs.has(p));
|
|
1177
|
+
// 嵌套 helper 的支路仍由 returnDeps 负责抹
|
|
1178
|
+
const deps = returnDeps(subbed, remaining, info, memberNames);
|
|
1179
|
+
return si.params.map((p) => deps.has(p));
|
|
1180
|
+
}
|
|
1181
|
+
/** 把形参名替换成实参文本(跳字符串与模板字面量内部 —— 判不出就保守地留着形参) */
|
|
1182
|
+
function substituteParams(expr, litArgs) {
|
|
1183
|
+
let out = "";
|
|
1184
|
+
let quote = "";
|
|
1185
|
+
let i = 0;
|
|
1186
|
+
while (i < expr.length) {
|
|
1187
|
+
const ch = expr[i];
|
|
1188
|
+
if (quote) {
|
|
1189
|
+
out += ch;
|
|
1190
|
+
if (ch === "\\") {
|
|
1191
|
+
out += expr[i + 1] ?? "";
|
|
1192
|
+
i += 2;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (ch === quote)
|
|
1196
|
+
quote = "";
|
|
1197
|
+
i++;
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
if (ch === '"' || ch === "'") {
|
|
1201
|
+
quote = ch;
|
|
1202
|
+
out += ch;
|
|
1203
|
+
i++;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
// C4i:模板串不能像普通字符串那样整段跳过 —— `${flag ? "fixed" : n}` 里的
|
|
1207
|
+
// flag 是要代的。字面量部分照抄,只对 `${}` 内部递归代入。
|
|
1208
|
+
if (ch === "`") {
|
|
1209
|
+
out += ch;
|
|
1210
|
+
i++;
|
|
1211
|
+
while (i < expr.length) {
|
|
1212
|
+
const c = expr[i];
|
|
1213
|
+
if (c === "`")
|
|
1214
|
+
break; // 模板串内不可能有裸反引号(转义的已被下面处理)
|
|
1215
|
+
if (c === "\\") {
|
|
1216
|
+
out += expr.slice(i, i + 2);
|
|
1217
|
+
i += 2;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if (c === "$" && expr[i + 1] === "{") {
|
|
1221
|
+
let k = i + 2;
|
|
1222
|
+
let brace = 0;
|
|
1223
|
+
let inner = "";
|
|
1224
|
+
while (k < expr.length) {
|
|
1225
|
+
const d = expr[k];
|
|
1226
|
+
if (d === "{")
|
|
1227
|
+
brace++;
|
|
1228
|
+
else if (d === "}") {
|
|
1229
|
+
if (brace === 0)
|
|
1230
|
+
break;
|
|
1231
|
+
brace--;
|
|
1232
|
+
}
|
|
1233
|
+
inner += d;
|
|
1234
|
+
k++;
|
|
1235
|
+
}
|
|
1236
|
+
out += "${" + substituteParams(inner, litArgs) + "}";
|
|
1237
|
+
i = k + 1;
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
out += c;
|
|
1241
|
+
i++;
|
|
1242
|
+
}
|
|
1243
|
+
if (i < expr.length && expr[i] === "`") {
|
|
1244
|
+
out += "`";
|
|
1245
|
+
i++;
|
|
1246
|
+
}
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
const m = /^[A-Za-z_$][\w$]*/.exec(expr.slice(i));
|
|
1250
|
+
if (m && expr[i - 1] !== "." && litArgs.has(m[0])) {
|
|
1251
|
+
// 不加括号:代进去的永远是【原子字面量】(字符串/数字/布尔/null),
|
|
1252
|
+
// 不会改变优先级;而加括号会让 maskNonDependentArgsOnce 把参数串误判成嵌套
|
|
1253
|
+
// 调用(它的判据是「实参里还有 `(`」),内层就再也轮不到它来处理。
|
|
1254
|
+
out += litArgs.get(m[0]);
|
|
1255
|
+
i += m[0].length;
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
out += ch;
|
|
1259
|
+
i++;
|
|
1260
|
+
}
|
|
1261
|
+
return out;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* 递归解掉 cond 可判定的三元 / 短路;判不出就保持原样(保守侧)。
|
|
1265
|
+
*
|
|
1266
|
+
* C4i(2026-09-21)补两类:
|
|
1267
|
+
* - 模板串内部的 `${...}` 是独立表达式,折叠必须能进去 —— 否则
|
|
1268
|
+
* `` `${flag ? "fixed" : n}.md` `` 这种写法(现代代码里比裸三元更常见)
|
|
1269
|
+
* 永远收不住;
|
|
1270
|
+
* - 短路运算符 `||` / `&&` / `??`:与三元同形,只是判据从 cond 换成了
|
|
1271
|
+
* 左操作数的真假。JS 求值是从左到右,短路一发生右边整段根本不执行。
|
|
1272
|
+
*/
|
|
1273
|
+
function foldDecidableTernary(expr, depth = 0) {
|
|
1274
|
+
if (depth > 4)
|
|
1275
|
+
return expr;
|
|
1276
|
+
// 先脱掉包住整串的括号 —— 不脱的话 `(()flag ? a : b)` 这种写法切出来的 cond
|
|
1277
|
+
// 是 `((true)`,括号不配对 ⇒ 判不出真假 ⇒ 白白丢掉一次收精度的机会
|
|
1278
|
+
// (2026-09-20 探针:viaFlag(k, true) 一直没收住,根因就是这个)。
|
|
1279
|
+
const s = stripOuterParens(expr);
|
|
1280
|
+
// ① 模板串:先处理 `${}` 内部(外面那层字面量部分不参与判定)
|
|
1281
|
+
const tpl = foldTemplateSubsts(s, depth);
|
|
1282
|
+
if (tpl !== s)
|
|
1283
|
+
return tpl;
|
|
1284
|
+
// ② 三元
|
|
1285
|
+
const t = topLevelTernary(s);
|
|
1286
|
+
if (t) {
|
|
1287
|
+
const v = truthOf(t.cond);
|
|
1288
|
+
if (v !== null)
|
|
1289
|
+
return foldDecidableTernary(v ? t.a : t.b, depth + 1);
|
|
1290
|
+
}
|
|
1291
|
+
// ③ 短路
|
|
1292
|
+
const lg = topLevelLogical(s);
|
|
1293
|
+
if (lg) {
|
|
1294
|
+
const v = lg.op === "??" ? notNullish(lg.left) : truthOf(lg.left);
|
|
1295
|
+
if (v !== null) {
|
|
1296
|
+
// `||` 左为真 ⇒ 取左;`&&` 左为真 ⇒ 取右;`??` 左非空 ⇒ 取左
|
|
1297
|
+
const takeLeft = lg.op === "&&" ? !v : v;
|
|
1298
|
+
return foldDecidableTernary(takeLeft ? lg.left : lg.right, depth + 1);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return expr === s ? expr : s;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* 折叠模板串里每个 `${...}` 的内部;没有任何一处可折叠就**原样返回**(让调用方
|
|
1305
|
+
* 知道这一层没做事,好继续试三元/短路)。
|
|
1306
|
+
*/
|
|
1307
|
+
function foldTemplateSubsts(expr, depth) {
|
|
1308
|
+
if (!expr.includes("`"))
|
|
1309
|
+
return expr;
|
|
1310
|
+
let out = "";
|
|
1311
|
+
let changed = false;
|
|
1312
|
+
let i = 0;
|
|
1313
|
+
while (i < expr.length) {
|
|
1314
|
+
const ch = expr[i];
|
|
1315
|
+
if (ch !== "`") {
|
|
1316
|
+
out += ch;
|
|
1317
|
+
i++;
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
let seg = "";
|
|
1321
|
+
let j = i + 1;
|
|
1322
|
+
while (j < expr.length) {
|
|
1323
|
+
const c = expr[j];
|
|
1324
|
+
if (c === "\\") {
|
|
1325
|
+
seg += expr.slice(j, j + 2);
|
|
1326
|
+
j += 2;
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
if (c === "`")
|
|
1330
|
+
break;
|
|
1331
|
+
if (c === "$" && expr[j + 1] === "{") {
|
|
1332
|
+
// 找与 `${` 配对的 `}`(`${ {a:1}.a }` 里的花括号要跳过)
|
|
1333
|
+
let brace = 0;
|
|
1334
|
+
let k = j + 2;
|
|
1335
|
+
let inner = "";
|
|
1336
|
+
while (k < expr.length) {
|
|
1337
|
+
const d = expr[k];
|
|
1338
|
+
if (d === "{")
|
|
1339
|
+
brace++;
|
|
1340
|
+
else if (d === "}") {
|
|
1341
|
+
if (brace === 0)
|
|
1342
|
+
break;
|
|
1343
|
+
brace--;
|
|
1344
|
+
}
|
|
1345
|
+
inner += d;
|
|
1346
|
+
k++;
|
|
1347
|
+
}
|
|
1348
|
+
const folded = foldDecidableTernary(inner, depth + 1);
|
|
1349
|
+
if (folded !== inner)
|
|
1350
|
+
changed = true;
|
|
1351
|
+
seg += "${" + folded + "}";
|
|
1352
|
+
j = k + 1;
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
seg += c;
|
|
1356
|
+
j++;
|
|
1357
|
+
}
|
|
1358
|
+
out += "`" + seg + "`";
|
|
1359
|
+
i = j + 1;
|
|
1360
|
+
}
|
|
1361
|
+
return changed ? out : expr;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* 顶层短路运算符:**最左边**的一个。优先级从低到高依次找 `||` → `??` → `&&`
|
|
1365
|
+
* (TS 里 `??` 与 `||`/`&&` 同级混用必须加括号,加了括号就不是顶层了)。
|
|
1366
|
+
* 只认左右都非空的那一个。
|
|
1367
|
+
*/
|
|
1368
|
+
function topLevelLogical(expr) {
|
|
1369
|
+
for (const op of ["||", "??", "&&"]) {
|
|
1370
|
+
let paren = 0;
|
|
1371
|
+
let quote = "";
|
|
1372
|
+
let i = 0;
|
|
1373
|
+
while (i < expr.length) {
|
|
1374
|
+
const ch = expr[i];
|
|
1375
|
+
if (quote) {
|
|
1376
|
+
if (ch === "\\")
|
|
1377
|
+
i++;
|
|
1378
|
+
else if (ch === quote)
|
|
1379
|
+
quote = "";
|
|
1380
|
+
i++;
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1384
|
+
quote = ch;
|
|
1385
|
+
i++;
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
if (ch === "(" || ch === "[" || ch === "{")
|
|
1389
|
+
paren++;
|
|
1390
|
+
else if (ch === ")" || ch === "]" || ch === "}")
|
|
1391
|
+
paren--;
|
|
1392
|
+
else if (paren === 0 && expr.startsWith(op, i)) {
|
|
1393
|
+
// `??`/`||`/`&&` 各占两字符,注意别把 `a ||= b` 这类当短路(赋值语义不同)
|
|
1394
|
+
if (expr[i + 2] === "=") {
|
|
1395
|
+
i += 3;
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
const left = expr.slice(0, i).trim();
|
|
1399
|
+
const right = expr.slice(i + 2).trim();
|
|
1400
|
+
if (left !== "" && right !== "")
|
|
1401
|
+
return { left, right, op };
|
|
1402
|
+
}
|
|
1403
|
+
i++;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
/** 左操作数**确定不是** null/undefined 时返回 true;判不出返回 null */
|
|
1409
|
+
function notNullish(s) {
|
|
1410
|
+
const lit = litOf(s);
|
|
1411
|
+
if (!lit)
|
|
1412
|
+
return null;
|
|
1413
|
+
return lit.k !== "null";
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* 顶层三元的三段。要点是"顶层":`a ? b ? c : d : e` 里第一个顶层 `:` 属于内层
|
|
1417
|
+
* 三元,不能拿来切 —— 否则会把 b 误当成整个真值分支。
|
|
1418
|
+
*/
|
|
1419
|
+
function topLevelTernary(expr) {
|
|
1420
|
+
let paren = 0;
|
|
1421
|
+
let qDepth = 0;
|
|
1422
|
+
let qPos = -1;
|
|
1423
|
+
let cPos = -1;
|
|
1424
|
+
let quote = "";
|
|
1425
|
+
let i = 0;
|
|
1426
|
+
while (i < expr.length) {
|
|
1427
|
+
const ch = expr[i];
|
|
1428
|
+
if (quote) {
|
|
1429
|
+
if (ch === "\\")
|
|
1430
|
+
i++;
|
|
1431
|
+
else if (ch === quote)
|
|
1432
|
+
quote = "";
|
|
1433
|
+
i++;
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1437
|
+
quote = ch;
|
|
1438
|
+
i++;
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (ch === "(" || ch === "[" || ch === "{")
|
|
1442
|
+
paren++;
|
|
1443
|
+
else if (ch === ")" || ch === "]" || ch === "}")
|
|
1444
|
+
paren--;
|
|
1445
|
+
else if (paren === 0) {
|
|
1446
|
+
if (ch === "?") {
|
|
1447
|
+
if (qDepth === 0)
|
|
1448
|
+
qPos = i;
|
|
1449
|
+
qDepth++;
|
|
1450
|
+
}
|
|
1451
|
+
else if (ch === ":" && qDepth > 0) {
|
|
1452
|
+
if (qDepth === 1) {
|
|
1453
|
+
cPos = i;
|
|
1454
|
+
break;
|
|
1455
|
+
}
|
|
1456
|
+
qDepth--;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
i++;
|
|
1460
|
+
}
|
|
1461
|
+
if (qPos < 0 || cPos < 0)
|
|
1462
|
+
return null;
|
|
1463
|
+
return { cond: expr.slice(0, qPos), a: expr.slice(qPos + 1, cPos), b: expr.slice(cPos + 1) };
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* 脱掉**包住整串**的括号(`(x)` ⇒ `x`,可能多层)。只脱真的配到串尾的那种 ——
|
|
1467
|
+
* `(a) + (b)` 的首个括号不到底,不会被误脱。
|
|
1468
|
+
*/
|
|
1469
|
+
function stripOuterParens(expr) {
|
|
1470
|
+
let cur = expr.trim();
|
|
1471
|
+
for (let guard = 0; guard < 8; guard++) {
|
|
1472
|
+
if (!cur.startsWith("("))
|
|
1473
|
+
break;
|
|
1474
|
+
const close = matchingParen(cur, 0);
|
|
1475
|
+
if (close !== cur.length - 1)
|
|
1476
|
+
break;
|
|
1477
|
+
const inner = cur.slice(1, -1).trim();
|
|
1478
|
+
if (inner === "")
|
|
1479
|
+
break;
|
|
1480
|
+
cur = inner;
|
|
1481
|
+
}
|
|
1482
|
+
return cur;
|
|
1483
|
+
}
|
|
1484
|
+
/** 判得出来就返回 true/false,判不出返回 null */
|
|
1485
|
+
function truthOf(cond) {
|
|
1486
|
+
const s = stripOuterParens(cond);
|
|
1487
|
+
if (/^!\s*\S/.test(s)) {
|
|
1488
|
+
const v = truthOf(s.replace(/^!\s*/, ""));
|
|
1489
|
+
return v === null ? null : !v;
|
|
1490
|
+
}
|
|
1491
|
+
const lit = litOf(s);
|
|
1492
|
+
if (lit) {
|
|
1493
|
+
if (lit.k === "bool")
|
|
1494
|
+
return lit.v === "true";
|
|
1495
|
+
if (lit.k === "null")
|
|
1496
|
+
return false; // null / undefined
|
|
1497
|
+
if (lit.k === "num")
|
|
1498
|
+
return Number(lit.v) !== 0;
|
|
1499
|
+
return lit.v.length > 0; // 字符串
|
|
1500
|
+
}
|
|
1501
|
+
const eq = /^(.+?)\s*(===|!==)\s*(.+)$/.exec(s);
|
|
1502
|
+
if (eq) {
|
|
1503
|
+
const l = litOf(eq[1]);
|
|
1504
|
+
const r = litOf(eq[3]);
|
|
1505
|
+
// 两边都是字面量但类型不同(`"1" === 1`)—— JS 语义下也是 false/true,可判
|
|
1506
|
+
if (l && r) {
|
|
1507
|
+
const same = l.k === r.k && l.v === r.v;
|
|
1508
|
+
return eq[2] === "===" ? same : !same;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
/** 字面量种类:字符串 / 数字 / 布尔 / null-undefined;不是字面量返回 null */
|
|
1514
|
+
function litOf(s) {
|
|
1515
|
+
const clean = stripOuterParens(s.trim()); // 代入时实参被套了括号:`(true) === true`
|
|
1516
|
+
return litKind(clean);
|
|
1517
|
+
}
|
|
1518
|
+
function litKind(s) {
|
|
1519
|
+
if (/^"(?:\\.|[^"\\])*"$/.test(s) || /^'(?:\\.|[^'\\])*'$/.test(s) || /^`(?:\\.|[^`\\])*`$/.test(s)) {
|
|
1520
|
+
return { k: "str", v: s.slice(1, -1) };
|
|
1521
|
+
}
|
|
1522
|
+
if (/^-?\d+(?:\.\d+)?$/.test(s))
|
|
1523
|
+
return { k: "num", v: s };
|
|
1524
|
+
if (s === "true" || s === "false")
|
|
1525
|
+
return { k: "bool", v: s };
|
|
1526
|
+
if (s === "null" || s === "undefined")
|
|
1527
|
+
return { k: "null", v: s };
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
/** s[open] 是开括号时,返回与之配对的闭括号下标;不配对返回 -1 */
|
|
1531
|
+
function matchingParen(s, open) {
|
|
1532
|
+
let depth = 0;
|
|
1533
|
+
for (let i = open; i < s.length; i++) {
|
|
1534
|
+
if (s[i] === "(")
|
|
1535
|
+
depth++;
|
|
1536
|
+
else if (s[i] === ")") {
|
|
1537
|
+
depth--;
|
|
1538
|
+
if (depth === 0)
|
|
1539
|
+
return i;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return -1;
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* 同名多处的依赖集合并:按**形参位**取并集(OR)—— 任何一个实现在某个位置
|
|
1546
|
+
* 会流出污点,那个位置就得当成会流出(合并到保守侧)。
|
|
1547
|
+
* 形参表不一致时不合并 return 表达式(`rets=[]` ⇒ 调用点折叠自动退回,
|
|
1548
|
+
* 只按并集传播),因为把 A 的形参名代进 B 的 return 是错的。
|
|
1549
|
+
*/
|
|
1550
|
+
function mergeShapers(infos) {
|
|
1551
|
+
if (infos.length === 0)
|
|
1552
|
+
return { params: [], deps: new Set(), rets: [], restAt: -1 };
|
|
1553
|
+
const widest = infos.reduce((a, b) => (b.params.length > a.params.length ? b : a));
|
|
1554
|
+
const params = widest.params;
|
|
1555
|
+
const flags = params.map(() => false);
|
|
1556
|
+
let sameShape = true;
|
|
1557
|
+
for (const si of infos) {
|
|
1558
|
+
if (si.params.length !== params.length || si.params.some((p, i) => p !== params[i]))
|
|
1559
|
+
sameShape = false;
|
|
1560
|
+
si.params.forEach((p, i) => { if (i < flags.length && si.deps.has(p))
|
|
1561
|
+
flags[i] = true; });
|
|
1562
|
+
}
|
|
1563
|
+
const rets = sameShape ? infos.flatMap((si) => si.rets) : [];
|
|
1564
|
+
return { params, deps: new Set(params.filter((_, i) => flags[i])), rets, restAt: widest.restAt };
|
|
1565
|
+
}
|
|
1566
|
+
/** return 表达式真正依赖哪些形参:把「已知 helper 调用里不被依赖的实参」抹掉后,
|
|
930
1567
|
* 剩下的形参名就是会流进返回值的那些。`wrap(n) { return discard(n); }` ⇒ 空集。
|
|
931
1568
|
*/
|
|
932
1569
|
function returnDeps(exprs, params, info, memberNames = new Set()) {
|
|
@@ -956,8 +1593,12 @@ function returnExprsOf(body) {
|
|
|
956
1593
|
function pureShaperFunctionNames(project) {
|
|
957
1594
|
const sinkRe = tsSinkCallRegex();
|
|
958
1595
|
const cands = [];
|
|
1596
|
+
const takenNames = new Set();
|
|
1597
|
+
const groups = new Map();
|
|
1598
|
+
// 模块级字面量常量(值一并留着:C4i 的计算属性名 `[KEY]` 要靠它解)
|
|
1599
|
+
const constTexts = moduleConstTexts(project);
|
|
959
1600
|
/** 登记一个候选:三条判据 —— 有形参 / 有返回值 / 体内无 sink */
|
|
960
|
-
const consider = (name, paramNodes, declText, bodyNode) => {
|
|
1601
|
+
const consider = (key, name, group, paramNodes, restAt, curry, declText, bodyNode) => {
|
|
961
1602
|
const paramList = paramNodes.filter(Boolean);
|
|
962
1603
|
const params = new Set(paramList);
|
|
963
1604
|
if (params.size === 0)
|
|
@@ -968,114 +1609,215 @@ function pureShaperFunctionNames(project) {
|
|
|
968
1609
|
// 用 AST 取 return 表达式,而不是正则 —— 模板字面量里的 `${}` 会
|
|
969
1610
|
// 让「遇到花括号就停」的朴素正则在 `path.join(a,b) + \`.${c}\`` 上截断。
|
|
970
1611
|
const rets = returnExprsOf(bodyNode);
|
|
971
|
-
|
|
1612
|
+
// 柯里化:return 出来的是函数 ⇒ 判据①看的是【内层】有没有返回表达式
|
|
1613
|
+
if (rets.length === 0 && curry === null)
|
|
972
1614
|
return; // ①
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1615
|
+
let pub = name;
|
|
1616
|
+
if (pub !== null) {
|
|
1617
|
+
if (takenNames.has(pub))
|
|
1618
|
+
pub = null; // 同名重复登记:先到先得,后者降到资格凭证
|
|
1619
|
+
else
|
|
1620
|
+
takenNames.add(pub);
|
|
1621
|
+
}
|
|
1622
|
+
cands.push({ key, name: pub, group, params, paramList, rets, restAt, curry });
|
|
1623
|
+
if (group !== null) {
|
|
1624
|
+
const list = groups.get(group) ?? [];
|
|
1625
|
+
list.push(key);
|
|
1626
|
+
groups.set(group, list);
|
|
1627
|
+
}
|
|
976
1628
|
};
|
|
977
1629
|
for (const sf of project.getSourceFiles()) {
|
|
978
|
-
// (a) 函数声明
|
|
979
|
-
|
|
1630
|
+
// (a) 函数声明 —— 顶层 + namespace 内
|
|
1631
|
+
// C4i:旧代码用 sf.getFunctions(),实测取不到 `namespace P { export
|
|
1632
|
+
// function toPath(){} }` 里的函数,整片漏(探针 F1)。改遍历声明节点,
|
|
1633
|
+
// 但**只收顶层 / namespace 链上的**:函数体内嵌套声明的作用域与调用点
|
|
1634
|
+
// 不同,按同一裸名发布会张冠李戴。
|
|
1635
|
+
for (const fn of sf.getDescendantsOfKind(ts.SyntaxKind.FunctionDeclaration)) {
|
|
1636
|
+
const parent = fn.getParent();
|
|
1637
|
+
// 顶层 / namespace 体内两种;函数体内嵌套的声明作用域与调用点不同,不收
|
|
1638
|
+
if (!ts_morph_1.Node.isSourceFile(parent) && !ts_morph_1.Node.isModuleBlock(parent))
|
|
1639
|
+
continue;
|
|
980
1640
|
const name = fn.getName();
|
|
981
1641
|
if (!name)
|
|
982
1642
|
continue;
|
|
983
1643
|
const body = fn.getBody();
|
|
984
1644
|
if (!body)
|
|
985
1645
|
continue; // 重载签名 / declare 只有签名
|
|
986
|
-
|
|
1646
|
+
const ps = fn.getParameters();
|
|
1647
|
+
const paramNodes = ps.flatMap(paramNamesOf);
|
|
1648
|
+
const restAt = restIndexOf(ps);
|
|
1649
|
+
const curry = currySpecOf(paramNodes, body);
|
|
1650
|
+
const owner = moduleQualifier(fn);
|
|
1651
|
+
// namespace 内却认不出宿主链 ⇒ 不收(宁可漏,不可张冠李戴)
|
|
1652
|
+
if (!ts_morph_1.Node.isSourceFile(parent) && owner === null)
|
|
1653
|
+
continue;
|
|
1654
|
+
// namespace 内:`P.toPath(k)` 是主要写法。裸名同时登记是安全的 ——
|
|
1655
|
+
// takenNames 先到先得,一个名字只会落到一处定义上(另一处会降级为资格凭证)。
|
|
1656
|
+
if (owner !== null) {
|
|
1657
|
+
consider(`fn:${owner}.${name}`, `${owner}.${name}`, null, paramNodes, restAt, curry, fn.getText(), body);
|
|
1658
|
+
}
|
|
1659
|
+
consider(`fn:${name}`, name, null, paramNodes, restAt, curry, fn.getText(), body);
|
|
987
1660
|
}
|
|
988
1661
|
// (b) 变量承载的箭头 / 函数表达式 —— 现代 TS 里 helper 的主力写法
|
|
989
1662
|
// `export const toPath = (n: string) => n + ".md";`
|
|
990
1663
|
// sf.getFunctions() 收不到这些,2026-09-20 实测整片漏。
|
|
1664
|
+
// C4i 再解一层 IIFE(functionLikeOf)。
|
|
991
1665
|
for (const vd of sf.getVariableDeclarations()) {
|
|
992
1666
|
const init = vd.getInitializer();
|
|
993
1667
|
if (!init)
|
|
994
1668
|
continue;
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
if (!isArrow && !isFnExpr)
|
|
1669
|
+
const fnLike = functionLikeOf(init);
|
|
1670
|
+
if (!fnLike)
|
|
998
1671
|
continue;
|
|
999
|
-
const fnLike = init;
|
|
1000
1672
|
const body = fnLike.getBody();
|
|
1001
1673
|
if (!body)
|
|
1002
1674
|
continue;
|
|
1003
|
-
|
|
1675
|
+
const ps = fnLike.getParameters();
|
|
1676
|
+
const paramNodes = ps.flatMap(paramNamesOf);
|
|
1677
|
+
consider(`var:${vd.getName()}`, vd.getName(), null, paramNodes, restIndexOf(ps), currySpecOf(paramNodes, body), init.getText(), // IIFE 时含整条调用 ⇒ sink 检查仍覆盖得到
|
|
1678
|
+
body);
|
|
1004
1679
|
}
|
|
1005
1680
|
}
|
|
1006
|
-
//
|
|
1007
|
-
// `const Util = { toPath(n) {…} }`、`class Paths { toPath(n) {…} }
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
|
|
1681
|
+
// ── 成员形态的 helper(对象字面量方法 / 类方法 / 属性箭头 / 属性赋值箭头)──
|
|
1682
|
+
// `const Util = { toPath(n) {…} }`、`class Paths { toPath(n) {…} }`、
|
|
1683
|
+
// `const Util = { toPath: (n) => n + ".md" }`、`Util.toPath = (n) => …`
|
|
1684
|
+
// 都不在 getFunctions() 里。C4e 时刻意不做的理由仍在(R11):按裸名放行会
|
|
1685
|
+
// 让别的对象上的同名条目被误认成塑形。所以这里只登记**限定名** Owner.method;
|
|
1686
|
+
// 裸名(`.method(` 这种成员调用位)留给下面的 C4h-②,按「同名每一处都被
|
|
1687
|
+
// 确证」这条不变量统一放行。
|
|
1688
|
+
let memberSeq = 0;
|
|
1689
|
+
/** 登记一个成员形态的函数体;owner 认得出时按限定名发布 */
|
|
1690
|
+
const considerMember = (bare, owner, paramNodes, restAt, curry, declText, body) => {
|
|
1691
|
+
memberSeq += 1;
|
|
1692
|
+
consider(`mb:${memberSeq}`, owner !== null ? `${owner}.${bare}` : null, bare, paramNodes, restAt, curry, declText, body);
|
|
1693
|
+
};
|
|
1013
1694
|
for (const sf of project.getSourceFiles()) {
|
|
1695
|
+
// (c) 对象字面量方法 / 类方法
|
|
1014
1696
|
for (const md of sf.getDescendantsOfKind(ts.SyntaxKind.MethodDeclaration)) {
|
|
1015
|
-
const bare = md
|
|
1016
|
-
if (!bare)
|
|
1017
|
-
continue;
|
|
1697
|
+
const bare = propertyNameOf(md, constTexts);
|
|
1698
|
+
if (!bare || !/^[A-Za-z_$][\w$]*$/.test(bare))
|
|
1699
|
+
continue; // 算不出的属性名 ⇒ 不收
|
|
1018
1700
|
const body = md.getBody();
|
|
1019
1701
|
if (!body)
|
|
1020
1702
|
continue; // 重载签名 / declare
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1703
|
+
const ps = md.getParameters();
|
|
1704
|
+
const pns = ps.flatMap(paramNamesOf);
|
|
1705
|
+
considerMember(bare, ownerQualifier(md, constTexts), pns, restIndexOf(ps), currySpecOf(pns, body), md.getText(), body);
|
|
1706
|
+
}
|
|
1707
|
+
// (d) 属性承载的箭头 / 函数表达式 —— 现代工程里 Objects-as-namespace 的常态。
|
|
1708
|
+
// ownerQualifier 从父链走(对象字面量 → 属性赋值 → 变量声明),嵌套也认。
|
|
1709
|
+
for (const pa of sf.getDescendantsOfKind(ts.SyntaxKind.PropertyAssignment)) {
|
|
1710
|
+
const init = pa.getInitializer();
|
|
1711
|
+
if (!init)
|
|
1712
|
+
continue;
|
|
1713
|
+
if (!ts_morph_1.Node.isArrowFunction(init) && !ts_morph_1.Node.isFunctionExpression(init))
|
|
1714
|
+
continue;
|
|
1715
|
+
const body = init.getBody();
|
|
1716
|
+
if (!body)
|
|
1717
|
+
continue;
|
|
1718
|
+
const bare = propertyNameOf(pa, constTexts);
|
|
1719
|
+
if (!bare || !/^[A-Za-z_$][\w$]*$/.test(bare))
|
|
1720
|
+
continue;
|
|
1721
|
+
const ps = init.getParameters();
|
|
1722
|
+
const pns = ps.flatMap(paramNamesOf);
|
|
1723
|
+
considerMember(bare, ownerQualifier(pa, constTexts), pns, restIndexOf(ps), currySpecOf(pns, body), init.getText(), body);
|
|
1023
1724
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1725
|
+
// (e) 后挂上去的属性箭头 `Util.toPath = (n) => …`(monkey patch / 渐进导出)
|
|
1726
|
+
for (const be of sf.getDescendantsOfKind(ts.SyntaxKind.BinaryExpression)) {
|
|
1727
|
+
if (be.getOperatorToken().getKind() !== ts.SyntaxKind.EqualsToken)
|
|
1728
|
+
continue;
|
|
1729
|
+
const left = be.getLeft();
|
|
1730
|
+
const right = be.getRight();
|
|
1731
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(left))
|
|
1732
|
+
continue;
|
|
1733
|
+
if (!ts_morph_1.Node.isArrowFunction(right) && !ts_morph_1.Node.isFunctionExpression(right))
|
|
1734
|
+
continue;
|
|
1735
|
+
const body = right.getBody();
|
|
1736
|
+
if (!body)
|
|
1737
|
+
continue;
|
|
1738
|
+
const qname = left.getText();
|
|
1739
|
+
// 只收 `Name.member` 这种纯静态链(`this.x` / `a[b].x` 一律不认)
|
|
1740
|
+
if (!/^[A-Za-z_$][\w$]*(?:\.[\w$]+)+$/.test(qname))
|
|
1741
|
+
continue;
|
|
1742
|
+
const bare = qname.split(".").pop();
|
|
1743
|
+
const ps = right.getParameters();
|
|
1744
|
+
const pns = ps.flatMap(paramNamesOf);
|
|
1745
|
+
considerMember(bare, qname.slice(0, -(bare.length + 1)), pns, restIndexOf(ps), currySpecOf(pns, body), right.getText(), body);
|
|
1040
1746
|
}
|
|
1041
|
-
if (m.qualified)
|
|
1042
|
-
consider(m.qualified, params, text, body);
|
|
1043
1747
|
}
|
|
1044
1748
|
// 形参之外允许出现的自由标识符 = 模块级字面量常量 ∪ 确证的 path 模块别名
|
|
1045
|
-
const extraIdents =
|
|
1749
|
+
const extraIdents = new Set(constTexts.keys());
|
|
1046
1750
|
for (const a of pathModuleAliases(project))
|
|
1047
1751
|
extraIdents.add(a);
|
|
1048
1752
|
const names = new Set();
|
|
1049
1753
|
const info = new Map();
|
|
1754
|
+
const acceptedKeys = new Set();
|
|
1755
|
+
const keyInfo = new Map();
|
|
1756
|
+
// 给 maskNonDependentArgsOnce 用的成员名候选(**尚未确证**)。这里宽一点是安全
|
|
1757
|
+
// 的:那里的替换只会针对 info 里确证过的名字发生,这个集合只决定「按成员位还是
|
|
1758
|
+
// 按裸名位去匹配」。
|
|
1759
|
+
const prelimMember = new Set(groups.keys());
|
|
1760
|
+
const publish = (name, si) => {
|
|
1761
|
+
if (names.has(name))
|
|
1762
|
+
return;
|
|
1763
|
+
names.add(name);
|
|
1764
|
+
info.set(name, si);
|
|
1765
|
+
};
|
|
1050
1766
|
for (let round = 0; round < 2; round++) {
|
|
1051
1767
|
let added = false;
|
|
1052
1768
|
for (const c of cands) {
|
|
1053
|
-
if (
|
|
1769
|
+
if (acceptedKeys.has(c.key))
|
|
1054
1770
|
continue;
|
|
1055
|
-
|
|
1056
|
-
if (
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1771
|
+
let si;
|
|
1772
|
+
if (c.curry !== null) {
|
|
1773
|
+
// 柯里化:判据落在【内层】的 return 上,形参集合是两层并起来
|
|
1774
|
+
const all = new Set([...c.params, ...c.curry.innerParams]);
|
|
1775
|
+
if (!c.curry.rets.every((e) => isPureShaperExpr(e, all, names, extraIdents)))
|
|
1776
|
+
continue;
|
|
1777
|
+
acceptedKeys.add(c.key);
|
|
1778
|
+
// 外层名照发:闭包会捕获外层实参(`const g = withExt(k)` 也是污的)。
|
|
1779
|
+
// rets 留空 ⇒ 调用点折叠自动退回「全部依赖」(保守侧)。
|
|
1780
|
+
si = { params: c.paramList, deps: new Set(c.paramList), rets: [], restAt: c.restAt };
|
|
1781
|
+
}
|
|
1782
|
+
else {
|
|
1783
|
+
if (!c.rets.every((e) => isPureShaperExpr(e, c.params, names, extraIdents)))
|
|
1784
|
+
continue;
|
|
1785
|
+
acceptedKeys.add(c.key);
|
|
1786
|
+
si = {
|
|
1787
|
+
params: c.paramList,
|
|
1788
|
+
deps: returnDeps(c.rets, c.paramList, info, prelimMember),
|
|
1789
|
+
rets: c.rets,
|
|
1790
|
+
restAt: c.restAt,
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
keyInfo.set(c.key, si);
|
|
1794
|
+
if (c.name !== null)
|
|
1795
|
+
publish(c.name, si);
|
|
1063
1796
|
added = true;
|
|
1064
1797
|
}
|
|
1065
1798
|
if (!added)
|
|
1066
1799
|
break;
|
|
1067
1800
|
}
|
|
1801
|
+
// C4i:柯里化名录。只有**发布了名字**的才登记 —— 名字被别处同名定义占用时
|
|
1802
|
+
// (pub 降为 null),`withExt(a)(b)` 里的 withExt 可能指的不是这个 helper,
|
|
1803
|
+
// 展开它会张冠李戴 ⇒ 宁可不展(保守侧,退化为漏报而非误报)。
|
|
1804
|
+
const curried = new Map();
|
|
1805
|
+
for (const c of cands) {
|
|
1806
|
+
if (c.curry !== null && c.name !== null && acceptedKeys.has(c.key)) {
|
|
1807
|
+
curried.set(c.name, c.curry);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1068
1810
|
// C4f:依赖分析有次序依赖 —— `wrap(n) { return discard(n); }` 若在 discard 之前
|
|
1069
1811
|
// 被接受,那一刻 info 里还没有 discard ⇒ deps 偏宽(偏宽 = 继续传播 = 保守侧,
|
|
1070
1812
|
// 不会误报,但收不紧)。名录定稿后按完整的 info 再算两轮,把次序依赖消掉。
|
|
1071
|
-
const
|
|
1813
|
+
const candByKey = new Map(cands.map((c) => [c.key, c]));
|
|
1072
1814
|
for (let round = 0; round < 2; round++) {
|
|
1073
1815
|
let changed = false;
|
|
1074
|
-
for (const [
|
|
1075
|
-
const c =
|
|
1816
|
+
for (const [key, si] of keyInfo) {
|
|
1817
|
+
const c = candByKey.get(key);
|
|
1076
1818
|
if (!c)
|
|
1077
1819
|
continue;
|
|
1078
|
-
const d = returnDeps(c.rets, si.params, info,
|
|
1820
|
+
const d = returnDeps(c.rets, si.params, info, prelimMember);
|
|
1079
1821
|
if (d.size !== si.deps.size || [...d].some((x) => !si.deps.has(x))) {
|
|
1080
1822
|
si.deps = d;
|
|
1081
1823
|
changed = true;
|
|
@@ -1084,7 +1826,26 @@ function pureShaperFunctionNames(project) {
|
|
|
1084
1826
|
if (!changed)
|
|
1085
1827
|
break;
|
|
1086
1828
|
}
|
|
1087
|
-
|
|
1829
|
+
// ── C4h-②(2026-09-20):同名【成员】多处,只要**每一处**都被确证为纯塑形 ──
|
|
1830
|
+
// 旧规则(C4g)是「该方法名全项目唯一才登记裸名」。唯一性只是【不可能认错】这个
|
|
1831
|
+
// 不变量的一个充分条件,不是必要条件:同名两处都被确证了,`.toPath(` 这个成员
|
|
1832
|
+
// 调用位同样不可能落到别的实现上。把"唯一"换成"全部确证",两个类各有一个同名
|
|
1833
|
+
// helper 时两边都能接上;依赖集按形参位取并集(保守侧)。
|
|
1834
|
+
//
|
|
1835
|
+
// 顺带修掉一处真实缺陷:旧代码在 consider() 之后【无条件】把裸名塞进
|
|
1836
|
+
// memberNames,于是「方法名唯一但三条判据不成立」的方法,其成员调用位照样被
|
|
1837
|
+
// 当成塑形 ⇒ 误标。探针实测(2026-09-20):
|
|
1838
|
+
// `class Reg { lookup(n) { return registry[n]; } }`(registry 是模块级非字面量
|
|
1839
|
+
// 对象 ⇒ 判据③不成立)之后写 `r.lookup(k)`,照样MARKED。
|
|
1840
|
+
// 现在改成确证之后才登记,这个口子自然封上。
|
|
1841
|
+
const memberNames = new Set();
|
|
1842
|
+
for (const [bare, keys] of groups) {
|
|
1843
|
+
if (!keys.every((k) => acceptedKeys.has(k)))
|
|
1844
|
+
continue;
|
|
1845
|
+
memberNames.add(bare);
|
|
1846
|
+
publish(bare, mergeShapers(keys.map((k) => keyInfo.get(k)).filter(Boolean)));
|
|
1847
|
+
}
|
|
1848
|
+
return { names, memberNames, info, curried };
|
|
1088
1849
|
}
|
|
1089
1850
|
const TS_FILE_SINK_NAMES = [
|
|
1090
1851
|
"readFile", "readFileSync", "writeFile", "writeFileSync",
|
|
@@ -1336,14 +2097,19 @@ function taintedViaShaper(text, tainted, shapers) {
|
|
|
1336
2097
|
const rhs = m[2];
|
|
1337
2098
|
if (tainted.has(name))
|
|
1338
2099
|
continue;
|
|
1339
|
-
|
|
1340
|
-
|
|
2100
|
+
// C4i:柯里化调用先在证据里摊开 —— `withExt(".md")(k)` ⇒ `k + ".md"`,
|
|
2101
|
+
// 摊开后的串自己就是拼接,shaped 也顺理成章成立。
|
|
2102
|
+
const expanded = shapers ? expandCurried(rhs, shapers.curried) : rhs;
|
|
2103
|
+
const shaped = TS_PATH_SHAPER_RE.test(expanded) ||
|
|
2104
|
+
TS_CONCAT_RE.test(expanded) ||
|
|
1341
2105
|
(localShaperRe !== null && localShaperRe.test(rhs)) ||
|
|
1342
2106
|
(memberShaperRe !== null && memberShaperRe.test(rhs));
|
|
1343
2107
|
// 证据看的是【抹掉不被依赖的实参之后】的 rhs:`discard(k)` 里 k 不流出,
|
|
1344
2108
|
// 那条支路就不该算证据。`shaped` 仍看原式 —— helper 是不是塑形,与它的
|
|
1345
2109
|
// 返回值依不依赖实参是两件事。
|
|
1346
|
-
const evidence = shaperInfo && memberNames
|
|
2110
|
+
const evidence = shaperInfo && memberNames
|
|
2111
|
+
? maskNonDependentArgs(expanded, shaperInfo, memberNames)
|
|
2112
|
+
: expanded;
|
|
1347
2113
|
if (taintRe.test(evidence) && shaped) {
|
|
1348
2114
|
tainted.add(name);
|
|
1349
2115
|
added = true;
|