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