unity-find-fault
Version:
A tool to find fault in unity project.
427 lines • 17.9 kB
JavaScript
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