feat: 最大递归深度限制

This commit is contained in:
Pan Qiancheng 2026-01-04 18:17:59 +08:00
parent 78449b584b
commit b777ef0ad9
3 changed files with 50 additions and 189 deletions

View File

@ -6,6 +6,7 @@ const ts = tslib_1.__importStar(require("typescript"));
const path = tslib_1.__importStar(require("path"));
const fs = tslib_1.__importStar(require("fs"));
const verboseLogging = false;
const MAX_RECURSION_DEPTH = 10;
const log = (...args) => {
if (verboseLogging) {
console.log('[tscBuilder]', ...args);
@ -83,7 +84,7 @@ function performCustomChecks(program, typeChecker, customConfig) {
continue;
}
// 遍历 AST 节点
visitNode(sourceFile);
visitNode(sourceFile, 0);
}
function resolveModuleName(moduleName, containingFile) {
const compilerOptions = program.getCompilerOptions();
@ -96,13 +97,17 @@ function performCustomChecks(program, typeChecker, customConfig) {
.replace(/\\/g, '/')
.replace(/\.(ts|tsx|js|jsx|d\.ts)$/, '');
}
function visitNode(node) {
function visitNode(node, deepth) {
if (deepth > MAX_RECURSION_DEPTH) {
log(`[DEBUG] 达到最大递归深度,停止进一步检查`);
return;
}
// 检查 AsyncContext 方法调用
checkAsyncContextMethodCall(node);
//检查函数调用(检查是否调用了包含 context 的函数)
checkFunctionCall(node);
// 递归遍历子节点
ts.forEachChild(node, visitNode);
ts.forEachChild(node, (i) => visitNode(i, deepth + 1));
}
function checkAsyncContextMethodCall(node) {
// 只检查方法调用表达式
@ -214,6 +219,10 @@ function performCustomChecks(program, typeChecker, customConfig) {
if (type.isUnion()) {
return type.types.some(t => isAsyncContextType(t, modules));
}
// 检查交叉类型
if (type.isIntersection()) {
return type.types.some(t => isAsyncContextType(t, modules));
}
// 检查基类型(继承关系)
const baseTypes = type.getBaseTypes?.() || [];
for (const baseType of baseTypes) {

View File

@ -1,190 +1,31 @@
const ts = require('typescript');
const path = require('path');
const fs = require('fs');
const { cwd } = require('process');
const { execSync } = require('child_process');
// 解析命令行参数
function parseArgs() {
const args = process.argv.slice(2);
let configPath = 'tsconfig.json';
for (let i = 0; i < args.length; i++) {
if (args[i] === '-p' || args[i] === '--project') {
if (i + 1 < args.length) {
configPath = args[i + 1];
break;
} else {
console.error('error: option \'-p, --project\' argument missing');
process.exit(1);
}
function tryRequire(modulePath) {
try {
require.resolve(modulePath);
return require(modulePath);
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
return null;
}
throw e; // 如果是其他错误,继续抛出
}
return configPath;
}
// ANSI 颜色代码
const colors = {
reset: '\x1b[0m',
cyan: '\x1b[36m',
red: '\x1b[91m',
yellow: '\x1b[93m',
gray: '\x1b[90m'
};
const tscBuilder = tryRequire('../lib/compiler/tscBuilder.js');
function compile(configPath) {
// 读取 tsconfig.json
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
if (configFile.error) {
console.error(ts.formatDiagnostic(configFile.error, {
getCanonicalFileName: f => f,
getCurrentDirectory: process.cwd,
getNewLine: () => '\n'
}));
process.exit(1);
}
// 解析配置
const parsedConfig = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
path.dirname(configPath)
);
if (parsedConfig.errors.length > 0) {
parsedConfig.errors.forEach(diagnostic => {
console.error(ts.formatDiagnostic(diagnostic, {
getCanonicalFileName: f => f,
getCurrentDirectory: process.cwd,
getNewLine: () => '\n'
}));
});
process.exit(1);
}
// 创建编译程序
// 根据配置决定是否使用增量编译
let program;
if (parsedConfig.options.incremental || parsedConfig.options.composite) {
// 对于增量编译,使用 createIncrementalProgram
const host = ts.createIncrementalCompilerHost(parsedConfig.options);
program = ts.createIncrementalProgram({
rootNames: parsedConfig.fileNames,
options: parsedConfig.options,
host: host,
configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(parsedConfig),
});
} else {
// 普通编译
program = ts.createProgram({
rootNames: parsedConfig.fileNames,
options: parsedConfig.options,
});
}
// 执行编译
const emitResult = program.emit();
// 获取诊断信息
const allDiagnostics = ts
.getPreEmitDiagnostics(program)
.concat(emitResult.diagnostics);
// 输出诊断信息
allDiagnostics.forEach(diagnostic => {
if (diagnostic.file) {
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
diagnostic.start
);
const message = ts.flattenDiagnosticMessageText(
diagnostic.messageText,
'\n'
);
const isError = diagnostic.category === ts.DiagnosticCategory.Error;
const category = isError ? 'error' : 'warning';
const categoryColor = isError ? colors.red : colors.yellow;
console.log(
`${colors.cyan}${diagnostic.file.fileName}${colors.reset}:${colors.yellow}${line + 1}${colors.reset}:${colors.yellow}${character + 1}${colors.reset} - ${categoryColor}${category}${colors.reset} ${colors.gray}TS${diagnostic.code}${colors.reset}: ${message}`
);
} else {
const isError = diagnostic.category === ts.DiagnosticCategory.Error;
const category = isError ? 'error' : 'warning';
const categoryColor = isError ? colors.red : colors.yellow;
console.log(
`${categoryColor}${category}${colors.reset} ${colors.gray}TS${diagnostic.code}${colors.reset}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`
);
}
});
// 输出编译统计
const errorCount = allDiagnostics.filter(d => d.category === ts.DiagnosticCategory.Error).length;
const warningCount = allDiagnostics.filter(d => d.category === ts.DiagnosticCategory.Warning).length;
if (errorCount > 0 || warningCount > 0) {
if (allDiagnostics.length > 0) {
console.log('');
}
const parts = [];
if (errorCount > 0) {
parts.push(`${errorCount} error${errorCount !== 1 ? 's' : ''}`);
}
if (warningCount > 0) {
parts.push(`${warningCount} warning${warningCount !== 1 ? 's' : ''}`);
}
console.log(`Found ${parts.join(' and ')}.`);
}
// tsc 的行为:
// 1. 默认情况下(noEmitOnError: false):
// - 即使有类型错误也会生成 .js 文件
// - 但如果有错误,退出码是 1
// 2. noEmitOnError: true 时:
// - 有错误时不生成文件(emitSkipped 为 true)
// - 退出码是 1
// 3. 没有错误时:
// - 生成文件,退出码 0
// 无论 emitSkipped 与否,只要有错误就应该退出 1
if (errorCount > 0) {
process.exit(1);
}
console.log('Compilation completed successfully.');
}
// 执行编译
const configPathArg = parseArgs();
let configPath;
// 判断参数是目录还是文件
if (fs.existsSync(configPathArg)) {
const stat = fs.statSync(configPathArg);
if (stat.isDirectory()) {
// 如果是目录,拼接 tsconfig.json
configPath = path.resolve(configPathArg, 'tsconfig.json');
} else {
// 如果是文件,直接使用
configPath = path.resolve(configPathArg);
}
if (tscBuilder && tscBuilder.build) {
console.log('Using tscBuilder for compilation');
tscBuilder.build(process.cwd(), process.argv);
} else {
// 尝试作为相对路径解析
configPath = path.resolve(cwd(), configPathArg);
if (!fs.existsSync(configPath)) {
// 如果还是不存在,尝试添加 tsconfig.json
const dirPath = path.resolve(cwd(), configPathArg);
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
configPath = path.join(dirPath, 'tsconfig.json');
}
console.log('tscBuilder not found, falling back to tsc');
try {
execSync('npx tsc', {
stdio: 'inherit',
cwd: process.cwd()
});
} catch (tscError) {
console.error('Failed to run tsc:', tscError.message);
process.exit(1);
}
}
if (!fs.existsSync(configPath)) {
console.error(`error TS5058: The specified path does not exist: '${configPath}'.`);
process.exit(1);
}
compile(configPath);
}

View File

@ -3,6 +3,7 @@ import * as path from 'path';
import * as fs from 'fs';
const verboseLogging = false;
const MAX_RECURSION_DEPTH = 10; // 最大递归深度,防止无限递归
const log = (...args: any[]) => {
if (verboseLogging) {
@ -135,7 +136,7 @@ function performCustomChecks(
}
// 遍历 AST 节点
visitNode(sourceFile);
visitNode(sourceFile, 0);
}
function resolveModuleName(moduleName: string, containingFile: string): string | undefined {
@ -157,7 +158,12 @@ function performCustomChecks(
.replace(/\.(ts|tsx|js|jsx|d\.ts)$/, '');
}
function visitNode(node: ts.Node): void {
function visitNode(node: ts.Node, deepth: number): void {
if (deepth > MAX_RECURSION_DEPTH) {
log(`[DEBUG] 达到最大递归深度,停止进一步检查`);
return;
}
// 检查 AsyncContext 方法调用
checkAsyncContextMethodCall(node);
@ -165,7 +171,7 @@ function performCustomChecks(
checkFunctionCall(node);
// 递归遍历子节点
ts.forEachChild(node, visitNode);
ts.forEachChild(node, (i) => visitNode(i, deepth + 1));
}
function checkAsyncContextMethodCall(node: ts.Node): void {
@ -307,6 +313,11 @@ function performCustomChecks(
return type.types.some(t => isAsyncContextType(t, modules));
}
// 检查交叉类型
if (type.isIntersection()) {
return type.types.some(t => isAsyncContextType(t, modules));
}
// 检查基类型(继承关系)
const baseTypes = type.getBaseTypes?.() || [];
for (const baseType of baseTypes) {
@ -466,7 +477,7 @@ function performCustomChecks(
return;
}
// 添加这里 - 打印所有函数调用
// 打印所有函数调用
const sourceFile = node.getSourceFile();
const callText = node.expression.getText(sourceFile);
log(`[DEBUG] 检查函数调用: ${callText}`);