UNPKG

unity-find-fault

Version:

A tool to find fault in unity project.

427 lines 17.9 kB
import fs from "fs-extra"; import fg from "fast-glob"; import path from "path"; import { Project, SyntaxKind } from "ts-morph"; import { eitherExists } from "./vendor.js"; import { toolchain } from "../toolchain.js"; export class CodeStriper { async removeUseless() { const project = new Project({ tsConfigFilePath: path.join(toolchain.opts.projectRoot, 'TsScripts/tsconfig.json') }); // await this.warnEmptyMethods(project); // await this.processUIPathData(project); // await this.processGetCfg(project); await this.removeUnnecessaryCodes(project); await this.removeEmptyFolders(); } async processUIPathData(project) { // index.d.ts const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts'); const files = await fg('**/UIPathData.ts', { cwd: tsRoot }); if (files.length == 0) { console.error('no UIPathData.ts'); return; } const updFile = path.join(tsRoot, files[0]); console.log('UIPathData:', updFile); const updSrc = project.getSourceFile(updFile); const updCls = updSrc.getClass('UIPathData'); let cnt = 0; updCls.getStaticProperties().forEach((value) => { if (!this.isNodeReferedOutside(value, [updFile])) { value.remove(); cnt++; } }); updSrc.save(); console.log(`${cnt} useless UIPath removed.`); } isNodeReferedOutside(node, excludes) { const refs = node.findReferencesAsNodes(); if (refs.length == 0) return false; let recursiveCnt = 0; for (const ref of refs) { if (node.getSourceFile() != ref.getSourceFile()) { return true; } let p = ref.getParent(); while (p) { if (p == node) { recursiveCnt++; break; } p = p.getParent(); } } return recursiveCnt < refs.length; } async processGetCfg(project) { // 收集所有json const jsonRoot = eitherExists(path.join(toolchain.opts.projectRoot, 'Assets/AssetSources/data'), path.join(toolchain.opts.projectRoot, 'assets/data')); if (!jsonRoot) { console.error('no json root'); return; } const allJson = await fg('*.json', { cwd: jsonRoot }); const jsonMap = {}; for (const j of allJson) { const jp = path.parse(j); jsonMap[jp.name] = true; } const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts'); const tsFiles = await fg('**/*.ts', { cwd: tsRoot }); for (const ts of tsFiles) { const file = path.join(tsRoot, ts); const content = await fs.readFile(file, 'utf-8'); const lines = content.split(/\r?\n/); let modified = false; for (let i = 0, len = lines.length; i < len; i++) { const line = lines[i]; if (line.match(/^\s*\/\//)) continue; const rst = line.match(/\.getCfg(?:<[\.\w]+>)?\((['|"])(?:data\/)?(\w+)(?:\.json+)?\1\)/); if (rst != null) { const desierJson = rst[2]; if (!jsonMap[desierJson]) { console.log('json not exists:', desierJson); lines[i] = line.replace(desierJson, desierJson + '//'); modified = true; } } } if (modified) { await fs.writeFile(file, lines.join('\n'), 'utf-8'); } } } async warnEmptyMethods(project) { const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts'); const tsFiles = await fg('**/*.ts', { cwd: tsRoot, ignore: ['**/*.d.ts'] }); let modified = false; for (let i = 0, len = tsFiles.length; i < len; i++) { const ts = tsFiles[i]; // if (!ts.endsWith('AvatarChanger.ts')) continue; const file = path.join(tsRoot, ts); const src = project.getSourceFile(file); if (!src) { console.error('SourceFile not exists:', file); continue; } const classes = src.getClasses(); for (const cls of classes) { if (cls.isAbstract()) continue; const mtds = cls.getMethods(); for (const m of mtds) { const b = m.getBody(); if (b?.isKind(SyntaxKind.Block) && b.getStatements().length == 0) { const mn = m.getName(); if (mn == 'onCfgReady' || mn == 'onTipMarkChange' || mn == 'onContainerChange' || mn == 'onCurrencyChange') { modified = true; m.remove(); } else { console.warn('empty method: ', file, cls.getName(), mn); } } } } } await project.save(); } async removeUnnecessaryCodes(project) { await this.removeUnusedTsFiles(project); await this.removeUnusedMethods(project); await this.removeUnnecessaryOverrides(); // 删除没用的identifier const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts'); const ruis = toolchain.opts.cfg?.codeCfg?.reserveUnusedIdentifiers; const ignore = ['**/*.d.ts']; if (ruis) { ignore.push(...ruis); } const tss = await fg('**/*.ts', { cwd: tsRoot, ignore }); for (const ts of tss) { const tsf = path.join(tsRoot, ts); const src = project.getSourceFileOrThrow(tsf); src.fixUnusedIdentifiers(); } await project.save(); } async removeUnusedTsFiles(project) { console.log('removing unused ts files...'); const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts'); const tsFiles = await fg('**/*.ts', { cwd: tsRoot, ignore: ['**/*.d.ts', 'root.ts', 'minigame.ts', 'uts/*.ts', '**/SysDefines.ts'] }); for (const ts of tsFiles) { const file = path.join(tsRoot, ts); const src = project.getSourceFile(file); if (!src) { console.error('SourceFile not exists:', file); continue; } if (src.getReferencingSourceFiles().length == 0) { console.log('delete:', ts); await src.deleteImmediately(); } } } async removeUnusedMethods(project) { console.log('removing unused methods...'); const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts'); const tsFiles = await fg('**/*.ts', { cwd: tsRoot, ignore: ['**/*.d.ts'] }); let modified = false; for (let i = 1412, len = tsFiles.length; i < len; i++) { const ts = tsFiles[i]; // if (!ts.endsWith('AvatarChanger.ts')) continue; console.log(`${i}/${len}`, ts); const file = path.join(tsRoot, ts); const src = project.getSourceFile(file); if (!src) { console.error('SourceFile not exists:', file); continue; } const classes = src.getClasses(); for (const cls of classes) { const clsRefs = cls.findReferencesAsNodes(); let isClsUseless = true; for (let i = 0, len = clsRefs.length; i < len; i++) { const r = clsRefs[i]; if (r.isKind(SyntaxKind.Identifier) && r.getParentIf((parent) => Boolean(!parent?.isKind(SyntaxKind.ImportSpecifier) && !parent?.isKind(SyntaxKind.ClassDeclaration)))) { isClsUseless = false; break; } } if (isClsUseless) { console.log('useless class: ', cls.getName()); cls.remove(); modified = true; continue; } const methods = cls.getMembers(); for (const mtd of methods) { if (mtd.isKind(SyntaxKind.ClassStaticBlockDeclaration) || mtd.isKind(SyntaxKind.Constructor)) continue; if (!this.isNodeReferedOutside(mtd, [])) { console.log('remove:', mtd.getName()); mtd.remove(); modified = true; // } else { // console.log('keep:', mtd.getName()); } } } let isSrcUseless = true; src.forEachChild((n) => { if (!n.isKind(SyntaxKind.EndOfFileToken)) { isSrcUseless = false; } }); if (isSrcUseless) { // console.log('delete src:', ts); // await fs.unlink(file); await src.deleteImmediately(); } else if (modified) { await src.save(); } } } async removeUnnecessaryOverrides() { console.log('removing unnecessary overrides...'); const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts'); const tsFiles = await fg('**/*.ts', { cwd: tsRoot, ignore: ['**/*.d.ts'] }); for (const ts of tsFiles) { const file = path.join(tsRoot, ts); const content = await fs.readFile(file, 'utf-8'); const lines = content.split(/\r?\n/); let deleteLines = [], inForm = false, funcStartLine = -1, funcBodyExists = false; for (let i = 0, len = lines.length; i < len; i++) { const line = lines[i]; if (funcStartLine > 0) { if (line.match(/^ \}/)) { if (!funcBodyExists) { for (let j = funcStartLine; j <= i; j++) { deleteLines.push(j); } } funcStartLine = -1; } else { if (!line.match(/^\s*\/\//) && line.match(/\S+/) && !line.match('super\.')) { funcBodyExists = true; } } } else if (inForm) { if (line.match(/^\}/)) { inForm = false; } else { if (line.match(/\s+(?:initElements|initListeners|onClose|onOpen|open)\(\)/)) { funcStartLine = i; funcBodyExists = false; } } } else if (line.match(/class \w+ extends (?:CommonForm|TabForm|TabSubForm)/)) { inForm = true; } } if (deleteLines.length > 0) { console.log('Unnecessary code removed:', ts); await fs.writeFile(file, lines.filter((v, i) => !deleteLines.includes(i)).join('\n'), 'utf-8'); } } } async removeEmptyFolders() { console.log('removing empty folders...'); const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts'); const folders = await fg('**/*', { cwd: tsRoot, onlyDirectories: true }); folders.sort((a, b) => b.length - a.length); for (const f of folders) { const folder = path.join(tsRoot, f); const subs = await fg('**/*', { cwd: folder }); if (subs.length == 0) { await fs.rmdir(folder); } } } /**查找潜在未使用的CommonForm */ async findSuspectedUselessUI() { const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts'); const tsFiles = await fg('**/*.ts', { cwd: tsRoot }); // 再查找所有的CommonForm、TabForm const project = new Project({ tsConfigFilePath: path.join(tsRoot, 'tsconfig.json') }); // 先查找所有调用了createForm的面板 const parentMap = {}, subFormMap = {}, dirOpenedForms = {}, commonForms = []; for (const f of tsFiles) { const tsf = path.join(tsRoot, f); const content = await fs.readFile(tsf, 'utf-8'); const lines = content.split(/\r?\n/).filter((v) => !v.trimStart().startsWith('//')); const mchs = lines.join('\n').matchAll(/(?:createForm|createChildForm|dominate)(?:<\w+>)?\((\w+)/g); for (const mch of mchs) { dirOpenedForms[mch[1]] = true; } const src = project.getSourceFile(tsf); if (!src) { console.error('SourceFile not exists:', tsf); continue; } const classes = src.getClasses(); for (const cls of classes) { const parents = []; let isCommonForm = false, isTabForm = false; let bs = cls.getBaseClass(); while (bs != null) { const bsn = bs.getName(); if (bsn) parents.push(bsn); if (bsn == 'TabForm') { isTabForm = true; } if (bsn == 'CommonForm') { isCommonForm = true; break; } bs = bs.getBaseClass(); } if (isCommonForm) { const cn = cls.getName(); if (cn) { commonForms.push(cn); parentMap[cn] = parents; } } if (isTabForm) { const cn = cls.getName(); if (cn) { // 收集所属子页签 const ctors = cls.getConstructors(); for (const ctor of ctors) { const ctorBody = ctor.getBody(); if (ctorBody) { ctorBody.forEachChild((n) => { if (n.isKind(SyntaxKind.ExpressionStatement)) { const exp0 = n.getExpression(); if (exp0.isKind(SyntaxKind.CallExpression)) { const exp = exp0.getExpression(); if (exp.isKind(SyntaxKind.SuperKeyword)) { const subForms = []; const args = exp0.getArguments(); for (let i = 1, len = args.length; i < len; i++) { subForms.push(args[i].getText()); } subFormMap[cn] = subForms; } } } }); } } } } } } const openedMap = {}; const markOpened = (f) => { if (openedMap[f]) return; openedMap[f] = true; // 标记所有subform const subs = subFormMap[f]; if (subs) { subs.forEach((v) => { markOpened(v); }); } // 标记所有父类 const parents = parentMap[f]; if (parents) { parents.forEach((v) => { markOpened(v); }); } }; for (const f of commonForms) { if (dirOpenedForms[f]) { markOpened(f); } } const uselessForms = []; for (const f of commonForms) { if (!openedMap[f]) { uselessForms.push(f); } } console.log('useless forms: ', uselessForms.length); console.log('-------------------------'); uselessForms.forEach((v) => console.log(v)); } async showCodeHint() { const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts'); const tsFiles = await fg('**/*.ts', { cwd: tsRoot }); // 再查找所有的CommonForm、TabForm const project = new Project({ tsConfigFilePath: path.join(tsRoot, 'tsconfig.json') }); for (const f of tsFiles) { const tsf = path.join(tsRoot, f); const src = project.getSourceFile(tsf); if (!src) { console.error('SourceFile not exists:', tsf); continue; } const classes = src.getClasses(); for (const cls of classes) { if (cls.getMethods().length == 0 && cls.getStaticMethods().length == 0 && cls.getGetAccessors().length == 0 && cls.getSetAccessors().length == 0 && cls.getConstructors().length == 0) { console.log(f, cls.getName()); } } } } } //# sourceMappingURL=CodeStriper.js.map