"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.build = exports.OAK_IGNORE_TAGS = void 0; exports.performCustomChecks = performCustomChecks; const tslib_1 = require("tslib"); const ts = tslib_1.__importStar(require("typescript")); const path = tslib_1.__importStar(require("path")); const fs = tslib_1.__importStar(require("fs")); const glob_1 = require("../utils/glob"); const identifier_1 = require("./identifier"); const lodash_1 = require("lodash"); const ARRAY_METHODS = ['map', 'forEach', 'filter', 'reduce', 'some', 'every', 'find', 'findIndex', 'flatMap']; const ARRAY_TRANSFORM_METHODS = ['map', 'filter', 'flatMap']; const PROMISE_METHODS = ['then', 'catch', 'finally']; const PROMISE_STATIC_METHODS = ['all', 'race', 'allSettled', 'any']; const LOCALE_FILE_NAMES = ['zh_CN.json', 'zh-CN.json']; // 判断是否是函数类声明 const isFunctionLikeDeclaration = (node) => { // FunctionDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ConstructorDeclaration | FunctionExpression | ArrowFunction; return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node); }; // 查找最近的函数作用域 const findNearestFunctionScope = (node) => { let current = node; while (current && !isFunctionLikeDeclaration(current) && !ts.isSourceFile(current)) { current = current.parent; } return current && !ts.isSourceFile(current) ? current : undefined; }; // 查找最近的函数或块作用域 const findNearestScope = (node) => { let current = node; while (current && !isFunctionLikeDeclaration(current) && !ts.isSourceFile(current) && !ts.isBlock(current)) { current = current.parent; } return current; }; // 判断是否是 Promise 静态方法调用 const isPromiseStaticMethodCall = (node) => { if (!ts.isPropertyAccessExpression(node.expression)) { return false; } const object = node.expression.expression; if (!ts.isIdentifier(object) || object.text !== 'Promise') { return false; } const methodName = node.expression.name.text; return PROMISE_STATIC_METHODS.includes(methodName); }; // 判断是否是数组方法调用 const isArrayMethodCall = (node, methodNames) => { if (!ts.isPropertyAccessExpression(node.expression)) { return false; } const methodName = node.expression.name.text; return methodNames.includes(methodName); }; // 检查节点是否在数组方法回调中 const isInArrayMethodCallback = (node, methodNames = ARRAY_METHODS) => { let current = node; while (current) { if (isFunctionLikeDeclaration(current)) { const parent = current.parent; if (ts.isCallExpression(parent) && isArrayMethodCall(parent, methodNames)) { return true; } break; } current = current.parent; } return false; }; // 检查变量是否在节点中被引用 const isSymbolReferencedInNode = (node, symbol, typeChecker) => { if (ts.isIdentifier(node)) { const nodeSymbol = typeChecker.getSymbolAtLocation(node); return nodeSymbol === symbol; } let found = false; ts.forEachChild(node, (child) => { if (!found && isSymbolReferencedInNode(child, symbol, typeChecker)) { found = true; } }); return found; }; // 辅助函数:检查节点是否是透明包装(括号、类型断言等) const isTransparentWrapper = (node) => { return ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node); }; // 辅助函数:获取去除透明包装后的实际节点 const unwrapTransparentWrappers = (node) => { let current = node; while (isTransparentWrapper(current)) { if (ts.isParenthesizedExpression(current)) { current = current.expression; } else if (ts.isAsExpression(current)) { current = current.expression; } else if (ts.isTypeAssertionExpression(current)) { current = current.expression; } else if (ts.isNonNullExpression(current)) { current = current.expression; } else { break; } } return current; }; /** * 检查类型是否是 Promise 类型 * @param type ts.Type * @returns boolean */ const isPromiseType = (type, typeChecker) => { // 检查类型符号 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, typeChecker)); } // 检查基类型 const baseTypes = type.getBaseTypes?.() || []; for (const baseType of baseTypes) { if (isPromiseType(baseType, typeChecker)) { return true; } } return false; }; // 类型守卫:检查节点是否是标识符且匹配指定符号 const isIdentifierWithSymbol = (node, symbol, typeChecker) => { if (!ts.isIdentifier(node)) { return false; } const nodeSymbol = typeChecker.getSymbolAtLocation(node); return nodeSymbol === symbol; }; /** * 判断文件是否在检查范围内 * @param fileName 文件完整路径 * @param customConfig 自定义配置 * @returns boolean */ const isFileInCheckScope = (pwd, fileName, customConfig) => { const patterns = customConfig.context?.filePatterns || ['**/*.ts', '**/*.tsx']; const normalizedFileName = path.normalize(path.relative(pwd, fileName)).replace(/\\/g, '/'); for (const pattern of patterns) { if ((0, glob_1.matchGlobPattern)(normalizedFileName, pattern.replace(/\\/g, '/'))) { return true; } } return false; }; const verboseLogging = false; const log = verboseLogging ? (...args) => console.log('[tscBuilder]', ...args) : () => { }; // 空函数,避免参数计算开销 exports.OAK_IGNORE_TAGS = [ '@oak-ignore', '@oak-ignore-asynccontext', ]; // ANSI 颜色代码 const colors = { reset: '\x1b[0m', cyan: '\x1b[36m', red: '\x1b[91m', yellow: '\x1b[93m', gray: '\x1b[90m', green: '\x1b[92m', }; // 解析命令行参数 function parseArgs(pargs) { const args = pargs.slice(2); const options = { project: 'tsconfig.json', noEmit: false }; for (let i = 0; i < args.length; i++) { if (args[i] === '-p' || args[i] === '--project') { if (i + 1 < args.length) { options.project = args[i + 1]; break; } else { console.error('error: option \'-p, --project\' argument missing'); process.exit(1); } } else if (args[i] === '--noEmit') { options.noEmit = true; } } return options; } const getContextLocationText = (pwd, callNode) => { const sourceFile = callNode.getSourceFile(); const { line, character } = sourceFile.getLineAndCharacterOfPosition(callNode.getStart()); // const callText = callNode.getText(sourceFile); // return `${callText}@${path.relative(process.cwd(), sourceFile.fileName)}:${line + 1}:${character + 1}`; return `${path.relative(pwd, sourceFile.fileName)}:${line + 1}:${character + 1}`; }; function printDiagnostic(pwd, diagnostic, index) { const isCustom = 'callChain' in diagnostic; 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(`\n${colors.cyan}┌─ Issue #${index + 1}${colors.reset}`); console.log(`${colors.cyan}│${colors.reset} ${colors.cyan}${path.relative(pwd, diagnostic.file.fileName)}${colors.reset}:${colors.yellow}${line + 1}${colors.reset}:${colors.yellow}${character + 1}${colors.reset}`); console.log(`${colors.cyan}│${colors.reset} ${categoryColor}${category}${colors.reset} ${colors.gray}TS${diagnostic.code}${colors.reset}: ${message}`); // 显示代码片段 const sourceFile = diagnostic.file; const lineStart = sourceFile.getPositionOfLineAndCharacter(line, 0); const lineEnd = sourceFile.getPositionOfLineAndCharacter(line + 1, 0); let lineText = sourceFile.text.substring(lineStart, lineEnd).trimEnd(); // 确保只显示单行内容,去除换行符 const newlineIndex = lineText.indexOf('\n'); if (newlineIndex !== -1) { lineText = lineText.substring(0, newlineIndex); } // 限制显示的最大字符数,避免输出过长 const maxDisplayLength = 100; const isTruncated = lineText.length > maxDisplayLength; const displayText = isTruncated ? lineText.substring(0, maxDisplayLength) + colors.gray + '...' + colors.reset : lineText; // 计算实际显示的文本长度(不包括颜色代码) const actualDisplayLength = Math.min(lineText.length, maxDisplayLength); // 调整错误标记的显示位置和长度 const effectiveCharacter = Math.min(character, actualDisplayLength); const maxPossibleLength = actualDisplayLength - effectiveCharacter; const effectiveLength = Math.min(Math.max(1, diagnostic.length || 1), maxPossibleLength); console.log(`${colors.cyan}│${colors.reset}`); console.log(`${colors.cyan}│${colors.reset} ${colors.gray}${line + 1}${colors.reset} │ ${displayText}`); console.log(`${colors.cyan}│${colors.reset} ${' '.repeat(String(line + 1).length)}│ ${' '.repeat(effectiveCharacter)}${colors.red}${'~'.repeat(effectiveLength)}${colors.reset}`); // 如果是自定义诊断,显示额外信息 if (isCustom) { const customDiag = diagnostic; // 显示调用链 if (customDiag.callChain && customDiag.callChain.length > 1) { console.log(`${colors.cyan}│${colors.reset}`); console.log(`${colors.cyan}│${colors.reset} ${colors.yellow}调用链:${colors.reset}`); customDiag.callChain.forEach((func, idx) => { console.log(`${colors.cyan}│${colors.reset} ${colors.gray}→ ${func}${colors.reset}`); }); } // 显示实际的context调用位置 if (customDiag.contextCallNode) { console.log(`${colors.cyan}│${colors.reset}`); console.log(`${colors.cyan}│${colors.reset} ${colors.red}→ ${getContextLocationText(pwd, customDiag.contextCallNode)}${colors.reset}`); } // 显示检测原因 if (customDiag.reason) { console.log(`${colors.cyan}│${colors.reset}`); console.log(`${colors.cyan}│${colors.reset} ${colors.yellow}检测原因: ${customDiag.reason}${colors.reset}`); } // 显示详细原因 if (customDiag.reasonDetails && customDiag.reasonDetails.length > 0) { console.log(`${colors.cyan}│${colors.reset}`); console.log(`${colors.cyan}│${colors.reset} ${colors.yellow}详细分析:${colors.reset}`); customDiag.reasonDetails.forEach(detail => { // 根据符号选择颜色 let color = colors.gray; if (detail.includes('✓')) { color = colors.green; } else if (detail.includes('✘')) { color = colors.red; } else if (detail.includes('¡')) { color = colors.cyan; } console.log(`${colors.cyan}│${colors.reset} ${color}${detail}${colors.reset}`); }); } } console.log(`${colors.cyan}└─${colors.reset}`); } else { const isError = diagnostic.category === ts.DiagnosticCategory.Error; const category = isError ? 'error' : 'warning'; const categoryColor = isError ? colors.red : colors.yellow; console.log(`\n${index + 1}. ${categoryColor}${category}${colors.reset} ${colors.gray}TS${diagnostic.code}${colors.reset}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`); } } /** * 执行自定义检查 * @param program ts.Program * @param typeChecker ts.TypeChecker * @param customConfig 自定义配置 * @returns CustomDiagnostic[] 自定义诊断列表 */ function performCustomChecks(pwd, program, typeChecker, customConfig) { const diagnostics = []; const enableAsyncContextCheck = customConfig.context?.checkAsyncContext !== false; const enableTCheck = customConfig.locale?.checkI18nKeys !== false; console.log(`Custom AsyncContext checks are ${enableAsyncContextCheck ? colors.green + 'enabled' + colors.reset : colors.gray + 'disabled' + colors.reset}.`); console.log(`Custom i18n key checks are ${enableTCheck ? colors.green + 'enabled' + colors.reset : colors.gray + 'disabled' + colors.reset}.`); // 数据结构定义 const functionsWithContextCalls = new Set(); const functionDeclarations = new Map(); const functionCallGraph = new Map(); // 谁调用了谁 const directContextCalls = new Map(); // 函数内的直接 context 调用 const allContextCalls = []; // 所有 context 调用点 // 缓存:函数符号 -> 参数索引 -> 是否被处理 const functionParameterHandling = new Map(); // 缓存:变量符号 -> 是否被正确处理 const variableHandlingCache = new Map(); // 正在检查的变量集合(防止循环) const checkingVariables = new Set(); const typeCache = new Map(); // 文件和对应需要检查的调用点缓存 const callsToCheckByFile = new Map(); // 忽略注释节点缓存 const ignoreCommentNodes = new Set(); // 符号缓存 const symbolCache = new Map(); // t函数调用符号缓存 const tFunctionSymbolCache = new Set(); const getSymbolCached = (node) => { if (symbolCache.has(node)) { return symbolCache.get(node); } const symbol = typeChecker.getSymbolAtLocation(node); symbolCache.set(node, symbol); return symbol; }; const addDiagnostic = (node, messageText, code, options) => { const sourceFile = node.getSourceFile(); // 构建调用链文本 let callChain; if (options?.callChain && options.callChain.length > 0) { callChain = options.callChain.map(symbol => { const decl = functionDeclarations.get(symbol); if (decl && decl.name && ts.isIdentifier(decl.name)) { const file = decl.getSourceFile(); const { line } = file.getLineAndCharacterOfPosition(decl.getStart()); return `${symbol.name} (${path.basename(file.fileName)}:${line + 1})`; } return symbol.name; }); } diagnostics.push({ file: sourceFile, start: node.getStart(sourceFile), length: node.getWidth(sourceFile), messageText, category: ts.DiagnosticCategory.Warning, code, callChain, contextCallNode: options?.contextCall, reason: options?.reason, reasonDetails: options?.reasonDetails }); }; const shouldReportUnawaitedCall = (callNode, sourceFile) => { // 1. 检查忽略注释(现在很快,因为有缓存) if (hasIgnoreComment(callNode, sourceFile)) { return false; } // 2. 检查是否 await(通常很快) if (isAwaited(callNode)) { return false; } // 3. 检查特定场景(很快) if (shouldSkipCheck(callNode)) { return false; } // 4. 最后检查复杂的赋值处理(较慢) if (isCallAssignedAndHandled(callNode)) { return false; } return true; }; // 分析为什么调用未被正确处理,返回详细原因 const analyzeUnhandledReason = (callNode) => { const details = []; let reason = '未正确处理Promise'; let suggestedFix = ''; // 检查是否被 await if (!isAwaited(callNode)) { details.push('✘ 调用未使用 await 关键字'); suggestedFix = `await ${callNode.getText()}`; } else { details.push('✓ 调用已使用 await'); } // 检查是否在 return 语句中 const parent = callNode.parent; if (ts.isReturnStatement(parent)) { details.push('✓ 在 return 语句中(已传递给调用者)'); } else if (ts.isArrowFunction(parent) && parent.body === callNode) { details.push('✓ 作为箭头函数返回值(已传递给调用者)'); } else { details.push('✘ 未通过 return 传递给调用者'); } // 检查是否赋值给变量 if (ts.isVariableDeclaration(parent) && parent.initializer === callNode) { const variableDecl = parent; if (variableDecl.name && ts.isIdentifier(variableDecl.name)) { const variableName = variableDecl.name.text; const variableSymbol = typeChecker.getSymbolAtLocation(variableDecl.name); if (variableSymbol) { details.push(`¡ 赋值给变量: ${variableName}`); // 检查变量的各种处理方式 const scope = findNearestFunctionScope(variableDecl); if (scope) { const funcLike = scope; if (funcLike.body) { const check = checkPromiseHandlingWithInstanceOf(funcLike.body, variableSymbol, { stopAtFunctionBoundary: true }); if (check.hasAwait) { details.push(` ✓ 变量 ${variableName} 后续被 await`); } else { details.push(` ✘ 变量 ${variableName} 未被 await`); } if (check.hasPromiseMethod) { details.push(` ✓ 变量 ${variableName} 调用了 .then/.catch/.finally`); } else { details.push(` ✘ 变量 ${variableName} 未调用 .then/.catch/.finally`); } if (check.hasPromiseStatic) { details.push(` ✓ 变量 ${variableName} 被传入 Promise.all/race 等`); } else { details.push(` ✘ 变量 ${variableName} 未被传入 Promise.all/race 等`); } if (check.hasReturn) { details.push(` ✓ 变量 ${variableName} 被 return`); } else { details.push(` ✘ 变量 ${variableName} 未被 return`); } if (check.hasInstanceOf) { details.push(` ✓ 变量 ${variableName} 在 instanceof Promise 检查后被处理`); } // 检查是否传递给其他函数 if (isSymbolPassedToHandlingFunction(variableSymbol, funcLike.body)) { details.push(` ✓ 变量 ${variableName} 被传递给会处理Promise的函数`); } else { details.push(` ✘ 变量 ${variableName} 未被传递给会处理Promise的函数`); } } } reason = `变量 ${variableName} 未被正确处理`; if (!suggestedFix) { suggestedFix = `await ${variableName}`; } } } } else { details.push('✘ 未赋值给变量进行后续处理'); } // 检查是否作为参数传递 if (ts.isCallExpression(parent)) { const argIndex = parent.arguments.indexOf(callNode); if (argIndex !== -1) { details.push(`¡ 作为第 ${argIndex + 1} 个参数传递给函数`); if (isCallPassedToHandlingFunction(callNode)) { details.push(' ✓ 该函数会正确处理Promise参数'); } else { details.push(' ✘ 该函数不会处理Promise参数'); reason = '传递给不处理Promise的函数'; } } } // 检查是否在数组方法中 if (isInArrayMethodCallback(callNode, ARRAY_TRANSFORM_METHODS)) { details.push('¡ 在数组转换方法(map/filter/flatMap)的回调中'); if (isCallInHandledArrayMethod(callNode)) { details.push(' ✓ 数组方法的结果被正确处理(如传入Promise.all)'); } else { details.push(' ✘ 数组方法的结果未被正确处理'); reason = '在数组方法中但结果未被处理'; suggestedFix = '将数组方法结果传入 Promise.all()'; } } return { reason, details, suggestedFix }; }; const isCallAssignedAndHandled = (callNode) => { const parent = callNode.parent; if (ts.isVariableDeclaration(parent) && parent.initializer === callNode) { if (parent.name && ts.isIdentifier(parent.name)) { const variableSymbol = typeChecker.getSymbolAtLocation(parent.name); if (variableSymbol) { // 如果变量被 return,则在 shouldSkipCheck 中已经处理,这里不需要再检查 if (isVariableReturned(variableSymbol, parent)) { return true; // 被 return 的变量,认为已处理 } // 否则检查是否在作用域内被正确处理 if (isPromiseProperlyHandled(variableSymbol, parent)) { return true; } } } } // 检查是否作为参数传递给会处理 Promise 的函数 if (isCallPassedToHandlingFunction(callNode)) { return true; } return false; }; const resolveModuleName = (moduleName, containingFile) => { const compilerOptions = program.getCompilerOptions(); const resolvedModule = ts.resolveModuleName(moduleName, containingFile, compilerOptions, ts.sys); return resolvedModule.resolvedModule?.resolvedFileName; }; const pathCache = new Map(); const normalizeModulePath = (filePath) => { if (pathCache.has(filePath)) { return pathCache.get(filePath); } const normalized = filePath .replace(/\\/g, '/') .replace(/\.(ts|tsx|js|jsx|d\.ts)$/, ''); pathCache.set(filePath, normalized); return normalized; }; const preprocessIgnoreComments = (sourceFile) => { const fullText = sourceFile.getFullText(); const visit = (node) => { const nodeFullStart = node.getFullStart(); const leadingComments = ts.getLeadingCommentRanges(fullText, nodeFullStart); if (leadingComments) { for (const comment of leadingComments) { const commentText = fullText.substring(comment.pos, comment.end); if (isIgnoreComment(commentText)) { // 标记当前节点及其子节点 markNodeAndChildren(node); return; // 不需要继续遍历子节点 } } } ts.forEachChild(node, visit); }; const markNodeAndChildren = (node) => { ignoreCommentNodes.add(node); ts.forEachChild(node, markNodeAndChildren); }; visit(sourceFile); }; // 修改 hasIgnoreComment 函数 const hasIgnoreComment = (node, sourceFile) => { // 直接查缓存 if (ignoreCommentNodes.has(node)) { return true; } // 向上查找父节点(最多3层) let current = node.parent; let depth = 0; while (current && depth < 3) { if (ignoreCommentNodes.has(current)) { return true; } current = current.parent; depth++; } return false; }; const isIgnoreComment = (commentText) => { return exports.OAK_IGNORE_TAGS.some(tag => commentText.includes(tag)); }; const isAsyncContextType = (type, modules) => { if (typeCache.has(type)) { return typeCache.get(type); } const result = checkIsAsyncContextType(type, modules); typeCache.set(type, result); return result; }; const checkIsAsyncContextType = (type, modules) => { // 检查类型本身 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; }; const checkTypeSymbol = (type, modules) => { 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; }; const isSymbolRelated = (symbol, moduleSymbol) => { // 检查符号是否与模块相关 const exports = typeChecker.getExportsOfModule(moduleSymbol); return exports.some(exp => exp === symbol || exp.name === symbol.name); }; /** * 检查节点是否被 await 修饰 * @param node ts.Node * @returns boolean */ const isAwaited = (node) => { let parent = node.parent; // 向上遍历父节点,查找 await 表达式 while (parent) { // 检查是否被 await 修饰(考虑透明包装) if (ts.isAwaitExpression(parent)) { // 去除 await 表达式中的透明包装层 const awaitedExpression = unwrapTransparentWrappers(parent.expression); // 检查去除包装后是否就是目标节点 if (awaitedExpression === node) { return true; } // 也检查是否在子树中(处理更复杂的嵌套情况) if (isNodeInSubtree(parent.expression, node)) { return true; } } // 如果遇到函数边界,停止向上查找 if (isFunctionLikeDeclaration(parent)) { break; } parent = parent.parent; } return false; }; const isNodeInSubtree = (root, target) => { if (root === target) { return true; } let found = false; ts.forEachChild(root, (child) => { if (found) return; if (child === target || isNodeInSubtree(child, target)) { found = true; } }); return found; }; // 辅助函数:获取函数调用的名称 const getFunctionCallName = (node, sourceFile) => { 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); }; // 辅助函数:获取声明的符号 const getSymbolOfDeclaration = (declaration) => { // 处理有名称的声明(函数声明、方法声明等) if ('name' in declaration && declaration.name) { return typeChecker.getSymbolAtLocation(declaration.name); } // 处理箭头函数:尝试从父节点(变量声明)获取符号 if (isFunctionLikeDeclaration(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; }; const collectAndCheck = (sourceFile) => { let currentFunction; const localCallsToCheck = []; // 收集需要检查的调用 const visit = (node) => { // 记录函数声明 if (isFunctionLikeDeclaration(node)) { const symbol = getSymbolOfDeclaration(node); if (symbol) { functionDeclarations.set(symbol, node); const previousFunction = currentFunction; currentFunction = symbol; ts.forEachChild(node, visit); currentFunction = previousFunction; return; } } // 处理调用表达式 - 合并原来的两个逻辑 if (ts.isCallExpression(node)) { if (enableAsyncContextCheck) { // 检查 context 调用 if (ts.isPropertyAccessExpression(node.expression)) { const objectType = typeChecker.getTypeAtLocation(node.expression.expression); const targetModules = customConfig.context?.targetModules || ['@project/context/BackendRuntimeContext']; if (isAsyncContextType(objectType, targetModules)) { if (!hasIgnoreComment(node, sourceFile)) { allContextCalls.push(node); if (currentFunction) { if (!directContextCalls.has(currentFunction)) { directContextCalls.set(currentFunction, []); } directContextCalls.get(currentFunction).push(node); } } } } // 记录函数调用关系 + 收集需要检查的调用 if (currentFunction) { const signature = typeChecker.getResolvedSignature(node); if (signature) { const declaration = signature.getDeclaration(); if (declaration) { const calledSymbol = getSymbolOfDeclaration(declaration); if (calledSymbol && calledSymbol !== currentFunction) { if (!functionCallGraph.has(currentFunction)) { functionCallGraph.set(currentFunction, new Set()); } functionCallGraph.get(currentFunction).add(calledSymbol); } } } // 收集可能需要检查的调用(延迟到标记传播后) localCallsToCheck.push(node); } } if (enableTCheck && (0, identifier_1.isTCall)(node, typeChecker, customConfig.locale?.tFunctionModules || ['oak-frontend-base'])) { tFunctionSymbolCache.add(node); } } ts.forEachChild(node, visit); }; visit(sourceFile); // 保存需要检查的调用 callsToCheckByFile.set(sourceFile, localCallsToCheck); }; // 过滤掉非 Promise 返回的 context 调用 const filterAsyncContextCalls = () => { // 过滤 allContextCalls const asyncContextCalls = []; for (const callNode of allContextCalls) { // 添加忽略检查 const sourceFile = callNode.getSourceFile(); if (hasIgnoreComment(callNode, sourceFile)) { continue; } const signature = typeChecker.getResolvedSignature(callNode); if (signature) { const returnType = typeChecker.getReturnTypeOfSignature(signature); if (isPromiseType(returnType, typeChecker)) { asyncContextCalls.push(callNode); } } } allContextCalls.length = 0; allContextCalls.push(...asyncContextCalls); // 过滤 directContextCalls const newDirectContextCalls = new Map(); for (const [functionSymbol, calls] of directContextCalls.entries()) { const asyncCalls = calls.filter(callNode => { // 添加忽略检查 const sourceFile = callNode.getSourceFile(); if (hasIgnoreComment(callNode, sourceFile)) { return false; } const signature = typeChecker.getResolvedSignature(callNode); if (!signature) return false; const returnType = typeChecker.getReturnTypeOfSignature(signature); return isPromiseType(returnType, typeChecker); }); // 只保留有异步调用的函数 if (asyncCalls.length > 0) { newDirectContextCalls.set(functionSymbol, asyncCalls); } } directContextCalls.clear(); newDirectContextCalls.forEach((calls, symbol) => { directContextCalls.set(symbol, calls); }); }; const propagateContextMarks = () => { // 初始标记:直接包含 context 调用的函数 const markedFunctions = new Set(directContextCalls.keys()); // 迭代传播标记 let changed = true; while (changed) { changed = false; for (const [caller, callees] of functionCallGraph.entries()) { if (markedFunctions.has(caller)) continue; // 如果调用了任何标记的函数,则标记当前函数 for (const callee of callees) { if (markedFunctions.has(callee)) { markedFunctions.add(caller); functionsWithContextCalls.add(caller); changed = true; break; } } } } // 更新全局标记 markedFunctions.forEach(symbol => functionsWithContextCalls.add(symbol)); }; // 检查直接的 context 调用 const checkDirectContextCalls = () => { for (const callNode of allContextCalls) { if (shouldSkipCheck(callNode)) continue; const signature = typeChecker.getResolvedSignature(callNode); if (!signature) continue; const returnType = typeChecker.getReturnTypeOfSignature(signature); if (!isPromiseType(returnType, typeChecker)) continue; // 检查是否直接 await const sourceFile = callNode.getSourceFile(); if (shouldReportUnawaitedCall(callNode, sourceFile)) { const propertyAccess = callNode.expression; const methodName = propertyAccess.name.getText(sourceFile); const objectName = propertyAccess.expression.getText(sourceFile); // 分析具体原因 // const analysis = analyzeUnhandledReason(callNode); addDiagnostic(callNode, `未await的context调用可能导致事务不受控: ${objectName}.${methodName}()`, 9100, { contextCall: callNode, // reason: analysis.reason, // reasonDetails: analysis.details }); } } }; // 检查间接调用(使用缓存的调用列表) const checkIndirectCalls = () => { // 使用之前收集的调用列表,避免重复遍历 for (const [sourceFile, callsToCheck] of callsToCheckByFile.entries()) { if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) { continue; } for (const node of callsToCheck) { if (!ts.isCallExpression(node)) continue; if (shouldSkipCheck(node)) continue; const signature = typeChecker.getResolvedSignature(node); if (!signature) continue; const declaration = signature.getDeclaration(); if (!declaration) continue; const symbol = getSymbolOfDeclaration(declaration); if (!symbol) continue; // 检查是否调用了标记的函数 if (functionsWithContextCalls.has(symbol)) { const returnType = typeChecker.getReturnTypeOfSignature(signature); if (!isPromiseType(returnType, typeChecker)) continue; if (shouldReportUnawaitedCall(node, sourceFile)) { const functionName = getFunctionCallName(node, sourceFile); // 追踪调用链 const callChain = traceCallChain(symbol); const contextCall = findContextCallInChain(symbol); // 分析具体原因 // const analysis = analyzeUnhandledReason(node); addDiagnostic(node, `未await的函数调用可能导致事务不受控: ${functionName}()`, 9101, { callChain, contextCall, // reason: analysis.reason, // reasonDetails: analysis.details }); } } } } }; // 追踪调用链 const traceCallChain = (symbol) => { const chain = [symbol]; const visited = new Set(); const trace = (currentSymbol) => { if (visited.has(currentSymbol)) return false; visited.add(currentSymbol); // 如果直接包含context调用,返回true if (directContextCalls.has(currentSymbol)) { return true; } // 检查调用的函数 const callees = functionCallGraph.get(currentSymbol); if (callees) { for (const callee of callees) { if (functionsWithContextCalls.has(callee)) { chain.push(callee); if (trace(callee)) { return true; } chain.pop(); } } } return false; }; trace(symbol); return chain; }; // 找到调用链中的context调用 const findContextCallInChain = (symbol) => { const visited = new Set(); const find = (currentSymbol) => { if (visited.has(currentSymbol)) return undefined; visited.add(currentSymbol); // 如果直接包含context调用,返回第一个 const calls = directContextCalls.get(currentSymbol); if (calls && calls.length > 0) { return calls[0]; } // 递归查找 const callees = functionCallGraph.get(currentSymbol); if (callees) { for (const callee of callees) { if (functionsWithContextCalls.has(callee)) { const result = find(callee); if (result) return result; } } } return undefined; }; return find(symbol); }; const shouldSkipCheck = (node) => { // 直接在 return 语句中 if (ts.isReturnStatement(node.parent)) { if (isInArrayMethodCallback(node)) { return false; } return true; } // 箭头函数的直接返回值 if (ts.isArrowFunction(node.parent) && node.parent.body === node) { if (isInArrayMethodCallback(node)) { return false; } return true; } // 检查是否赋值给变量,然后该变量被 return if (ts.isVariableDeclaration(node.parent) && node.parent.initializer === node) { const variableDecl = node.parent; if (variableDecl.name && ts.isIdentifier(variableDecl.name)) { const variableSymbol = typeChecker.getSymbolAtLocation(variableDecl.name); if (variableSymbol && isVariableReturned(variableSymbol, variableDecl)) { return true; } } } return false; }; // 检查变量是否在其作用域内被 return const isVariableReturned = (symbol, declarationNode) => { const scope = findNearestFunctionScope(declarationNode); if (!scope) { return false; } let found = false; const visit = (node) => { if (found) return; if (ts.isReturnStatement(node) && node.expression) { if (isSymbolReferencedInNode(node.expression, symbol, typeChecker)) { found = true; return; } } // 不进入嵌套函数 if (isFunctionLikeDeclaration(node)) { return; } ts.forEachChild(node, visit); }; const funcLike = scope; if (funcLike.body) { visit(funcLike.body); } return found; }; // 检查节点中是否包含指定变量 const containsVariable = (node, symbol) => { return isSymbolReferencedInNode(node, symbol, typeChecker); }; const checkPromiseHandlingInNode = (node, symbol, options = {}) => { const result = {}; const visit = (n) => { // 检查 await if (ts.isAwaitExpression(n)) { const expression = unwrapTransparentWrappers(n.expression); if (isIdentifierWithSymbol(expression, symbol, typeChecker)) { result.hasAwait = true; return; } } // 检查 Promise 实例方法 if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) { const object = n.expression.expression; if (ts.isIdentifier(object)) { const objSymbol = getSymbolCached(object); if (objSymbol === symbol) { const methodName = n.expression.name.text; if (PROMISE_METHODS.includes(methodName)) { result.hasPromiseMethod = true; return; } } } // 检查 Promise 静态方法 if (isPromiseStaticMethodCall(n)) { for (const arg of n.arguments) { if (isSymbolReferencedInNode(arg, symbol, typeChecker)) { result.hasPromiseStatic = true; return; } } } } // 检查 return if (ts.isReturnStatement(n) && n.expression) { if (isSymbolReferencedInNode(n.expression, symbol, typeChecker)) { result.hasReturn = true; return; } } // 函数边界处理 if (options.stopAtFunctionBoundary && isFunctionLikeDeclaration(n)) { return; } ts.forEachChild(n, visit); }; visit(node); return result; }; const checkPromiseHandlingWithInstanceOf = (node, symbol, options = {}) => { const result = {}; const visit = (n) => { // 复用基础检查 const baseCheck = checkPromiseHandlingInNode(n, symbol, options); Object.assign(result, baseCheck); // 额外检查 instanceof Promise if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) { const left = n.left; if (ts.isIdentifier(left)) { const leftSymbol = getSymbolCached(left); if (leftSymbol === symbol) { const right = n.right; if (ts.isIdentifier(right) && right.text === 'Promise') { // 找到 instanceof Promise 所在的 if 语句 let ifStatement = n.parent; while (ifStatement && !ts.isIfStatement(ifStatement)) { ifStatement = ifStatement.parent; } if (ifStatement && ts.isIfStatement(ifStatement)) { const thenBlock = ifStatement.thenStatement; if (isPromiseHandledInBlock(thenBlock, symbol)) { result.hasInstanceOf = true; return; } } } } } } if (options.stopAtFunctionBoundary && isFunctionLikeDeclaration(n)) { return; } ts.forEachChild(n, visit); }; visit(node); return result; }; // 检查符号是否作为参数传递给会处理 Promise 的函数 const isSymbolPassedToHandlingFunction = (symbol, scope) => { let found = false; const visit = (node) => { if (found) return; if (ts.isCallExpression(node)) { // 检查直接参数传递 for (let i = 0; i < node.arguments.length; i++) { const arg = node.arguments[i]; if (ts.isIdentifier(arg)) { const argSymbol = getSymbolCached(arg); if (argSymbol === symbol) { const signature = typeChecker.getResolvedSignature(node); if (signature) { const declaration = signature.getDeclaration(); if (declaration) { const calledSymbol = getSymbolOfDeclaration(declaration); if (calledSymbol) { const paramHandling = getFunctionParameterHandling(calledSymbol); if (paramHandling.get(i) === true) { found = true; return; } // 处理剩余参数 if ('parameters' in declaration) { const params = declaration.parameters; if (params.length > 0) { const lastParam = params[params.length - 1]; if (lastParam?.dotDotDotToken && i >= params.length - 1) { if (paramHandling.get(params.length - 1) === true) { found = true; return; } } } } } } } } } } // 检查回调函数中的处理 for (const arg of node.arguments) { if (isFunctionLikeDeclaration(arg)) { if (arg.body && containsVariable(arg.body, symbol)) { const callbackCheck = checkPromiseHandlingInNode(arg.body, symbol, { stopAtFunctionBoundary: true }); if (callbackCheck.hasAwait || callbackCheck.hasPromiseMethod || callbackCheck.hasPromiseStatic || callbackCheck.hasReturn) { found = true; return; } } } } } if (isFunctionLikeDeclaration(node)) { return; } ts.forEachChild(node, visit); }; visit(scope); return found; }; // 检查调用节点是否在数组方法回调中,且该数组方法的结果被正确处理 const isCallInHandledArrayMethod = (callNode) => { let current = callNode; // 向上查找,看是否在数组方法的回调中 while (current) { // 跳过 return 语句 if (ts.isReturnStatement(current)) { current = current.parent; continue; } // 检查是否是箭头函数或函数表达式 if (isFunctionLikeDeclaration(current)) { const functionParent = current.parent; // 检查这个函数是否是数组方法的参数 if (ts.isCallExpression(functionParent) && isArrayMethodCall(functionParent, ARRAY_TRANSFORM_METHODS)) { // 检查这个数组方法调用是否被正确处理 return isArrayMethodResultHandled(functionParent); } break; } current = current.parent; } return false; }; // 检查数组方法的结果是否被正确处理 const isArrayMethodResultHandled = (arrayMethodCall) => { let current = arrayMethodCall; while (current) { const currentParent = current.parent; // 跳过透明包装 if (isTransparentWrapper(currentParent)) { current = currentParent; continue; } // 情况1:直接作为 Promise 静态方法的参数 if (ts.isCallExpression(currentParent) && isPromiseStaticMethodCall(currentParent)) { return true; } // 情况2:赋值给变量 if (ts.isVariableDeclaration(currentParent) && currentParent.initializer === current) { if (currentParent.name && ts.isIdentifier(currentParent.name)) { const variableSymbol = getSymbolCached(currentParent.name); if (variableSymbol) { return isVariablePassedToPromiseAll(variableSymbol, currentParent); } } } // 情况3:作为属性赋值(暂不处理) if (ts.isPropertyAssignment(currentParent) && currentParent.initializer === current) { return false; } break; } return false; }; // 检查变量是否被传给 Promise.all const isVariablePassedToPromiseAll = (symbol, declarationNode) => { // 使用函数作用域而不是块作用域,以便找到更远的 Promise.all 调用 const scope = findNearestFunctionScope(declarationNode); if (!scope) { return false; } let found = false; const visit = (node) => { if (found) return; if (ts.isIfStatement(node)) { const condition = node.expression; if (ts.isBinaryExpression(condition) && condition.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) { // 检查条件左侧是否引用了目标变量(支持数组元素访问) const left = condition.left; let isTargetVariable = false; if (ts.isIdentifier(left)) { const leftSymbol = getSymbolCached(left); isTargetVariable = leftSymbol === symbol; } else if (ts.isElementAccessExpression(left)) { // 处理 childLegalAuths[0] 这种情况 if (ts.isIdentifier(left.expression)) { const arraySymbol = getSymbolCached(left.expression); isTargetVariable = arraySymbol === symbol; } } // 检查条件右侧是否是 Promise const right = condition.right; if (isTargetVariable && ts.isIdentifier(right) && right.text === 'Promise') { // 在 then 分支中递归查找 Promise.all const thenBlock = node.thenStatement; const visitThen = (n) => { if (found) return; if (ts.isCallExpression(n) && isPromiseStaticMethodCall(n)) { for (const arg of n.arguments) { if (ts.isIdentifier(arg)) { const argSymbol = getSymbolCached(arg); if (argSymbol === symbol) { found = true; return; } } if (ts.isArrayLiteralExpression(arg)) { for (const element of arg.elements) { if (ts.isSpreadElement(element) && ts.isIdentifier(element.expression)) { const spreadSymbol = getSymbolCached(element.expression); if (spreadSymbol === symbol) { found = true; return; } } } } } } ts.forEachChild(n, visitThen); }; visitThen(thenBlock); if (found) return; } } } // 检查是否是 Promise 静态方法调用 if (ts.isCallExpression(node) && isPromiseStaticMethodCall(node)) { // 检查参数中是否包含该变量 for (const arg of node.arguments) { if (ts.isIdentifier(arg)) { const argSymbol = getSymbolCached(arg); if (argSymbol === symbol) { found = true; return; } } // 检查展开运算符:Promise.all([...variable]) if (ts.isArrayLiteralExpression(arg)) { for (const element of arg.elements) { if (ts.isSpreadElement(element) && ts.isIdentifier(element.expression)) { const spreadSymbol = getSymbolCached(element.expression); if (spreadSymbol === symbol) { found = true; return; } } } } } } // 不进入嵌套函数 if (node !== scope && isFunctionLikeDeclaration(node)) { return; } ts.forEachChild(node, visit); }; visit(scope); return found; }; // 检查在代码块中是否处理了 Promise const isPromiseHandledInBlock = (block, symbol) => { const check = checkPromiseHandlingInNode(block, symbol, { stopAtFunctionBoundary: true }); return !!(check.hasAwait || check.hasPromiseMethod || check.hasPromiseStatic || check.hasReturn); }; // 综合检查:Promise 是否被正确处理 const isPromiseProperlyHandled = (symbol, declarationNode) => { // 检查缓存 if (variableHandlingCache.has(symbol)) { return variableHandlingCache.get(symbol); } // 防止循环检查 if (checkingVariables.has(symbol)) { return false; } checkingVariables.add(symbol); try { const scope = findNearestFunctionScope(declarationNode); if (!scope) { variableHandlingCache.set(symbol, false); return false; } const funcLike = scope; if (!funcLike.body) { variableHandlingCache.set(symbol, false); return false; } // 使用统一的检查函数 const check = checkPromiseHandlingWithInstanceOf(funcLike.body, symbol, { stopAtFunctionBoundary: true }); // 使用统一的参数传递检查 const passedToFunction = isSymbolPassedToHandlingFunction(symbol, funcLike.body); const result = !!(check.hasAwait || check.hasPromiseMethod || check.hasPromiseStatic || check.hasReturn || check.hasInstanceOf || passedToFunction); variableHandlingCache.set(symbol, result); return result; } finally { checkingVariables.delete(symbol); } }; // 分析函数参数是否在函数体内被正确处理 const analyzeParameterHandling = (functionSymbol, declaration) => { const parameterHandling = new Map(); if (!declaration.parameters || declaration.parameters.length === 0) { return parameterHandling; } // 获取函数体 const body = declaration.body; if (!body) { return parameterHandling; } // 分析每个参数 declaration.parameters.forEach((param, index) => { if (!param.name) { parameterHandling.set(index, false); return; } // 处理标识符参数 if (ts.isIdentifier(param.name)) { const paramSymbol = typeChecker.getSymbolAtLocation(param.name); if (paramSymbol) { const isHandled = isPromiseProperlyHandled(paramSymbol, body); parameterHandling.set(index, isHandled); return; } } // 处理解构参数 - 保守处理,标记为未处理 // TODO: 可以进一步分析解构后的变量是否被处理 parameterHandling.set(index, false); }); return parameterHandling; }; // 获取函数参数处理信息(带缓存) const getFunctionParameterHandling = (functionSymbol) => { if (functionParameterHandling.has(functionSymbol)) { return functionParameterHandling.get(functionSymbol); } const declaration = functionDeclarations.get(functionSymbol); if (!declaration) { return new Map(); } const handling = analyzeParameterHandling(functionSymbol, declaration); functionParameterHandling.set(functionSymbol, handling); return handling; }; // 检查调用是否作为参数传递给会处理 Promise 的函数 const isCallPassedToHandlingFunction = (callNode) => { const parent = callNode.parent; // 检查是否作为函数调用的参数 if (ts.isCallExpression(parent)) { const argIndex = parent.arguments.indexOf(callNode); if (argIndex === -1) { return false; } const signature = typeChecker.getResolvedSignature(parent); if (!signature) { return false; } const declaration = signature.getDeclaration(); if (!declaration) { return false; } const calledSymbol = getSymbolOfDeclaration(declaration); if (!calledSymbol) { return false; } // 检查剩余参数 if ('parameters' in declaration) { const params = declaration.parameters; const lastParam = params[params.length - 1]; if (lastParam?.dotDotDotToken && argIndex >= params.length - 1) { const paramHandling = getFunctionParameterHandling(calledSymbol); return paramHandling.get(params.length - 1) === true; } } // 检查该函数是否会处理这个参数位置的 Promise const paramHandling = getFunctionParameterHandling(calledSymbol); return paramHandling.get(argIndex) === true; } // 检查是否作为数组元素传递给 Promise.all 等 if (ts.isArrayLiteralExpression(parent)) { const grandParent = parent.parent; if (ts.isCallExpression(grandParent) && isPromiseStaticMethodCall(grandParent)) { return true; } } // 检查是否在数组方法回调中返回,且该数组方法的结果被正确处理 return isCallInHandledArrayMethod(callNode); }; // 信息收集 for (const sourceFile of program.getSourceFiles()) { if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) { continue; } // 文件名是否符合检查范围 if (!isFileInCheckScope(pwd, sourceFile.fileName, customConfig)) { continue; } preprocessIgnoreComments(sourceFile); // 预处理注释 collectAndCheck(sourceFile); // 合并后的收集和检查 } // 过滤掉非异步的 context 调用 filterAsyncContextCalls(); // 预分析所有函数的参数处理情况 for (const [symbol, declaration] of functionDeclarations.entries()) { getFunctionParameterHandling(symbol); } // 标记传播 propagateContextMarks(); checkDirectContextCalls(); // 检查直接调用 checkIndirectCalls(); // 检查间接调用(使用缓存的调用列表) const i18nDiagnostics = checkI18nKeys(pwd, tFunctionSymbolCache, program, typeChecker); return diagnostics.concat(i18nDiagnostics); } const loadI18nData = (dirPath) => { // 尝试加载 LOCALE_FILE_NAMES 中的文件 for (const fileName of LOCALE_FILE_NAMES) { const filePath = path.join(dirPath, fileName); if (fs.existsSync(filePath)) { try { const fileContent = fs.readFileSync(filePath, 'utf-8'); const data = JSON.parse(fileContent); return data; } catch (error) { console.error(`Error reading or parsing i18n file: ${filePath}`, error); } } } return null; }; /** * 检查 key 是否存在于 i18n 数据中 * @param i18nData i18n 数据对象 * @param key i18n key,支持点号分隔的嵌套路径 * @returns boolean */ const isKeyExistsInI18nData = (i18nData, key) => { if (!i18nData) { return false; } // 辅助递归函数 const checkPath = (obj, remainingKey) => { if (!obj || typeof obj !== 'object') { return false; } // 如果剩余key完整存在于当前对象,直接返回true if (obj.hasOwnProperty(remainingKey)) { return true; } // 尝试所有可能的分割点 for (let i = 1; i <= remainingKey.length; i++) { const firstPart = remainingKey.substring(0, i); const restPart = remainingKey.substring(i + 1); // +1 跳过点号 // 如果当前部分存在且后面还有内容(有点号分隔) if (obj.hasOwnProperty(firstPart) && i < remainingKey.length && remainingKey[i] === '.') { // 递归检查剩余部分 if (checkPath(obj[firstPart], restPart)) { return true; } } } return false; }; return checkPath(i18nData, key); }; /** * 分析表达式的类型信息 */ const analyzeExpressionType = (expr, typeChecker) => { const result = { isLiteralUnion: false, literalValues: [], isString: false, isNullable: false, hasNonNullAssertion: false, originalType: typeChecker.getTypeAtLocation(expr) }; // 检查是否有非空断言 let actualExpr = expr; if (ts.isNonNullExpression(expr)) { result.hasNonNullAssertion = true; actualExpr = expr.expression; } const type = typeChecker.getTypeAtLocation(actualExpr); // 检查是否是联合类型 if (type.isUnion()) { const nonNullTypes = []; for (const subType of type.types) { // 检查是否包含 null 或 undefined if (subType.flags & ts.TypeFlags.Null || subType.flags & ts.TypeFlags.Undefined) { result.isNullable = true; } else { nonNullTypes.push(subType); } } // 如果有非空断言,忽略 nullable if (result.hasNonNullAssertion) { result.isNullable = false; } // 检查非 null 类型是否都是字面量 if (nonNullTypes.length > 0) { const allLiterals = nonNullTypes.every(t => t.isStringLiteral() || (t.flags & ts.TypeFlags.StringLiteral)); if (allLiterals) { result.isLiteralUnion = true; result.literalValues = nonNullTypes.map(t => { if (t.isStringLiteral()) { return t.value; } return typeChecker.typeToString(t).replace(/['"]/g, ''); }); } } } else { // 单一类型 if (type.flags & ts.TypeFlags.Null || type.flags & ts.TypeFlags.Undefined) { result.isNullable = true; if (result.hasNonNullAssertion) { result.isNullable = false; } } else if (type.isStringLiteral() || (type.flags & ts.TypeFlags.StringLiteral)) { result.isLiteralUnion = true; result.literalValues = [ type.isStringLiteral() ? type.value : typeChecker.typeToString(type).replace(/['"]/g, '') ]; } else if (type.flags & ts.TypeFlags.String) { result.isString = true; } } return result; }; const checkI18nKeys = (pwd, callSet, program, typeChecker) => { const diagnostics = []; const addDiagnostic = (node, messageText, code, options) => { const sourceFile = node.getSourceFile(); diagnostics.push({ file: sourceFile, start: node.getStart(sourceFile), length: node.getWidth(sourceFile), messageText, category: ts.DiagnosticCategory.Warning, code, reason: options?.reason, reasonDetails: options?.reasonDetails }); }; const groupedCalls = new Map(); // 按照文件路径分组,这样方便后面去读取 i18n 文件 const commonLocaleCache = {}; // 公共缓存,避免重复读取文件 const entityLocaleCache = {}; // 实体缓存,避免重复读取文件 const commonLocaleKeyRegex = /^([a-zA-Z0-9_.-]+)::/; // 以语言代码开头,后面跟两个冒号 const entityLocaleKeyRegex = /^([a-zA-Z0-9_.-]+):/; // 以实体代码开头,后面跟一个冒号 callSet.forEach(callNode => { const sourceFile = callNode.getSourceFile(); const filePath = path.normalize(path.relative(pwd, sourceFile.fileName)); if (!groupedCalls.has(filePath)) { groupedCalls.set(filePath, []); } groupedCalls.get(filePath).push(callNode); }); const getCommonLocaleData = (namespace) => { if (commonLocaleCache.hasOwnProperty(namespace)) { return commonLocaleCache[namespace]; } // 尝试加载公共 i18n 文件,在pwd/src/locales/${namespace}/???.json const localeDir = path.join(pwd, 'src', 'locales', namespace); if (fs.existsSync(localeDir) && fs.statSync(localeDir).isDirectory()) { const localeData = {}; LOCALE_FILE_NAMES.forEach(fileName => { const filePath = path.join(localeDir, fileName); if (fs.existsSync(filePath)) { try { const fileContent = fs.readFileSync(filePath, 'utf-8'); const data = JSON.parse(fileContent); Object.assign(localeData, data); } catch (error) { console.error(`Error reading or parsing common i18n file: ${filePath}`, error); } } }); commonLocaleCache[namespace] = localeData; return localeData; } else { commonLocaleCache[namespace] = null; return null; } }; const getEntityLocaleData = (entity) => { if (entityLocaleCache.hasOwnProperty(entity)) { return entityLocaleCache[entity]; } // 尝试加载实体 i18n 文件,在pwd/src/oak-app-domain/${大学开头entity}/locales/zh_CN.json 这里一定是zh_CN.json const entityDir = path.join(pwd, 'src', 'oak-app-domain', (0, lodash_1.upperFirst)(entity), 'locales'); if (fs.existsSync(entityDir) && fs.statSync(entityDir).isDirectory()) { const localeData = {}; try { const filePath = path.join(entityDir, 'zh_CN.json'); if (fs.existsSync(filePath)) { const fileContent = fs.readFileSync(filePath, 'utf-8'); const data = JSON.parse(fileContent); Object.assign(localeData, data); } } catch (error) { console.error(`Error reading or parsing entity i18n file: ${entityDir}`, error); } entityLocaleCache[entity] = localeData; return localeData; } else { entityLocaleCache[entity] = null; return null; } }; /** * 生成模板字符串的所有可能组合 */ const generateKeyVariants = (head, spans) => { // 如果任何一个 span 的 values 为 null(表示无法确定),返回 null if (spans.some(span => span.values === null)) { return null; } const results = []; const generate = (index, current) => { if (index >= spans.length) { results.push(current); return; } const span = spans[index]; const values = span.values; for (const value of values) { generate(index + 1, current + value + span.text); } }; generate(0, head); return results; }; /** * 检查模板表达式中的 i18n key */ const checkTemplateExpressionKey = (templateExpr, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString) => { const head = templateExpr.head.text; const spans = []; let hasUncheckableSpan = false; let hasNullableSpan = false; const spanAnalyses = []; // 分析每个模板片段 for (const span of templateExpr.templateSpans) { const analysis = analyzeExpressionType(span.expression, typeChecker); spanAnalyses.push({ span, analysis }); if (analysis.isLiteralUnion) { // 字面量联合类型,可以检查 spans.push({ text: span.literal.text, values: analysis.literalValues }); } else if (analysis.isString) { // string 类型,无法检查 hasUncheckableSpan = true; spans.push({ text: span.literal.text, values: null }); } else if (analysis.isNullable) { // 可能为 null/undefined hasNullableSpan = true; spans.push({ text: span.literal.text, values: [''] // 空字符串表示 null/undefined 的情况 }); } else { // 其他类型,尝试获取字符串表示 const typeString = typeChecker.typeToString(analysis.originalType); hasUncheckableSpan = true; spans.push({ text: span.literal.text, values: null }); } } // 生成所有可能的 key 组合 const keyVariants = generateKeyVariants(head, spans); if (keyVariants === null) { // 有无法确定的占位符 if (hasUncheckableSpan) { const sourceFile = callNode.getSourceFile(); const { line } = sourceFile.getLineAndCharacterOfPosition(templateExpr.getStart()); addDiagnostic(callNode, `i18n key 使用了模板字符串,但包含无法确定范围的占位符变量(类型为 string),无法进行完整检查。`, 9203, { reason: 'UncheckableTemplate', reasonDetails: spanAnalyses .filter(({ analysis }) => analysis.isString) .map(({ span }) => { const exprText = span.expression.getText(sourceFile); return `✘ 占位符 \${${exprText}} 的类型为 string,范围太大`; }) }); } return; } // 检查所有可能的 key const missingKeys = []; const foundKeys = []; for (const key of keyVariants) { const exists = isKeyExistsInI18nData(i18nData, key) || checkWithCommonStringExists(i18nData, key); if (exists) { foundKeys.push(key); } else { missingKeys.push(key); } } // 如果有 nullable 的情况,需要特别处理 if (hasNullableSpan) { const nullableSpans = spanAnalyses.filter(({ analysis }) => analysis.isNullable); if (missingKeys.length > 0) { const sourceFile = callNode.getSourceFile(); const details = []; details.push(`¡ 模板字符串包含可能为 null/undefined 的占位符`); nullableSpans.forEach(({ span, analysis }) => { const exprText = span.expression.getText(sourceFile); const hasAssertion = analysis.hasNonNullAssertion ? ' (有非空断言)' : ''; details.push(` ${analysis.hasNonNullAssertion ? '✓' : '✘'} \${${exprText}}${hasAssertion}`); }); details.push(''); details.push('需要检查的 key 变体:'); missingKeys.forEach(key => { details.push(` ✘ "${key}" - 未找到`); }); foundKeys.forEach(key => { details.push(` ✓ "${key}" - 已找到`); }); addDiagnostic(callNode, `i18n key 模板字符串的某些变体未找到: ${missingKeys.join(', ')}`, 9200, { reason: 'MissingTemplateVariants', reasonDetails: details }); } } else { // 没有 nullable,所有 key 都必须存在 if (missingKeys.length > 0) { const sourceFile = callNode.getSourceFile(); const details = []; if (keyVariants.length > 1) { details.push(`¡ 模板字符串有 ${keyVariants.length} 个可能的变体`); details.push(''); } missingKeys.forEach(key => { details.push(` ✘ "${key}" - 未找到`); }); foundKeys.forEach(key => { details.push(` ✓ "${key}" - 已找到`); }); addDiagnostic(callNode, `i18n key 模板字符串的某些变体未找到: ${missingKeys.join(', ')}`, 9200, { reason: 'MissingTemplateVariants', reasonDetails: details }); } } }; /** * 检查是否存在(不添加诊断,仅返回结果) */ const checkWithCommonStringExists = (i18nData, key) => { if (commonLocaleKeyRegex.test(key)) { const parts = commonLocaleKeyRegex.exec(key); if (parts && parts.length >= 2) { const namespace = parts[1]; const localeData = getCommonLocaleData(namespace); const actualKey = key.substring(namespace.length + 2); return isKeyExistsInI18nData(localeData, actualKey); } } else if (entityLocaleKeyRegex.test(key)) { const parts = entityLocaleKeyRegex.exec(key); if (parts && parts.length >= 2) { const entity = parts[1]; const localeData = getEntityLocaleData(entity); const actualKey = key.substring(entity.length + 1); return isKeyExistsInI18nData(localeData, actualKey); } } else { return isKeyExistsInI18nData(i18nData, key); } return false; }; /** * 检查变量引用的 i18n key */ const checkVariableReferenceKey = (identifier, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString) => { const analysis = analyzeExpressionType(identifier, typeChecker); const sourceFile = callNode.getSourceFile(); const varName = identifier.getText(sourceFile); if (analysis.isLiteralUnion) { // 字面量联合类型,检查所有可能的值 const missingKeys = []; const foundKeys = []; for (const value of analysis.literalValues) { const exists = isKeyExistsInI18nData(i18nData, value) || checkWithCommonStringExists(i18nData, value); if (exists) { foundKeys.push(value); } else { missingKeys.push(value); } } if (missingKeys.length > 0) { const details = []; details.push(`¡ 变量 ${varName} 的类型为: ${analysis.literalValues.map(v => `"${v}"`).join(' | ')}`); details.push(''); missingKeys.forEach(key => { details.push(` ✘ "${key}" - 未找到`); }); foundKeys.forEach(key => { details.push(` ✓ "${key}" - 已找到`); }); addDiagnostic(callNode, `变量 ${varName} 的某些可能值在 i18n 中未找到: ${missingKeys.join(', ')}`, 9200, { reason: 'MissingVariableVariants', reasonDetails: details }); } } else if (analysis.isNullable) { // 可能为 null/undefined addDiagnostic(callNode, `变量 ${varName} 可能为 null 或 undefined,这可能导致 i18n 查找失败。`, 9204, { reason: 'NullableVariable', reasonDetails: [ `✘ 变量 ${varName} 的类型包含 null 或 undefined`, analysis.hasNonNullAssertion ? '✓ 使用了非空断言 (!)' : '建议: 添加非空断言或进行空值检查' ] }); } else if (analysis.isString) { // string 类型,无法检查 addDiagnostic(callNode, `变量 ${varName} 的类型为 string,范围太大无法检查 i18n key 是否存在。`, 9203, { reason: 'UncheckableVariable', reasonDetails: [ `✘ 变量 ${varName} 的类型为 string`, '建议: 使用字面量联合类型限制可能的值,如: "key1" | "key2"' ] }); } else { // 其他类型 const typeString = typeChecker.typeToString(analysis.originalType); addDiagnostic(callNode, `变量 ${varName} 的类型 (${typeString}) 不是有效的 i18n key 类型。`, 9205, { reason: 'InvalidVariableType', reasonDetails: [ `✘ 变量 ${varName} 的类型为: ${typeString}`, '期望: string 字面量或字面量联合类型' ] }); } }; const checkWithCommonString = (i18nData, key, callNode, localePath) => { if (commonLocaleKeyRegex.test(key)) { // 公共字符串,格式如 common::title, 这里把namespace提取出来 const parts = commonLocaleKeyRegex.exec(key); if (parts && parts.length >= 2) { const namespace = parts[1]; const localeData = getCommonLocaleData(namespace); const actualKey = key.substring(namespace.length + 2); // 去掉 namespace 和 :: if (isKeyExistsInI18nData(localeData, actualKey)) { return; // 找到,直接返回 } else { addDiagnostic(callNode, `i18n key "${key}" not found in public locale files: namespace "${namespace}".`, 9200); return; } } else { addDiagnostic(callNode, `i18n key "${key}" has invalid format.`, 9201, { reason: 'InvalidFormat', reasonDetails: [ `Expected format: ::, e.g., common::title` ] }); return; } } else if (entityLocaleKeyRegex.test(key)) { // 实体字符串,格式如 entity:attr.name const parts = entityLocaleKeyRegex.exec(key); if (parts && parts.length >= 2) { const entity = parts[1]; const localeData = getEntityLocaleData(entity); const actualKey = key.substring(entity.length + 1); if (isKeyExistsInI18nData(localeData, actualKey)) { return; // 找到,直接返回 } else { addDiagnostic(callNode, `i18n key "${key}" not found in entity locale files: entity "${entity}".`, 9200); return; } } else { addDiagnostic(callNode, `i18n key "${key}" has invalid format.`, 9201, { reason: 'InvalidFormat', reasonDetails: [ `Expected format: :, e.g., user:attr.name` ] }); return; } } else { // 普通字符串,直接检查 if (isKeyExistsInI18nData(i18nData, key)) { return; // 找到,直接返回 } else { addDiagnostic(callNode, `i18n key "${key}" not found in its locale files: ${localePath}.`, 9200); return; } } }; // 逐文件处理 groupedCalls.forEach((calls, filePath) => { // 如果这个文件同级目录下没有index.ts, 暂时不做检查 const dirPath = path.dirname(path.resolve(pwd, filePath)); const localePath = path.join(dirPath, 'locales'); const indexTsPath = path.join(dirPath, 'index.ts'); if (!fs.existsSync(indexTsPath)) { return; } const i18nData = loadI18nData(localePath); if (!i18nData) { // 全部加入警告 calls.forEach(callNode => { const args = callNode.arguments; if (args.length === 0) { return; // 没有参数,跳过 } const firstArg = args[0]; if (ts.isStringLiteral(firstArg)) { const key = firstArg.text; // 检查是否是 entity 或 common 格式 if (commonLocaleKeyRegex.test(key) || entityLocaleKeyRegex.test(key)) { // 是 entity/common 格式,使用空对象作为本地 i18n 数据,让 checkWithCommonString 处理 checkWithCommonString({}, key, callNode, localePath); } else { // 普通格式的 key,但本地没有 i18n 文件 addDiagnostic(callNode, `i18n key "${key}" cannot be checked because no i18n data file found in directory: ${localePath}.`, 9202); } } else if (ts.isTemplateExpression(firstArg) || ts.isNoSubstitutionTemplateLiteral(firstArg) || ts.isIdentifier(firstArg) || ts.isPropertyAccessExpression(firstArg) || ts.isElementAccessExpression(firstArg)) { // TODO: 对于非字面量的情况,暂时跳过(因为无法确定是否是 entity/common 格式) // 可以考虑添加更详细的类型分析 } }); return; } // 逐调用检查 calls.forEach(callNode => { const args = callNode.arguments; if (args.length === 0) { return; // 没有参数,跳过 } const firstArg = args[0]; if (ts.isStringLiteral(firstArg)) { // 字符串字面量 const key = firstArg.text; checkWithCommonString(i18nData, key, callNode, localePath); } else if (ts.isTemplateExpression(firstArg)) { // 模板字符串 checkTemplateExpressionKey(firstArg, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString); } else if (ts.isNoSubstitutionTemplateLiteral(firstArg)) { // 无替换的模板字面量 `key` const key = firstArg.text; checkWithCommonString(i18nData, key, callNode, localePath); } else if (ts.isIdentifier(firstArg)) { // 变量引用 checkVariableReferenceKey(firstArg, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString); } else if (ts.isPropertyAccessExpression(firstArg) || ts.isElementAccessExpression(firstArg)) { // 属性访问或元素访问,尝试分析类型 const analysis = analyzeExpressionType(firstArg, typeChecker); const sourceFile = callNode.getSourceFile(); const exprText = firstArg.getText(sourceFile); if (analysis.isLiteralUnion) { // 可以检查 const missingKeys = []; const foundKeys = []; for (const value of analysis.literalValues) { const exists = isKeyExistsInI18nData(i18nData, value) || checkWithCommonStringExists(i18nData, value); if (exists) { foundKeys.push(value); } else { missingKeys.push(value); } } if (missingKeys.length > 0) { addDiagnostic(callNode, `表达式 ${exprText} 的某些可能值在 i18n 中未找到: ${missingKeys.join(', ')}`, 9200, { reason: 'MissingExpressionVariants', reasonDetails: [ `¡ 表达式 ${exprText} 的类型为: ${analysis.literalValues.map(v => `"${v}"`).join(' | ')}`, '', ...missingKeys.map(key => ` ✘ "${key}" - 未找到`), ...foundKeys.map(key => ` ✓ "${key}" - 已找到`) ] }); } } else if (analysis.isString) { addDiagnostic(callNode, `表达式 ${exprText} 的类型为 string,范围太大无法检查。`, 9203, { reason: 'UncheckableExpression', reasonDetails: [`✘ 表达式 ${exprText} 的类型为 string`] }); } else if (analysis.isNullable) { addDiagnostic(callNode, `表达式 ${exprText} 可能为 null 或 undefined。`, 9204, { reason: 'NullableExpression', reasonDetails: [`✘ 表达式 ${exprText} 可能为 null 或 undefined`] }); } } else { // 其他复杂表达式 const sourceFile = callNode.getSourceFile(); const exprText = firstArg.getText(sourceFile); addDiagnostic(callNode, `i18n key 参数使用了复杂表达式 (${exprText}),无法进行检查。`, 9206, { reason: 'ComplexExpression', reasonDetails: [`✘ 表达式 ${exprText} 过于复杂,无法确定其值`] }); } }); }); return diagnostics; }; const compile = (pwd, options) => { // 读取 tsconfig.json const configFile = ts.readConfigFile(options.project, ts.sys.readFile); // 读取自定义配置 const customConfig = configFile.config.oakBuildChecks || {}; 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(options.project)); 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) { 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(pwd, program, typeChecker, customConfig); if (customDiagnostics.length > 0) { // 输出统计 console.log(`${colors.cyan}发现 ${customDiagnostics.length} 个潜在问题:${colors.reset}`); // 输出详细信息 customDiagnostics.forEach((diagnostic, index) => printDiagnostic(pwd, diagnostic, index)); // 输出忽略提示 console.log(`\n${colors.yellow}═══════════════════════════════════════════════════════════${colors.reset}`); console.log(`${colors.cyan}如果确定逻辑正确,可以使用以下注释标签来忽略检查:${colors.reset}`); exports.OAK_IGNORE_TAGS.forEach(tag => { console.log(` ${colors.green}// ${tag}${colors.reset}`); }); console.log(`${colors.yellow}═══════════════════════════════════════════════════════════${colors.reset}\n`); } let emitResult = { emitSkipped: true, diagnostics: [] }; if (!options.noEmit) { // 执行编译 emitResult = program.emit(); } // 获取诊断信息 const allDiagnostics = [ ...ts.getPreEmitDiagnostics(program), ...emitResult.diagnostics, ]; // 输出诊断信息 allDiagnostics.forEach((diagnostic, index) => printDiagnostic(pwd, diagnostic, index)); // 输出编译统计 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 ')}.`); } if (errorCount > 0) { console.log(`${colors.red}Compilation failed due to errors.${colors.reset}`); process.exit(1); } console.log(`${colors.green}Compilation completed successfully.${colors.reset}`); }; const build = (pwd, args) => { // 执行编译 const options = parseArgs(args); let configPath; // 判断参数是目录还是文件 if (fs.existsSync(options.project)) { const stat = fs.statSync(options.project); if (stat.isDirectory()) { configPath = path.resolve(options.project, 'tsconfig.json'); } else { configPath = path.resolve(options.project); } } else { configPath = path.resolve(pwd, options.project); if (!fs.existsSync(configPath)) { const dirPath = path.resolve(pwd, options.project); 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); } options.project = configPath; compile(pwd, options); }; exports.build = build;