feat: 支持了文件名匹配

This commit is contained in:
Pan Qiancheng 2026-01-06 11:41:30 +08:00
parent e42bfe2337
commit d247b5c405
6 changed files with 881 additions and 161 deletions

View File

@ -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 {};

View File

@ -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) => {
// 执行编译 // 执行编译

30
lib/utils/glob.d.ts vendored Normal file
View File

@ -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;

286
lib/utils/glob.js Normal file
View File

@ -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;

View File

@ -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[]) => {

316
src/utils/glob.ts Normal file
View File

@ -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;
};