Files
TankIC/3DTank/tools/apply_names.js
T
TermiNexus b6f2cb71ee feat(3dtank): 协议逆向工程与自研服务器入库(工具链 + 协议表 + 空间回放 + MySQL)
工具链(3DTank/tools/):extract(模块提取)、mine_evidence/apply_names(命名证据与别名回填)、extract_codec_fields(字段表 + 全库槽位类型 + PrimClassWire/ResourceClasses)、mine_downlink(下行字段表)、extract_space_init/rebase_space_init/audit_space_init(空间模板与记录切分审计)、extract_space_session(把真实会话切成按上行触发的回放模板)、pair_flow(请求→应答配对)、probe_containers、ingest/decrypt_frames(抓包入库与解密)、serve/fetch_assets(本地站点镜像)、import_db(抓包对象仓库导入 MySQL)。

服务器(3DTank/tools/server/):link/entry/codec/container/dispatch/world/space/semantic(协议层与 lt/pt/zt 三个容器布局)、session(会话回放,顺序推进)、db + schema.sql(MySQL 连接层与对象仓库表结构)、store/responder(查库组帧与定向应答;接管范围默认只开 ba.lcr)。

同时修 pair_flow 的时间轴偏移:timeline_c2s 与 timeline_s2c 的 0 点相差 144~4453ms,改前配对整体偏约 2 秒(实测 space-100073070 偏 2084ms),改后 22.4% 的触发者被纠正。

src/、src_named/、packets/、3DTank/local/(含数据库口令)不进库,可用上述工具重新生成。
2026-09-17 20:00:41 +08:00

