2785 lines
123 KiB
JavaScript
2785 lines
123 KiB
JavaScript
"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<T> | 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 allComments = [];
|
||
const scanComments = (pos, end) => {
|
||
const leading = ts.getLeadingCommentRanges(fullText, pos);
|
||
const trailing = ts.getTrailingCommentRanges(fullText, end);
|
||
if (leading) {
|
||
leading.forEach(comment => {
|
||
const text = fullText.substring(comment.pos, comment.end);
|
||
allComments.push({
|
||
pos: comment.pos,
|
||
end: comment.end,
|
||
text,
|
||
isIgnore: isIgnoreComment(text)
|
||
});
|
||
});
|
||
}
|
||
if (trailing) {
|
||
trailing.forEach(comment => {
|
||
const text = fullText.substring(comment.pos, comment.end);
|
||
allComments.push({
|
||
pos: comment.pos,
|
||
end: comment.end,
|
||
text,
|
||
isIgnore: isIgnoreComment(text)
|
||
});
|
||
});
|
||
}
|
||
};
|
||
// 扫描整个文件的注释
|
||
const scanNode = (node) => {
|
||
scanComments(node.getFullStart(), node.getEnd());
|
||
ts.forEachChild(node, scanNode);
|
||
};
|
||
scanNode(sourceFile);
|
||
// 检查节点是否在忽略注释的影响范围内
|
||
const isNodeCoveredByIgnoreComment = (node) => {
|
||
const nodeStart = node.getStart(sourceFile);
|
||
const nodeEnd = node.getEnd();
|
||
for (const comment of allComments) {
|
||
if (!comment.isIgnore)
|
||
continue;
|
||
// 情况1:注释在节点之前(前导注释)
|
||
// 允许注释和节点之间有少量空白(最多50个字符)
|
||
if (comment.end <= nodeStart && nodeStart - comment.end <= 50) {
|
||
return true;
|
||
}
|
||
// 情况2:注释在节点之后(尾随注释)
|
||
// 允许节点和注释之间有少量空白(最多50个字符)
|
||
if (comment.pos >= nodeEnd && comment.pos - nodeEnd <= 50) {
|
||
return true;
|
||
}
|
||
// 情况3:注释在节点内部(JSX 表达式中的注释)
|
||
if (comment.pos >= nodeStart && comment.end <= nodeEnd) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
const visit = (node) => {
|
||
// 检查当前节点是否被忽略注释覆盖
|
||
if (isNodeCoveredByIgnoreComment(node)) {
|
||
markNodeAndChildren(node);
|
||
return;
|
||
}
|
||
// 特殊处理:JSX 表达式
|
||
if (ts.isJsxExpression(node)) {
|
||
// 检查 JSX 表达式内部是否有忽略注释
|
||
if (node.expression && isNodeCoveredByIgnoreComment(node.expression)) {
|
||
markNodeAndChildren(node.expression);
|
||
}
|
||
}
|
||
// 特殊处理:条件表达式(三元运算符)
|
||
if (ts.isConditionalExpression(node)) {
|
||
// 检查 whenTrue 和 whenFalse 分支
|
||
if (isNodeCoveredByIgnoreComment(node.whenTrue)) {
|
||
markNodeAndChildren(node.whenTrue);
|
||
}
|
||
if (isNodeCoveredByIgnoreComment(node.whenFalse)) {
|
||
markNodeAndChildren(node.whenFalse);
|
||
}
|
||
}
|
||
// 特殊处理:二元表达式
|
||
if (ts.isBinaryExpression(node)) {
|
||
// 检查左右操作数
|
||
if (isNodeCoveredByIgnoreComment(node.left)) {
|
||
markNodeAndChildren(node.left);
|
||
}
|
||
if (isNodeCoveredByIgnoreComment(node.right)) {
|
||
markNodeAndChildren(node.right);
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
// 如果文件开头有忽略注释,整个文件都忽略
|
||
const fileStartComments = ts.getLeadingCommentRanges(sourceFile.getFullText(), 0);
|
||
if (fileStartComments) {
|
||
for (const comment of fileStartComments) {
|
||
const commentText = sourceFile.getFullText().substring(comment.pos, comment.end);
|
||
if (isIgnoreComment(commentText)) {
|
||
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);
|
||
};
|
||
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 parseBinaryExpression = (expr, typeChecker) => {
|
||
// 如果是二元表达式且是 + 运算符
|
||
if (ts.isBinaryExpression(expr) &&
|
||
expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
||
// 递归解析左右两侧
|
||
const leftParts = parseBinaryExpression(expr.left, typeChecker);
|
||
const rightParts = parseBinaryExpression(expr.right, typeChecker);
|
||
if (leftParts === null || rightParts === null) {
|
||
return null;
|
||
}
|
||
return [...leftParts, ...rightParts];
|
||
}
|
||
// 基础情况:单个表达式
|
||
if (ts.isStringLiteral(expr)) {
|
||
// 字符串字面量
|
||
return [{
|
||
values: [expr.text],
|
||
isLiteral: true,
|
||
expression: expr
|
||
}];
|
||
}
|
||
else if (ts.isNoSubstitutionTemplateLiteral(expr)) {
|
||
// 模板字面量(无替换)
|
||
return [{
|
||
values: [expr.text],
|
||
isLiteral: true,
|
||
expression: expr
|
||
}];
|
||
}
|
||
else {
|
||
// 其他表达式(变量、属性访问等)
|
||
const analysis = analyzeExpressionType(expr, typeChecker);
|
||
if (analysis.isLiteralUnion) {
|
||
return [{
|
||
values: analysis.literalValues,
|
||
isLiteral: false,
|
||
expression: expr,
|
||
analysis
|
||
}];
|
||
}
|
||
else if (analysis.isString) {
|
||
// string 类型,无法确定
|
||
return [{
|
||
values: null,
|
||
isLiteral: false,
|
||
expression: expr,
|
||
analysis
|
||
}];
|
||
}
|
||
else if (analysis.isNullable) {
|
||
// 可能为 null/undefined
|
||
return [{
|
||
values: [''], // 空字符串表示 null/undefined
|
||
isLiteral: false,
|
||
expression: expr,
|
||
analysis
|
||
}];
|
||
}
|
||
else {
|
||
// 其他类型,无法处理
|
||
return null;
|
||
}
|
||
}
|
||
};
|
||
/**
|
||
* 生成字符串拼接的所有可能组合
|
||
*/
|
||
const generateConcatenationVariants = (parts) => {
|
||
// 如果任何部分无法确定,返回 null
|
||
if (parts.some(part => part.values === null)) {
|
||
return null;
|
||
}
|
||
const results = [];
|
||
const generate = (index, current) => {
|
||
if (index >= parts.length) {
|
||
results.push(current);
|
||
return;
|
||
}
|
||
const part = parts[index];
|
||
const values = part.values;
|
||
for (const value of values) {
|
||
generate(index + 1, current + value);
|
||
}
|
||
};
|
||
generate(0, '');
|
||
return results;
|
||
};
|
||
/**
|
||
* 检查二元表达式(字符串拼接)的 i18n key
|
||
*/
|
||
const checkBinaryExpressionKey = (binaryExpr, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString) => {
|
||
const sourceFile = callNode.getSourceFile();
|
||
// 解析二元表达式
|
||
const parts = parseBinaryExpression(binaryExpr, typeChecker);
|
||
if (parts === null) {
|
||
// 无法解析
|
||
addDiagnostic(callNode, `i18n key 使用了复杂的字符串拼接表达式,无法进行检查。`, 9206, {
|
||
reason: 'ComplexConcatenation',
|
||
reasonDetails: [
|
||
`✘ 表达式 ${binaryExpr.getText(sourceFile)} 过于复杂`
|
||
]
|
||
});
|
||
return;
|
||
}
|
||
// 检查是否有无法确定的部分
|
||
const uncheckableParts = parts.filter(part => part.values === null);
|
||
const nullableParts = parts.filter(part => part.analysis?.isNullable && !part.analysis?.hasNonNullAssertion);
|
||
if (uncheckableParts.length > 0) {
|
||
// 有无法确定的部分
|
||
const details = [];
|
||
details.push('¡ 字符串拼接包含无法确定范围的部分:');
|
||
details.push('');
|
||
parts.forEach(part => {
|
||
const exprText = part.expression.getText(sourceFile);
|
||
if (part.values === null) {
|
||
details.push(` ✘ ${exprText} - 类型为 string,范围太大`);
|
||
}
|
||
else if (part.isLiteral) {
|
||
details.push(` ✓ "${part.values[0]}" - 字面量`);
|
||
}
|
||
else if (part.analysis?.isLiteralUnion) {
|
||
details.push(` ✓ ${exprText} - 字面量联合类型: ${part.values.map(v => `"${v}"`).join(' | ')}`);
|
||
}
|
||
else if (part.analysis?.isNullable) {
|
||
const hasAssertion = part.analysis.hasNonNullAssertion;
|
||
details.push(` ${hasAssertion ? '✓' : '✘'} ${exprText} - 可能为 null/undefined${hasAssertion ? ' (有非空断言)' : ''}`);
|
||
}
|
||
});
|
||
addDiagnostic(callNode, `i18n key 使用了字符串拼接,但包含无法确定范围的部分,无法进行完整检查。`, 9203, {
|
||
reason: 'UncheckableConcatenation',
|
||
reasonDetails: details
|
||
});
|
||
return;
|
||
}
|
||
// 生成所有可能的组合
|
||
const keyVariants = generateConcatenationVariants(parts);
|
||
if (keyVariants === null) {
|
||
// 理论上不应该到这里,因为前面已经检查过了
|
||
return;
|
||
}
|
||
// 检查所有可能的 key
|
||
const missingKeys = [];
|
||
const foundKeys = [];
|
||
for (const key of keyVariants) {
|
||
const result = isKeyExistsInI18nData(i18nData, key);
|
||
const commonResult = result.exists ? result : checkWithCommonStringExists(i18nData, key);
|
||
const exists = commonResult.exists;
|
||
// 如果找到了 key,检查占位符
|
||
if (exists && commonResult.placeholders.length > 0) {
|
||
checkSecondArgument(callNode, commonResult.placeholders, addDiagnostic);
|
||
}
|
||
if (exists) {
|
||
foundKeys.push(key);
|
||
}
|
||
else {
|
||
missingKeys.push(key);
|
||
}
|
||
}
|
||
// 如果有 nullable 的情况,需要特别处理
|
||
if (nullableParts.length > 0) {
|
||
if (missingKeys.length > 0) {
|
||
const details = [];
|
||
details.push(`¡ 字符串拼接包含可能为 null/undefined 的部分`);
|
||
nullableParts.forEach(part => {
|
||
const exprText = part.expression.getText(sourceFile);
|
||
const hasAssertion = part.analysis?.hasNonNullAssertion ? ' (有非空断言)' : '';
|
||
details.push(` ${part.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: 'MissingConcatenationVariants',
|
||
reasonDetails: details
|
||
});
|
||
}
|
||
}
|
||
else {
|
||
// 没有 nullable,所有 key 都必须存在
|
||
if (missingKeys.length > 0) {
|
||
const details = [];
|
||
if (keyVariants.length > 1) {
|
||
details.push(`¡ 字符串拼接有 ${keyVariants.length} 个可能的变体`);
|
||
details.push('');
|
||
details.push('拼接部分:');
|
||
parts.forEach(part => {
|
||
const exprText = part.expression.getText(sourceFile);
|
||
if (part.isLiteral) {
|
||
details.push(` ✓ "${part.values[0]}" - 字面量`);
|
||
}
|
||
else if (part.analysis?.isLiteralUnion) {
|
||
details.push(` ✓ ${exprText} - 可能值: ${part.values.map(v => `"${v}"`).join(' | ')}`);
|
||
}
|
||
});
|
||
details.push('');
|
||
}
|
||
missingKeys.forEach(key => {
|
||
details.push(` ✘ "${key}" - 未找到`);
|
||
});
|
||
foundKeys.forEach(key => {
|
||
details.push(` ✓ "${key}" - 已找到`);
|
||
});
|
||
addDiagnostic(callNode, `i18n key 字符串拼接的某些变体未找到: ${missingKeys.join(', ')}`, 9200, {
|
||
reason: 'MissingConcatenationVariants',
|
||
reasonDetails: details
|
||
});
|
||
}
|
||
}
|
||
};
|
||
/**
|
||
* 生成模板字符串的所有可能组合
|
||
*/
|
||
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 result = isKeyExistsInI18nData(i18nData, key);
|
||
const commonResult = result.exists ? result : checkWithCommonStringExists(i18nData, key);
|
||
const exists = commonResult.exists;
|
||
// 如果找到了 key,检查占位符
|
||
if (exists && commonResult.placeholders.length > 0) {
|
||
checkSecondArgument(callNode, commonResult.placeholders, addDiagnostic);
|
||
}
|
||
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 { exists: false, placeholders: [] };
|
||
};
|
||
/**
|
||
* 检查 t 函数的第二个参数是否提供了所需的占位符
|
||
*/
|
||
const checkSecondArgument = (callNode, placeholders, addDiagnostic) => {
|
||
if (placeholders.length === 0) {
|
||
return; // 没有占位符,不需要检查
|
||
}
|
||
const args = callNode.arguments;
|
||
if (args.length < 2) {
|
||
// 缺少第二个参数
|
||
addDiagnostic(callNode, `i18n 值包含占位符 ${placeholders.map(p => `%{${p}}`).join(', ')},但未提供第二个参数。`, 9210, {
|
||
reason: 'MissingSecondArgument',
|
||
reasonDetails: [
|
||
`✘ 需要的占位符: ${placeholders.join(', ')}`,
|
||
'建议: 添加第二个参数对象,如: t("key", { url: "..." })'
|
||
]
|
||
});
|
||
return;
|
||
}
|
||
const secondArg = args[1];
|
||
// 检查第二个参数是否是对象字面量
|
||
if (!ts.isObjectLiteralExpression(secondArg)) {
|
||
addDiagnostic(callNode, `i18n 值包含占位符,但第二个参数不是字面对象,无法检查占位符是否提供。`, 9211, {
|
||
reason: 'NonLiteralSecondArgument',
|
||
reasonDetails: [
|
||
`✘ 第二个参数类型: ${secondArg.getText()}`,
|
||
`需要的占位符: ${placeholders.join(', ')}`,
|
||
'建议: 使用对象字面量,如: { url: "..." }'
|
||
]
|
||
});
|
||
return;
|
||
}
|
||
// 提取对象字面量中的属性名
|
||
const providedKeys = new Set();
|
||
secondArg.properties.forEach(prop => {
|
||
if (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) {
|
||
if (ts.isIdentifier(prop.name)) {
|
||
providedKeys.add(prop.name.text);
|
||
}
|
||
else if (ts.isStringLiteral(prop.name)) {
|
||
providedKeys.add(prop.name.text);
|
||
}
|
||
else if (ts.isComputedPropertyName(prop.name)) {
|
||
// 计算属性名,无法静态检查
|
||
// 可以选择跳过或警告
|
||
}
|
||
}
|
||
else if (ts.isSpreadAssignment(prop)) {
|
||
// 展开运算符,无法静态检查所有属性
|
||
// 可以选择跳过检查
|
||
return;
|
||
}
|
||
});
|
||
// 检查是否有展开运算符
|
||
const hasSpread = secondArg.properties.some(prop => ts.isSpreadAssignment(prop));
|
||
// 检查缺失的占位符
|
||
const missingPlaceholders = placeholders.filter(p => !providedKeys.has(p));
|
||
if (missingPlaceholders.length > 0) {
|
||
const details = [];
|
||
if (hasSpread) {
|
||
details.push('¡ 第二个参数包含展开运算符,可能提供了额外的属性');
|
||
}
|
||
details.push('需要的占位符:');
|
||
placeholders.forEach(p => {
|
||
if (providedKeys.has(p)) {
|
||
details.push(` ✓ ${p} - 已提供`);
|
||
}
|
||
else {
|
||
details.push(` ✘ ${p} - 缺失`);
|
||
}
|
||
});
|
||
if (!hasSpread) {
|
||
addDiagnostic(callNode, `i18n 第二个参数缺少占位符: ${missingPlaceholders.join(', ')}`, 9212, {
|
||
reason: 'MissingPlaceholders',
|
||
reasonDetails: details
|
||
});
|
||
}
|
||
else {
|
||
// 有展开运算符,降级为警告
|
||
addDiagnostic(callNode, `i18n 第二个参数可能缺少占位符: ${missingPlaceholders.join(', ')} (包含展开运算符,无法完全确定)`, 9213, {
|
||
reason: 'PossiblyMissingPlaceholders',
|
||
reasonDetails: details
|
||
});
|
||
}
|
||
}
|
||
};
|
||
/**
|
||
* 检查变量引用的 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 result = isKeyExistsInI18nData(i18nData, value);
|
||
const commonResult = result.exists ? result : checkWithCommonStringExists(i18nData, value);
|
||
const exists = commonResult.exists;
|
||
if (exists) {
|
||
foundKeys.push(value);
|
||
// 检查占位符(对于字面量联合类型,只在所有值都找到时检查)
|
||
if (commonResult.placeholders.length > 0) {
|
||
checkSecondArgument(callNode, commonResult.placeholders, addDiagnostic);
|
||
}
|
||
}
|
||
else {
|
||
missingKeys.push(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) => {
|
||
let result;
|
||
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);
|
||
result = isKeyExistsInI18nData(localeData, actualKey);
|
||
if (result.exists) {
|
||
// 检查占位符
|
||
if (result.placeholders.length > 0) {
|
||
checkSecondArgument(callNode, result.placeholders, addDiagnostic);
|
||
}
|
||
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: <namespace>::<key>, e.g., common::title`
|
||
]
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
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);
|
||
result = isKeyExistsInI18nData(localeData, actualKey);
|
||
if (result.exists) {
|
||
// 检查占位符
|
||
if (result.placeholders.length > 0) {
|
||
checkSecondArgument(callNode, result.placeholders, addDiagnostic);
|
||
}
|
||
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: <entity>:<key>, e.g., user:attr.name`
|
||
]
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
else {
|
||
result = isKeyExistsInI18nData(i18nData, key);
|
||
if (result.exists) {
|
||
// 检查占位符
|
||
if (result.placeholders.length > 0) {
|
||
checkSecondArgument(callNode, result.placeholders, addDiagnostic);
|
||
}
|
||
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 sourceFile = callNode.getSourceFile();
|
||
if (hasIgnoreComment(callNode, sourceFile)) {
|
||
return; // 有忽略注释,跳过检查
|
||
}
|
||
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}" 无法检查,因为找不到locales文件: ${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 sourceFile = callNode.getSourceFile();
|
||
if (hasIgnoreComment(callNode, sourceFile)) {
|
||
return; // 有忽略注释,跳过检查
|
||
}
|
||
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.isBinaryExpression(firstArg) &&
|
||
firstArg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
||
// 字符串拼接表达式
|
||
checkBinaryExpressionKey(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 result = isKeyExistsInI18nData(i18nData, value);
|
||
const commonResult = result.exists ? result : checkWithCommonStringExists(i18nData, value);
|
||
const exists = commonResult.exists;
|
||
if (exists) {
|
||
foundKeys.push(value);
|
||
// 检查占位符(对于字面量联合类型,只在所有值都找到时检查)
|
||
if (commonResult.placeholders.length > 0) {
|
||
checkSecondArgument(callNode, commonResult.placeholders, addDiagnostic);
|
||
}
|
||
}
|
||
else {
|
||
missingKeys.push(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;
|
||
};
|
||
// 信息收集
|
||
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;
|
||
};
|
||
/**
|
||
* 从 i18n 值中提取占位符
|
||
* 支持格式:%{key} 或 {{key}}
|
||
*/
|
||
const extractPlaceholders = (value) => {
|
||
const placeholders = [];
|
||
// 匹配 %{xxx} 格式
|
||
const percentPattern = /%\{([^}]+)\}/g;
|
||
let match;
|
||
while ((match = percentPattern.exec(value)) !== null) {
|
||
placeholders.push(match[1]);
|
||
}
|
||
// 匹配 {{xxx}} 格式(如果需要)
|
||
const bracePattern = /\{\{([^}]+)\}\}/g;
|
||
while ((match = bracePattern.exec(value)) !== null) {
|
||
if (!placeholders.includes(match[1])) {
|
||
placeholders.push(match[1]);
|
||
}
|
||
}
|
||
return placeholders;
|
||
};
|
||
const isKeyExistsInI18nData = (i18nData, key) => {
|
||
if (!i18nData) {
|
||
return { exists: false, placeholders: [] };
|
||
}
|
||
const checkPath = (obj, remainingKey) => {
|
||
if (!obj || typeof obj !== 'object') {
|
||
return { exists: false, placeholders: [] };
|
||
}
|
||
// 如果剩余key完整存在于当前对象
|
||
if (obj.hasOwnProperty(remainingKey)) {
|
||
const value = obj[remainingKey];
|
||
return {
|
||
exists: true,
|
||
value: typeof value === 'string' ? value : undefined,
|
||
placeholders: typeof value === 'string' ? extractPlaceholders(value) : []
|
||
};
|
||
}
|
||
// 尝试所有可能的分割点
|
||
for (let i = 1; i <= remainingKey.length; i++) {
|
||
const firstPart = remainingKey.substring(0, i);
|
||
const restPart = remainingKey.substring(i + 1);
|
||
if (obj.hasOwnProperty(firstPart) && i < remainingKey.length && remainingKey[i] === '.') {
|
||
const result = checkPath(obj[firstPart], restPart);
|
||
if (result.exists) {
|
||
return result;
|
||
}
|
||
}
|
||
}
|
||
return { exists: false, placeholders: [] };
|
||
};
|
||
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 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;
|