506 lines
22 KiB
JavaScript
506 lines
22 KiB
JavaScript
// gen_slice_plan.js —— 从 module_10032 抽取某个根状态切片的「状态字段 + reducer 分支表」
|
||
//
|
||
// 用途:为逐个迁移大切片(shop / user / screen / garage …)提供机器可读证据,
|
||
// 避免人工在 1 万行 reducer 里数位置。产出 3DTank/names/slice_<field>.json:
|
||
// State : 状态类(class / 构造参数 / 槽位 / toString 字段 / 默认值 / copy 方法)
|
||
// Copy : copy 方法的参数个数(== 字段数才认为可信)
|
||
// Branches : 每个顶层动作分支 { Local, Name, Kind, Changed:[{Field, Slot, Index, Value}], Expr }
|
||
// Kind = copy / reset / noop / conditional;Value 保留原表达式文本,供人工翻译
|
||
// SubReducers : 顶层分支里内嵌的 `(function (t, n) { … })(t, n.<子状态>)` 子 reducer
|
||
// (例如 shop 的 x4 分支会把 shopCategories/shopItems 两个嵌套模型交给子 reducer)
|
||
// { Var, Arg, Style(chain|ternary), ArmCount, Locals:[{Local,Name}], Arms:[{Local,Expr}] }
|
||
// Counts : TotalInstanceof / TopLevelBranches / SubReducerArms,用来说明「为什么不是一句话」
|
||
// Locals : 局部动作类 → 动作名(action_inventory 里查不到则为 null)
|
||
//
|
||
// 用法:cd 3DTank/tools && node gen_slice_plan.js shop [user screen ...]
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const P = path.resolve(__dirname, '..');
|
||
const T = fs.readFileSync(path.join(P, 'src', 'module_10032.js'), 'utf8');
|
||
const Inv = JSON.parse(fs.readFileSync(path.join(P, 'names', 'action_inventory_10032.json'), 'utf8'));
|
||
const LocalToName = new Map();
|
||
for (const R of Inv.Rows) if (!LocalToName.has(R.Local)) LocalToName.set(R.Local, R.Name);
|
||
LocalToName.set('iz', 'InitState');
|
||
|
||
// ---------------------------------------------------------------- 基础扫描
|
||
|
||
function BracePairs(Text) {
|
||
const Pairs = new Map();
|
||
const Stack = [];
|
||
let I = 0;
|
||
while (I < Text.length) {
|
||
const C = Text[I];
|
||
if (C === '"' || C === "'" || C === '`') {
|
||
const Q = C; I++;
|
||
while (I < Text.length) { if (Text[I] === '\\') { I += 2; continue; } if (Text[I] === Q) { I++; break; } I++; }
|
||
continue;
|
||
}
|
||
if (C === '/' && Text[I + 1] === '/') { const N = Text.indexOf('\n', I); I = N < 0 ? Text.length : N; continue; }
|
||
if (C === '/' && Text[I + 1] === '*') { const N = Text.indexOf('*/', I + 2); I = N < 0 ? Text.length : N + 2; continue; }
|
||
if (C === '{') Stack.push(I);
|
||
else if (C === '}') { const O = Stack.pop(); if (O !== undefined) Pairs.set(O, I); }
|
||
I++;
|
||
}
|
||
return Pairs;
|
||
}
|
||
const Pairs = BracePairs(T);
|
||
|
||
const Fns = new Map();
|
||
for (const M of T.matchAll(/(?<![\w$.])function ([A-Za-z0-9_$]+)\(([^)]*)\)\s*\{/g)) {
|
||
const Open = M.index + M[0].length - 1;
|
||
const End = Pairs.get(Open);
|
||
if (End !== undefined) Fns.set(M[1], { Name: M[1], Params: M[2], Body: Open + 1, End });
|
||
}
|
||
const Methods = [];
|
||
for (const M of T.matchAll(/\(([A-Za-z_$][\w$]*)\(([A-Za-z0-9_$]+)\)\.([A-Za-z0-9_$]+) = function \(([^)]*)\)\s*\{/g)) {
|
||
const Open = M.index + M[0].length - 1;
|
||
const End = Pairs.get(Open);
|
||
if (End !== undefined) Methods.push({ Obj: M[2], Method: M[3], Params: M[4], Start: M.index, Body: Open + 1, End });
|
||
}
|
||
function FindMethod(Obj, Method) { return Methods.find((M) => M.Obj === Obj && M.Method === Method) || null; }
|
||
|
||
// ---------------------------------------------------------------- 根 reducer / 根状态
|
||
|
||
const RootReducerFn = [...Fns.values()].find((F) => T.slice(F.Body, F.End).includes('return n.cti('));
|
||
if (!RootReducerFn) throw new Error('找不到根 reducer');
|
||
const CopyMethod = Methods.find((M) => M.Method === 'cti');
|
||
const RootClass = CopyMethod.Obj;
|
||
const RootClassFn = Fns.get(RootClass);
|
||
const RootBody = T.slice(RootClassFn.Body, RootClassFn.End);
|
||
|
||
const SlotOfParam = new Map();
|
||
for (const M of RootBody.matchAll(/\(this\.(\w+) = (\w+)\)/g)) SlotOfParam.set(M[2], M[1]);
|
||
const DefaultOfParam = new Map();
|
||
for (const M of RootBody.matchAll(/\((\w+) = \1 === cd \? ([\s\S]*?) : \1\)/g)) DefaultOfParam.set(M[1], M[2].replace(/\s+/g, ' ').trim());
|
||
for (const M of RootBody.matchAll(/\((\w+) = \1 !== cd && \1\)/g)) DefaultOfParam.set(M[1], 'false');
|
||
for (const M of RootBody.matchAll(/\((\w+) = \1 === cd \|\| \1\)/g)) DefaultOfParam.set(M[1], 'true');
|
||
|
||
const ToStringStart = T.indexOf(`(ld(${RootClass}).toString = function`);
|
||
const ToStringEnd = T.indexOf(`(ld(${RootClass}).hashCode`, ToStringStart);
|
||
const RootFields = [];
|
||
for (const M of T.slice(ToStringStart, ToStringEnd).matchAll(/"([^"]*?)([A-Za-z0-9_]+)="\s*\+\s*(?:[A-Za-z_$][\w$]*\()?this\.(\w+)/g)) {
|
||
RootFields.push({ Field: M[2], Slot: M[3] });
|
||
}
|
||
|
||
/** 根 reducer 顶层赋值 [{Name, Expr}]。 */
|
||
function TopLevelAssignments(Body) {
|
||
const Out = [];
|
||
let I = 0, Depth = 0;
|
||
while (I < Body.length) {
|
||
const C = Body[I];
|
||
if (C === '"' || C === "'" || C === '`') {
|
||
const Q = C; I++;
|
||
while (I < Body.length) { if (Body[I] === '\\') { I += 2; continue; } if (Body[I] === Q) { I++; break; } I++; }
|
||
continue;
|
||
}
|
||
if ('([{'.includes(C)) { Depth++; I++; continue; }
|
||
if (')]}'.includes(C)) { Depth--; I++; continue; }
|
||
if (Depth === 0) {
|
||
const M = /^([A-Za-z_$][\w$]*)\s*=\s*/.exec(Body.slice(I));
|
||
if (M && (I === 0 || /[;{}\s]/.test(Body[I - 1]))) {
|
||
let J = I + M[0].length, D = 0;
|
||
while (J < Body.length) {
|
||
const D0 = Body[J];
|
||
if (D0 === '"' || D0 === "'" || D0 === '`') {
|
||
const Q = D0; J++;
|
||
while (J < Body.length) { if (Body[J] === '\\') { J += 2; continue; } if (Body[J] === Q) { J++; break; } J++; }
|
||
continue;
|
||
}
|
||
if ('([{'.includes(D0)) D++;
|
||
else if (')]}'.includes(D0)) D--;
|
||
else if ((D0 === ',' || D0 === ';') && D === 0) break;
|
||
J++;
|
||
}
|
||
Out.push({ Name: M[1], Expr: Body.slice(I + M[0].length, J).trim() });
|
||
I = J + 1; continue;
|
||
}
|
||
}
|
||
I++;
|
||
}
|
||
return Out;
|
||
}
|
||
const Assignments = TopLevelAssignments(T.slice(RootReducerFn.Body, RootReducerFn.End));
|
||
|
||
// ---------------------------------------------------------------- 工具
|
||
|
||
/** 顶层逗号切分(按掩码文本判定深度,字符串里的逗号不切)。 */
|
||
function SplitTop(Text) {
|
||
const M = MaskStrings(Text);
|
||
const Out = []; let D = 0, Cur = '';
|
||
for (let I = 0; I < Text.length; I++) {
|
||
const C = M[I];
|
||
if ('([{'.includes(C)) D++;
|
||
else if (')]}'.includes(C)) D--;
|
||
if (C === ',' && D === 0) { Out.push(Cur.trim()); Cur = ''; } else Cur += Text[I];
|
||
}
|
||
if (Cur.trim()) Out.push(Cur.trim());
|
||
return Out;
|
||
}
|
||
|
||
/**
|
||
* 把字符串/注释内容替换成同长度空格(保留长度,便于用同一套下标扫描括号)。
|
||
* 不做这一步的话,字符串里的 `[` `{` 会让括号深度错位,漏掉后面的分支。
|
||
*/
|
||
function MaskStrings(Text) {
|
||
const Out = Text.split('');
|
||
let I = 0;
|
||
while (I < Text.length) {
|
||
const C = Text[I];
|
||
if (C === '"' || C === "'" || C === '`') {
|
||
const Q = C;
|
||
Out[I] = ' ';
|
||
I++;
|
||
while (I < Text.length) {
|
||
if (Text[I] === '\\') { Out[I] = ' '; Out[I + 1] = ' '; I += 2; continue; }
|
||
if (Text[I] === Q) { Out[I] = ' '; I++; break; }
|
||
if (Text[I] !== '\n') Out[I] = ' ';
|
||
I++;
|
||
}
|
||
continue;
|
||
}
|
||
if (C === '/' && Text[I + 1] === '/') {
|
||
const N = Text.indexOf('\n', I);
|
||
const E = N < 0 ? Text.length : N;
|
||
for (let J = I; J < E; J++) Out[J] = ' ';
|
||
I = E;
|
||
continue;
|
||
}
|
||
if (C === '/' && Text[I + 1] === '*') {
|
||
const N = Text.indexOf('*/', I + 2);
|
||
const E = N < 0 ? Text.length : N + 2;
|
||
for (let J = I; J < E; J++) if (Text[J] !== '\n') Out[J] = ' ';
|
||
I = E;
|
||
continue;
|
||
}
|
||
I++;
|
||
}
|
||
return Out.join('');
|
||
}
|
||
|
||
/** 从 Text[Open] 的 `{` 找到配对的 `}` 下标(掩码判定)。 */
|
||
function MatchBrace(Text, Open) {
|
||
const M = MaskStrings(Text);
|
||
let D = 0;
|
||
for (let I = Open; I < Text.length; I++) {
|
||
const C = M[I];
|
||
if (C === '{') D++;
|
||
else if (C === '}') { D--; if (D === 0) return I; }
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/** 找顶层 `if (… instanceof …)` 守卫(掩码判深度)。 */
|
||
function FindGuards(Text) {
|
||
const M = MaskStrings(Text);
|
||
const Out = [];
|
||
let I = 0, Depth = 0;
|
||
while (I < Text.length) {
|
||
const C = M[I];
|
||
if ('([{'.includes(C)) { Depth++; I++; continue; }
|
||
if (')]}'.includes(C)) { Depth--; I++; continue; }
|
||
if (Depth === 0 && M.startsWith('if (', I) && M.slice(I, I + 80).includes('instanceof')) {
|
||
const OpenP = I + 3;
|
||
let D = 0, J = OpenP;
|
||
for (; J < Text.length; J++) { const D0 = M[J]; if (D0 === '(') D++; else if (D0 === ')') { D--; if (D === 0) break; } }
|
||
Out.push({ At: I, GuardEnd: J, Cond: Text.slice(OpenP + 1, J).replace(/\s+/g, ' ').trim() });
|
||
I = J + 1; continue;
|
||
}
|
||
I++;
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
/** 从 chunk 里取最浅层的 `i = <expr>`(掩码判深度)。 */
|
||
function OuterAssign(Chunk) {
|
||
const MC = MaskStrings(Chunk);
|
||
let Best = null, BestDepth = Infinity, I2 = 0, D2 = 0;
|
||
while (I2 < Chunk.length) {
|
||
const C2 = MC[I2];
|
||
if ('([{'.includes(C2)) { D2++; I2++; continue; }
|
||
if (')]}'.includes(C2)) { D2--; I2++; continue; }
|
||
if (MC.startsWith('i =', I2)) {
|
||
let J2 = I2 + 3, DD = 0;
|
||
while (J2 < Chunk.length) {
|
||
const D0 = MC[J2];
|
||
if ('([{'.includes(D0)) DD++;
|
||
else if (')]}'.includes(D0)) { if (DD === 0) break; DD--; }
|
||
else if (D0 === ';' && DD === 0) break;
|
||
J2++;
|
||
}
|
||
if (D2 < BestDepth) { BestDepth = D2; Best = Chunk.slice(I2 + 3, J2).trim(); }
|
||
I2 = J2; continue;
|
||
}
|
||
I2++;
|
||
}
|
||
return Best;
|
||
}
|
||
|
||
function LocalsIn(Text) {
|
||
const Out = [];
|
||
for (const M of Text.matchAll(/t instanceof ([A-Za-z0-9_$]+)/g)) Out.push(M[1]);
|
||
for (const M of Text.matchAll(/Xd\(t, ([A-Za-z0-9_$]+)\)/g)) Out.push(M[1]);
|
||
return [...new Set(Out)];
|
||
}
|
||
|
||
/** 把 `t instanceof A ? e1 : t instanceof B ? e2 : e3` 切成手。 */
|
||
function TernaryArms(Text) {
|
||
const Hits = [...Text.matchAll(/t instanceof ([A-Za-z0-9_$]+)\s*\?/g)];
|
||
return Hits.map((H, Hi) => {
|
||
const From = H.index + H[0].length;
|
||
const To = Hi + 1 < Hits.length ? Hits[Hi + 1].index : Text.length;
|
||
return {
|
||
Local: H[1],
|
||
Expr: Text.slice(From, To).replace(/^\s*/, '').replace(/[,\s]*:\s*$/, '').replace(/\s*:\s*$/, '').trim(),
|
||
};
|
||
});
|
||
}
|
||
|
||
/** 槽位 → 字段名 / 字段名 → 索引(按 toString 顺序)。 */
|
||
const FieldOfSlot = new Map(RootFields.map((F) => [F.Slot, F.Field]));
|
||
|
||
function KotlinName(Field) { return Field.charAt(0).toUpperCase() + Field.slice(1); }
|
||
|
||
// ---------------------------------------------------------------- 单切片计划
|
||
|
||
function PlanSlice(Field) {
|
||
const F = RootFields.find((X) => X.Field === Field);
|
||
if (!F) throw new Error('根状态里没有字段 ' + Field);
|
||
// 委派有两种形状:
|
||
// 1) 根 reducer 内联写 `n.<slot>`(多数切片,或 `n.<slot>.方法(t)`)
|
||
// 2) 根 reducer 直接调用具名 reducer `hF(t, n)`,切片槽位只在 hF 体内出现(garage)
|
||
// 第 2 种需要回查被调函数的函数体,确认它确实读写 `n.<slot>`。
|
||
let A = Assignments.find((X) => new RegExp('\\bn\\.' + F.Slot + '\\b').test(X.Expr));
|
||
let NamedReducer = null;
|
||
if (!A) {
|
||
for (const X of Assignments) {
|
||
const Call = /^([A-Za-z0-9_$]+)\(t,\s*n\)$/.exec(X.Expr.replace(/\s+/g, ' ').trim());
|
||
if (!Call || !Fns.has(Call[1])) continue;
|
||
const Fn = Fns.get(Call[1]);
|
||
if (new RegExp('\\bn\\.' + F.Slot + '\\b').test(T.slice(Fn.Body, Fn.End))) {
|
||
A = X;
|
||
NamedReducer = Call[1];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!A) throw new Error(Field + ' 在根 reducer 里找不到委派');
|
||
|
||
const Param = [...SlotOfParam.keys()].find((P2) => SlotOfParam.get(P2) === F.Slot);
|
||
const SliceClass = (DefaultOfParam.get(Param) || '').match(/^new ([A-Za-z0-9_$]+)\(/);
|
||
const ClassName = SliceClass ? SliceClass[1] : null;
|
||
|
||
const Info = {
|
||
Field, Slot: F.Slot, Class: ClassName, ClassSource: DefaultOfParam.get(Param) || '',
|
||
NamedReducer, Params: [], Fields: [], CopyMethod: null, Branches: [], SubReducers: [], Counts: {}, Locals: {},
|
||
Delegate: A.Expr.replace(/\s+/g, ' ').trim().slice(0, 400),
|
||
};
|
||
|
||
if (ClassName) {
|
||
const CFn = Fns.get(ClassName);
|
||
Info.Params = CFn ? CFn.Params.split(',').map((S) => S.trim()).filter(Boolean) : [];
|
||
const CB = CFn ? T.slice(CFn.Body, CFn.End) : '';
|
||
const SOfP = new Map();
|
||
for (const M of CB.matchAll(/\(this\.([A-Za-z_$][\w$]*) = ([A-Za-z_$][\w$]*)\)/g)) SOfP.set(M[2], M[1]);
|
||
const DOfP = new Map();
|
||
for (const M of CB.matchAll(/\(([A-Za-z_$][\w$]*) = \1 === cd \? ([\s\S]*?) : \1\)/g)) DOfP.set(M[1], M[2].replace(/\s+/g, ' ').trim());
|
||
for (const M of CB.matchAll(/\(([A-Za-z_$][\w$]*) = \1 !== cd && \1\)/g)) DOfP.set(M[1], 'false');
|
||
for (const M of CB.matchAll(/\(([A-Za-z_$][\w$]*) = \1 === cd \|\| \1\)/g)) DOfP.set(M[1], 'true');
|
||
const S = T.indexOf(`(ld(${ClassName}).toString = function`);
|
||
const E = T.indexOf(`(ld(${ClassName}).hashCode`, S);
|
||
if (S >= 0 && E > S) {
|
||
for (const M of T.slice(S, E).matchAll(/"([^"]*?)([A-Za-z0-9_]+)="\s*\+\s*(?:[A-Za-z_$][\w$]*\()?this\.(\w+)/g)) {
|
||
const P2 = [...SOfP.keys()].find((K) => SOfP.get(K) === M[3]);
|
||
Info.Fields.push({ Field: M[2], Slot: M[3], Kotlin: KotlinName(M[2]), Default: P2 ? DOfP.get(P2) || '' : '' });
|
||
}
|
||
}
|
||
}
|
||
|
||
const CopyName = (A.Expr.match(/\bn\.([A-Za-z0-9_$]+)\(/) || [])[1] || null;
|
||
// 具名 reducer 的副本方法只在函数体里(`o.hri(...)`),按「参数个数 == 字段数」锁定。
|
||
let NamedBody = null;
|
||
if (NamedReducer) {
|
||
const Fn = Fns.get(NamedReducer);
|
||
NamedBody = T.slice(Fn.Body, Fn.End);
|
||
const ByArity = [];
|
||
for (const M of NamedBody.matchAll(/\bo\.([A-Za-z0-9_$]+)\(/g)) {
|
||
let O = M.index + M[0].length, D = 0, E = -1;
|
||
for (let J = O; J < NamedBody.length; J++) {
|
||
const C2 = NamedBody[J];
|
||
if (C2 === '(') D++;
|
||
else if (C2 === ')') { D--; if (D === 0) { E = J; break; } }
|
||
}
|
||
if (E < 0) continue;
|
||
const N = SplitTop(NamedBody.slice(O, E)).length;
|
||
ByArity.push({ Name: M[1], Count: N });
|
||
}
|
||
const Hit = ByArity.find((X) => X.Count === Info.Fields.length);
|
||
Info.CopyMethod = Hit ? Hit.Name : (ByArity[0] || {}).Name || null;
|
||
} else {
|
||
Info.CopyMethod = CopyName;
|
||
}
|
||
const CopyMethodName = Info.CopyMethod;
|
||
if (CopyMethodName && ClassName) {
|
||
const M = Methods.find((X) => X.Obj === ClassName && X.Method === CopyMethodName);
|
||
Info.CopyParamCount = M ? M.Params.split(',').map((S) => S.trim()).filter(Boolean).length : null;
|
||
Info.CopyMatchesFields = Info.CopyParamCount === Info.Fields.length;
|
||
}
|
||
|
||
// ---- 分支抽取:内联 IIFE 或具名 reducer(`hF(t, n)`)
|
||
const Expr = A.Expr;
|
||
let Body;
|
||
if (NamedReducer) {
|
||
Body = NamedBody;
|
||
} else if (/^\(function \(t, n\)/.test(Expr)) {
|
||
// IIFE 的 { 位置要对到全局源码:用 Pos 定位
|
||
const Pos = T.indexOf(A.Expr, RootReducerFn.Body);
|
||
const GlobalOpen = T.indexOf('{', T.indexOf('function', Pos));
|
||
const GlobalClose = Pairs.get(GlobalOpen);
|
||
Body = T.slice(GlobalOpen + 1, GlobalClose);
|
||
} else {
|
||
Info.Branches = [{ Guard: '(具名 reducer)', Locals: [], Kind: 'named', Expr: Expr.replace(/\s+/g, ' ').slice(0, 200) }];
|
||
Info.Counts = { TotalInstanceof: null, TopLevelBranches: 1, SubReducerArms: 0 };
|
||
return Info;
|
||
}
|
||
|
||
function Decode(BranchExpr) {
|
||
const E = BranchExpr.replace(/\s+/g, ' ').trim();
|
||
const Fields = [];
|
||
if (/^n$/.test(E)) return { Kind: 'noop', Changed: [] };
|
||
const CopyCall = new RegExp('\\b(?:n|o)\\.' + (CopyMethodName || 'qt6') + '\\(');
|
||
const M = CopyCall.exec(E);
|
||
if (/new [A-Za-z0-9_$]+\(/.test(E) && !M) return { Kind: 'reset', Changed: [] };
|
||
if (!M) return { Kind: 'conditional', Changed: [] };
|
||
// 找该调用的参数(最外层)
|
||
let O = E.indexOf('(', M.index), D = 0, End = -1;
|
||
for (let J = O; J < E.length; J++) { const C2 = E[J]; if (C2 === '(') D++; else if (C2 === ')') { D--; if (D === 0) { End = J; break; } } }
|
||
const Args = SplitTop(E.slice(O + 1, End));
|
||
Args.forEach((Arg, Index) => {
|
||
if (Arg === 'cd') return;
|
||
const FI = Info.Fields[Index];
|
||
Fields.push({ Index, Field: FI ? FI.Field : null, Slot: FI ? FI.Slot : null, Kotlin: FI ? FI.Kotlin : null, Value: Arg });
|
||
});
|
||
const HasHelper = Fields.some((X) => /[A-Za-z_$][\w$]*\(/.test(X.Value) && !/^t\.[A-Za-z0-9_$]+$/.test(X.Value));
|
||
const Simple = Fields.length > 0 && Fields.every((X) => /^t\.[A-Za-z0-9_$]+$/.test(X.Value) || /^(null|!0|!1|true|false|-?\d+)$/.test(X.Value));
|
||
return { Kind: Simple || !HasHelper ? 'copy' : 'copy-helper', Changed: Fields };
|
||
}
|
||
|
||
/** 顶层 if/else-if 链 → 分支表。 */
|
||
function ChainBranches(Text) {
|
||
const Guards = FindGuards(Text);
|
||
const Out = [];
|
||
Guards.forEach((G, Gi) => {
|
||
const NextGuard = Guards[Gi + 1];
|
||
const ChunkEnd = NextGuard ? Text.lastIndexOf('else', NextGuard.At) : Text.length;
|
||
const Chunk = Text.slice(G.GuardEnd + 1, ChunkEnd);
|
||
let BranchExpr = OuterAssign(Chunk);
|
||
// 块体分支(例如 KU):`{ var o = t.irl_1; i = null == o ? n : n.qt6(…); }`
|
||
// 先剥掉最外层花括号,再取块内最后一个顶层赋值,避免被后续 else 链的三元污染。
|
||
const ChunkTrim = Chunk.trim();
|
||
if (ChunkTrim.startsWith('{')) {
|
||
const Open = Chunk.indexOf('{');
|
||
const Close = MatchBrace(Chunk, Open);
|
||
if (Close > Open) {
|
||
const Inner = OuterAssign(Chunk.slice(Open + 1, Close));
|
||
if (Inner !== null) BranchExpr = Inner;
|
||
}
|
||
}
|
||
if (BranchExpr === null) return;
|
||
Out.push({ Guard: G.Cond, Locals: LocalsIn(G.Cond), ...Decode(BranchExpr), Expr: BranchExpr.replace(/\s+/g, ' ').trim().slice(0, 300), _Chunk: Chunk });
|
||
});
|
||
// 末尾 else:`else i = t instanceof m5 ? … : t instanceof f5 ? … : n;`
|
||
if (Guards.length) {
|
||
const LastEnd = Guards[Guards.length - 1].GuardEnd;
|
||
const Tail = Text.slice(LastEnd);
|
||
const ElseAt = Tail.lastIndexOf('else');
|
||
if (ElseAt >= 0) {
|
||
const BranchExpr = OuterAssign(Tail.slice(ElseAt));
|
||
if (BranchExpr) {
|
||
const Arms = TernaryArms(BranchExpr);
|
||
for (const Arm of Arms) {
|
||
Out.push({ Guard: 't instanceof ' + Arm.Local, Locals: [Arm.Local], ...Decode(Arm.Expr), Expr: Arm.Expr.replace(/\s+/g, ' ').trim().slice(0, 300) });
|
||
}
|
||
if (!Arms.length) Out.push({ Guard: '(else)', Locals: [], ...Decode(BranchExpr), Expr: BranchExpr.replace(/\s+/g, ' ').trim().slice(0, 300) });
|
||
}
|
||
}
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
/** 分支里内嵌的 `(function (t, n) { … })(t, <arg>)` 子 reducer。 */
|
||
function NestedReducers(Chunk) {
|
||
const Out = [];
|
||
const Re = /(?:var\s+([A-Za-z0-9_$]+)\s*=\s*)?\(function \(t, n\)\s*\{/g;
|
||
let M;
|
||
while ((M = Re.exec(Chunk))) {
|
||
const Open = Chunk.indexOf('{', M.index + M[0].length - 1);
|
||
const Close = MatchBrace(Chunk, Open);
|
||
if (Close < 0) continue;
|
||
const Body2 = Chunk.slice(Open + 1, Close);
|
||
const After = Chunk.slice(Close + 1, Close + 120);
|
||
const CallArg = (After.match(/^\)\(t,\s*([^)]+)\)/) || [])[1] || null;
|
||
const IsTernary = /\breturn\s+t instanceof [A-Za-z0-9_$]+\s*\?/.test(Body2);
|
||
let Arms;
|
||
if (IsTernary) {
|
||
const Ret = Body2.slice(Body2.search(/\breturn\s+t instanceof /));
|
||
Arms = TernaryArms(Ret).map((Arm) => ({ Local: Arm.Local, Expr: Arm.Expr.replace(/\s+/g, ' ').slice(0, 200) }));
|
||
} else {
|
||
Arms = ChainBranches(Body2).map((B) => ({
|
||
Local: (B.Locals || [])[0] || null,
|
||
Guard: B.Guard,
|
||
Kind: B.Kind,
|
||
Expr: B.Expr,
|
||
}));
|
||
}
|
||
Out.push({
|
||
Var: M[1] || null,
|
||
Arg: CallArg ? CallArg.trim() : null,
|
||
Style: IsTernary ? 'ternary' : 'chain',
|
||
ArmCount: Arms.length,
|
||
Locals: Arms.map((X) => X.Local).filter(Boolean).map((L) => ({ Local: L, Name: LocalToName.get(L) || null })),
|
||
Arms,
|
||
});
|
||
Re.lastIndex = Close + 1;
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
const Top = ChainBranches(Body);
|
||
Info.Branches = Top.map(({ _Chunk, ...Rest }) => Rest);
|
||
for (const B of Top) {
|
||
if (B._Chunk) Info.SubReducers.push(...NestedReducers(B._Chunk));
|
||
}
|
||
|
||
Info.Counts = {
|
||
TotalInstanceof: (Body.match(/\bt instanceof [A-Za-z0-9_$]+/g) || []).length,
|
||
TopLevelBranches: Info.Branches.length,
|
||
SubReducerArms: Info.SubReducers.reduce((N, S) => N + S.ArmCount, 0),
|
||
};
|
||
|
||
// 局部类命名情况
|
||
const Collect = (L) => { if (!Info.Locals[L]) Info.Locals[L] = { Name: LocalToName.get(L) || null }; };
|
||
for (const B of Info.Branches) for (const L of B.Locals || []) Collect(L);
|
||
for (const S of Info.SubReducers) for (const X of S.Locals) Collect(X.Local);
|
||
return Info;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 输出
|
||
|
||
const Want = process.argv.slice(2);
|
||
if (!Want.length) throw new Error('用法:node gen_slice_plan.js <field> [field ...]');
|
||
for (const Field of Want) {
|
||
const Info = PlanSlice(Field);
|
||
const OutFile = path.join(P, 'names', 'slice_' + Field + '.json');
|
||
fs.writeFileSync(OutFile, JSON.stringify({ generatedAt: new Date().toISOString(), module: 'module_10032.js', ...Info }, null, 2) + '\n');
|
||
const Kinds = {};
|
||
for (const B of Info.Branches) Kinds[B.Kind] = (Kinds[B.Kind] || 0) + 1;
|
||
const Anon = Object.entries(Info.Locals).filter(([, V]) => !V.Name).map(([K]) => K);
|
||
const C = Info.Counts || {};
|
||
console.log(`${Field}: class=${Info.Class} 字段=${Info.Fields.length} copy=${Info.CopyMethod}(${Info.CopyParamCount}) 顶层分支=${Info.Branches.length} ${JSON.stringify(Kinds)}`);
|
||
console.log(` 实例判定=${C.TotalInstanceof} 子reducer=${(Info.SubReducers || []).length} 子reducer分支=${C.SubReducerArms}`);
|
||
for (const S of Info.SubReducers || []) console.log(` 子reducer var=${S.Var} arg=${S.Arg} style=${S.Style} arms=${S.ArmCount}`);
|
||
if (Anon.length) console.log(' 未具名局部类: ' + Anon.join(', '));
|
||
}
|