1691 lines
72 KiB
JavaScript
1691 lines
72 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 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 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 = (fileName, customConfig) => {
|
||
const patterns = customConfig.context?.filePatterns || ['**/*.ts', '**/*.tsx'];
|
||
const normalizedFileName = path.normalize(path.relative(process.cwd(), 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);
|
||
let configPath = 'tsconfig.json';
|
||
for (let i = 0; i < args.length; i++) {
|
||
if (args[i] === '-p' || args[i] === '--project') {
|
||
if (i + 1 < args.length) {
|
||
configPath = args[i + 1];
|
||
break;
|
||
}
|
||
else {
|
||
console.error('error: option \'-p, --project\' argument missing');
|
||
process.exit(1);
|
||
}
|
||
}
|
||
}
|
||
return configPath;
|
||
}
|
||
// 读取自定义配置
|
||
function readCustomConfig(configPath) {
|
||
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
||
if (configFile.error) {
|
||
return {}; // 返回默认配置
|
||
}
|
||
const config = configFile.config;
|
||
// 返回自定义配置,如果不存在则返回默认值
|
||
return config.oakBuildChecks || {};
|
||
}
|
||
const getContextLocationText = (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(process.cwd(), sourceFile.fileName)}:${line + 1}:${character + 1}`;
|
||
};
|
||
function printDiagnostic(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(process.cwd(), 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(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(program, typeChecker, customConfig) {
|
||
const diagnostics = [];
|
||
// 如果自定义检查被禁用,直接返回
|
||
if (!customConfig.context?.checkAsyncContext) {
|
||
console.log('Custom AsyncContext checks are disabled.');
|
||
return diagnostics;
|
||
}
|
||
// 数据结构定义
|
||
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();
|
||
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)) {
|
||
// 检查 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);
|
||
}
|
||
}
|
||
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(sourceFile.fileName, customConfig)) {
|
||
continue;
|
||
}
|
||
preprocessIgnoreComments(sourceFile); // 预处理注释
|
||
collectAndCheck(sourceFile); // 合并后的收集和检查
|
||
}
|
||
// 过滤掉非异步的 context 调用
|
||
filterAsyncContextCalls();
|
||
// 预分析所有函数的参数处理情况
|
||
for (const [symbol, declaration] of functionDeclarations.entries()) {
|
||
getFunctionParameterHandling(symbol);
|
||
}
|
||
// 标记传播
|
||
propagateContextMarks();
|
||
checkDirectContextCalls(); // 检查直接调用
|
||
checkIndirectCalls(); // 检查间接调用(使用缓存的调用列表)
|
||
return diagnostics;
|
||
}
|
||
const compile = (configPath) => {
|
||
// 读取自定义配置
|
||
const customConfig = readCustomConfig(configPath);
|
||
// 读取 tsconfig.json
|
||
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
||
if (configFile.error) {
|
||
console.error(ts.formatDiagnostic(configFile.error, {
|
||
getCanonicalFileName: (f) => f,
|
||
getCurrentDirectory: process.cwd,
|
||
getNewLine: () => '\n'
|
||
}));
|
||
process.exit(1);
|
||
}
|
||
// 解析配置
|
||
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath));
|
||
if (parsedConfig.errors.length > 0) {
|
||
parsedConfig.errors.forEach((diagnostic) => {
|
||
console.error(ts.formatDiagnostic(diagnostic, {
|
||
getCanonicalFileName: (f) => f,
|
||
getCurrentDirectory: process.cwd,
|
||
getNewLine: () => '\n'
|
||
}));
|
||
});
|
||
process.exit(1);
|
||
}
|
||
// 创建编译程序
|
||
let program;
|
||
if (parsedConfig.options.incremental || parsedConfig.options.composite) {
|
||
const host = ts.createIncrementalCompilerHost(parsedConfig.options);
|
||
const incrementalProgram = ts.createIncrementalProgram({
|
||
rootNames: parsedConfig.fileNames,
|
||
options: parsedConfig.options,
|
||
host: host,
|
||
configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(parsedConfig),
|
||
});
|
||
program = incrementalProgram.getProgram();
|
||
}
|
||
else {
|
||
program = ts.createProgram({
|
||
rootNames: parsedConfig.fileNames,
|
||
options: parsedConfig.options,
|
||
});
|
||
}
|
||
// 获取类型检查器
|
||
const typeChecker = program.getTypeChecker();
|
||
// 执行自定义检查(传入自定义配置)
|
||
const customDiagnostics = performCustomChecks(program, typeChecker, customConfig);
|
||
if (customDiagnostics.length > 0) {
|
||
// 按错误代码分组
|
||
const groupedDiagnostics = new Map();
|
||
customDiagnostics.forEach(diag => {
|
||
if (!groupedDiagnostics.has(diag.code)) {
|
||
groupedDiagnostics.set(diag.code, []);
|
||
}
|
||
groupedDiagnostics.get(diag.code).push(diag);
|
||
});
|
||
// 输出统计
|
||
console.log(`${colors.cyan}发现 ${customDiagnostics.length} 个潜在问题:${colors.reset}`);
|
||
groupedDiagnostics.forEach((diags, code) => {
|
||
const description = code === 9100 ? '直接context调用' : '间接context调用';
|
||
console.log(` • ${description}: ${diags.length} 个`);
|
||
});
|
||
// 输出详细信息
|
||
let index = 0;
|
||
groupedDiagnostics.forEach((diags, code) => {
|
||
const title = code === 9100 ? '直接Context调用问题' : '间接Context调用问题';
|
||
console.log(`\n${colors.yellow}▼ ${title}${colors.reset}`);
|
||
diags.forEach(diag => {
|
||
printDiagnostic(diag, 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`);
|
||
}
|
||
// 执行编译
|
||
const emitResult = program.emit();
|
||
// 获取诊断信息
|
||
const allDiagnostics = [
|
||
...ts.getPreEmitDiagnostics(program),
|
||
...emitResult.diagnostics,
|
||
];
|
||
// 输出诊断信息
|
||
allDiagnostics.forEach(printDiagnostic);
|
||
// 输出编译统计
|
||
const errorCount = allDiagnostics.filter(d => d.category === ts.DiagnosticCategory.Error).length;
|
||
const warningCount = allDiagnostics.filter(d => d.category === ts.DiagnosticCategory.Warning).length;
|
||
if (errorCount > 0 || warningCount > 0) {
|
||
if (allDiagnostics.length > 0) {
|
||
console.log('');
|
||
}
|
||
const parts = [];
|
||
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 configPathArg = parseArgs(args);
|
||
let configPath;
|
||
// 判断参数是目录还是文件
|
||
if (fs.existsSync(configPathArg)) {
|
||
const stat = fs.statSync(configPathArg);
|
||
if (stat.isDirectory()) {
|
||
configPath = path.resolve(configPathArg, 'tsconfig.json');
|
||
}
|
||
else {
|
||
configPath = path.resolve(configPathArg);
|
||
}
|
||
}
|
||
else {
|
||
configPath = path.resolve(pwd, configPathArg);
|
||
if (!fs.existsSync(configPath)) {
|
||
const dirPath = path.resolve(pwd, configPathArg);
|
||
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
||
configPath = path.join(dirPath, 'tsconfig.json');
|
||
}
|
||
}
|
||
}
|
||
if (!fs.existsSync(configPath)) {
|
||
console.error(`error TS5058: The specified path does not exist: '${configPath}'.`);
|
||
process.exit(1);
|
||
}
|
||
compile(configPath);
|
||
};
|
||
exports.build = build;
|