811 lines
34 KiB
JavaScript
811 lines
34 KiB
JavaScript
// gen_action_kt.js —— 生成 S3 状态层的 Kotlin 数据类(动作 + 状态)
|
||
//
|
||
// 输入:
|
||
// names/action_inventory_10032.json 类名 / 构造参数 / toString 字段顺序
|
||
// names/action_calls_10032.json 每个类的调用点实参表达式
|
||
// names/codec_fields.json 协议字段的 Prop → 线上类型(类型反查的权威来源)
|
||
// 输出:
|
||
// TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt
|
||
//
|
||
// 类型判定(认不出就写 Any? 并附调用点原文,不静默猜):
|
||
// !0 / !1 → Boolean
|
||
// 纯数字字面量 → Int(带小数点 → Double)
|
||
// "…" → String
|
||
// <x>.<prop> → 查 codec_fields 的 Prop 得到协议类型再映射 Kotlin
|
||
// new <Cls>(…) / <Cls>() → 该 Cls(同表内的类)或 Any?
|
||
// 其它 → Any?(注释里留原文)
|
||
//
|
||
// 用法: node gen_action_kt.js [--out <文件>]
|
||
'use strict';
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const PROJECT = path.resolve(__dirname, '..');
|
||
const Argv = process.argv.slice(2);
|
||
const Arg = (N, D) => { const I = Argv.indexOf('--' + N); return I >= 0 && Argv[I + 1] ? Argv[I + 1] : D; };
|
||
// --audit:对「已有运行时真值」的字段也跑一遍推断,输出冲突,用来校验推断规则是否可靠
|
||
const Audit = Argv.indexOf('--audit') >= 0;
|
||
const Conflicts = [];
|
||
// --stats:统计仍然认不出的实参形态(决定下一条推断规则该打哪里)
|
||
const Stats = Argv.indexOf('--stats') >= 0;
|
||
// --why <类名.字段>:打印该字段每个调用点的推断结果,用于排查错型
|
||
const Why = Arg('why', '');
|
||
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')));
|
||
|
||
const Inventory = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'action_inventory_10032.json'), 'utf8'));
|
||
const Calls = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'action_calls_10032.json'), 'utf8')).Calls || {};
|
||
const Codec = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'codec_fields.json'), 'utf8'));
|
||
// 运行时真值(客户端 dispatch 打点解析出来的字段类型)优先级最高
|
||
let RuntimeTypes = {};
|
||
let RuntimeModels = {};
|
||
let RuntimeSamples = {};
|
||
try {
|
||
const J = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'action_types_10032.json'), 'utf8'));
|
||
RuntimeTypes = J.Types || {};
|
||
RuntimeModels = J.Models || {};
|
||
RuntimeSamples = J.Samples || {};
|
||
} catch (E) { RuntimeTypes = {}; RuntimeModels = {}; RuntimeSamples = {}; }
|
||
|
||
// Prop → 协议类型(重复项若类型不一致则不采用)
|
||
const PropType = new Map();
|
||
const PropBad = new Set();
|
||
for (const C of Object.values(Codec.Codecs)) {
|
||
for (const F of C.Fields || []) {
|
||
if (!F.Prop) continue;
|
||
if (PropBad.has(F.Prop)) continue;
|
||
const Prev = PropType.get(F.Prop);
|
||
if (Prev && Prev !== F.Type) { PropType.delete(F.Prop); PropBad.add(F.Prop); continue; }
|
||
if (!Prev) PropType.set(F.Prop, F.Type);
|
||
}
|
||
}
|
||
|
||
// 协议类型 → Kotlin
|
||
function KotlinOf(ProtoType) {
|
||
let T = String(ProtoType || '').trim();
|
||
let Suffix = '';
|
||
if (T.startsWith('opt<')) { T = T.slice(4, -1).trim(); Suffix = '?'; }
|
||
const L = /^list<([\s\S]+)>$/.exec(T);
|
||
if (L) return 'List<' + KotlinOf(L[1]).replace(/\?$/, '') + '>' + Suffix;
|
||
const M = /^map<([\s\S]+)>$/.exec(T);
|
||
if (M) {
|
||
const P = M[1].split(',').map((X) => KotlinOf(X).replace(/\?$/, ''));
|
||
return 'Map<' + P[0] + ', ' + P[1] + '>' + Suffix;
|
||
}
|
||
if (T.startsWith('enum<')) return 'Int' + Suffix;
|
||
const Base = {
|
||
bool: 'Boolean', i8: 'Int', i16: 'Int', i32: 'Int', i64: 'Long',
|
||
f32: 'Float', f64: 'Double', string: 'String', bytes: 'ByteArray',
|
||
vector3: 'Vector3',
|
||
}[T];
|
||
return (Base || 'Any?') + (Base ? Suffix : '');
|
||
}
|
||
|
||
const Names = new Set(Inventory.Rows.map((R) => R.Name));
|
||
// 运行时 toString 里出现的嵌套对象类(EmissionColors / Coloring …)。
|
||
// 有三种情况:
|
||
// ① 已有同名数据类且字段集覆盖运行时字段 → 同一个类,直接用(并把它选为该名字的代表行)
|
||
// ② 有同名数据类但字段集对不上 → 不是同一个类,放弃,避免张冠李戴
|
||
// ③ 没有同名数据类 → 按运行时字段现生成一个 data class
|
||
const RowsByName = new Map();
|
||
for (const R of Inventory.Rows) {
|
||
const List = RowsByName.get(R.Name) || [];
|
||
List.push(R);
|
||
RowsByName.set(R.Name, List);
|
||
}
|
||
const ModelDefs = {};
|
||
const KnownModels = new Set();
|
||
const EmitModels = new Set();
|
||
const RowPick = new Map(); // 名字 → 与运行时证据吻合的那一行
|
||
for (const [Name, Fields] of Object.entries(RuntimeModels)) {
|
||
const Rows0 = RowsByName.get(Name);
|
||
if (Rows0) {
|
||
const Keys = Object.keys(Fields);
|
||
const Match = Rows0.find((R) => {
|
||
const Fs = new Set(R.ToStringFields.length ? R.ToStringFields : R.Params);
|
||
return Keys.every((K) => Fs.has(K));
|
||
});
|
||
if (!Match) continue;
|
||
KnownModels.add(Name);
|
||
RowPick.set(Name, Match);
|
||
continue;
|
||
}
|
||
ModelDefs[Name] = Fields;
|
||
KnownModels.add(Name);
|
||
EmitModels.add(Name);
|
||
}
|
||
// 同名多行时,除了上面的对象类证据,动作的运行时字段集同样能指出「哪一行才是真的」。
|
||
// 优先用「样例那一行」的字段集(同名不同类的类型会互相污染,合并后的 RuntimeTypes 可能多出不属于该类的字段)。
|
||
function SampleFieldNames(Raw) {
|
||
const Open = String(Raw).indexOf('(');
|
||
const Close = String(Raw).lastIndexOf(')');
|
||
if (Open < 0 || Close <= Open) return [];
|
||
const Body = String(Raw).slice(Open + 1, Close);
|
||
if (!Body.trim()) return [];
|
||
const Parts = [];
|
||
let Depth = 0;
|
||
let Cur = '';
|
||
for (const C of Body) {
|
||
if (C === '(' || C === '[' || C === '{') Depth++;
|
||
else if (C === ')' || C === ']' || C === '}') Depth--;
|
||
if (C === ',' && Depth === 0) { Parts.push(Cur); Cur = ''; continue; }
|
||
Cur += C;
|
||
}
|
||
if (Cur.trim()) Parts.push(Cur);
|
||
const Out = [];
|
||
for (const P of Parts) {
|
||
const Eq = P.indexOf('=');
|
||
if (Eq > 0) Out.push(P.slice(0, Eq).trim());
|
||
}
|
||
return Out;
|
||
}
|
||
for (const [Name, Raw] of Object.entries(RuntimeSamples)) {
|
||
const Rows0 = RowsByName.get(Name);
|
||
if (!Rows0) continue;
|
||
const Keys = SampleFieldNames(Raw);
|
||
if (!Keys.length) continue;
|
||
const Match = Rows0.find((R) => {
|
||
const Fs = new Set(R.ToStringFields.length ? R.ToStringFields : R.Params);
|
||
return Keys.every((K) => Fs.has(K));
|
||
});
|
||
if (Match) RowPick.set(Name, Match);
|
||
}
|
||
for (const [Name, Fields] of Object.entries(RuntimeTypes)) {
|
||
if (RowPick.has(Name)) continue;
|
||
const Rows0 = RowsByName.get(Name);
|
||
if (!Rows0) continue;
|
||
const Keys = Object.keys(Fields);
|
||
const Match = Rows0.find((R) => {
|
||
const Fs = new Set(R.ToStringFields.length ? R.ToStringFields : R.Params);
|
||
return Keys.every((K) => Fs.has(K));
|
||
});
|
||
if (Match) RowPick.set(Name, Match);
|
||
}
|
||
// Kotlin 关键字做属性名时要加反引号
|
||
const KotlinKeyword = new Set(['as', 'break', 'class', 'continue', 'do', 'else', 'false', 'for', 'fun', 'if', 'in',
|
||
'interface', 'is', 'null', 'object', 'package', 'return', 'super', 'this', 'throw', 'true', 'try', 'typealias',
|
||
'typeof', 'val', 'var', 'when', 'while', 'by', 'catch', 'constructor', 'delegate', 'dynamic', 'field', 'file',
|
||
'finally', 'get', 'import', 'init', 'param', 'property', 'receiver', 'set', 'setparam', 'where', 'actual',
|
||
'abstract', 'annotation', 'companion', 'const', 'crossinline', 'data', 'enum', 'expect', 'external', 'final',
|
||
'infix', 'inline', 'inner', 'internal', 'lateinit', 'noinline', 'open', 'operator', 'out', 'override', 'private',
|
||
'protected', 'public', 'reified', 'sealed', 'suspend', 'tailrec', 'vararg']);
|
||
function EscapeName(N) {
|
||
return KotlinKeyword.has(N) ? '`' + N + '`' : N;
|
||
}
|
||
const PrimitiveKinds = new Set(['Int', 'Long', 'Double', 'Boolean', 'String', 'Any?']);
|
||
|
||
function InferType(Expr) {
|
||
const E = String(Expr || '').trim();
|
||
if (!E) return null;
|
||
if (E === '!0' || E === '!1') return 'Boolean';
|
||
if (/^-?\d+$/.test(E)) return 'Int';
|
||
if (/^-?\d+\.\d+$/.test(E)) return 'Double';
|
||
if (/^".*"$/.test(E)) return 'String';
|
||
const Prop = /^[A-Za-z_$][\w$]*\.([A-Za-z0-9_$]+)$/.exec(E);
|
||
if (Prop) {
|
||
const T = PropType.get(Prop[1]);
|
||
if (T) return KotlinOf(T);
|
||
return null;
|
||
}
|
||
const New = /^new\s+([A-Za-z0-9_$]+)\s*\(/.exec(E);
|
||
if (New && Names.has(New[1])) return New[1];
|
||
// 布尔表达式要判在顶层:`(i = i === cd ? null : i)` 这种「赋值 + 三元」不是 Boolean
|
||
const B = Booleanish(E);
|
||
if (B) return B;
|
||
return null;
|
||
}
|
||
|
||
function SkipStr(S, I) {
|
||
const Q = S[I];
|
||
I++;
|
||
while (I < S.length) {
|
||
if (S[I] === '\\') { I += 2; continue; }
|
||
if (S[I] === Q) return I + 1;
|
||
I++;
|
||
}
|
||
return I;
|
||
}
|
||
function HasTopLevel(S, Needle) {
|
||
let Depth = 0;
|
||
for (let I = 0; I < S.length; I++) {
|
||
const C = S[I];
|
||
if (C === '"' || C === "'" || C === '`') { I = SkipStr(S, I) - 1; continue; }
|
||
if (C === '(' || C === '[' || C === '{') { Depth++; continue; }
|
||
if (C === ')' || C === ']' || C === '}') { Depth--; continue; }
|
||
if (Depth === 0 && S.startsWith(Needle, I)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
function StripOuterParens(S) {
|
||
let T = S;
|
||
while (T.startsWith('(') && T.endsWith(')')) {
|
||
let Depth = 0;
|
||
let ClosedEarly = false;
|
||
for (let I = 0; I < T.length; I++) {
|
||
const C = T[I];
|
||
if (C === '"' || C === "'" || C === '`') { I = SkipStr(T, I) - 1; continue; }
|
||
if (C === '(') Depth++;
|
||
else if (C === ')') { Depth--; if (Depth === 0 && I < T.length - 1) { ClosedEarly = true; break; } }
|
||
}
|
||
if (ClosedEarly) break;
|
||
T = T.slice(1, -1).trim();
|
||
}
|
||
return T;
|
||
}
|
||
// 顶层赋值 `X = expr` → 返回 `=` 的下标(排除 ==/!=/<=/>=)
|
||
function TopLevelAssign(S) {
|
||
let Depth = 0;
|
||
for (let I = 0; I < S.length; I++) {
|
||
const C = S[I];
|
||
if (C === '"' || C === "'" || C === '`') { I = SkipStr(S, I) - 1; continue; }
|
||
if (C === '(' || C === '[' || C === '{') { Depth++; continue; }
|
||
if (C === ')' || C === ']' || C === '}') { Depth--; continue; }
|
||
if (Depth === 0 && C === '=' && S[I + 1] !== '=' && '=!<>'.indexOf(S[I - 1] || '') < 0) return I;
|
||
}
|
||
return -1;
|
||
}
|
||
function Booleanish(E) {
|
||
const S = StripOuterParens(String(E || '').trim());
|
||
if (!S) return null;
|
||
if (S[0] === '!') return 'Boolean';
|
||
const Asg = TopLevelAssign(S);
|
||
if (Asg >= 0) return Booleanish(S.slice(Asg + 1));
|
||
if (HasTopLevel(S, '?')) return null; // 三元表达式类型不定
|
||
if (/\.equals\(/.test(S) || /\binstanceof\b/.test(S)) return 'Boolean';
|
||
for (const Op of ['===', '!==', '==', '!=', '>=', '<=', '>', '<']) {
|
||
if (HasTopLevel(S, Op)) return 'Boolean';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 只接受「当前工程里确实存在」的 Kotlin 类型,否则退回 Any?(避免生成引用不到的名字而编译失败)
|
||
// · 标量 / Vector3 / 它们的容器 → 接受
|
||
// · 运行时 toString 里的模型类名 → 拒绝(那是 JS 模型名,本工程没有对应 Kotlin 类)
|
||
// · 调用点 new <动作类>(...) → 接受(同文件内确有该 data class,且不能是自身)
|
||
const Primitive = new Set(['Boolean', 'Int', 'Long', 'Float', 'Double', 'String', 'ByteArray', 'Vector3']);
|
||
function CheckType(T, Source, Self) {
|
||
const S = String(T).trim().replace(/\?$/, '');
|
||
if (Primitive.has(S)) return { Ok: true, Type: T };
|
||
const L = /^List<([\s\S]+)>$/.exec(S);
|
||
if (L) {
|
||
const I = CheckType(L[1], Source, Self);
|
||
return I.Ok ? { Ok: true, Type: 'List<' + I.Type.replace(/\?$/, '') + '>' + (/\?$/.test(T) ? '?' : '') } : { Ok: false, Bad: T };
|
||
}
|
||
const M = /^Map<([\s\S]+)>$/.exec(S);
|
||
if (M) {
|
||
const P = M[1].split(',').map((X) => CheckType(X.trim(), Source, Self));
|
||
return P.every((X) => X.Ok)
|
||
? { Ok: true, Type: 'Map<' + P[0].Type.replace(/\?$/, '') + ', ' + P[1].Type.replace(/\?$/, '') + '>' + (/\?$/.test(T) ? '?' : '') }
|
||
: { Ok: false, Bad: T };
|
||
}
|
||
// 运行时 toString 里出现过的对象类(已确认与同名数据类一致,或已现生成),可直接当类型用
|
||
if (Source === 'runtime' && KnownModels.has(S) && S !== Self) return { Ok: true, Type: T };
|
||
if ((Source === 'callsite' || Source === 'dataflow') && Names.has(S) && S !== Self) return { Ok: true, Type: T };
|
||
return { Ok: false, Bad: T };
|
||
}
|
||
|
||
// ---- 纯静态局部数据流:实参是变量时,回溯形参/局部赋值 ----
|
||
// 只做两种最可靠的形状,且深度受限、冲突即放弃(不猜):
|
||
// ① 标识符是某个函数的形参 → 找该函数的调用点,推断对应位置的实参类型
|
||
// ② 标识符最近一次被赋值 → 推断赋值右侧表达式
|
||
const SrcDir = path.join(PROJECT, 'src');
|
||
const SrcCache = new Map();
|
||
function SrcOf(Mod) {
|
||
if (!SrcCache.has(Mod)) {
|
||
let Text = '';
|
||
try { Text = fs.readFileSync(path.join(SrcDir, 'module_' + String(Mod).replace(/^module_/, '') + '.js'), 'utf8'); } catch (E) { Text = ''; }
|
||
SrcCache.set(Mod, Text);
|
||
}
|
||
return SrcCache.get(Mod);
|
||
}
|
||
|
||
// 模块内所有 `function Name(params) {`(带花括号配对得到的函数体范围)
|
||
const FnIndexCache = new Map();
|
||
const BraceCache = new Map();
|
||
function BracePairs(Text) {
|
||
if (BraceCache.has(Text)) return BraceCache.get(Text);
|
||
const Pairs = new Map();
|
||
const Stack = [];
|
||
let I = 0;
|
||
while (I < Text.length) {
|
||
const C = Text[I];
|
||
if (C === '"' || C === "'" || C === '`') { I = SkipString(Text, I); continue; }
|
||
if (C === '/' && Text[I + 1] === '/') { const N = Text.indexOf('\n', I); if (N < 0) break; I = N; continue; }
|
||
if (C === '/' && Text[I + 1] === '*') { const N = Text.indexOf('*/', I + 2); if (N < 0) break; I = N + 2; continue; }
|
||
if (C === '{') Stack.push(I);
|
||
else if (C === '}') { const O = Stack.pop(); if (O !== undefined) Pairs.set(O, I); }
|
||
I++;
|
||
}
|
||
BraceCache.set(Text, Pairs);
|
||
return Pairs;
|
||
}
|
||
function SkipString(Text, I) {
|
||
const Q = Text[I];
|
||
I++;
|
||
while (I < Text.length) {
|
||
if (Text[I] === '\\') { I += 2; continue; }
|
||
if (Text[I] === Q) return I + 1;
|
||
I++;
|
||
}
|
||
return I;
|
||
}
|
||
function FnIndex(Mod) {
|
||
if (!FnIndexCache.has(Mod)) {
|
||
const Text = SrcOf(Mod);
|
||
const Pairs = BracePairs(Text);
|
||
const List = [];
|
||
for (const M of Text.matchAll(/(?<![\w$.])function\s+([A-Za-z0-9_$]+)\s*\(([^)]*)\)\s*\{/g)) {
|
||
const Open = M.index + M[0].length - 1;
|
||
const End = Pairs.get(Open);
|
||
if (End === undefined) continue;
|
||
List.push({
|
||
Name: M[1],
|
||
Params: M[2].trim() ? M[2].split(',').map((S) => S.trim()) : [],
|
||
Start: M.index,
|
||
End,
|
||
});
|
||
}
|
||
FnIndexCache.set(Mod, List);
|
||
}
|
||
return FnIndexCache.get(Mod);
|
||
}
|
||
// 包住 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 (!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) {
|
||
const Key = Mod + '|' + Name;
|
||
if (FnCallsCache.has(Key)) return FnCallsCache.get(Key);
|
||
const Text = SrcOf(Mod);
|
||
const Out = [];
|
||
const Re = new RegExp('(?<![\\w$.])' + Name.replace(/[$]/g, '\\$') + '\\s*\\(', 'g');
|
||
let M;
|
||
while ((M = Re.exec(Text)) && Out.length < 40) {
|
||
if (/function\s+$/.test(Text.slice(Math.max(0, M.index - 10), M.index))) continue;
|
||
const End = MatchParen(Text, Text.indexOf('(', M.index));
|
||
if (End < 0) continue;
|
||
Out.push({ Args: SplitArgs(Text.slice(Text.indexOf('(', M.index) + 1, End)), At: M.index });
|
||
}
|
||
FnCallsCache.set(Key, Out);
|
||
return Out;
|
||
}
|
||
|
||
function MatchParen(Text, Open) {
|
||
let Depth = 0;
|
||
for (let I = Open; I < Text.length; I++) {
|
||
const C = Text[I];
|
||
if (C === '(') Depth++;
|
||
else if (C === ')') { Depth--; if (Depth === 0) return I; }
|
||
}
|
||
return -1;
|
||
}
|
||
function SplitArgs(Text) {
|
||
const Out = [];
|
||
let Depth = 0;
|
||
let Cur = '';
|
||
for (const C of Text) {
|
||
if (C === '(' || C === '[' || C === '{') Depth++;
|
||
else if (C === ')' || C === ']' || C === '}') Depth--;
|
||
if (C === ',' && Depth === 0) { Out.push(Cur.trim()); Cur = ''; continue; }
|
||
Cur += C;
|
||
}
|
||
if (Cur.trim()) Out.push(Cur.trim());
|
||
return Out;
|
||
}
|
||
// [From, To) 范围内最近一次 `X = <expr>`
|
||
function LastAssign(Text, Name, From, To) {
|
||
const Re = new RegExp('(?<![\\w$.])' + Name.replace(/[$]/g, '\\$') + '\\s*=\\s*([^;\\n]{1,80})', 'g');
|
||
let Best = null;
|
||
let M;
|
||
while ((M = Re.exec(Text)) && M.index < To) {
|
||
if (M.index >= From) Best = M[1];
|
||
}
|
||
if (!Best) return null;
|
||
const E = Best.trim().replace(/[,)]+$/, '').trim();
|
||
return /^[A-Za-z0-9_$"]/.test(E) || /^!/.test(E) ? E : null;
|
||
}
|
||
// 多来源类型合一:全同 → 该类型;Int/Long 混合 → Long;否则放弃
|
||
function Unify(List) {
|
||
const S = new Set(List.filter(Boolean));
|
||
if (!S.size) return null;
|
||
if (S.size === 1) return [...S][0];
|
||
if (S.size === 2 && S.has('Int') && S.has('Long')) return 'Long';
|
||
return null;
|
||
}
|
||
|
||
const FlowSeen = new Set();
|
||
function InferDeep(Expr, Mod, Off, Depth) {
|
||
const E = String(Expr || '').trim();
|
||
if (!E) return null;
|
||
const Base = InferType(E);
|
||
if (Base) return Base;
|
||
if (Depth <= 0 || !Mod) return null;
|
||
const Text = SrcOf(Mod);
|
||
if (!Text) return null;
|
||
// a.b.c 形式:用最后一段查协议字段表
|
||
const Chain = /^(?:[A-Za-z_$][\w$]*\.)+([A-Za-z0-9_$]+)$/.exec(E);
|
||
if (Chain) {
|
||
const P = PropType.get(Chain[1]);
|
||
return P ? KotlinOf(P) : null;
|
||
}
|
||
if (!/^[A-Za-z_$][\w$]*$/.test(E)) return null;
|
||
const Guard = Mod + '|' + Off + '|' + E + '|' + Depth;
|
||
if (FlowSeen.has(Guard)) return null;
|
||
FlowSeen.add(Guard);
|
||
try {
|
||
// ① 形参 → 调用点 / IIFE 实参(由内向外逐层试,按调用点自身的作用域继续回溯)
|
||
const Scopes = ScopesFor(Mod, Off, E);
|
||
for (const Scope of Scopes) {
|
||
const Idx = Scope.Params.indexOf(E);
|
||
const Types = [];
|
||
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, Scopes.length ? Scopes[0].Start : 0, Off);
|
||
if (Assign && Assign !== E) return InferDeep(Assign, Mod, Off, Depth - 1);
|
||
} finally {
|
||
FlowSeen.delete(Guard);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 个别字段的调用点/运行时证据只能是 Any?(例如 GoTo 样例里的 screen=[object Object]),
|
||
// 但切片 reducer 已经能给出强类型时在这里显式指定。键:`类名.字段`。
|
||
// GoTo.screen:QK.grm_1 在 screen 切片里只按 ScreenId 使用(instanceof LX/RX + m()/equals),
|
||
// 类型定义见 TankiOffline/.../App/State/ScreenId.kt。
|
||
const FieldTypeOverrides = new Map([
|
||
['GoTo.screen', 'ScreenId'],
|
||
]);
|
||
|
||
const Emitted = [];
|
||
// 动作目录:类名 + 字段(名, 类型),用于生成 ActionCatalog.kt(运行时按名字查类)
|
||
const Catalog = [];
|
||
const Seen = new Set();
|
||
let TypedFields = 0;
|
||
let TotalFields = 0;
|
||
for (const R of Inventory.Rows) {
|
||
if (Seen.has(R.Name)) continue;
|
||
// 同名多行时,优先用与运行时证据吻合的那一行
|
||
const Pick = RowPick.get(R.Name);
|
||
if (Pick && Pick !== R) continue;
|
||
Seen.add(R.Name);
|
||
// 调用点按「动作名|局部类名」索引:同名不同类(如 Init 的 1 字段版/18 字段版)不能混用
|
||
const Sites = Calls[R.Name + '|' + R.Local] || [];
|
||
const Fields = R.ToStringFields.length ? R.ToStringFields : R.Params.map((P, I) => 'field' + (I + 1));
|
||
const Lines = [];
|
||
const FieldPairs = [];
|
||
Fields.forEach((F, I) => {
|
||
TotalFields++;
|
||
// ① 运行时打点(真值)→ ② 调用点/数据流反推 → ③ Any?
|
||
// 运行时也可能「什么也没说」(值全是空列表 / [object Object]),这时不能挡住调用点推断
|
||
const RtRaw = RuntimeTypes[R.Name] && RuntimeTypes[R.Name][F];
|
||
// Opaque = 运行时看到对象但说不出类型,此时不能用调用点猜;其余「没信息」类型才让位
|
||
const Rt = !RtRaw ? null
|
||
: (RtRaw === 'Opaque' ? 'Any?'
|
||
: (/^(Any\?|List<Any\?>|Map<String, Any\?>|Map<String, Any>)$/.test(RtRaw) ? null : RtRaw));
|
||
let Inferred = null;
|
||
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 (Why === R.Name + '.' + F) {
|
||
const Direct = InferType(A);
|
||
const DeepOne = Direct || InferDeep(A, S.Module, S.At, 2);
|
||
console.log(' [why] ' + S.Module + '@' + S.At + ' 实参=' + String(A).replace(/\s+/g, ' ').slice(0, 70) +
|
||
' 直接=' + (Direct || '-') + ' 深推=' + (DeepOne || '-'));
|
||
}
|
||
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'; } }
|
||
else {
|
||
if (Notes.length < 2) Notes.push(A.slice(0, 40));
|
||
const D = InferDeep(A, S.Module, S.At, 2);
|
||
if (D) Deep.push(D);
|
||
}
|
||
}
|
||
if (!Inferred) {
|
||
const U = Unify(Deep);
|
||
if (U) { Inferred = U; InferredSource = 'dataflow'; }
|
||
}
|
||
}
|
||
// 协议字段表的 opt<> 只说明「线上可选」,动作字段本身没观察到 null 就当非空。
|
||
// 但运行时若见过 null(RtRaw 为 Any? 或带 ?),保留可空性。
|
||
if (Inferred && !NullSeen) {
|
||
const RuntimeNullish = RtRaw === 'Any?' || (RtRaw && RtRaw.endsWith('?'));
|
||
if (!RuntimeNullish) Inferred = Inferred.replace(/\?$/, '');
|
||
}
|
||
if (Audit && Rt && Inferred && Rt.replace(/\?$/, '') !== Inferred.replace(/\?$/, '')) {
|
||
Conflicts.push(R.Name + '.' + F + ' 运行时=' + Rt + ' 推断=' + Inferred + '(' + InferredSource + ')');
|
||
}
|
||
let Type = Rt || Inferred;
|
||
let Source = Rt ? 'runtime' : InferredSource;
|
||
let Note = '';
|
||
if (Type) {
|
||
const C = CheckType(Type, Source, R.Name);
|
||
if (C.Ok) Type = C.Type;
|
||
else { Note = '// 未收录类型: ' + C.Bad + '(' + Source + ')'; Type = null; }
|
||
}
|
||
const Override = FieldTypeOverrides.get(R.Name + '.' + F);
|
||
if (Override) { Type = Override; Source = 'override'; Note = ''; }
|
||
if (Type) TypedFields++;
|
||
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);
|
||
Lines.push(' val ' + F + ': ' + (Type || 'Any?') + ', ' + Note);
|
||
FieldPairs.push([F, Type || 'Any?']);
|
||
});
|
||
Emitted.push('data class ' + R.Name + '(\n' + Lines.join('\n') + '\n)');
|
||
Catalog.push({ Name: R.Name, Fields: FieldPairs });
|
||
}
|
||
|
||
// 嵌套模型类(字段名来自 runtime toString,类型来自样例值)
|
||
function ModelFieldType(T) {
|
||
const S = String(T || '').trim();
|
||
// 可空性直接透传(Any? 本身已经是可空)
|
||
if (S.endsWith('?') && S !== 'Any?') {
|
||
const Inner = ModelFieldType(S.slice(0, -1));
|
||
return Inner === 'Any?' ? 'Any?' : Inner + '?';
|
||
}
|
||
if (PrimitiveKinds.has(S)) return S;
|
||
const L = /^List<([\s\S]+)>$/.exec(S);
|
||
if (L) {
|
||
const I = ModelFieldType(L[1]);
|
||
return 'List<' + (I === 'Any?' ? 'Any?' : I) + '>';
|
||
}
|
||
const M = /^Map<([\s\S]+),\s*([\s\S]+)>$/.exec(S);
|
||
if (M) return 'Map<String, ' + ModelFieldType(M[2]) + '>';
|
||
return KnownModels.has(S) ? S : 'Any?';
|
||
}
|
||
const ModelEmitted = [];
|
||
let ModelTyped = 0;
|
||
let ModelTotal = 0;
|
||
for (const Name of [...EmitModels].sort()) {
|
||
const Fields = ModelDefs[Name];
|
||
const Lines = [];
|
||
for (const [F, T] of Object.entries(Fields)) {
|
||
ModelTotal++;
|
||
const K = ModelFieldType(T);
|
||
if (K !== 'Any?') ModelTyped++;
|
||
Lines.push(' val ' + EscapeName(F) + ': ' + K + ', // ' + (K === 'Any?' ? '未收录: ' + T : 'runtime'));
|
||
}
|
||
if (!Lines.length) continue;
|
||
ModelEmitted.push('data class ' + Name + '(\n' + Lines.join('\n') + '\n)');
|
||
}
|
||
|
||
const Head = [
|
||
'// 本文件由 3DTank/tools/gen_action_kt.js 生成。',
|
||
'// 来源:module_10032 的动作/状态类(类名与字段顺序取自 toString 证据,类型由调用点 + 协议字段表反推)。',
|
||
'// 标注 Any? 的字段表示调用点认不出类型,行尾注释给出原始实参,便于人工补。',
|
||
'package App.State',
|
||
'',
|
||
'import Core.Vector3',
|
||
'',
|
||
'// 动作/状态类共 ' + Emitted.length + ' 个,字段 ' + TotalFields + ' 个(已定类型 ' + TypedFields + ' 个)',
|
||
'',
|
||
];
|
||
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
||
fs.writeFileSync(OUT, Head.join('\n') + Emitted.join('\n\n') + '\n\n' +
|
||
'// ---- 嵌套对象类(来自运行时 toString 样例:字段名是证据,类型由样例值推断)----\n' +
|
||
'// 共 ' + ModelEmitted.length + ' 个,字段 ' + ModelTotal + ' 个(已定类型 ' + ModelTyped + ' 个)\n\n' +
|
||
ModelEmitted.join('\n\n') + '\n', 'utf8');
|
||
console.log('生成 ' + Emitted.length + ' 个 data class,字段 ' + TotalFields + ',已定类型 ' + TypedFields +
|
||
'(' + ((TypedFields * 100 / Math.max(1, TotalFields)).toFixed(1)) + '%)');
|
||
console.log('生成 ' + ModelEmitted.length + ' 个嵌套对象类,字段 ' + ModelTotal + ',已定类型 ' + ModelTyped);
|
||
|
||
// ---- 动作目录:名字 → Kotlin 类 + 字段类型(运行时按 dispatch 打点里的名字反查)----
|
||
const CatalogPath = path.join(path.dirname(OUT), 'ActionCatalog.kt');
|
||
const TracedNames = Object.keys(RuntimeTypes).sort();
|
||
// 不在 module_10032 里、但有 toString 证据的动作。
|
||
// InitState:module_10849 的 ct,toString 为 `InitState(type=…, action=…)`,已在 State 模块实现
|
||
const ExternalActions = [
|
||
{ Name: 'InitState', Type: 'InitState', Fields: [['type', 'Any?'], ['action', 'Any?']] },
|
||
];
|
||
const CatalogLines = [];
|
||
for (const Row of Catalog) {
|
||
const Fields = Row.Fields.map(([N, T]) => 'ActionFieldMeta("' + N + '", "' + T + '")').join(', ');
|
||
CatalogLines.push(' ActionMeta("' + Row.Name + '", ' + Row.Name + '::class, listOf(' + Fields + ')),');
|
||
}
|
||
for (const Row of ExternalActions) {
|
||
const Fields = Row.Fields.map(([N, T]) => 'ActionFieldMeta("' + N + '", "' + T + '")').join(', ');
|
||
CatalogLines.push(' ActionMeta("' + Row.Name + '", ' + Row.Type + '::class, listOf(' + Fields + ')),');
|
||
}
|
||
const CatalogText = [
|
||
'// 本文件由 3DTank/tools/gen_action_kt.js 生成。',
|
||
'// 动作/状态类目录:把 dispatch 打点里的字符串类名映射到 Kotlin 类(字段类型一并带上)。',
|
||
'package App.State',
|
||
'',
|
||
'import kotlin.reflect.KClass',
|
||
'import State.InitState',
|
||
'',
|
||
'data class ActionFieldMeta(val Name: String, val Type: String)',
|
||
'',
|
||
'data class ActionMeta(val Name: String, val Type: KClass<*>, val Fields: List<ActionFieldMeta>)',
|
||
'',
|
||
'object ActionCatalog {',
|
||
' val All: List<ActionMeta> = listOf(',
|
||
CatalogLines.join('\n'),
|
||
' )',
|
||
'',
|
||
' val ByName: Map<String, ActionMeta> = All.associateBy { it.Name }',
|
||
'',
|
||
' /** 运行时 dispatch 打点里真实出现过的动作名(真值证据)。 */',
|
||
' val TracedNames: Set<String> = setOf(' + TracedNames.map((N) => '"' + N + '"').join(', ') + ')',
|
||
'',
|
||
' /** 打点里出现但目录里没有的名字(正常应为空)。 */',
|
||
' fun MissingTraced(): List<String> = TracedNames.filter { !ByName.containsKey(it) }.sorted()',
|
||
'}',
|
||
'',
|
||
].join('\n');
|
||
fs.writeFileSync(CatalogPath, CatalogText, 'utf8');
|
||
console.log('生成动作目录 ' + (Catalog.length + ExternalActions.length) + ' 条(打点出现过 ' + TracedNames.length + ' 个名字)');
|
||
|
||
// ---- 回放样例:每个动作在运行时打点里的第一行原文(供 Kotlin 侧字段/类型对照)----
|
||
const SamplesPath = path.join(path.dirname(OUT), 'ActionSamples.kt');
|
||
const EscapeKt = (S) => String(S).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/\r?\n/g, '\\n');
|
||
const SampleNames = Object.keys(RuntimeSamples).sort();
|
||
const SampleLines = SampleNames.map((N) => ' "' + N + '" to "' + EscapeKt(RuntimeSamples[N]) + '",');
|
||
const SamplesText = [
|
||
'// 本文件由 3DTank/tools/gen_action_kt.js 生成。',
|
||
'// 每个动作在运行时 dispatch 打点里的第一行原文(真值样例),用于回放对照。',
|
||
'package App.State',
|
||
'',
|
||
'object ActionSamples {',
|
||
' val Raw: Map<String, String> = mapOf(',
|
||
SampleLines.join('\n'),
|
||
' )',
|
||
'}',
|
||
'',
|
||
].join('\n');
|
||
fs.writeFileSync(SamplesPath, SamplesText, 'utf8');
|
||
console.log('生成回放样例 ' + SampleNames.length + ' 条');
|
||
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);
|