787 lines
48 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// apply_names.js - 命名回填:把 names/ 里的命名证据写回 src/ 的反编译源码,产出可读版本 src_named/
// 用法:
// node apply_names.js [--in ../src] [--out ../src_named] [--report ../names/rename_report.json] [--dry] [--aliases]
//
// --aliases:额外读 names/command_alias.json,把「派生命名」(我们读源码证实语义后起的别名,**不是**原始名)
// 一并回填进 src_named/。这一类与证据命名分开统计(报告里的 Aliases 段、控制台的别名段)。
// 比证据命名多一道闸门:属性名必须「全库只有本模块这一处定义」才改(同名跨模块调用点不受影响)。
//
// 命名分层(严格遵守):
// ① 模块内局部名(类名 / 函数名 / 局部变量名)—— 只在所属模块内生效;
// ② 属性名(t.xxx / 对象字面量键)—— 全局共享,必须在全部模块里一次性同步替换;
// ③ 白名单名(宿主 API / Kotlin-JS 运行时关键名 / 含 $_$ 的导出)—— 永不改。
//
// 安全规则:
// · 证据不唯一(同一短名在同一作用域内指向多个实体)→ 跳过,计入 Conflict;
// · 短名出现在任何字符串字面量里 → 跳过(改字符串会破坏「字符串集合一致」这条硬校验);
// · 属性名的目标名如果已是既有的属性名 → 跳过(否则会把两个不同字段合并成一个);
// · 局部名要求「全模块唯一绑定 + 所有引用都落在该绑定作用域内」,否则跳过(无法保证一致);
// · 只改标识符,不碰字符串内容、不碰代码结构。
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
// ---------------------------------------------------------------- 依赖探测
// acorn 在 3DTank/tools/node_modules(也可能在仓库根 / sockets 下);找不到就明确报错,不静默降级
function RequireAcorn() {
const Project = path.resolve(__dirname, '..');
const Candidates = [
path.join(__dirname, 'node_modules', 'acorn'),
path.join(Project, 'node_modules', 'acorn'),
path.resolve(Project, '..', 'node_modules', 'acorn'),
path.resolve(Project, '..', 'sockets', 'node_modules', 'acorn'),
];
for (const Dir of Candidates) {
if (!fs.existsSync(Dir)) continue;
try { return require(Dir); } catch (Err) { /* 继续探测下一个 */ }
}
try { return require('acorn'); } catch (Err) { /* 全部失败 */ }
console.error('找不到 acorn。已尝试以下位置:');
for (const Dir of Candidates) console.error(' ' + Dir);
console.error('请先安装依赖:cd "' + path.join(Project, 'tools') + '" ; npm install');
process.exit(2);
return null;
}
const Acorn = RequireAcorn();
// ---------------------------------------------------------------- 参数
const Argv = process.argv.slice(2);
const Arg = (Name, Def) => {
const I = Argv.indexOf('--' + Name);
return I >= 0 && Argv[I + 1] ? Argv[I + 1] : Def;
};
const ToolsDir = __dirname;
const InDir = path.resolve(ToolsDir, Arg('in', path.join('..', 'src')));
const OutDir = path.resolve(ToolsDir, Arg('out', path.join('..', 'src_named')));
const ReportPath = path.resolve(ToolsDir, Arg('report', path.join('..', 'names', 'rename_report.json')));
const DryRun = Argv.indexOf('--dry') >= 0;
const UseAliases = Argv.indexOf('--aliases') >= 0;
const NamesDir = path.resolve(ToolsDir, '..', 'names');
// ---------------------------------------------------------------- 白名单
const ReservedWords = new Set([
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else',
'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof',
'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void',
'while', 'with', 'yield', 'let', 'static', 'await',
]);
// 宿主 / 内置 API
const BuiltinNames = new Set([
'window', 'globalThis', 'self', 'document', 'navigator', 'location', 'history', 'screen', 'console',
'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol', 'BigInt', 'Math', 'JSON', 'Date', 'RegExp',
'Error', 'TypeError', 'RangeError', 'SyntaxError', 'EvalError', 'URIError', 'ReferenceError', 'AggregateError',
'Promise', 'Proxy', 'Reflect', 'Function', 'Map', 'Set', 'WeakMap', 'WeakSet', 'WeakRef', 'FinalizationRegistry',
'ArrayBuffer', 'SharedArrayBuffer', 'DataView', 'Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array',
'Uint16Array', 'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array',
'TextEncoder', 'TextDecoder', 'URL', 'URLSearchParams', 'Blob', 'File', 'FileReader', 'FormData', 'Headers',
'Request', 'Response', 'Image', 'ImageData', 'Audio', 'AudioContext', 'CanvasRenderingContext2D', 'Path2D',
'WebSocket', 'Worker', 'MessageChannel', 'MessagePort', 'BroadcastChannel', 'EventSource', 'XMLHttpRequest',
'Event', 'CustomEvent', 'MouseEvent', 'KeyboardEvent', 'TouchEvent', 'PointerEvent', 'WheelEvent', 'ErrorEvent',
'Notification', 'localStorage', 'sessionStorage', 'indexedDB', 'crypto', 'performance', 'WebAssembly', 'process',
'undefined', 'null', 'NaN', 'Infinity', 'arguments', 'this', 'super', 'global', 'eval', 'module', 'exports',
'require', 'define', 'amd', 'Gamepad', 'WebGLRenderingContext', 'WebGL2RenderingContext', 'AudioBuffer',
'OffscreenCanvas', 'setImmediate', 'structuredClone', 'queueMicrotask', 'postMessage', 'addEventListener',
'removeEventListener', 'dispatchEvent', 'alert', 'confirm', 'prompt', 'atob', 'btoa', 'fetch',
'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent',
'escape', 'unescape', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
'requestAnimationFrame', 'cancelAnimationFrame', 'TextMetrics', 'DOMParser', 'XMLSerializer', 'IntersectionObserver',
]);
// Kotlin/JS 运行时关键名(改名会破坏元数据 / 协议读取器 / 编译器契约)
const KotlinRuntimeNames = new Set([
'equals', 'hashCode', 'toString', 'callableName', '$metadata$', '$metadata', 'prototype', 'constructor',
'e1', 'g1', 'h1', 'd3', 'p57', 'q7i', 'o7g', 'p7g', 'q7g', 'n7f', 'l7f', 'm7f', 'e2b', 'v7o', 'i7q', 'z7f', 'u7r_1',
]);
// 目标名危险表:这些名字一旦落在普通对象上会触发宿主 / 运行时的既有行为
const UnsafeTargets = new Set([
'__proto__', 'constructor', 'prototype', 'then', 'length', 'call', 'apply', 'bind', 'hasOwnProperty',
'isPrototypeOf', 'propertyIsEnumerable', 'valueOf', 'toLocaleString', 'caller', 'arguments', 'eval',
]);
// 名字合法性(AsTarget=true 时额外挡住危险目标名)
function NameProblem(Name, AsTarget) {
if (!/^[A-Za-z_$][\w$]*$/.test(Name)) return '不是合法标识符';
if (ReservedWords.has(Name)) return 'JS 保留字';
if (BuiltinNames.has(Name)) return '宿主/内置 API 名';
if (KotlinRuntimeNames.has(Name)) return 'Kotlin/JS 运行时关键名';
if (AsTarget && UnsafeTargets.has(Name)) return '目标名会触发宿主既有行为';
return '';
}
const HasExportSigil = (Name) => Name.indexOf('$_$') >= 0; // 以 $_$ 形式出现的导出一律不动
// ---------------------------------------------------------------- 通用 AST 工具
const IsFnNode = (N) => N.type === 'FunctionDeclaration' || N.type === 'FunctionExpression' || N.type === 'ArrowFunctionExpression';
const IsScopeNode = (N) => N.type === 'Program' || IsFnNode(N);
const ChildKeys = (N) => Object.keys(N).filter((K) => K !== 'type' && K !== 'start' && K !== 'end' && K !== 'loc' && K !== 'range');
const IdentOf = (N) => (N && N.type === 'Identifier' ? N.name : null);
// 名字表一律用无原型对象,避免 constructor / toString 之类的名字撞上 Object.prototype
const NewTable = () => Object.create(null);
function WalkWithParent(Node, Parent, Cb, Grand) {
if (!Node || typeof Node.type !== 'string') return;
Cb(Node, Parent, Grand);
for (const K of ChildKeys(Node)) {
const V = Node[K];
if (Array.isArray(V)) { for (const C of V) if (C && typeof C.type === 'string') WalkWithParent(C, Node, Cb, Parent); }
else if (V && typeof V.type === 'string') WalkWithParent(V, Node, Cb, Parent);
}
}
function PatternNames(Node, Out) {
Out = Out || [];
if (!Node || typeof Node.type !== 'string') return Out;
if (Node.type === 'Identifier') Out.push(Node.name);
else if (Node.type === 'ObjectPattern') for (const P of Node.properties) PatternNames(P.type === 'RestElement' ? P.argument : P.value, Out);
else if (Node.type === 'ArrayPattern') for (const E of Node.elements) if (E) PatternNames(E, Out);
else if (Node.type === 'AssignmentPattern') PatternNames(Node.left, Out);
else if (Node.type === 'RestElement') PatternNames(Node.argument, Out);
return Out;
}
// 「名字 → 绑定位置列表」;每项带该绑定的作用域范围(var/函数声明 = 最近的外层函数体,参数 = 函数自身)
function CollectBindings(Ast, ScopeStart, ScopeEnd, Out) {
if (!Ast || typeof Ast.type !== 'string') return Out;
Out = Out || {};
const Push = (Name, Start, End) => { if (!Out[Name]) Out[Name] = []; Out[Name].push({ Start, End }); };
if (Ast.type === 'FunctionDeclaration' && Ast.id) Push(Ast.id.name, ScopeStart, ScopeEnd);
if (Ast.type === 'ClassDeclaration' && Ast.id) Push(Ast.id.name, ScopeStart, ScopeEnd);
if (Ast.type === 'VariableDeclarator') for (const N of PatternNames(Ast.id)) Push(N, ScopeStart, ScopeEnd);
if (Ast.type === 'CatchClause' && Ast.param) for (const N of PatternNames(Ast.param)) Push(N, ScopeStart, ScopeEnd);
let S1 = ScopeStart;
let S2 = ScopeEnd;
if (IsScopeNode(Ast)) { S1 = Ast.start; S2 = Ast.end; }
if (IsFnNode(Ast) && Ast.params) for (const P of Ast.params) for (const N of PatternNames(P)) Push(N, S1, S2);
for (const K of ChildKeys(Ast)) {
const V = Ast[K];
if (Array.isArray(V)) { for (const C of V) if (C && typeof C.type === 'string') CollectBindings(C, S1, S2, Out); }
else if (V && typeof V.type === 'string') CollectBindings(V, S1, S2, Out);
}
return Out;
}
// 字符串字面量 token 原文列表(含引号),用于「字符串集合一致」校验
function StringTokenList(Text) {
const Tokens = [];
try {
Acorn.parse(Text, { ecmaVersion: 'latest', sourceType: 'script', allowReturnOutsideFunction: true, onToken: Tokens });
} catch (Err) { return null; }
return Tokens.filter((T) => T.type.label === 'string' || T.type.label === 'template').map((T) => Text.slice(T.start, T.end));
}
const ParseSrc = (Text) => Acorn.parse(Text, { ecmaVersion: 'latest', sourceType: 'script', allowReturnOutsideFunction: true });
// ---------------------------------------------------------------- 单模块扫描
// 一次遍历同时得到:属性位名字 / 值位名字 / 简写属性 / 绑定表 / 闭包原名 / 数据类名 / 枚举三元组
function ScanModule(Text, Ast) {
const Mod = {
PropNames: NewTable(), // 属性位置的名字 → [Identifier 节点]
PropDefs: NewTable(), // 属性「定义处」的名字 → [Identifier 节点](用于判断某个属性名全库是否只有一处定义)
ValueNames: NewTable(), // 值位置(引用或绑定)的名字 → [Identifier 节点]
Shorthand: new Set(),
Bindings: NewTable(),
Closures: NewTable(), // 短名 → Set(callableName)(AST 版,比 mine_evidence 的正则更全)
ClassNames: NewTable(), // 类短名 → Set(toString 里的真名)
Enums: [], // { Cls, Field, Member, Value, Site }
};
const Push = (Table, Name, Node) => { if (!Table[Name]) Table[Name] = []; Table[Name].push(Node); };
WalkWithParent(Ast, null, (Node, Parent, Grand) => {
if (Node.type === 'Identifier') {
const IsPropMember = Parent && Parent.type === 'MemberExpression' && Parent.property === Node && !Parent.computed;
const IsPropKey = Parent && (Parent.type === 'Property' || Parent.type === 'PropertyDefinition' || Parent.type === 'MethodDefinition') && Parent.key === Node && !Parent.computed;
if (IsPropMember || IsPropKey) {
Push(Mod.PropNames, Node.name, Node);
// 定义处 = 对象字面量键 / 类成员,或「成员表达式出现在赋值左侧 / 自增自减」
const IsDef = IsPropKey ||
(Parent.type === 'MemberExpression' && Grand && (
(Grand.type === 'AssignmentExpression' && Grand.left === Parent) ||
(Grand.type === 'UpdateExpression' && Grand.argument === Parent)));
if (IsDef) Push(Mod.PropDefs, Node.name, Node);
if (IsPropKey && Parent.shorthand) Mod.Shorthand.add(Node.name);
} else Push(Mod.ValueNames, Node.name, Node);
}
// 闭包原名:X.callableName = "…" 与 (X = function(){…}).callableName = "…" 两种形态
if (Node.type === 'AssignmentExpression' && Node.left.type === 'MemberExpression' && IdentOf(Node.left.property) === 'callableName' && Node.right.type === 'Literal' && typeof Node.right.value === 'string') {
const Obj = Node.left.object;
let Short = null;
if (Obj.type === 'Identifier') Short = Obj.name;
else if (Obj.type === 'AssignmentExpression' && IdentOf(Obj.left)) Short = Obj.left.name;
if (Short) { if (!Mod.Closures[Short]) Mod.Closures[Short] = new Set(); Mod.Closures[Short].add(Node.right.value); }
}
// 数据类真名:Q(Cls).toString = function () { return "Name(…" }
// 与 mine_evidence.js 的规则等价(函数体第一条就是 return,且返回串以 "<标识符>(" 开头),
// 只是换成 AST 判定,避免正则漏配;不额外放宽,防止凭空造名字。
if (Node.type === 'AssignmentExpression' && Node.left.type === 'MemberExpression' && IdentOf(Node.left.property) === 'toString') {
const Obj = Node.left.object;
if (Obj.type === 'CallExpression' && Obj.arguments.length === 1 && IdentOf(Obj.arguments[0]) && Node.right.type === 'FunctionExpression') {
const Cls = Obj.arguments[0].name;
const Body = Node.right.body && Node.right.body.body;
const Ret = Body && Body.length && Body[0].type === 'ReturnStatement' ? Body[0] : null;
if (Ret && Ret.argument) {
let Head = Ret.argument;
while (Head.type === 'BinaryExpression') Head = Head.left;
if (Head.type === 'Literal' && typeof Head.value === 'string') {
const M = /^([A-Za-z_$][\w$]*)\(/.exec(Head.value);
if (M) { if (!Mod.ClassNames[Cls]) Mod.ClassNames[Cls] = new Set(); Mod.ClassNames[Cls].add(M[1]); }
}
}
}
}
// 枚举成员:Field = new Cls("Member", 数字)
if (Node.type === 'AssignmentExpression' && Node.left.type === 'Identifier' && Node.right.type === 'NewExpression' &&
IdentOf(Node.right.callee) && Node.right.arguments.length === 2 &&
Node.right.arguments[0].type === 'Literal' && typeof Node.right.arguments[0].value === 'string' &&
Node.right.arguments[1].type === 'Literal' && typeof Node.right.arguments[1].value === 'number') {
Mod.Enums.push({ Cls: Node.right.callee.name, Field: Node.left.name, Member: Node.right.arguments[0].value, Value: Node.right.arguments[1].value, Site: Node.start });
}
});
Mod.Bindings = CollectBindings(Ast, 0, Text.length, NewTable());
return Mod;
}
// ---------------------------------------------------------------- 载入证据
function LoadJson(File, Must) {
const P = path.join(NamesDir, File);
if (!fs.existsSync(P)) { if (Must) { console.error('缺少证据文件:' + P); process.exit(2); } return null; }
return JSON.parse(fs.readFileSync(P, 'utf8'));
}
const Evidence = LoadJson('evidence.json', true);
const CodecFields = LoadJson('codec_fields.json', false);
const UplinkTable = LoadJson('uplink_table.json', false);
const AliasTable = LoadJson('command_alias.json', false); // 派生命名词典,仅 --aliases 时使用
// ---------------------------------------------------------------- 扫描源码
const SrcFiles = fs.readdirSync(InDir).filter((F) => /^module_\d+\.js$/.test(F)).sort();
const OtherFiles = fs.readdirSync(InDir).filter((F) => !/^module_\d+\.js$/.test(F) && fs.statSync(path.join(InDir, F)).isFile());
const Modules = NewTable();
const GlobalPropNames = NewTable();
const GlobalPropDefs = NewTable(); // 属性「定义处」的全局计数
const GlobalValueNames = NewTable();
const GlobalStrings = new Set();
const GlobalShorthand = new Set();
let ParseFail = 0;
for (const F of SrcFiles) {
const Text = fs.readFileSync(path.join(InDir, F), 'utf8');
const Id = String(Number(/module_(\d+)\.js$/.exec(F)[1]));
let Ast;
try { Ast = ParseSrc(Text); } catch (Err) { ParseFail++; console.error('解析失败 ' + F + ':' + Err.message); continue; }
const Mod = ScanModule(Text, Ast);
const Strings = StringTokenList(Text) || [];
for (const S of Strings) GlobalStrings.add(S.slice(1, -1));
for (const [K, V] of Object.entries(Mod.PropNames)) GlobalPropNames[K] = (GlobalPropNames[K] || 0) + V.length;
for (const [K, V] of Object.entries(Mod.PropDefs)) GlobalPropDefs[K] = (GlobalPropDefs[K] || 0) + V.length;
for (const [K, V] of Object.entries(Mod.ValueNames)) GlobalValueNames[K] = (GlobalValueNames[K] || 0) + V.length;
for (const S of Mod.Shorthand) GlobalShorthand.add(S);
Modules[Id] = { Id, File: F, Text, Mod, Strings, Ast };
}
if (ParseFail) { console.error('有 ' + ParseFail + ' 个模块解析失败,终止'); process.exit(2); }
const EvidenceByModule = NewTable();
for (const [Key, Val] of Object.entries(Evidence.byModule || {})) EvidenceByModule[String(Number(Key))] = Val;
// ---------------------------------------------------------------- 计划容器
const Categories = ['数据类名', '闭包名', '枚举成员', '字段→组件名', '属性名', '命令字段', '别名类名', '别名方法', '别名字段'];
const Reasons = ['Conflict', 'InStringLiteral', 'Whitelist', 'MissingEvidence'];
const Plans = [];
const Skips = [];
const AddPlan = (Module, Category, Namespace, Short, Long, Nodes) => {
Plans.push({ Module, Category, Namespace, Short, Long, Nodes: Nodes || [] });
};
const AddSkip = (Module, Category, Namespace, Short, Long, Reason, Detail) => {
Skips.push({ Module, Category, Namespace, Short, Long, Reason, Detail });
};
// ================================================================ ① 属性名(全局共享)
// 来源 A:evidence.fieldsGlobal(短字段 → 组件名) → 类别「字段→组件名」
// 来源 B:codec_fields 的 Slot / Prop → 字段名 → 类别「属性名」
const PropCandidates = [];
const GroupMap = (Obj) => {
const Out = {};
for (const [Short, Longs] of Object.entries(Obj || {})) {
for (const L of Longs) { if (!Out[Short]) Out[Short] = new Set(); Out[Short].add(L); }
}
return Out;
};
for (const [Short, Longs] of Object.entries(GroupMap(Evidence.fieldsGlobal))) {
// 每条证据已经带有「组件名」语义,取唯一值
const L = [...Longs];
if (L.length > 1) { AddSkip('*', '字段→组件名', 'Property', Short, L.join('/'), 'Conflict', '同一短字段对应 ' + L.length + ' 个组件名'); continue; }
PropCandidates.push({ Short, Long: L[0], Category: '字段→组件名' });
}
if (CodecFields && CodecFields.Codecs) {
const SlotMap = NewTable();
const PropMap = NewTable();
for (const Codec of Object.values(CodecFields.Codecs)) {
for (const F of Codec.Fields || []) {
const Long = F.Name;
if (!Long || Long === '?' || Long === '(匿名)') continue;
if (F.Slot) { if (!SlotMap[F.Slot]) SlotMap[F.Slot] = new Set(); SlotMap[F.Slot].add(Long); }
if (F.Prop) { if (!PropMap[F.Prop]) PropMap[F.Prop] = new Set(); PropMap[F.Prop].add(Long); }
}
}
for (const [Short, Longs] of Object.entries(SlotMap)) {
const L = [...Longs];
if (L.length > 1) { AddSkip('*', '属性名', 'Property', Short, L.join('/'), 'Conflict', 'codec 槽位在不同 codec 里对应不同字段名'); continue; }
PropCandidates.push({ Short, Long: L[0], Category: '属性名', Kind: 'codec 槽位' });
}
for (const [Short, Longs] of Object.entries(PropMap)) {
const L = [...Longs];
if (L.length > 1) { AddSkip('*', '属性名', 'Property', Short, L.join('/'), 'Conflict', '模型属性对应多个字段名'); continue; }
PropCandidates.push({ Short, Long: L[0], Category: '属性名', Kind: '模型属性' });
}
}
const PropertyRename = {}; // Short → Long
PropCandidates.sort((A, B) => (A.Short < B.Short ? -1 : A.Short > B.Short ? 1 : 0));
for (const C of PropCandidates) {
const { Short, Long, Category } = C;
const Rec = (Reason, Detail) => AddSkip('*', Category, 'Property', Short, Long, Reason, Detail);
if (HasExportSigil(Short) || HasExportSigil(Long)) { Rec('Whitelist', '含 $_$ 的导出名'); continue; }
const PS = NameProblem(Short, false);
if (PS) { Rec('Whitelist', '短名命中白名单:' + PS); continue; }
const PL = NameProblem(Long, true);
if (PL) { Rec('Whitelist', '目标名命中白名单:' + PL); continue; }
if (Short === Long) { Rec('Conflict', '目标名与短名相同'); continue; }
if (GlobalStrings.has(Short)) { Rec('InStringLiteral', '短名出现在字符串字面量里'); continue; }
if (GlobalPropNames[Short] === undefined) { Rec('MissingEvidence', '源码里找不到该属性名'); continue; }
if (GlobalPropNames[Long] !== undefined) { Rec('Conflict', '目标名已是既有属性名(会把两个不同字段合并成一个)'); continue; }
if (GlobalShorthand.has(Short)) { Rec('Conflict', '存在同名的简写属性(属性名与局部名是同一节点)'); continue; }
PropertyRename[Short] = Long;
}
for (const [Short, Long] of Object.entries(PropertyRename)) {
for (const M of Object.values(Modules)) if (M.Mod.PropNames[Short]) AddPlan(M.Id, null, 'Property', Short, Long, M.Mod.PropNames[Short]);
}
// 属性名类别需要区分「字段→组件名」与「属性名」——回填类别
const PropCategoryOf = {};
for (const C of PropCandidates) if (PropertyRename[C.Short] === C.Long && !PropCategoryOf[C.Short]) PropCategoryOf[C.Short] = C.Category;
for (const P of Plans) if (P.Namespace === 'Property') P.Category = PropCategoryOf[P.Short] || '属性名';
// ================================================================ ② 局部名(逐模块)
// 通用准入:全模块唯一绑定 + 所有值位引用都在该绑定作用域内 + 目标名在该模块的值命名空间里是新的
// 返回 '' 表示准入通过;否则返回跳过原因(同时已写入 Skips)
function LocalRenameProblem(M, Short, Long, Category) {
const Rec = (Reason, Detail) => { AddSkip(M.Id, Category, 'Local', Short, Long, Reason, Detail); return Reason; };
if (HasExportSigil(Short) || HasExportSigil(Long)) return Rec('Whitelist', '含 $_$ 的导出名');
const PS = NameProblem(Short, false);
if (PS) return Rec('Whitelist', '短名命中白名单:' + PS);
const PL = NameProblem(Long, true);
if (PL) return Rec('Whitelist', '目标名命中白名单:' + PL);
if (Short === Long) return Rec('Conflict', '目标名与短名相同');
if (GlobalStrings.has(Short)) return Rec('InStringLiteral', '短名出现在字符串字面量里');
const Refs = M.Mod.ValueNames[Short];
if (!Refs || !Refs.length) return Rec('MissingEvidence', '源码里找不到该局部名的使用点');
const B = M.Mod.Bindings[Short];
if (!B || !B.length) return Rec('MissingEvidence', '源码里找不到该名字的绑定声明');
if (B.length > 1) return Rec('Conflict', '全模块有 ' + B.length + ' 处同名绑定(存在遮蔽),改名无法保证一致');
const Scope = B[0];
for (const N of Refs) if (N.start < Scope.Start || N.end > Scope.End) return Rec('Conflict', '有引用落在绑定作用域之外(可能指向别处)');
if (M.Mod.Shorthand.has(Short)) return Rec('Conflict', '存在同名的简写属性');
if (M.Mod.ValueNames[Long]) return Rec('Conflict', '目标名在本模块的值命名空间里已被占用');
return '';
}
const LocalRenameAllowed = (M, Short, Long, Category) => LocalRenameProblem(M, Short, Long, Category) === '';
const LocalTargetUsed = NewTable(); // 模块号 → Set(目标名)
for (const M of Object.values(Modules)) {
const Used = new Set();
LocalTargetUsed[M.Id] = Used;
const Try = (Short, Long, Category) => {
if (!LocalRenameAllowed(M, Short, Long, Category)) return false;
if (Used.has(Long)) { AddSkip(M.Id, Category, 'Local', Short, Long, 'Conflict', '目标名在本模块已被其它改名占用'); return false; }
Used.add(Long);
AddPlan(M.Id, Category, 'Local', Short, Long, M.Mod.ValueNames[Short]);
return true;
};
// ---- 数据类名:直接用 AST 重新推导(比 mine_evidence 的正则更准),并与证据交叉核对
const Ev = EvidenceByModule[M.Id] || {};
const EvClassNames = Ev.classNames || {};
for (const [Cls, Names] of Object.entries(M.Mod.ClassNames)) {
const L = [...Names];
if (L.length > 1) { AddSkip(M.Id, '数据类名', 'Local', Cls, L.join('/'), 'Conflict', '同一个类名短名对应多个数据类真名'); continue; }
if (EvClassNames[Cls] && EvClassNames[Cls] !== L[0]) { AddSkip(M.Id, '数据类名', 'Local', Cls, L[0], 'Conflict', '与证据 ' + EvClassNames[Cls] + ' 不一致'); continue; }
Try(Cls, L[0], '数据类名');
}
for (const [Cls, Name] of Object.entries(EvClassNames)) {
if (!M.Mod.ClassNames[Cls]) AddSkip(M.Id, '数据类名', 'Local', Cls, Name, 'MissingEvidence', '证据里有但源码里复核不到该 toString 模式');
}
// ---- 闭包名:X.callableName = "…"
for (const [Short, Names] of Object.entries(M.Mod.Closures)) {
const L = [...Names];
if (L.length > 1) { AddSkip(M.Id, '闭包名', 'Local', Short, L.join('/'), 'Conflict', '同一短名对应 ' + L.length + ' 个闭包原名'); continue; }
Try(Short, L[0], '闭包名');
}
// ---- 枚举成员:Field = new Cls("Member", n)
const EvEnums = Ev.enums || {};
const SeenTriple = new Set();
for (const E of M.Mod.Enums) {
const Key = E.Cls + '|' + E.Field + '|' + E.Member;
if (SeenTriple.has(Key)) continue;
SeenTriple.add(Key);
const InEvidence = (EvEnums[E.Cls] || []).some((X) => X.field === E.Field && X.member === E.Member && X.value === E.Value);
if (!InEvidence) { AddSkip(M.Id, '枚举成员', 'Local', E.Field, E.Member, 'MissingEvidence', '证据里没有枚举 ' + E.Cls + ' 的该成员'); continue; }
Try(E.Field, E.Member, '枚举成员');
}
for (const [Cls, Members] of Object.entries(EvEnums)) {
for (const X of Members) {
if (!M.Mod.Enums.some((E) => E.Cls === Cls && E.Field === X.field && E.Member === X.member && X.value === E.Value)) {
AddSkip(M.Id, '枚举成员', 'Local', X.field, X.member, 'MissingEvidence', '证据里有但源码里复核不到(' + Cls + ')');
}
}
}
}
// ================================================================ ③ 命令字段
// uplink_table 的负载字段只有「命令对象上的 codec 槽位」(如 this.jh9_1.o7g(...)),源码里没有 codec_<名字> getter,
// 因此没有可回填的可读名 —— 全部按「证据缺失」计入报告(见 uplink_table.json 的 Note 自述)。
if (UplinkTable && UplinkTable.Commands) {
for (const Cmd of UplinkTable.Commands) {
// uplink_table 的 Module 形如 "module_10283",统一成去前导零的模块号,和报告其它地方保持一致
const Mid = String(Number(String(Cmd.Module || '0').replace(/^module_/, '')) || 0);
for (const F of Cmd.Fields || []) {
AddSkip(Mid, '命令字段', 'Property', F.Field, '', 'MissingEvidence', '上行命令负载字段没有保留可读名(无 codec_<名字> getter)');
}
}
}
// ================================================================ ③.5 派生命名(--aliases)
// 来源:names/command_alias.json。这些**不是**原始名,是我们读源码证实语义后起的别名,
// 与证据命名同表共存、但分开统计(报告 Aliases 段 + 控制台「派生命名」段)。
// 比证据命名多一道闸门:命名空间是 Property 时,该属性名在全库里必须**只有本模块这一处定义**
// (别的模块里的同名出现只允许是「读」)—— 属性名跨模块共享,若别处还有同名定义就是另一个字段,
// 全局改名会把两者合并成一个,直接跳过。
const AliasReport = {
Enabled: UseAliases, File: path.join(NamesDir, 'command_alias.json'), Loaded: !!AliasTable,
Entries: 0, Applied: [], Skipped: [], Note: [],
};
if (UseAliases && !AliasTable) AliasReport.Note.push('找不到 command_alias.json,本次没有别名可回填');
if (UseAliases && AliasTable) {
const AliasPropTargets = new Set(); // 别名已经占用的属性目标名
const AliasPropShorts = new Set(); // 已经被别名处理过的属性短名
const Applied = (E, Namespace, Count) => AliasReport.Applied.push({
Key: E.Key, Kind: E.Kind, Module: E.Module, Owner: E.Owner || '', Short: E.Short, Long: E.Long, Namespace, Count,
});
const Entries = [];
for (const [Key, Val] of Object.entries(AliasTable.Classes || {})) {
const P = String(Key).split('::');
Entries.push({ Key, Kind: 'Classes', Module: P[0], Owner: '', Short: P[1], Long: Val.Alias, Category: '别名类名', Namespace: 'Local' });
}
for (const Kind of ['Methods', 'Fields']) {
for (const [Key, Val] of Object.entries(AliasTable[Kind] || {})) {
const P = String(Key).split('::');
const Seg = String(P[1] || '').split('.');
Entries.push({
Key, Kind, Module: P[0], Owner: Seg.slice(0, -1).join('.'),
Short: Seg[Seg.length - 1], Long: Val.Alias,
Category: Kind === 'Methods' ? '别名方法' : '别名字段', Namespace: 'Property',
});
}
}
Entries.sort((A, B) => (A.Key < B.Key ? -1 : A.Key > B.Key ? 1 : 0));
AliasReport.Entries = Entries.length;
for (const E of Entries) {
const M = Modules[E.Module];
const Rec = (Reason, Detail) => {
AddSkip('*', E.Category, E.Namespace === 'Local' ? 'Local' : 'Property', E.Short, E.Long, Reason, Detail);
AliasReport.Skipped.push({ Key: E.Key, Kind: E.Kind, Short: E.Short, Long: E.Long, Reason, Detail });
};
if (!M) { Rec('MissingEvidence', '模块 ' + E.Module + ' 不在本次输入里'); continue; }
if (E.Namespace === 'Local') {
const Reason = LocalRenameProblem(M, E.Short, E.Long, E.Category);
if (Reason) { AliasReport.Skipped.push({ Key: E.Key, Kind: E.Kind, Short: E.Short, Long: E.Long, Reason, Detail: '局部名准入未通过,详见 SkipExamples' }); continue; }
const Used = LocalTargetUsed[E.Module];
if (Used.has(E.Long)) { Rec('Conflict', '目标名在本模块已被其它改名占用'); continue; }
const Nodes = M.Mod.ValueNames[E.Short];
Used.add(E.Long);
AddPlan(E.Module, E.Category, 'Local', E.Short, E.Long, Nodes);
Applied(E, 'Local', Nodes.length);
continue;
}
// ---- Property(属性名,全局共享)
if (HasExportSigil(E.Short) || HasExportSigil(E.Long)) { Rec('Whitelist', '含 $_$ 的导出名'); continue; }
const PS = NameProblem(E.Short, false);
if (PS) { Rec('Whitelist', '短名命中白名单:' + PS); continue; }
const PL = NameProblem(E.Long, true);
if (PL) { Rec('Whitelist', '目标名命中白名单:' + PL); continue; }
if (E.Short === E.Long) { Rec('Conflict', '目标名与短名相同'); continue; }
const Nodes = M.Mod.PropNames[E.Short];
if (!Nodes || !Nodes.length) { Rec('MissingEvidence', '本模块里找不到该属性名'); continue; }
if (GlobalStrings.has(E.Short)) { Rec('InStringLiteral', '短名出现在字符串字面量里'); continue; }
// 闸门:这个属性名在全库里必须**只有本模块这一处定义**。
// 别的模块里出现同名,只允许是「读」(调用点 / 取字段)—— 唯一的定义在我们这里,
// 那些读访问读的就只可能是这个属性,全局改名安全。
// 若别处还有同名定义,说明是另一个字段(Kotlin/JS 的短名会跨类复用),改名会把两者合并,必须跳过。
const DefsHere = (M.Mod.PropDefs[E.Short] || []).length;
const DefsAll = GlobalPropDefs[E.Short] || 0;
if (!DefsHere) { Rec('MissingEvidence', '本模块里找不到该属性名的定义处'); continue; }
if (DefsAll !== DefsHere) {
Rec('Conflict', '全库还有 ' + (DefsAll - DefsHere) + ' 处同名属性定义(全库 ' + DefsAll + ' 处 / 本模块 ' + DefsHere + ' 处),可能是另一个字段');
continue;
}
if (M.Mod.Shorthand.has(E.Short)) { Rec('Conflict', '存在同名的简写属性'); continue; }
if (PropertyRename[E.Short]) { Rec('Conflict', '该短名已被证据命名占用(→' + PropertyRename[E.Short] + ')'); continue; }
if (AliasPropShorts.has(E.Short)) { Rec('Conflict', '该短名已被另一条别名处理'); continue; }
if (GlobalPropNames[E.Long] !== undefined) { Rec('Conflict', '目标名已是既有属性名(会把两个不同字段合并成一个)'); continue; }
if (AliasPropTargets.has(E.Long) || Object.values(PropertyRename).indexOf(E.Long) >= 0) { Rec('Conflict', '目标名已被其它改名占用'); continue; }
AliasPropTargets.add(E.Long);
AliasPropShorts.add(E.Short);
let Count = 0;
for (const Other of Object.values(Modules)) {
if (!Other.Mod.PropNames[E.Short]) continue;
AddPlan(Other.Id, E.Category, 'Property', E.Short, E.Long, Other.Mod.PropNames[E.Short]);
Count += Other.Mod.PropNames[E.Short].length;
}
Applied(E, 'Property', Count);
}
}
// ================================================================ ④ 应用改名并写出
for (const P of Plans) P.Count = P.Nodes.length;
const EditsByModule = {};
for (const P of Plans) {
if (!EditsByModule[P.Module]) EditsByModule[P.Module] = [];
for (const N of P.Nodes) EditsByModule[P.Module].push({ Start: N.start, End: N.end, Text: P.Long });
}
// 从后往前替换,避免位移;同时检查区间不重叠
function ApplyEdits(Text, Edits) {
const Sorted = Edits.slice().sort((A, B) => B.Start - A.Start);
let Out = Text;
let Last = Infinity;
for (const E of Sorted) {
if (E.End > Last) throw new Error('替换区间重叠 @' + E.Start + '-' + E.End);
Out = Out.slice(0, E.Start) + E.Text + Out.slice(E.End);
Last = E.Start;
}
return Out;
}
const Outputs = {}; // 模块号 → 改名后的源码
for (const M of Object.values(Modules)) Outputs[M.Id] = ApplyEdits(M.Text, EditsByModule[M.Id] || []);
if (!DryRun) {
fs.mkdirSync(OutDir, { recursive: true });
for (const M of Object.values(Modules)) fs.writeFileSync(path.join(OutDir, M.File), Outputs[M.Id], 'utf8');
for (const F of OtherFiles) fs.copyFileSync(path.join(InDir, F), path.join(OutDir, F));
}
// ================================================================ ⑤ 校验
// ① node --check 语法
// ② 字符串字面量集合与原文逐模块完全一致
// ③ 标识符出现次数自检:被改的短名在各命名空间里清零、目标名增量等于替换次数
const Validation = { SyntaxChecked: 0, SyntaxFailed: [], StringIdentical: 0, StringMismatch: [], CountChecked: 0, CountMismatch: [], TotalIdentSame: true };
function ScanNamesOf(Text) {
const Ast = ParseSrc(Text);
const Mod = ScanModule(Text, Ast);
return Mod;
}
// 期望:按 (模块, 命名空间, 目标名) 汇总替换次数
const ExpectByTarget = {};
const ExpectShortGone = [];
for (const P of Plans) {
const Key = P.Module + '|' + P.Namespace + '|' + P.Long;
ExpectByTarget[Key] = (ExpectByTarget[Key] || 0) + P.Count;
ExpectShortGone.push({ Module: P.Module, Namespace: P.Namespace, Short: P.Short, Category: P.Category });
}
for (const M of Object.values(Modules)) {
const Text = Outputs[M.Id];
// ① 语法
if (!DryRun) {
const Res = spawnSync(process.execPath, ['--check', path.join(OutDir, M.File)], { encoding: 'utf8' });
Validation.SyntaxChecked++;
if (Res.status !== 0) Validation.SyntaxFailed.push(M.File + ':' + String(Res.stderr || '').split('\n').slice(0, 2).join(' '));
} else {
try { ParseSrc(Text); Validation.SyntaxChecked++; } catch (Err) { Validation.SyntaxFailed.push(M.File + ':' + Err.message); }
}
// ② 字符串字面量集合(逐模块多重集比对)
const After = StringTokenList(Text);
const Before = M.Strings;
const A = After ? After.slice().sort().join('\u0001') : '';
const B = Before.slice().sort().join('\u0001');
if (After && A === B) Validation.StringIdentical++;
else Validation.StringMismatch.push(M.File);
// ③ 标识符计数
let AfterMod;
try { AfterMod = ScanNamesOf(Text); } catch (Err) { AfterMod = null; }
if (!AfterMod) { Validation.CountMismatch.push(M.File + ':改名后无法解析'); continue; }
const CountIn = (Table, Name) => (Table[Name] ? Table[Name].length : 0);
let Bad = false;
for (const E of ExpectShortGone) {
if (E.Module !== M.Id) continue;
const Table = E.Namespace === 'Property' ? AfterMod.PropNames : AfterMod.ValueNames;
if (CountIn(Table, E.Short) !== 0) { Validation.CountMismatch.push(M.File + ':短名 ' + E.Short + ' 仍有残留(' + E.Category + ')'); Bad = true; }
}
for (const [Key, Expect] of Object.entries(ExpectByTarget)) {
const Parts = Key.split('|');
if (Parts[0] !== M.Id) continue;
const Table = Parts[1] === 'Property' ? AfterMod.PropNames : AfterMod.ValueNames;
const Actual = CountIn(Table, Parts[2]);
if (Actual !== Expect) { Validation.CountMismatch.push(M.File + ':' + Parts[2] + '(' + Parts[1] + ') 期望 ' + Expect + ' 实际 ' + Actual); Bad = true; }
}
// 总标识符出现次数必须与原文一致(只换了名字,不增删标识符)
const TotalBefore = Object.values(M.Mod.PropNames).reduce((A, V) => A + V.length, 0) + Object.values(M.Mod.ValueNames).reduce((A, V) => A + V.length, 0);
const TotalAfter = Object.values(AfterMod.PropNames).reduce((A, V) => A + V.length, 0) + Object.values(AfterMod.ValueNames).reduce((A, V) => A + V.length, 0);
if (TotalBefore !== TotalAfter) { Validation.TotalIdentSame = false; Bad = true; }
if (!Bad) Validation.CountChecked++;
}
// ================================================================ ⑥ 报告
const SumBy = (List, KeyFn) => List.reduce((A, V) => { const K = KeyFn(V); A[K] = (A[K] || 0) + 1; return A; }, {});
const Renamed = {};
for (const C of Categories) {
const L = Plans.filter((P) => P.Category === C);
const Entries = new Set(L.map((P) => P.Short + '→' + P.Long));
Renamed[C] = {
Entries: Entries.size, // 唯一「短名→可读名」条数
Edits: L.reduce((A, P) => A + P.Count, 0), // 实际替换的标识符出现次数
Examples: L.slice(0, 15).map((P) => ({ Module: P.Module, Short: P.Short, Long: P.Long, Count: P.Count })),
};
}
const Skipped = {};
const SkipExamples = {};
for (const C of Categories) {
Skipped[C] = {};
for (const R of Reasons) Skipped[C][R] = 0;
}
const SkipGroups = {};
for (const S of Skips) {
if (!Skipped[S.Category]) { Skipped[S.Category] = {}; for (const R of Reasons) Skipped[S.Category][R] = 0; }
Skipped[S.Category][S.Reason] = (Skipped[S.Category][S.Reason] || 0) + 1;
const K = S.Category + '|' + S.Reason;
SkipGroups[K] = SkipGroups[K] || { Count: 0, Detail: S.Detail, Examples: [] };
SkipGroups[K].Count++;
if (SkipGroups[K].Examples.length < 10) SkipGroups[K].Examples.push({ Module: S.Module, Short: S.Short, Long: S.Long, Detail: S.Detail });
}
for (const [K, V] of Object.entries(SkipGroups)) SkipExamples[K] = { Count: V.Count, Examples: V.Examples };
// 证据复核:AST 重新推导 vs 证据。要求「证据里的条目 100% 被 AST 复现」,反向多出来的必须是同源模式
const Recheck = {
ClassNamesEvidence: 0, ClassNamesFromAst: 0, ClassNamesEvidenceMissed: [],
ClosuresEvidence: 0, ClosuresFromAst: 0, ClosuresEvidenceMissed: [],
EnumMembersEvidence: 0, EnumMembersFromAst: 0, EnumMembersEvidenceMissed: [],
};
for (const [Id, Ev] of Object.entries(EvidenceByModule)) {
const M = Modules[Id];
if (!M) continue;
for (const [Cls, Name] of Object.entries(Ev.classNames || {})) {
Recheck.ClassNamesEvidence++;
if (!M.Mod.ClassNames[Cls] || !M.Mod.ClassNames[Cls].has(Name)) Recheck.ClassNamesEvidenceMissed.push('module_' + Id + ' ' + Cls + '=' + Name);
}
for (const [Short, Names] of Object.entries(Ev.closures || {})) {
for (const N of Names) {
Recheck.ClosuresEvidence++;
if (!M.Mod.Closures[Short] || !M.Mod.Closures[Short].has(N)) Recheck.ClosuresEvidenceMissed.push('module_' + Id + ' ' + Short + '=' + N);
}
}
for (const [Cls, Members] of Object.entries(Ev.enums || {})) {
for (const X of Members) {
Recheck.EnumMembersEvidence++;
if (!M.Mod.Enums.some((E) => E.Cls === Cls && E.Field === X.field && E.Member === X.member && E.Value === X.value)) Recheck.EnumMembersEvidenceMissed.push('module_' + Id + ' ' + Cls + ' ' + X.field + '=' + X.member);
}
}
}
for (const M of Object.values(Modules)) {
Recheck.ClassNamesFromAst += Object.keys(M.Mod.ClassNames).length;
Recheck.ClosuresFromAst += Object.values(M.Mod.Closures).reduce((A, V) => A + V.size, 0);
Recheck.EnumMembersFromAst += M.Mod.Enums.length;
}
const Report = {
GeneratedAt: new Date().toISOString(),
In: InDir,
Out: OutDir,
DryRun,
Parser: 'acorn (AST)',
ModuleCount: Object.keys(Modules).length,
OtherFilesCopied: OtherFiles,
Note: [
'属性名(JS 属性访问 / 对象字面量键)是全局共享的:先收集全局映射,再在全部模块里一次性写回。',
'局部名(类名 / 闭包名 / 枚举成员)只在所属模块内改,且要求「全模块唯一绑定 + 所有引用都在该作用域内」。',
'【规则取舍】原需求同时写了「属性名可全局一起改(定义处 + 字符串处一起改)」与「字符串字面量集合必须与原文完全一致」,二者不可兼得。',
' 本工具选择「绝不碰字符串」:短名只要在任何字符串字面量里出现过就跳过(属性名与局部名一视同仁)。',
' 对 s/t/n 这类单字母短名,「字符串处一起改」会把所有恰好等于该串的无关字面量(如单位后缀 "s")一起改掉,风险不可接受。',
'属性名目标若已是既有属性名:跳过(否则会把两个不同字段合并成一个)。',
'命令字段(uplink_table 的 555 条负载槽位)在源码里没有保留可读名,全部计入证据缺失。',
'闭包名 / 数据类名用与 mine_evidence.js 同源但更抗格式化的 AST 规则重新推导,并与证据交叉核对(见 Recheck)。',
' 证据里的条目必须 100% 被 AST 复现(*EvidenceMissed 应为空);AST 反向多出来的只是同一模式的漏配(如 prettier 换行导致正则抓不到)。',
'【派生命名】只有 --aliases 打开时才回填;来自 command_alias.json,是「读源码证实语义后起的别名」而非原始名,单独记在 Aliases 段,不计入上面的证据改名统计。',
],
Renamed,
Skipped,
SkipExamples,
Recheck,
Aliases: AliasReport,
Validation,
};
fs.mkdirSync(path.dirname(ReportPath), { recursive: true });
fs.writeFileSync(ReportPath, JSON.stringify(Report, null, 1), 'utf8');
// ---------------------------------------------------------------- 控制台摘要
console.log('源目录 : ' + InDir);
console.log('输出目录 : ' + OutDir + (DryRun ? ' (--dry:未写出)' : ''));
console.log('模块 : ' + Object.keys(Modules).length + ' 个;非 module 文件 ' + OtherFiles.length + ' 个');
console.log('');
console.log('=== 改名统计(Entries = 唯一短名→可读名 条数,Edits = 替换的标识符出现次数)===');
for (const C of Categories) {
const R = Renamed[C];
console.log(' ' + C.padEnd(7) + ' Entries ' + String(R.Entries).padStart(5) + ' Edits ' + String(R.Edits).padStart(7));
for (const E of R.Examples.slice(0, 4)) console.log(' 例: ' + C + ' ' + E.Short + ' → ' + E.Long + ' [' + (E.Module === '*' ? '全局' : 'module_' + E.Module) + ' ×' + E.Count + ']');
}
console.log('');
console.log('=== 跳过统计 ===');
const ReasonLabel = { Conflict: '冲突', InStringLiteral: '出现在字符串字面量里', Whitelist: '不可改名白名单', MissingEvidence: '证据缺失' };
for (const C of Categories) {
const R = Skipped[C] || {};
const Line = Reasons.map((X) => ReasonLabel[X] + ' ' + (R[X] || 0)).join(' ');
console.log(' ' + C.padEnd(7) + ' ' + Line);
}
console.log('');
console.log('=== 典型跳过例子 ===');
for (const [K, V] of Object.entries(SkipExamples)) {
if (!V.Count) continue;
const [Cat, Reason] = K.split('|');
console.log(' [' + Cat + ' / ' + ReasonLabel[Reason] + ' ×' + V.Count + ']');
for (const E of V.Examples.slice(0, 3)) console.log(' ' + (E.Module === '*' ? '全局' : 'module_' + E.Module) + ' ' + E.Short + (E.Long ? ' → ' + E.Long : '') + ' ' + E.Detail);
}
console.log('');
console.log('=== 派生命名(' + (UseAliases ? '已启用' : '未启用:加 --aliases 才会回填') + ')===');
console.log(' 词典 : ' + AliasReport.File + (AliasReport.Loaded ? '' : '(未找到)'));
console.log(' 条目 : ' + AliasReport.Entries + ' 条;采用 ' + AliasReport.Applied.length + ' 条,跳过 ' + AliasReport.Skipped.length + ' 条');
if (UseAliases) {
const ByCat = {};
for (const A of AliasReport.Applied) { const K = A.Kind; ByCat[K] = (ByCat[K] || 0) + 1; }
for (const A of AliasReport.Applied.slice(0, 12)) {
console.log(' ✓ ' + A.Key + ' ' + A.Short + ' → ' + A.Long + ' [' + A.Namespace + ' ×' + A.Count + ']');
}
if (AliasReport.Applied.length > 12) console.log(' … 另 ' + (AliasReport.Applied.length - 12) + ' 条见报告 Aliases.Applied');
const SkipByReason = {};
for (const S of AliasReport.Skipped) SkipByReason[S.Reason] = (SkipByReason[S.Reason] || 0) + 1;
for (const A of AliasReport.Skipped.slice(0, 8)) {
console.log(' × ' + A.Key + ' ' + A.Short + ' → ' + A.Long + ' [' + A.Reason + '] ' + A.Detail);
}
if (AliasReport.Skipped.length > 8) console.log(' … 另 ' + (AliasReport.Skipped.length - 8) + ' 条见报告 Aliases.Skipped');
console.log(' 采用明细: ' + Object.entries(ByCat).map(([K, V]) => K + ' ' + V).join(' ') || '(无)');
}
console.log('');
console.log('=== 校验 ===');
console.log(' ① node --check : 通过 ' + Validation.SyntaxChecked + ' / ' + Object.keys(Modules).length + (Validation.SyntaxFailed.length ? ' 失败: ' + Validation.SyntaxFailed.slice(0, 5).join(' | ') : ' ✓'));
console.log(' ② 字符串字面量集合 : 一致 ' + Validation.StringIdentical + ' / ' + Object.keys(Modules).length + (Validation.StringMismatch.length ? ' 不一致: ' + Validation.StringMismatch.slice(0, 5).join(', ') : ' ✓'));
console.log(' ③ 标识符计数自检 : 通过 ' + Validation.CountChecked + ' / ' + Object.keys(Modules).length + ';标识符总数不变 ' + (Validation.TotalIdentSame ? '✓' : '✗') + (Validation.CountMismatch.length ? ' 异常: ' + Validation.CountMismatch.slice(0, 5).join(' | ') : ' ✓'));
console.log(' ④ 证据复核(AST 复现): 数据类名 ' + Recheck.ClassNamesEvidence + '→' + Recheck.ClassNamesFromAst +
'(漏 ' + Recheck.ClassNamesEvidenceMissed.length + ');闭包名 ' + Recheck.ClosuresEvidence + '→' + Recheck.ClosuresFromAst +
'(漏 ' + Recheck.ClosuresEvidenceMissed.length + ');枚举成员 ' + Recheck.EnumMembersEvidence + '→' + Recheck.EnumMembersFromAst +
'(漏 ' + Recheck.EnumMembersEvidenceMissed.length + ')');
const Ok = !Validation.SyntaxFailed.length && !Validation.StringMismatch.length && !Validation.CountMismatch.length && Validation.TotalIdentSame &&
!Recheck.ClassNamesEvidenceMissed.length && !Recheck.ClosuresEvidenceMissed.length && !Recheck.EnumMembersEvidenceMissed.length;
console.log('');
console.log(Ok ? '全部校验通过。报告: ' + ReportPath : '校验未通过,请看上面的异常列表。报告: ' + ReportPath);
if (!Ok) process.exitCode = 1;