feat(state): 数据流覆盖匿名函数/IIFE/原型方法,剔除 copy 转发噪声

1) 作用域索引从「具名 function」扩到三类:具名函数、赋值到原型的方法(ld(Cls).m = function)、IIFE((function(t){...})(a,b));形参解析按由内向外逐层尝试。

2) 关键修正:Kotlin 给 data class 生成的 copy/with(原型方法里 return new Cls(同名形参…))是循环证据,不能当类型依据。剔除后覆盖率从虚高的 1185(55.7%) 回落到 689(32.4%),被剔除的 497 个字段标注为「仅 copy 转发(无类型信息)」。

3) 新增 --stats:输出仍认不出的实参形态分布,便于决定下一条规则。

4) 与 158 个运行时真值对比仍无新增冲突(保持 3 处真实分歧:AddBattle.suspicionLevel / UpdateEventPass.eventPassEndTime / SetDrone.resource)。
This commit is contained in:
TermiNexus committed 2026-10-21 21:42:17 +08:00
1 parent b77e11ad30
commit 5e93ba6f47
2 files changed
+915 -786

No files matched your search

+143 -14
View File
@@ -26,6 +26,25 @@ const Arg = (N, D) => { const I = Argv.indexOf('--' + N); return I >= 0 && Argv[
// --audit:对「已有运行时真值」的字段也跑一遍推断,输出冲突,用来校验推断规则是否可靠
const Audit = Argv.indexOf('--audit') >= 0;
const Conflicts = [];
// --stats:统计仍然认不出的实参形态(决定下一条推断规则该打哪里)
const Stats = Argv.indexOf('--stats') >= 0;
const ShapeHist = new Map();
const CallHist = new Map();
function Shape(Expr) {
const E = String(Expr || '').trim();
if (!E) return 'empty';
if (E === 'null') return 'null';
if (/^[A-Za-z_$][\w$]*$/.test(E)) return 'ident';
if (/^this\.[A-Za-z0-9_$]+$/.test(E)) return 'this.field';
if (/^(?:[A-Za-z_$][\w$]*\.)+[A-Za-z0-9_$]+$/.test(E)) return 'a.b.path';
if (/^[A-Za-z_$][\w$]*\(/.test(E) || /^(?:[A-Za-z_$][\w$]*\.)+[A-Za-z0-9_$]+\(/.test(E)) return 'call';
if (/^new\s+/.test(E)) return 'new';
if (/^function|^\(function/.test(E)) return 'func';
if (/^\[/.test(E)) return 'array';
if (/^\{/.test(E)) return 'object';
if (/^!/.test(E)) return 'not';
return 'other';
}
// TankIC/3DTank/tools → TankIC/TankiOffline/...
const OUT = path.resolve(Arg('out', path.join(PROJECT, '..', 'TankiOffline', 'app', 'src', 'jsMain', 'kotlin', 'App', 'State', 'Inventory.kt')));
@@ -183,16 +202,95 @@ function FnIndex(Mod) {
}
return FnIndexCache.get(Mod);
}
// 包住 Off、且形参表里含 Param 的最内层函数
function EnclosingFn(Mod, Off, Param) {
let Best = null;
// 包住 Off、且形参表里含 Param 的所有函数,由内向外(具名 / 赋值到方法 / IIFE 三种)
function ScopesFor(Mod, Off, Param) {
const Out = [];
for (const F of FnIndex(Mod)) {
if (F.Start <= Off && F.End >= Off && F.Params.indexOf(Param) >= 0) Out.push({ ...F, Kind: 'named' });
}
for (const F of AnonIndex(Mod)) {
if (F.Start > Off || F.End < Off || F.Params.indexOf(Param) < 0) continue;
if (F.Method) Out.push({ ...F, Kind: 'method', Name: F.Method });
if (F.Iife) Out.push({ ...F, Kind: 'iife' });
}
Out.sort((A, B) => B.Start - A.Start);
return Out;
}
// 匿名函数表达式:赋值到方法(`x.m = function (…)`)或立即调用(`(function (…) {…})(a, b)`)
const AnonCache = new Map();
function AnonIndex(Mod) {
if (AnonCache.has(Mod)) return AnonCache.get(Mod);
const Text = SrcOf(Mod);
const Pairs = BracePairs(Text);
const List = [];
for (const M of Text.matchAll(/(?<![\w$.])function\s*\(([^)]*)\)\s*\{/g)) {
const Open = M.index + M[0].length - 1;
const End = Pairs.get(Open);
if (End === undefined) continue;
const Asg = /\.([A-Za-z0-9_$]+)\s*=\s*$/.exec(Text.slice(Math.max(0, M.index - 90), M.index));
const Params = M[1].trim() ? M[1].split(',').map((S) => S.trim()) : [];
// 立即调用:} (…) 或 } ) (…)
let I = End + 1;
while (I < Text.length && /\s/.test(Text[I])) I++;
if (Text[I] === ')') { I++; while (I < Text.length && /\s/.test(Text[I])) I++; }
let Iife = null;
let IifeAt = -1;
if (Text[I] === '(') {
const Close = MatchParen(Text, I);
if (Close > 0) { Iife = SplitArgs(Text.slice(I + 1, Close)); IifeAt = I; }
}
List.push({ Params, Start: M.index, End, Method: Asg ? Asg[1] : null, Iife, IifeAt });
}
AnonCache.set(Mod, List);
return List;
}
// `x.<Name>(…)` 调用点(用于 `x.m = function (…)` 这种原型方法)
const MethodCallsCache = new Map();
function MethodCalls(Mod, Name) {
const Key = Mod + '|.' + Name;
if (MethodCallsCache.has(Key)) return MethodCallsCache.get(Key);
const Text = SrcOf(Mod);
const Out = [];
const Re = new RegExp('\\.' + Name.replace(/[$]/g, '\\$') + '\\s*\\(', 'g');
let M;
while ((M = Re.exec(Text)) && Out.length < 40) {
const Open = Text.indexOf('(', M.index);
if (/=\s*function\s*$/.test(Text.slice(Math.max(0, M.index - 20), M.index))) continue;
const End = MatchParen(Text, Open);
if (End < 0) continue;
Out.push({ Args: SplitArgs(Text.slice(Open + 1, End)), At: M.index });
}
MethodCallsCache.set(Key, Out);
return Out;
}
// 包住 Off 的最内层函数(不管形参是什么)
function InnermostScope(Mod, Off) {
let Best = null;
for (const F of FnIndex(Mod)) if (F.Start <= Off && F.End >= Off && (!Best || F.Start > Best.Start)) Best = { ...F, Kind: 'named' };
for (const F of AnonIndex(Mod)) {
if (F.Start > Off || F.End < Off) continue;
if (F.Params.indexOf(Param) < 0) continue;
if (!Best || F.Start > Best.Start) Best = F;
if (!Best || F.Start > Best.Start) Best = { ...F, Kind: F.Method ? 'method' : (F.Iife ? 'iife' : 'anon'), Name: F.Method };
}
return Best;
}
// 原样转发:实参就是所在函数的形参表,且该函数挂在原型上(`ld(Cls).m = function (…) { return new Cls(…) }`)。
// 这是 Kotlin 为 data class 生成的 copy/with,类型信息就是类自身的字段,属于循环,不能当证据。
// 自由函数里的 `make(a, b) { return new X(a, b) }` 不在此列(那条链是有信息的)。
const ForwardCache = new WeakSet();
const ForwardJudged = new WeakSet();
function IsForwarding(Mod, Site) {
if (ForwardJudged.has(Site)) return ForwardCache.has(Site);
ForwardJudged.add(Site);
const Sc = InnermostScope(Mod, Site.At);
if (!Sc || Sc.Kind !== 'method' || !Sc.Params.length) return false;
if (Sc.Params.length !== Site.Args.length) return false;
for (let I = 0; I < Sc.Params.length; I++) if (Site.Args[I].trim() !== Sc.Params[I]) return false;
ForwardCache.add(Site);
return true;
}
const FnCallsCache = new Map();
function FnCalls(Mod, Name) {
@@ -275,20 +373,25 @@ function InferDeep(Expr, Mod, Off, Depth) {
if (FlowSeen.has(Guard)) return null;
FlowSeen.add(Guard);
try {
// ① 形参 → 调用点(按调用点自身的作用域继续回溯)
const Fn = EnclosingFn(Mod, Off, E);
if (Fn) {
const Idx = Fn.Params.indexOf(E);
// ① 形参 → 调用点 / IIFE 实参(由内向外逐层试,按调用点自身的作用域继续回溯)
const Scopes = ScopesFor(Mod, Off, E);
for (const Scope of Scopes) {
const Idx = Scope.Params.indexOf(E);
const Types = [];
for (const Site of FnCalls(Mod, Fn.Name)) {
if (Site.Args.length <= Idx) continue;
Types.push(InferDeep(Site.Args[Idx], Mod, Site.At, Depth - 1));
if (Scope.Kind === 'iife') {
if (Scope.Iife.length > Idx) Types.push(InferDeep(Scope.Iife[Idx], Mod, Scope.IifeAt, Depth - 1));
} else {
const CallSites = Scope.Kind === 'method' ? MethodCalls(Mod, Scope.Name) : FnCalls(Mod, Scope.Name);
for (const Site of CallSites) {
if (Site.Args.length <= Idx) continue;
Types.push(InferDeep(Site.Args[Idx], Mod, Site.At, Depth - 1));
}
}
const U = Unify(Types);
if (U) return U;
}
// ② 所在函数体内的最近一次赋值
const Assign = LastAssign(Text, E, Fn ? Fn.Start : 0, Off);
const Assign = LastAssign(Text, E, Scopes.length ? Scopes[0].Start : 0, Off);
if (Assign && Assign !== E) return InferDeep(Assign, Mod, Off, Depth - 1);
} finally {
FlowSeen.delete(Guard);
@@ -315,11 +418,15 @@ for (const R of Inventory.Rows) {
let InferredSource = '';
const Notes = [];
let NullSeen = false;
let SiteSeen = 0;
let SiteSkipped = 0;
{
const Deep = [];
for (const S of Sites) {
const A = S.Args[I];
if (A === undefined) continue;
SiteSeen++;
if (IsForwarding(S.Module, S)) { SiteSkipped++; continue; }
if (A.trim() === 'null') NullSeen = true;
const T = InferType(A);
if (T) { if (!Inferred) { Inferred = T; InferredSource = 'callsite'; } }
@@ -346,7 +453,23 @@ for (const R of Inventory.Rows) {
else { Note = '// 未收录类型: ' + C.Bad + '(' + Source + ')'; Type = null; }
}
if (Type) TypedFields++;
if (!Type && !Note) Note = Notes.length ? '// 调用点: ' + Notes.join(' | ') : '// 无调用点';
if (Stats && !Type) {
for (const S of Sites) {
const A = S.Args[I];
if (A === undefined) continue;
const K = Shape(A);
ShapeHist.set(K, (ShapeHist.get(K) || 0) + 1);
if (K === 'call') {
const All = String(A).trim().match(/[A-Za-z0-9_$]+(?=\s*\()/g) || [];
const Name = All.length ? All[All.length - 1] : '?';
CallHist.set(Name, (CallHist.get(Name) || 0) + 1);
}
}
}
if (!Type && !Note) {
if (SiteSeen && SiteSkipped === SiteSeen) Note = '// 仅 copy 转发(无类型信息)';
else Note = Notes.length ? '// 调用点: ' + Notes.join(' | ') : '// 无调用点';
}
if (!Note) Note = '// ' + Source;
// 注释必须压成单行,且逗号要放在注释之前(否则逗号被 // 吞掉)
Note = Note.replace(/\s+/g, ' ').slice(0, 120);
@@ -374,4 +497,10 @@ if (Audit) {
console.log('推断 vs 运行时真值冲突 ' + Conflicts.length + ' 处');
for (const C of Conflicts.slice(0, 25)) console.log(' ' + C);
}
if (Stats) {
console.log('认不出的实参形态:');
for (const [K, V] of [...ShapeHist.entries()].sort((A, B) => B[1] - A[1])) console.log(' ' + K + ' = ' + V);
console.log('其中调用表达式的被调名 Top 15:');
for (const [K, V] of [...CallHist.entries()].sort((A, B) => B[1] - A[1]).slice(0, 15)) console.log(' ' + K + ' = ' + V);
}
console.log(' 输出: ' + OUT);
File diff suppressed because it is too large. Load diff