import * as ts from 'typescript'; import * as path from 'path'; import * as fs from 'fs'; const verboseLogging = false; const MAX_RECURSION_DEPTH = 10; // 最大递归深度,防止无限递归 const log = (...args: any[]) => { if (verboseLogging) { console.log('[tscBuilder]', ...args); } }; export const OAK_IGNORE_TAGS = [ '@oak-ignore', '@oak-ignore-asynccontext', ]; // 定义自定义配置的类型 interface OakBuildChecksConfig { context?: { checkAsyncContext?: boolean; // 是否启用 AsyncContext 检查,默认启用 targetModules?: string[]; // 目标模块列表,默认 ['@project/context/BackendRuntimeContext'] } } // 扩展的配置类型 interface ExtendedTsConfig { compilerOptions?: ts.CompilerOptions; oakBuildChecks?: OakBuildChecksConfig; [key: string]: any; // 允许其他自定义字段 } // 自定义诊断信息接口 interface CustomDiagnostic { file: ts.SourceFile; start: number; length: number; messageText: string; category: ts.DiagnosticCategory; code: number; } // ANSI 颜色代码 const colors = { reset: '\x1b[0m', cyan: '\x1b[36m', red: '\x1b[91m', yellow: '\x1b[93m', gray: '\x1b[90m' } as const; // 解析命令行参数 function parseArgs(pargs: string[]): string { const args: string[] = pargs.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); } } } return configPath; } // 读取自定义配置 function readCustomConfig(configPath: string): OakBuildChecksConfig { const configFile = ts.readConfigFile(configPath, ts.sys.readFile); if (configFile.error) { return {}; // 返回默认配置 } const config = configFile.config as ExtendedTsConfig; // 返回自定义配置,如果不存在则返回默认值 return config.oakBuildChecks || { }; } function printDiagnostic(diagnostic: ts.Diagnostic | CustomDiagnostic, index: number) { if (diagnostic.file && diagnostic.start !== undefined) { 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( `${index + 1}.→ ${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( `${index + 1}.→ ${categoryColor}${category}${colors.reset} ${colors.gray}TS${diagnostic.code}${colors.reset}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}` ); } } function performCustomChecks( program: ts.Program, typeChecker: ts.TypeChecker, customConfig: OakBuildChecksConfig ): CustomDiagnostic[] { const diagnostics: CustomDiagnostic[] = []; // 如果自定义检查被禁用,直接返回 if (!customConfig.context?.checkAsyncContext) { console.log('Custom AsyncContext checks are disabled.'); return diagnostics; } const functionsWithContextCalls = new Set(); const checkedFunctions = new Map(); // 遍历所有源文件 for (const sourceFile of program.getSourceFiles()) { // 跳过声明文件和 node_modules if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) { continue; } // 遍历 AST 节点 visitNode(sourceFile, 0); } function resolveModuleName(moduleName: string, containingFile: string): string | undefined { const compilerOptions = program.getCompilerOptions(); const resolvedModule = ts.resolveModuleName( moduleName, containingFile, compilerOptions, ts.sys ); return resolvedModule.resolvedModule?.resolvedFileName; } function normalizeModulePath(filePath: string): string { // 规范化路径,移除 node_modules 和文件扩展名 return filePath .replace(/\\/g, '/') .replace(/\.(ts|tsx|js|jsx|d\.ts)$/, ''); } function visitNode(node: ts.Node, deepth: number): void { if (deepth > MAX_RECURSION_DEPTH) { log(`[DEBUG] 达到最大递归深度,停止进一步检查`); return; } // 检查 AsyncContext 方法调用 checkAsyncContextMethodCall(node); //检查函数调用(检查是否调用了包含 context 的函数) checkFunctionCall(node); // 递归遍历子节点 ts.forEachChild(node, (i) => visitNode(i, deepth + 1)); } function checkAsyncContextMethodCall(node: ts.Node): void { // 只检查方法调用表达式 if (!ts.isCallExpression(node)) { return; } // 检查是否是属性访问表达式 if (!ts.isPropertyAccessExpression(node.expression)) { return; } // 如果上一层是return,则先跳过,让外层函数调用来处理 if (ts.isReturnStatement(node.parent)) { return; } // 检查箭头函数的隐式返回,让外层函数调用来处理 // 例如:() => context.select() if (ts.isArrowFunction(node.parent) && node.parent.body === node) { return; } const propertyAccess = node.expression; const objectExpression = propertyAccess.expression; // 获取对象的类型 const objectType = typeChecker.getTypeAtLocation(objectExpression); const targetModules = customConfig.context?.targetModules || ['@project/context/BackendRuntimeContext']; // 检查类型是否继承自 AsyncContext if (!isAsyncContextType(objectType, targetModules)) { return; } // 获取方法调用的返回类型 const callSignature = typeChecker.getResolvedSignature(node); if (!callSignature) { return; } const returnType = typeChecker.getReturnTypeOfSignature(callSignature); // 只检查返回 Promise 的方法 if (!isPromiseType(returnType)) { return; } // 检查是否被 await 修饰 if (!isAwaited(node)) { const sourceFile = node.getSourceFile(); // 检查是否有忽略注释 if (hasIgnoreComment(node, sourceFile)) { return; } const methodName = propertyAccess.name.getText(sourceFile); const objectName = objectExpression.getText(sourceFile); diagnostics.push({ file: sourceFile, start: node.getStart(sourceFile), length: node.getWidth(sourceFile), messageText: `未await的context调用可能导致事务不受控: ${objectName}.${methodName}() 需要使用 await`, category: ts.DiagnosticCategory.Warning, code: 9100 }); } } function hasIgnoreComment(node: ts.Node, sourceFile: ts.SourceFile): boolean { const fullText = sourceFile.getFullText(); // 检查当前节点及其父节点(向上查找最多3层) let currentNode: ts.Node | undefined = node; let depth = 0; const maxDepth = 5; while (currentNode && depth < maxDepth) { // 检查前导注释 const nodeFullStart = currentNode.getFullStart(); const leadingComments = ts.getLeadingCommentRanges(fullText, nodeFullStart); if (leadingComments && leadingComments.length > 0) { for (const comment of leadingComments) { const commentText = fullText.substring(comment.pos, comment.end); if (isIgnoreComment(commentText)) { return true; } } } // 检查尾随注释 const nodeEnd = currentNode.getEnd(); const trailingComments = ts.getTrailingCommentRanges(fullText, nodeEnd); if (trailingComments && trailingComments.length > 0) { for (const comment of trailingComments) { const commentText = fullText.substring(comment.pos, comment.end); if (isIgnoreComment(commentText)) { return true; } } } // 向上查找父节点 currentNode = currentNode.parent; depth++; // 如果遇到函数边界,停止查找 if (currentNode && ( ts.isFunctionDeclaration(currentNode) || ts.isFunctionExpression(currentNode) || ts.isArrowFunction(currentNode) || ts.isMethodDeclaration(currentNode) )) { break; } } return false; } function isIgnoreComment(commentText: string): boolean { return OAK_IGNORE_TAGS.some(tag => commentText.includes(tag)); } function isAsyncContextType(type: ts.Type, modules: string[]): boolean { // 检查类型本身 if (checkTypeSymbol(type, modules)) { return true; } // 检查联合类型(例如 RuntimeCxt = FRC | BRC) 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) { if (isAsyncContextType(baseType, modules)) { return true; } } return false; } function checkTypeSymbol(type: ts.Type, modules: string[]): boolean { const symbol = type.getSymbol(); if (!symbol) { return false; } const declarations = symbol.getDeclarations(); if (!declarations || declarations.length === 0) { return false; } for (const declaration of declarations) { const sourceFile = declaration.getSourceFile(); const fileName = sourceFile.fileName; const normalizedFileName = normalizeModulePath(fileName); // 检查是否来自目标模块 for (const moduleName of modules) { // 直接路径匹配(处理已解析的路径) if (normalizedFileName.includes(moduleName.replace(/\\/g, '/'))) { return true; } // 尝试解析模块别名 const resolvedPath = resolveModuleName(moduleName, sourceFile.fileName); if (resolvedPath) { const normalizedResolvedPath = normalizeModulePath(resolvedPath); if (normalizedFileName === normalizedResolvedPath || normalizedFileName.includes(normalizedResolvedPath)) { return true; } } // 检查模块说明符(从 import 语句中获取) const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration); for (const importDecl of importDeclarations) { if (importDecl.moduleSpecifier && ts.isStringLiteral(importDecl.moduleSpecifier)) { const importPath = importDecl.moduleSpecifier.text; if (importPath === moduleName || importPath.includes(moduleName)) { // 检查当前符号是否来自这个 import const importClause = importDecl.importClause; if (importClause) { const importSymbol = typeChecker.getSymbolAtLocation(importDecl.moduleSpecifier); if (importSymbol && isSymbolRelated(symbol, importSymbol)) { return true; } } } } } } } return false; } function isSymbolRelated(symbol: ts.Symbol, moduleSymbol: ts.Symbol): boolean { // 检查符号是否与模块相关 const exports = typeChecker.getExportsOfModule(moduleSymbol); return exports.some(exp => exp === symbol || exp.name === symbol.name); } function isAwaited(node: ts.Node): boolean { let parent = node.parent; // 向上遍历父节点,查找 await 表达式 while (parent) { // 直接被 await 修饰 if (ts.isAwaitExpression(parent) && parent.expression === node) { return true; } // 在 await 表达式的子表达式中 if (ts.isAwaitExpression(parent)) { const current = parent.expression; if (isNodeInSubtree(current, node)) { return true; } } // 如果遇到这些节点,停止向上查找 if (ts.isFunctionDeclaration(parent) || ts.isFunctionExpression(parent) || ts.isArrowFunction(parent) || ts.isMethodDeclaration(parent)) { break; } parent = parent.parent; } return false; } function isPromiseType(type: ts.Type): boolean { // 检查类型符号 const symbol = type.getSymbol(); if (symbol) { const name = symbol.getName(); if (name === 'Promise') { return true; } } // 检查类型字符串表示 const typeString = typeChecker.typeToString(type); if (typeString.startsWith('Promise<') || typeString === 'Promise') { return true; } // 检查联合类型(例如 Promise | undefined) if (type.isUnion()) { return type.types.some(t => isPromiseType(t)); } // 检查基类型 const baseTypes = type.getBaseTypes?.() || []; for (const baseType of baseTypes) { if (isPromiseType(baseType)) { return true; } } return false; } function isNodeInSubtree(root: ts.Node, target: ts.Node): boolean { if (root === target) { return true; } let found = false; ts.forEachChild(root, (child: ts.Node) => { if (found) return; if (child === target || isNodeInSubtree(child, target)) { found = true; } }); return found; } function checkFunctionCall(node: ts.Node): void { // 只检查调用表达式 if (!ts.isCallExpression(node)) { return; } // 打印所有函数调用 const sourceFile = node.getSourceFile(); const callText = node.expression.getText(sourceFile); log(`[DEBUG] 检查函数调用: ${callText}`); // 获取被调用函数的符号 const signature = typeChecker.getResolvedSignature(node); if (!signature) { log(`[DEBUG] ${callText} - 无法获取签名`); return; } const declaration = signature.getDeclaration(); if (!declaration) { log(`[DEBUG] ${callText} - 无法获取声明`); return; } log(`[DEBUG] ${callText} - 声明类型: ${ts.SyntaxKind[declaration.kind]}`); // 检查这个函数是否包含 context 调用 const hasContext = containsContextCall(declaration); log(`[DEBUG] ${callText} - 包含context调用: ${hasContext}`); if (hasContext) { const returnType = typeChecker.getReturnTypeOfSignature(signature); // 如果函数不返回 Promise,说明内部已经正确处理了异步调用 // 此时调用该函数不需要 await if (!isPromiseType(returnType)) { return; } // 标记当前所在的函数 const containingFunction = findContainingFunction(node); if (containingFunction) { const fname = getFunctionName(containingFunction); if (fname) { // 只有当有名称时才标记,但不影响后续检查 const symbol = typeChecker.getSymbolAtLocation(fname); if (symbol) { functionsWithContextCalls.add(symbol); } } } if (ts.isReturnStatement(node.parent)) { // 如果在 return 语句中,不在这里报警告 // 警告会传播到调用当前函数的地方 return; } // 检查箭头函数的隐式返回 // 例如:() => someFunction() if (ts.isArrowFunction(node.parent) && node.parent.body === node) { return; } // 检查是否被 await if (!isAwaited(node)) { const sourceFile = node.getSourceFile(); // 检查是否有忽略注释 if (hasIgnoreComment(node, sourceFile)) { return; } const functionName = getFunctionCallName(node, sourceFile); diagnostics.push({ file: sourceFile, start: node.getStart(sourceFile), length: node.getWidth(sourceFile), messageText: `未await的函数调用可能导致事务不受控: ${functionName}() 内部包含 context 调用,需要使用 await`, category: ts.DiagnosticCategory.Warning, code: 9101 }); } } } // 辅助函数:检查函数声明是否包含 context 调用 function containsContextCall(node: ts.Node): boolean { const symbol = getSymbolOfDeclaration(node as ts.SignatureDeclaration); log(`[DEBUG containsContextCall] 节点类型: ${ts.SyntaxKind[node.kind]}, 有符号: ${!!symbol}`); if (symbol && checkedFunctions.has(symbol)) { log(`[DEBUG containsContextCall] 使用缓存结果: ${checkedFunctions.get(symbol)}`); return checkedFunctions.get(symbol)!; } let hasContextCall = false; function visit(n: ts.Node): void { if (hasContextCall) return; // 检查是否是 context 方法调用 if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) { const objectType = typeChecker.getTypeAtLocation(n.expression.expression); const targetModules = customConfig.context?.targetModules || ['@project/context/BackendRuntimeContext']; if (isAsyncContextType(objectType, targetModules)) { log(`[DEBUG containsContextCall] 找到context调用: ${n.expression.getText()}`); hasContextCall = true; return; } } // 递归检查调用的其他函数 if (ts.isCallExpression(n)) { const signature = typeChecker.getResolvedSignature(n); if (signature) { const declaration = signature.getDeclaration(); if (declaration && !isSameOrDescendant(declaration, node)) { // 避免无限递归 const symbol = getSymbolOfDeclaration(declaration); // 先检查缓存 if (symbol && functionsWithContextCalls.has(symbol)) { hasContextCall = true; return; } // 如果缓存中没有,递归检查该函数 if (containsContextCall(declaration)) { hasContextCall = true; return; } } } } ts.forEachChild(n, visit); } visit(node); log(`[DEBUG containsContextCall] 最终结果: ${hasContextCall}`); if (symbol) { checkedFunctions.set(symbol, hasContextCall); } return hasContextCall; } // 辅助函数:查找包含当前节点的函数 function findContainingFunction(node: ts.Node): ts.FunctionLikeDeclaration | undefined { let current = node.parent; while (current) { if (ts.isFunctionDeclaration(current) || ts.isFunctionExpression(current) || ts.isArrowFunction(current) || ts.isMethodDeclaration(current)) { return current; } current = current.parent; } return undefined; } // 辅助函数:获取函数名称节点 function getFunctionName(func: ts.FunctionLikeDeclaration): ts.Node | undefined { if (ts.isFunctionDeclaration(func) || ts.isMethodDeclaration(func)) { return func.name; } return undefined; } // 辅助函数:获取函数调用的名称 function getFunctionCallName(node: ts.CallExpression, sourceFile: ts.SourceFile): string { if (ts.isIdentifier(node.expression)) { return node.expression.getText(sourceFile); } else if (ts.isPropertyAccessExpression(node.expression)) { return node.expression.name.getText(sourceFile); } return node.expression.getText(sourceFile); } // 辅助函数:检查是否是同一个节点或后代 function isSameOrDescendant(node: ts.Node, ancestor: ts.Node): boolean { let current: ts.Node | undefined = node; while (current) { if (current === ancestor) { return true; } current = current.parent; } return false; } // 辅助函数:获取声明的符号 function getSymbolOfDeclaration(declaration: ts.SignatureDeclaration): ts.Symbol | undefined { // 处理有名称的声明(函数声明、方法声明等) if ('name' in declaration && declaration.name) { return typeChecker.getSymbolAtLocation(declaration.name); } // 处理箭头函数:尝试从父节点(变量声明)获取符号 if (ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration)) { const parent = declaration.parent; if (ts.isVariableDeclaration(parent) && parent.name) { return typeChecker.getSymbolAtLocation(parent.name); } // 处理作为属性值的情况 if (ts.isPropertyAssignment(parent) && parent.name) { return typeChecker.getSymbolAtLocation(parent.name); } } return undefined; } return diagnostics; } function compile(configPath: string): void { // 读取自定义配置 const customConfig = readCustomConfig(configPath); // 读取 tsconfig.json const configFile = ts.readConfigFile(configPath, ts.sys.readFile); if (configFile.error) { console.error(ts.formatDiagnostic(configFile.error, { getCanonicalFileName: (f: string) => 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: ts.Diagnostic) => { console.error(ts.formatDiagnostic(diagnostic, { getCanonicalFileName: (f: string) => f, getCurrentDirectory: process.cwd, getNewLine: () => '\n' })); }); process.exit(1); } // 创建编译程序 let program: ts.Program; if (parsedConfig.options.incremental || parsedConfig.options.composite) { const host = ts.createIncrementalCompilerHost(parsedConfig.options); const incrementalProgram = ts.createIncrementalProgram({ rootNames: parsedConfig.fileNames, options: parsedConfig.options, host: host, configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(parsedConfig), }); program = incrementalProgram.getProgram(); } else { program = ts.createProgram({ rootNames: parsedConfig.fileNames, options: parsedConfig.options, }); } // 获取类型检查器 const typeChecker = program.getTypeChecker(); // 执行自定义检查(传入自定义配置) const customDiagnostics = performCustomChecks(program, typeChecker, customConfig); if (customDiagnostics.length > 0) { log(`Found ${customDiagnostics.length} custom diagnostics.`); console.log(`${colors.yellow}Oak-Compiler 发现了相关问题,如果确定逻辑正确,可以使用注释标签来忽略:${colors.reset}`); OAK_IGNORE_TAGS.forEach(tag => { console.log(` ${colors.cyan}// ${tag}${colors.reset}`); }); console.log(''); customDiagnostics.forEach(printDiagnostic); } // 执行编译 const emitResult = program.emit(); // 获取诊断信息 const allDiagnostics: readonly ts.Diagnostic[] = [ ...ts.getPreEmitDiagnostics(program), ...emitResult.diagnostics, ]; // 输出诊断信息 allDiagnostics.forEach(printDiagnostic); // 输出编译统计 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: string[] = []; 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 ')}.`); } if (errorCount > 0) { process.exit(1); } console.log('Compilation completed successfully.'); } export const build = (pwd: string, args: any[]) => { // 执行编译 const configPathArg: string = parseArgs(args); let configPath: string; // 判断参数是目录还是文件 if (fs.existsSync(configPathArg)) { const stat = fs.statSync(configPathArg); if (stat.isDirectory()) { configPath = path.resolve(configPathArg, 'tsconfig.json'); } else { configPath = path.resolve(configPathArg); } } else { configPath = path.resolve(pwd, configPathArg); if (!fs.existsSync(configPath)) { const dirPath = path.resolve(pwd, configPathArg); if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) { configPath = path.join(dirPath, 'tsconfig.json'); } } } if (!fs.existsSync(configPath)) { console.error(`error TS5058: The specified path does not exist: '${configPath}'.`); process.exit(1); } compile(configPath); }