工具链(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/(含数据库口令)不进库,可用上述工具重新生成。
787 lines
48 KiB
JavaScript
787 lines
48 KiB
JavaScript
// 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;
|