feat: 支持了文件名匹配
This commit is contained in:
parent
e42bfe2337
commit
d247b5c405
|
|
@ -1,2 +1,37 @@
|
||||||
|
import * as ts from 'typescript';
|
||||||
export declare const OAK_IGNORE_TAGS: string[];
|
export declare const OAK_IGNORE_TAGS: string[];
|
||||||
|
interface OakBuildChecksConfig {
|
||||||
|
context?: {
|
||||||
|
checkAsyncContext?: boolean;
|
||||||
|
targetModules?: string[];
|
||||||
|
filePatterns?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
interface CustomDiagnostic {
|
||||||
|
file: ts.SourceFile;
|
||||||
|
start: number;
|
||||||
|
length: number;
|
||||||
|
messageText: string;
|
||||||
|
category: ts.DiagnosticCategory;
|
||||||
|
code: number;
|
||||||
|
callChain?: string[];
|
||||||
|
contextCallNode?: ts.CallExpression;
|
||||||
|
reason?: string;
|
||||||
|
reasonDetails?: string[];
|
||||||
|
relatedInfo?: Array<{
|
||||||
|
file: ts.SourceFile;
|
||||||
|
start: number;
|
||||||
|
length: number;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 执行自定义检查
|
||||||
|
* @param program ts.Program
|
||||||
|
* @param typeChecker ts.TypeChecker
|
||||||
|
* @param customConfig 自定义配置
|
||||||
|
* @returns CustomDiagnostic[] 自定义诊断列表
|
||||||
|
*/
|
||||||
|
export declare function performCustomChecks(program: ts.Program, typeChecker: ts.TypeChecker, customConfig: OakBuildChecksConfig): CustomDiagnostic[];
|
||||||
export declare const build: (pwd: string, args: any[]) => void;
|
export declare const build: (pwd: string, args: any[]) => void;
|
||||||
|
export {};
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,26 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.build = exports.OAK_IGNORE_TAGS = void 0;
|
exports.build = exports.OAK_IGNORE_TAGS = void 0;
|
||||||
|
exports.performCustomChecks = performCustomChecks;
|
||||||
const tslib_1 = require("tslib");
|
const tslib_1 = require("tslib");
|
||||||
const ts = tslib_1.__importStar(require("typescript"));
|
const ts = tslib_1.__importStar(require("typescript"));
|
||||||
const path = tslib_1.__importStar(require("path"));
|
const path = tslib_1.__importStar(require("path"));
|
||||||
const fs = tslib_1.__importStar(require("fs"));
|
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_METHODS = ['map', 'forEach', 'filter', 'reduce', 'some', 'every', 'find', 'findIndex', 'flatMap'];
|
||||||
const ARRAY_TRANSFORM_METHODS = ['map', 'filter', 'flatMap'];
|
const ARRAY_TRANSFORM_METHODS = ['map', 'filter', 'flatMap'];
|
||||||
const PROMISE_METHODS = ['then', 'catch', 'finally'];
|
const PROMISE_METHODS = ['then', 'catch', 'finally'];
|
||||||
const PROMISE_STATIC_METHODS = ['all', 'race', 'allSettled', 'any'];
|
const PROMISE_STATIC_METHODS = ['all', 'race', 'allSettled', 'any'];
|
||||||
// 判断是否是函数类声明
|
// 判断是否是函数类声明
|
||||||
const isFunctionLikeDeclaration = (node) => {
|
const isFunctionLikeDeclaration = (node) => {
|
||||||
|
// FunctionDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ConstructorDeclaration | FunctionExpression | ArrowFunction;
|
||||||
return ts.isFunctionDeclaration(node) ||
|
return ts.isFunctionDeclaration(node) ||
|
||||||
|
ts.isMethodDeclaration(node) ||
|
||||||
|
ts.isGetAccessorDeclaration(node) ||
|
||||||
|
ts.isSetAccessorDeclaration(node) ||
|
||||||
|
ts.isConstructorDeclaration(node) ||
|
||||||
ts.isFunctionExpression(node) ||
|
ts.isFunctionExpression(node) ||
|
||||||
ts.isArrowFunction(node) ||
|
ts.isArrowFunction(node);
|
||||||
ts.isMethodDeclaration(node);
|
|
||||||
};
|
};
|
||||||
// 查找最近的函数作用域
|
// 查找最近的函数作用域
|
||||||
const findNearestFunctionScope = (node) => {
|
const findNearestFunctionScope = (node) => {
|
||||||
|
|
@ -84,13 +90,62 @@ const isSymbolReferencedInNode = (node, symbol, typeChecker) => {
|
||||||
});
|
});
|
||||||
return found;
|
return found;
|
||||||
};
|
};
|
||||||
// 检查节点是否是对 Promise 静态方法的调用,且包含指定符号
|
// 辅助函数:检查节点是否是透明包装(括号、类型断言等)
|
||||||
const isSymbolInPromiseStaticCall = (node, symbol, typeChecker) => {
|
const isTransparentWrapper = (node) => {
|
||||||
if (!ts.isCallExpression(node) || !isPromiseStaticMethodCall(node)) {
|
return ts.isParenthesizedExpression(node) ||
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const arg of node.arguments) {
|
return current;
|
||||||
if (isSymbolReferencedInNode(arg, symbol, typeChecker)) {
|
};
|
||||||
|
/**
|
||||||
|
* 检查类型是否是 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 true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +159,22 @@ const isIdentifierWithSymbol = (node, symbol, typeChecker) => {
|
||||||
const nodeSymbol = typeChecker.getSymbolAtLocation(node);
|
const nodeSymbol = typeChecker.getSymbolAtLocation(node);
|
||||||
return nodeSymbol === symbol;
|
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 verboseLogging = false;
|
||||||
const log = verboseLogging
|
const log = verboseLogging
|
||||||
? (...args) => console.log('[tscBuilder]', ...args)
|
? (...args) => console.log('[tscBuilder]', ...args)
|
||||||
|
|
@ -243,6 +314,13 @@ function printDiagnostic(diagnostic, index) {
|
||||||
console.log(`\n${index + 1}. ${categoryColor}${category}${colors.reset} ${colors.gray}TS${diagnostic.code}${colors.reset}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`);
|
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) {
|
function performCustomChecks(program, typeChecker, customConfig) {
|
||||||
const diagnostics = [];
|
const diagnostics = [];
|
||||||
// 如果自定义检查被禁用,直接返回
|
// 如果自定义检查被禁用,直接返回
|
||||||
|
|
@ -605,34 +683,11 @@ function performCustomChecks(program, typeChecker, customConfig) {
|
||||||
const exports = typeChecker.getExportsOfModule(moduleSymbol);
|
const exports = typeChecker.getExportsOfModule(moduleSymbol);
|
||||||
return exports.some(exp => exp === symbol || exp.name === symbol.name);
|
return exports.some(exp => exp === symbol || exp.name === symbol.name);
|
||||||
};
|
};
|
||||||
const isTransparentWrapper = (node) => {
|
/**
|
||||||
return ts.isParenthesizedExpression(node) ||
|
* 检查节点是否被 await 修饰
|
||||||
ts.isAsExpression(node) ||
|
* @param node ts.Node
|
||||||
ts.isTypeAssertionExpression(node) ||
|
* @returns boolean
|
||||||
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;
|
|
||||||
};
|
|
||||||
const isAwaited = (node) => {
|
const isAwaited = (node) => {
|
||||||
let parent = node.parent;
|
let parent = node.parent;
|
||||||
// 向上遍历父节点,查找 await 表达式
|
// 向上遍历父节点,查找 await 表达式
|
||||||
|
|
@ -651,43 +706,13 @@ function performCustomChecks(program, typeChecker, customConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果遇到函数边界,停止向上查找
|
// 如果遇到函数边界,停止向上查找
|
||||||
if (ts.isFunctionDeclaration(parent) ||
|
if (isFunctionLikeDeclaration(parent)) {
|
||||||
ts.isFunctionExpression(parent) ||
|
|
||||||
ts.isArrowFunction(parent) ||
|
|
||||||
ts.isMethodDeclaration(parent)) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
parent = parent.parent;
|
parent = parent.parent;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
const isPromiseType = (type) => {
|
|
||||||
// 检查类型符号
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
const isNodeInSubtree = (root, target) => {
|
const isNodeInSubtree = (root, target) => {
|
||||||
if (root === target) {
|
if (root === target) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -719,7 +744,7 @@ function performCustomChecks(program, typeChecker, customConfig) {
|
||||||
return typeChecker.getSymbolAtLocation(declaration.name);
|
return typeChecker.getSymbolAtLocation(declaration.name);
|
||||||
}
|
}
|
||||||
// 处理箭头函数:尝试从父节点(变量声明)获取符号
|
// 处理箭头函数:尝试从父节点(变量声明)获取符号
|
||||||
if (ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration)) {
|
if (isFunctionLikeDeclaration(declaration)) {
|
||||||
const parent = declaration.parent;
|
const parent = declaration.parent;
|
||||||
if (ts.isVariableDeclaration(parent) && parent.name) {
|
if (ts.isVariableDeclaration(parent) && parent.name) {
|
||||||
return typeChecker.getSymbolAtLocation(parent.name);
|
return typeChecker.getSymbolAtLocation(parent.name);
|
||||||
|
|
@ -736,8 +761,7 @@ function performCustomChecks(program, typeChecker, customConfig) {
|
||||||
const localCallsToCheck = []; // 收集需要检查的调用
|
const localCallsToCheck = []; // 收集需要检查的调用
|
||||||
const visit = (node) => {
|
const visit = (node) => {
|
||||||
// 记录函数声明
|
// 记录函数声明
|
||||||
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) ||
|
if (isFunctionLikeDeclaration(node)) {
|
||||||
ts.isArrowFunction(node) || ts.isMethodDeclaration(node)) {
|
|
||||||
const symbol = getSymbolOfDeclaration(node);
|
const symbol = getSymbolOfDeclaration(node);
|
||||||
if (symbol) {
|
if (symbol) {
|
||||||
functionDeclarations.set(symbol, node);
|
functionDeclarations.set(symbol, node);
|
||||||
|
|
@ -805,7 +829,7 @@ function performCustomChecks(program, typeChecker, customConfig) {
|
||||||
const signature = typeChecker.getResolvedSignature(callNode);
|
const signature = typeChecker.getResolvedSignature(callNode);
|
||||||
if (signature) {
|
if (signature) {
|
||||||
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
||||||
if (isPromiseType(returnType)) {
|
if (isPromiseType(returnType, typeChecker)) {
|
||||||
asyncContextCalls.push(callNode);
|
asyncContextCalls.push(callNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -825,7 +849,7 @@ function performCustomChecks(program, typeChecker, customConfig) {
|
||||||
if (!signature)
|
if (!signature)
|
||||||
return false;
|
return false;
|
||||||
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
||||||
return isPromiseType(returnType);
|
return isPromiseType(returnType, typeChecker);
|
||||||
});
|
});
|
||||||
// 只保留有异步调用的函数
|
// 只保留有异步调用的函数
|
||||||
if (asyncCalls.length > 0) {
|
if (asyncCalls.length > 0) {
|
||||||
|
|
@ -870,7 +894,7 @@ function performCustomChecks(program, typeChecker, customConfig) {
|
||||||
if (!signature)
|
if (!signature)
|
||||||
continue;
|
continue;
|
||||||
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
||||||
if (!isPromiseType(returnType))
|
if (!isPromiseType(returnType, typeChecker))
|
||||||
continue;
|
continue;
|
||||||
// 检查是否直接 await
|
// 检查是否直接 await
|
||||||
const sourceFile = callNode.getSourceFile();
|
const sourceFile = callNode.getSourceFile();
|
||||||
|
|
@ -912,7 +936,7 @@ function performCustomChecks(program, typeChecker, customConfig) {
|
||||||
// 检查是否调用了标记的函数
|
// 检查是否调用了标记的函数
|
||||||
if (functionsWithContextCalls.has(symbol)) {
|
if (functionsWithContextCalls.has(symbol)) {
|
||||||
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
||||||
if (!isPromiseType(returnType))
|
if (!isPromiseType(returnType, typeChecker))
|
||||||
continue;
|
continue;
|
||||||
if (shouldReportUnawaitedCall(node, sourceFile)) {
|
if (shouldReportUnawaitedCall(node, sourceFile)) {
|
||||||
const functionName = getFunctionCallName(node, sourceFile);
|
const functionName = getFunctionCallName(node, sourceFile);
|
||||||
|
|
@ -1505,6 +1529,10 @@ function performCustomChecks(program, typeChecker, customConfig) {
|
||||||
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) {
|
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// 文件名是否符合检查范围
|
||||||
|
if (!isFileInCheckScope(sourceFile.fileName, customConfig)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
preprocessIgnoreComments(sourceFile); // 预处理注释
|
preprocessIgnoreComments(sourceFile); // 预处理注释
|
||||||
collectAndCheck(sourceFile); // 合并后的收集和检查
|
collectAndCheck(sourceFile); // 合并后的收集和检查
|
||||||
}
|
}
|
||||||
|
|
@ -1625,9 +1653,10 @@ const compile = (configPath) => {
|
||||||
console.log(`Found ${parts.join(' and ')}.`);
|
console.log(`Found ${parts.join(' and ')}.`);
|
||||||
}
|
}
|
||||||
if (errorCount > 0) {
|
if (errorCount > 0) {
|
||||||
|
console.log(`${colors.red}Compilation failed due to errors.${colors.reset}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log('Compilation completed successfully.');
|
console.log(`${colors.green}Compilation completed successfully.${colors.reset}`);
|
||||||
};
|
};
|
||||||
const build = (pwd, args) => {
|
const build = (pwd, args) => {
|
||||||
// 执行编译
|
// 执行编译
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* 匹配 glob 模式
|
||||||
|
* @param filePath 文件路径(已标准化)
|
||||||
|
* @param pattern glob 模式
|
||||||
|
*/
|
||||||
|
export declare const matchGlobPattern: (filePath: string, pattern: string) => boolean;
|
||||||
|
/**
|
||||||
|
* 展开花括号模式 {a,b,c}
|
||||||
|
*/
|
||||||
|
export declare const expandBraces: (pattern: string) => string[];
|
||||||
|
/**
|
||||||
|
* 分割花括号选项(处理嵌套)
|
||||||
|
*/
|
||||||
|
export declare const splitBraceOptions: (content: string) => string[];
|
||||||
|
/**
|
||||||
|
* 递归匹配路径段
|
||||||
|
*/
|
||||||
|
export declare const matchSegments: (fileSegs: string[], fileIdx: number, patternSegs: string[], patternIdx: number) => boolean;
|
||||||
|
/**
|
||||||
|
* 匹配单个路径段(处理 *, ?, [] 通配符)
|
||||||
|
*/
|
||||||
|
export declare const matchSegment: (fileSeg: string, patternSeg: string) => boolean;
|
||||||
|
/**
|
||||||
|
* 查找匹配的右括号
|
||||||
|
*/
|
||||||
|
export declare const findClosingBracket: (pattern: string, startIdx: number) => number;
|
||||||
|
/**
|
||||||
|
* 匹配字符类 [abc] [a-z] [!abc]
|
||||||
|
*/
|
||||||
|
export declare const matchCharClass: (char: string, charClass: string) => boolean;
|
||||||
|
|
@ -0,0 +1,286 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.matchCharClass = exports.findClosingBracket = exports.matchSegment = exports.matchSegments = exports.splitBraceOptions = exports.expandBraces = exports.matchGlobPattern = void 0;
|
||||||
|
/**
|
||||||
|
* 匹配 glob 模式
|
||||||
|
* @param filePath 文件路径(已标准化)
|
||||||
|
* @param pattern glob 模式
|
||||||
|
*/
|
||||||
|
const matchGlobPattern = (filePath, pattern) => {
|
||||||
|
// 处理花括号展开 {a,b,c}
|
||||||
|
const expandedPatterns = (0, exports.expandBraces)(pattern);
|
||||||
|
// 只要有一个展开的模式匹配就返回 true
|
||||||
|
for (const expandedPattern of expandedPatterns) {
|
||||||
|
const fileSegments = filePath.split('/');
|
||||||
|
const patternSegments = expandedPattern.split('/');
|
||||||
|
if ((0, exports.matchSegments)(fileSegments, 0, patternSegments, 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
exports.matchGlobPattern = matchGlobPattern;
|
||||||
|
/**
|
||||||
|
* 展开花括号模式 {a,b,c}
|
||||||
|
*/
|
||||||
|
const expandBraces = (pattern) => {
|
||||||
|
const result = [];
|
||||||
|
let current = '';
|
||||||
|
let depth = 0;
|
||||||
|
let braceStart = -1;
|
||||||
|
for (let i = 0; i < pattern.length; i++) {
|
||||||
|
const char = pattern[i];
|
||||||
|
const prevChar = i > 0 ? pattern[i - 1] : '';
|
||||||
|
// 处理转义
|
||||||
|
if (prevChar === '\\') {
|
||||||
|
current += char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '{') {
|
||||||
|
if (depth === 0) {
|
||||||
|
braceStart = current.length;
|
||||||
|
}
|
||||||
|
depth++;
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
else if (char === '}' && depth > 0) {
|
||||||
|
depth--;
|
||||||
|
current += char;
|
||||||
|
if (depth === 0) {
|
||||||
|
// 提取花括号内容
|
||||||
|
const braceContent = current.substring(braceStart + 1, current.length - 1);
|
||||||
|
const options = (0, exports.splitBraceOptions)(braceContent);
|
||||||
|
const prefix = current.substring(0, braceStart);
|
||||||
|
const suffix = pattern.substring(i + 1);
|
||||||
|
// 递归展开每个选项
|
||||||
|
for (const option of options) {
|
||||||
|
const expanded = (0, exports.expandBraces)(prefix + option + suffix);
|
||||||
|
result.push(...expanded);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [pattern];
|
||||||
|
};
|
||||||
|
exports.expandBraces = expandBraces;
|
||||||
|
/**
|
||||||
|
* 分割花括号选项(处理嵌套)
|
||||||
|
*/
|
||||||
|
const splitBraceOptions = (content) => {
|
||||||
|
const options = [];
|
||||||
|
let current = '';
|
||||||
|
let depth = 0;
|
||||||
|
for (let i = 0; i < content.length; i++) {
|
||||||
|
const char = content[i];
|
||||||
|
const prevChar = i > 0 ? content[i - 1] : '';
|
||||||
|
if (prevChar === '\\') {
|
||||||
|
current += char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '{') {
|
||||||
|
depth++;
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
else if (char === '}') {
|
||||||
|
depth--;
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
else if (char === ',' && depth === 0) {
|
||||||
|
options.push(current);
|
||||||
|
current = '';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
options.push(current);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
exports.splitBraceOptions = splitBraceOptions;
|
||||||
|
/**
|
||||||
|
* 递归匹配路径段
|
||||||
|
*/
|
||||||
|
const matchSegments = (fileSegs, fileIdx, patternSegs, patternIdx) => {
|
||||||
|
// 都匹配完了,成功
|
||||||
|
if (fileIdx === fileSegs.length && patternIdx === patternSegs.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// pattern 匹配完了但文件路径还有,失败
|
||||||
|
if (patternIdx === patternSegs.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const patternSeg = patternSegs[patternIdx];
|
||||||
|
// 处理 **
|
||||||
|
if (patternSeg === '**') {
|
||||||
|
// ** 可以匹配 0 到多个目录段
|
||||||
|
// 尝试匹配 0 个(跳过 **)
|
||||||
|
if ((0, exports.matchSegments)(fileSegs, fileIdx, patternSegs, patternIdx + 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 尝试匹配 1 个或多个(消耗一个文件段,** 保持)
|
||||||
|
if (fileIdx < fileSegs.length) {
|
||||||
|
return (0, exports.matchSegments)(fileSegs, fileIdx + 1, patternSegs, patternIdx);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 文件路径匹配完了但 pattern 还有(且不是 **),失败
|
||||||
|
if (fileIdx === fileSegs.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const fileSeg = fileSegs[fileIdx];
|
||||||
|
// 处理普通段(可能包含 *, ?, [])
|
||||||
|
if ((0, exports.matchSegment)(fileSeg, patternSeg)) {
|
||||||
|
return (0, exports.matchSegments)(fileSegs, fileIdx + 1, patternSegs, patternIdx + 1);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
exports.matchSegments = matchSegments;
|
||||||
|
/**
|
||||||
|
* 匹配单个路径段(处理 *, ?, [] 通配符)
|
||||||
|
*/
|
||||||
|
const matchSegment = (fileSeg, patternSeg) => {
|
||||||
|
let fileIdx = 0;
|
||||||
|
let patternIdx = 0;
|
||||||
|
let starIdx = -1;
|
||||||
|
let matchIdx = 0;
|
||||||
|
while (fileIdx < fileSeg.length) {
|
||||||
|
if (patternIdx < patternSeg.length) {
|
||||||
|
const patternChar = patternSeg[patternIdx];
|
||||||
|
const prevChar = patternIdx > 0 ? patternSeg[patternIdx - 1] : '';
|
||||||
|
// 处理转义字符
|
||||||
|
if (prevChar === '\\') {
|
||||||
|
if (fileSeg[fileIdx] === patternChar) {
|
||||||
|
fileIdx++;
|
||||||
|
patternIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (starIdx !== -1) {
|
||||||
|
patternIdx = starIdx + 1;
|
||||||
|
matchIdx++;
|
||||||
|
fileIdx = matchIdx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 跳过转义符本身
|
||||||
|
if (patternChar === '\\') {
|
||||||
|
patternIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 处理 ?
|
||||||
|
if (patternChar === '?') {
|
||||||
|
fileIdx++;
|
||||||
|
patternIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 处理 []
|
||||||
|
if (patternChar === '[') {
|
||||||
|
const closeIdx = (0, exports.findClosingBracket)(patternSeg, patternIdx);
|
||||||
|
if (closeIdx !== -1) {
|
||||||
|
const charClass = patternSeg.substring(patternIdx + 1, closeIdx);
|
||||||
|
if ((0, exports.matchCharClass)(fileSeg[fileIdx], charClass)) {
|
||||||
|
fileIdx++;
|
||||||
|
patternIdx = closeIdx + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (starIdx !== -1) {
|
||||||
|
patternIdx = starIdx + 1;
|
||||||
|
matchIdx++;
|
||||||
|
fileIdx = matchIdx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理 *
|
||||||
|
if (patternChar === '*') {
|
||||||
|
starIdx = patternIdx;
|
||||||
|
matchIdx = fileIdx;
|
||||||
|
patternIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 普通字符匹配
|
||||||
|
if (fileSeg[fileIdx] === patternChar) {
|
||||||
|
fileIdx++;
|
||||||
|
patternIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 不匹配,回溯到上一个 *
|
||||||
|
if (starIdx !== -1) {
|
||||||
|
patternIdx = starIdx + 1;
|
||||||
|
matchIdx++;
|
||||||
|
fileIdx = matchIdx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 处理 pattern 末尾的 * 和转义符
|
||||||
|
while (patternIdx < patternSeg.length) {
|
||||||
|
const char = patternSeg[patternIdx];
|
||||||
|
if (char === '*' || char === '\\') {
|
||||||
|
patternIdx++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patternIdx === patternSeg.length;
|
||||||
|
};
|
||||||
|
exports.matchSegment = matchSegment;
|
||||||
|
/**
|
||||||
|
* 查找匹配的右括号
|
||||||
|
*/
|
||||||
|
const findClosingBracket = (pattern, startIdx) => {
|
||||||
|
for (let i = startIdx + 1; i < pattern.length; i++) {
|
||||||
|
if (pattern[i] === ']' && pattern[i - 1] !== '\\') {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
exports.findClosingBracket = findClosingBracket;
|
||||||
|
/**
|
||||||
|
* 匹配字符类 [abc] [a-z] [!abc]
|
||||||
|
*/
|
||||||
|
const matchCharClass = (char, charClass) => {
|
||||||
|
// 处理否定 [!...] 或 [^...]
|
||||||
|
const isNegated = charClass[0] === '!' || charClass[0] === '^';
|
||||||
|
const classContent = isNegated ? charClass.substring(1) : charClass;
|
||||||
|
let matched = false;
|
||||||
|
let i = 0;
|
||||||
|
while (i < classContent.length) {
|
||||||
|
const currentChar = classContent[i];
|
||||||
|
// 处理范围 a-z
|
||||||
|
if (i + 2 < classContent.length && classContent[i + 1] === '-') {
|
||||||
|
const start = currentChar.charCodeAt(0);
|
||||||
|
const end = classContent[i + 2].charCodeAt(0);
|
||||||
|
const charCode = char.charCodeAt(0);
|
||||||
|
if (charCode >= start && charCode <= end) {
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 3;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 单个字符
|
||||||
|
if (char === currentChar) {
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isNegated ? !matched : matched;
|
||||||
|
};
|
||||||
|
exports.matchCharClass = matchCharClass;
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import { matchGlobPattern } from '../utils/glob';
|
||||||
|
|
||||||
const ARRAY_METHODS = ['map', 'forEach', 'filter', 'reduce', 'some', 'every', 'find', 'findIndex', 'flatMap'];
|
const ARRAY_METHODS = ['map', 'forEach', 'filter', 'reduce', 'some', 'every', 'find', 'findIndex', 'flatMap'];
|
||||||
const ARRAY_TRANSFORM_METHODS = ['map', 'filter', 'flatMap'];
|
const ARRAY_TRANSFORM_METHODS = ['map', 'filter', 'flatMap'];
|
||||||
|
|
@ -9,10 +10,14 @@ const PROMISE_STATIC_METHODS = ['all', 'race', 'allSettled', 'any'];
|
||||||
|
|
||||||
// 判断是否是函数类声明
|
// 判断是否是函数类声明
|
||||||
const isFunctionLikeDeclaration = (node: ts.Node): node is ts.FunctionLikeDeclaration => {
|
const isFunctionLikeDeclaration = (node: ts.Node): node is ts.FunctionLikeDeclaration => {
|
||||||
|
// FunctionDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ConstructorDeclaration | FunctionExpression | ArrowFunction;
|
||||||
return ts.isFunctionDeclaration(node) ||
|
return ts.isFunctionDeclaration(node) ||
|
||||||
|
ts.isMethodDeclaration(node) ||
|
||||||
|
ts.isGetAccessorDeclaration(node) ||
|
||||||
|
ts.isSetAccessorDeclaration(node) ||
|
||||||
|
ts.isConstructorDeclaration(node) ||
|
||||||
ts.isFunctionExpression(node) ||
|
ts.isFunctionExpression(node) ||
|
||||||
ts.isArrowFunction(node) ||
|
ts.isArrowFunction(node);
|
||||||
ts.isMethodDeclaration(node);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 查找最近的函数作用域
|
// 查找最近的函数作用域
|
||||||
|
|
@ -92,24 +97,70 @@ const isSymbolReferencedInNode = (node: ts.Node, symbol: ts.Symbol, typeChecker:
|
||||||
return found;
|
return found;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查节点是否是对 Promise 静态方法的调用,且包含指定符号
|
// 辅助函数:检查节点是否是透明包装(括号、类型断言等)
|
||||||
const isSymbolInPromiseStaticCall = (
|
const isTransparentWrapper = (node: ts.Node): boolean => {
|
||||||
node: ts.Node,
|
return ts.isParenthesizedExpression(node) ||
|
||||||
symbol: ts.Symbol,
|
ts.isAsExpression(node) ||
|
||||||
typeChecker: ts.TypeChecker
|
ts.isTypeAssertionExpression(node) ||
|
||||||
): boolean => {
|
ts.isNonNullExpression(node);
|
||||||
if (!ts.isCallExpression(node) || !isPromiseStaticMethodCall(node)) {
|
};
|
||||||
return false;
|
|
||||||
|
// 辅助函数:获取去除透明包装后的实际节点
|
||||||
|
const unwrapTransparentWrappers = (node: ts.Node): ts.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: ts.Type, typeChecker: ts.TypeChecker): boolean => {
|
||||||
|
// 检查类型符号
|
||||||
|
const symbol = type.getSymbol();
|
||||||
|
if (symbol) {
|
||||||
|
const name = symbol.getName();
|
||||||
|
if (name === 'Promise') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const arg of node.arguments) {
|
// 检查类型字符串表示
|
||||||
if (isSymbolReferencedInNode(arg, symbol, typeChecker)) {
|
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 true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
// 类型守卫:检查节点是否是标识符且匹配指定符号
|
// 类型守卫:检查节点是否是标识符且匹配指定符号
|
||||||
const isIdentifierWithSymbol = (
|
const isIdentifierWithSymbol = (
|
||||||
|
|
@ -124,6 +175,23 @@ const isIdentifierWithSymbol = (
|
||||||
return nodeSymbol === symbol;
|
return nodeSymbol === symbol;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断文件是否在检查范围内
|
||||||
|
* @param fileName 文件完整路径
|
||||||
|
* @param customConfig 自定义配置
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
const isFileInCheckScope = (fileName: string, customConfig: OakBuildChecksConfig): boolean => {
|
||||||
|
const patterns = customConfig.context?.filePatterns || ['**/*.ts', '**/*.tsx'];
|
||||||
|
const normalizedFileName = path.normalize(path.relative(process.cwd(), fileName)).replace(/\\/g, '/');
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
if (matchGlobPattern(normalizedFileName, pattern.replace(/\\/g, '/'))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const verboseLogging = false;
|
const verboseLogging = false;
|
||||||
|
|
||||||
const log = verboseLogging
|
const log = verboseLogging
|
||||||
|
|
@ -148,6 +216,7 @@ interface OakBuildChecksConfig {
|
||||||
context?: {
|
context?: {
|
||||||
checkAsyncContext?: boolean; // 是否启用 AsyncContext 检查,默认启用
|
checkAsyncContext?: boolean; // 是否启用 AsyncContext 检查,默认启用
|
||||||
targetModules?: string[]; // 目标模块列表,默认 ['@project/context/BackendRuntimeContext']
|
targetModules?: string[]; // 目标模块列表,默认 ['@project/context/BackendRuntimeContext']
|
||||||
|
filePatterns?: string[]; // 需要检查的文件路径模式,默认 ['**/*.ts', '**/*.tsx']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -262,7 +331,7 @@ function printDiagnostic(diagnostic: ts.Diagnostic | CustomDiagnostic, index: nu
|
||||||
const lineStart = sourceFile.getPositionOfLineAndCharacter(line, 0);
|
const lineStart = sourceFile.getPositionOfLineAndCharacter(line, 0);
|
||||||
const lineEnd = sourceFile.getPositionOfLineAndCharacter(line + 1, 0);
|
const lineEnd = sourceFile.getPositionOfLineAndCharacter(line + 1, 0);
|
||||||
let lineText = sourceFile.text.substring(lineStart, lineEnd).trimEnd();
|
let lineText = sourceFile.text.substring(lineStart, lineEnd).trimEnd();
|
||||||
|
|
||||||
// 确保只显示单行内容,去除换行符
|
// 确保只显示单行内容,去除换行符
|
||||||
const newlineIndex = lineText.indexOf('\n');
|
const newlineIndex = lineText.indexOf('\n');
|
||||||
if (newlineIndex !== -1) {
|
if (newlineIndex !== -1) {
|
||||||
|
|
@ -278,7 +347,7 @@ function printDiagnostic(diagnostic: ts.Diagnostic | CustomDiagnostic, index: nu
|
||||||
|
|
||||||
// 计算实际显示的文本长度(不包括颜色代码)
|
// 计算实际显示的文本长度(不包括颜色代码)
|
||||||
const actualDisplayLength = Math.min(lineText.length, maxDisplayLength);
|
const actualDisplayLength = Math.min(lineText.length, maxDisplayLength);
|
||||||
|
|
||||||
// 调整错误标记的显示位置和长度
|
// 调整错误标记的显示位置和长度
|
||||||
const effectiveCharacter = Math.min(character, actualDisplayLength);
|
const effectiveCharacter = Math.min(character, actualDisplayLength);
|
||||||
const maxPossibleLength = actualDisplayLength - effectiveCharacter;
|
const maxPossibleLength = actualDisplayLength - effectiveCharacter;
|
||||||
|
|
@ -351,7 +420,14 @@ function printDiagnostic(diagnostic: ts.Diagnostic | CustomDiagnostic, index: nu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function performCustomChecks(
|
/**
|
||||||
|
* 执行自定义检查
|
||||||
|
* @param program ts.Program
|
||||||
|
* @param typeChecker ts.TypeChecker
|
||||||
|
* @param customConfig 自定义配置
|
||||||
|
* @returns CustomDiagnostic[] 自定义诊断列表
|
||||||
|
*/
|
||||||
|
export function performCustomChecks(
|
||||||
program: ts.Program,
|
program: ts.Program,
|
||||||
typeChecker: ts.TypeChecker,
|
typeChecker: ts.TypeChecker,
|
||||||
customConfig: OakBuildChecksConfig
|
customConfig: OakBuildChecksConfig
|
||||||
|
|
@ -796,32 +872,11 @@ function performCustomChecks(
|
||||||
return exports.some(exp => exp === symbol || exp.name === symbol.name);
|
return exports.some(exp => exp === symbol || exp.name === symbol.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTransparentWrapper = (node: ts.Node): boolean => {
|
/**
|
||||||
return ts.isParenthesizedExpression(node) ||
|
* 检查节点是否被 await 修饰
|
||||||
ts.isAsExpression(node) ||
|
* @param node ts.Node
|
||||||
ts.isTypeAssertionExpression(node) ||
|
* @returns boolean
|
||||||
ts.isNonNullExpression(node);
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:获取去除透明包装后的实际节点
|
|
||||||
const unwrapTransparentWrappers = (node: ts.Node): ts.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAwaited = (node: ts.Node): boolean => {
|
const isAwaited = (node: ts.Node): boolean => {
|
||||||
let parent = node.parent;
|
let parent = node.parent;
|
||||||
|
|
||||||
|
|
@ -844,10 +899,7 @@ function performCustomChecks(
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果遇到函数边界,停止向上查找
|
// 如果遇到函数边界,停止向上查找
|
||||||
if (ts.isFunctionDeclaration(parent) ||
|
if (isFunctionLikeDeclaration(parent)) {
|
||||||
ts.isFunctionExpression(parent) ||
|
|
||||||
ts.isArrowFunction(parent) ||
|
|
||||||
ts.isMethodDeclaration(parent)) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -857,38 +909,6 @@ function performCustomChecks(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNodeInSubtree = (root: ts.Node, target: ts.Node): boolean => {
|
const isNodeInSubtree = (root: ts.Node, target: ts.Node): boolean => {
|
||||||
if (root === target) {
|
if (root === target) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -923,7 +943,7 @@ function performCustomChecks(
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理箭头函数:尝试从父节点(变量声明)获取符号
|
// 处理箭头函数:尝试从父节点(变量声明)获取符号
|
||||||
if (ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration)) {
|
if (isFunctionLikeDeclaration(declaration)) {
|
||||||
const parent = declaration.parent;
|
const parent = declaration.parent;
|
||||||
if (ts.isVariableDeclaration(parent) && parent.name) {
|
if (ts.isVariableDeclaration(parent) && parent.name) {
|
||||||
return typeChecker.getSymbolAtLocation(parent.name);
|
return typeChecker.getSymbolAtLocation(parent.name);
|
||||||
|
|
@ -943,8 +963,7 @@ function performCustomChecks(
|
||||||
|
|
||||||
const visit = (node: ts.Node): void => {
|
const visit = (node: ts.Node): void => {
|
||||||
// 记录函数声明
|
// 记录函数声明
|
||||||
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) ||
|
if (isFunctionLikeDeclaration(node)) {
|
||||||
ts.isArrowFunction(node) || ts.isMethodDeclaration(node)) {
|
|
||||||
const symbol = getSymbolOfDeclaration(node as ts.SignatureDeclaration);
|
const symbol = getSymbolOfDeclaration(node as ts.SignatureDeclaration);
|
||||||
if (symbol) {
|
if (symbol) {
|
||||||
functionDeclarations.set(symbol, node as ts.FunctionLikeDeclaration);
|
functionDeclarations.set(symbol, node as ts.FunctionLikeDeclaration);
|
||||||
|
|
@ -1021,7 +1040,7 @@ function performCustomChecks(
|
||||||
const signature = typeChecker.getResolvedSignature(callNode);
|
const signature = typeChecker.getResolvedSignature(callNode);
|
||||||
if (signature) {
|
if (signature) {
|
||||||
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
||||||
if (isPromiseType(returnType)) {
|
if (isPromiseType(returnType, typeChecker)) {
|
||||||
asyncContextCalls.push(callNode);
|
asyncContextCalls.push(callNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1042,7 +1061,7 @@ function performCustomChecks(
|
||||||
const signature = typeChecker.getResolvedSignature(callNode);
|
const signature = typeChecker.getResolvedSignature(callNode);
|
||||||
if (!signature) return false;
|
if (!signature) return false;
|
||||||
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
||||||
return isPromiseType(returnType);
|
return isPromiseType(returnType, typeChecker);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 只保留有异步调用的函数
|
// 只保留有异步调用的函数
|
||||||
|
|
@ -1092,7 +1111,7 @@ function performCustomChecks(
|
||||||
if (!signature) continue;
|
if (!signature) continue;
|
||||||
|
|
||||||
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
||||||
if (!isPromiseType(returnType)) continue;
|
if (!isPromiseType(returnType, typeChecker)) continue;
|
||||||
|
|
||||||
// 检查是否直接 await
|
// 检查是否直接 await
|
||||||
const sourceFile = callNode.getSourceFile();
|
const sourceFile = callNode.getSourceFile();
|
||||||
|
|
@ -1142,7 +1161,7 @@ function performCustomChecks(
|
||||||
// 检查是否调用了标记的函数
|
// 检查是否调用了标记的函数
|
||||||
if (functionsWithContextCalls.has(symbol)) {
|
if (functionsWithContextCalls.has(symbol)) {
|
||||||
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
||||||
if (!isPromiseType(returnType)) continue;
|
if (!isPromiseType(returnType, typeChecker)) continue;
|
||||||
|
|
||||||
if (shouldReportUnawaitedCall(node, sourceFile)) {
|
if (shouldReportUnawaitedCall(node, sourceFile)) {
|
||||||
const functionName = getFunctionCallName(node, sourceFile);
|
const functionName = getFunctionCallName(node, sourceFile);
|
||||||
|
|
@ -1854,6 +1873,10 @@ function performCustomChecks(
|
||||||
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) {
|
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// 文件名是否符合检查范围
|
||||||
|
if (!isFileInCheckScope(sourceFile.fileName, customConfig)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
preprocessIgnoreComments(sourceFile); // 预处理注释
|
preprocessIgnoreComments(sourceFile); // 预处理注释
|
||||||
collectAndCheck(sourceFile); // 合并后的收集和检查
|
collectAndCheck(sourceFile); // 合并后的收集和检查
|
||||||
}
|
}
|
||||||
|
|
@ -2001,10 +2024,11 @@ const compile = (configPath: string): void => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorCount > 0) {
|
if (errorCount > 0) {
|
||||||
|
console.log(`${colors.red}Compilation failed due to errors.${colors.reset}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Compilation completed successfully.');
|
console.log(`${colors.green}Compilation completed successfully.${colors.reset}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const build = (pwd: string, args: any[]) => {
|
export const build = (pwd: string, args: any[]) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,316 @@
|
||||||
|
/**
|
||||||
|
* 匹配 glob 模式
|
||||||
|
* @param filePath 文件路径(已标准化)
|
||||||
|
* @param pattern glob 模式
|
||||||
|
*/
|
||||||
|
export const matchGlobPattern = (filePath: string, pattern: string): boolean => {
|
||||||
|
// 处理花括号展开 {a,b,c}
|
||||||
|
const expandedPatterns = expandBraces(pattern);
|
||||||
|
|
||||||
|
// 只要有一个展开的模式匹配就返回 true
|
||||||
|
for (const expandedPattern of expandedPatterns) {
|
||||||
|
const fileSegments = filePath.split('/');
|
||||||
|
const patternSegments = expandedPattern.split('/');
|
||||||
|
|
||||||
|
if (matchSegments(fileSegments, 0, patternSegments, 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展开花括号模式 {a,b,c}
|
||||||
|
*/
|
||||||
|
export const expandBraces = (pattern: string): string[] => {
|
||||||
|
const result: string[] = [];
|
||||||
|
let current = '';
|
||||||
|
let depth = 0;
|
||||||
|
let braceStart = -1;
|
||||||
|
|
||||||
|
for (let i = 0; i < pattern.length; i++) {
|
||||||
|
const char = pattern[i];
|
||||||
|
const prevChar = i > 0 ? pattern[i - 1] : '';
|
||||||
|
|
||||||
|
// 处理转义
|
||||||
|
if (prevChar === '\\') {
|
||||||
|
current += char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '{') {
|
||||||
|
if (depth === 0) {
|
||||||
|
braceStart = current.length;
|
||||||
|
}
|
||||||
|
depth++;
|
||||||
|
current += char;
|
||||||
|
} else if (char === '}' && depth > 0) {
|
||||||
|
depth--;
|
||||||
|
current += char;
|
||||||
|
|
||||||
|
if (depth === 0) {
|
||||||
|
// 提取花括号内容
|
||||||
|
const braceContent = current.substring(braceStart + 1, current.length - 1);
|
||||||
|
const options = splitBraceOptions(braceContent);
|
||||||
|
const prefix = current.substring(0, braceStart);
|
||||||
|
const suffix = pattern.substring(i + 1);
|
||||||
|
|
||||||
|
// 递归展开每个选项
|
||||||
|
for (const option of options) {
|
||||||
|
const expanded = expandBraces(prefix + option + suffix);
|
||||||
|
result.push(...expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pattern];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分割花括号选项(处理嵌套)
|
||||||
|
*/
|
||||||
|
export const splitBraceOptions = (content: string): string[] => {
|
||||||
|
const options: string[] = [];
|
||||||
|
let current = '';
|
||||||
|
let depth = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < content.length; i++) {
|
||||||
|
const char = content[i];
|
||||||
|
const prevChar = i > 0 ? content[i - 1] : '';
|
||||||
|
|
||||||
|
if (prevChar === '\\') {
|
||||||
|
current += char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '{') {
|
||||||
|
depth++;
|
||||||
|
current += char;
|
||||||
|
} else if (char === '}') {
|
||||||
|
depth--;
|
||||||
|
current += char;
|
||||||
|
} else if (char === ',' && depth === 0) {
|
||||||
|
options.push(current);
|
||||||
|
current = '';
|
||||||
|
} else {
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
options.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归匹配路径段
|
||||||
|
*/
|
||||||
|
export const matchSegments = (
|
||||||
|
fileSegs: string[],
|
||||||
|
fileIdx: number,
|
||||||
|
patternSegs: string[],
|
||||||
|
patternIdx: number
|
||||||
|
): boolean => {
|
||||||
|
// 都匹配完了,成功
|
||||||
|
if (fileIdx === fileSegs.length && patternIdx === patternSegs.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pattern 匹配完了但文件路径还有,失败
|
||||||
|
if (patternIdx === patternSegs.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patternSeg = patternSegs[patternIdx];
|
||||||
|
|
||||||
|
// 处理 **
|
||||||
|
if (patternSeg === '**') {
|
||||||
|
// ** 可以匹配 0 到多个目录段
|
||||||
|
// 尝试匹配 0 个(跳过 **)
|
||||||
|
if (matchSegments(fileSegs, fileIdx, patternSegs, patternIdx + 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 尝试匹配 1 个或多个(消耗一个文件段,** 保持)
|
||||||
|
if (fileIdx < fileSegs.length) {
|
||||||
|
return matchSegments(fileSegs, fileIdx + 1, patternSegs, patternIdx);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件路径匹配完了但 pattern 还有(且不是 **),失败
|
||||||
|
if (fileIdx === fileSegs.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileSeg = fileSegs[fileIdx];
|
||||||
|
|
||||||
|
// 处理普通段(可能包含 *, ?, [])
|
||||||
|
if (matchSegment(fileSeg, patternSeg)) {
|
||||||
|
return matchSegments(fileSegs, fileIdx + 1, patternSegs, patternIdx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配单个路径段(处理 *, ?, [] 通配符)
|
||||||
|
*/
|
||||||
|
export const matchSegment = (fileSeg: string, patternSeg: string): boolean => {
|
||||||
|
let fileIdx = 0;
|
||||||
|
let patternIdx = 0;
|
||||||
|
let starIdx = -1;
|
||||||
|
let matchIdx = 0;
|
||||||
|
|
||||||
|
while (fileIdx < fileSeg.length) {
|
||||||
|
if (patternIdx < patternSeg.length) {
|
||||||
|
const patternChar = patternSeg[patternIdx];
|
||||||
|
const prevChar = patternIdx > 0 ? patternSeg[patternIdx - 1] : '';
|
||||||
|
|
||||||
|
// 处理转义字符
|
||||||
|
if (prevChar === '\\') {
|
||||||
|
if (fileSeg[fileIdx] === patternChar) {
|
||||||
|
fileIdx++;
|
||||||
|
patternIdx++;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
if (starIdx !== -1) {
|
||||||
|
patternIdx = starIdx + 1;
|
||||||
|
matchIdx++;
|
||||||
|
fileIdx = matchIdx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过转义符本身
|
||||||
|
if (patternChar === '\\') {
|
||||||
|
patternIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 ?
|
||||||
|
if (patternChar === '?') {
|
||||||
|
fileIdx++;
|
||||||
|
patternIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 []
|
||||||
|
if (patternChar === '[') {
|
||||||
|
const closeIdx = findClosingBracket(patternSeg, patternIdx);
|
||||||
|
if (closeIdx !== -1) {
|
||||||
|
const charClass = patternSeg.substring(patternIdx + 1, closeIdx);
|
||||||
|
if (matchCharClass(fileSeg[fileIdx], charClass)) {
|
||||||
|
fileIdx++;
|
||||||
|
patternIdx = closeIdx + 1;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
if (starIdx !== -1) {
|
||||||
|
patternIdx = starIdx + 1;
|
||||||
|
matchIdx++;
|
||||||
|
fileIdx = matchIdx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 *
|
||||||
|
if (patternChar === '*') {
|
||||||
|
starIdx = patternIdx;
|
||||||
|
matchIdx = fileIdx;
|
||||||
|
patternIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通字符匹配
|
||||||
|
if (fileSeg[fileIdx] === patternChar) {
|
||||||
|
fileIdx++;
|
||||||
|
patternIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不匹配,回溯到上一个 *
|
||||||
|
if (starIdx !== -1) {
|
||||||
|
patternIdx = starIdx + 1;
|
||||||
|
matchIdx++;
|
||||||
|
fileIdx = matchIdx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 pattern 末尾的 * 和转义符
|
||||||
|
while (patternIdx < patternSeg.length) {
|
||||||
|
const char = patternSeg[patternIdx];
|
||||||
|
if (char === '*' || char === '\\') {
|
||||||
|
patternIdx++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return patternIdx === patternSeg.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找匹配的右括号
|
||||||
|
*/
|
||||||
|
export const findClosingBracket = (pattern: string, startIdx: number): number => {
|
||||||
|
for (let i = startIdx + 1; i < pattern.length; i++) {
|
||||||
|
if (pattern[i] === ']' && pattern[i - 1] !== '\\') {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配字符类 [abc] [a-z] [!abc]
|
||||||
|
*/
|
||||||
|
export const matchCharClass = (char: string, charClass: string): boolean => {
|
||||||
|
// 处理否定 [!...] 或 [^...]
|
||||||
|
const isNegated = charClass[0] === '!' || charClass[0] === '^';
|
||||||
|
const classContent = isNegated ? charClass.substring(1) : charClass;
|
||||||
|
|
||||||
|
let matched = false;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < classContent.length) {
|
||||||
|
const currentChar = classContent[i];
|
||||||
|
|
||||||
|
// 处理范围 a-z
|
||||||
|
if (i + 2 < classContent.length && classContent[i + 1] === '-') {
|
||||||
|
const start = currentChar.charCodeAt(0);
|
||||||
|
const end = classContent[i + 2].charCodeAt(0);
|
||||||
|
const charCode = char.charCodeAt(0);
|
||||||
|
|
||||||
|
if (charCode >= start && charCode <= end) {
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 3;
|
||||||
|
} else {
|
||||||
|
// 单个字符
|
||||||
|
if (char === currentChar) {
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isNegated ? !matched : matched;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue