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:
1 parent
b77e11ad30
commit
5e93ba6f47
2 files changed
+915
-786
No files matched your search
+143
-14
@@ -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
Reference in new issue
Block a user