oak-domain 5.1.32 → 5.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2823 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.build = exports.OAK_IGNORE_TAGS = void 0;
4
+ exports.performCustomChecks = performCustomChecks;
5
+ const tslib_1 = require("tslib");
6
+ const ts = tslib_1.__importStar(require("typescript"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const fs = tslib_1.__importStar(require("fs"));
9
+ const glob_1 = require("../utils/glob");
10
+ const identifier_1 = require("./identifier");
11
+ const lodash_1 = require("lodash");
12
+ const ARRAY_METHODS = ['map', 'forEach', 'filter', 'reduce', 'some', 'every', 'find', 'findIndex', 'flatMap'];
13
+ const ARRAY_TRANSFORM_METHODS = ['map', 'filter', 'flatMap'];
14
+ const PROMISE_METHODS = ['then', 'catch', 'finally'];
15
+ const PROMISE_STATIC_METHODS = ['all', 'race', 'allSettled', 'any'];
16
+ const LOCALE_FILE_NAMES = ['zh_CN.json', 'zh-CN.json'];
17
+ // 需要忽略的 i18n key 字段模式
18
+ const IGNORED_I18N_KEY_PATTERNS = {
19
+ startsWith: ['$', '$$'], // 以 $ 或 $$ 开头的字段
20
+ endsWith: ['Id', 'id'], // 以 Id 或 id 结尾的字段
21
+ exact: ['id', 'seq', 'createAt', 'updateAt', 'deleteAt'] // 精确匹配的字段名
22
+ };
23
+ // 判断是否是函数类声明
24
+ const isFunctionLikeDeclaration = (node) => {
25
+ // FunctionDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ConstructorDeclaration | FunctionExpression | ArrowFunction;
26
+ return ts.isFunctionDeclaration(node) ||
27
+ ts.isMethodDeclaration(node) ||
28
+ ts.isGetAccessorDeclaration(node) ||
29
+ ts.isSetAccessorDeclaration(node) ||
30
+ ts.isConstructorDeclaration(node) ||
31
+ ts.isFunctionExpression(node) ||
32
+ ts.isArrowFunction(node);
33
+ };
34
+ // 查找最近的函数作用域
35
+ const findNearestFunctionScope = (node) => {
36
+ let current = node;
37
+ while (current && !isFunctionLikeDeclaration(current) && !ts.isSourceFile(current)) {
38
+ current = current.parent;
39
+ }
40
+ return current && !ts.isSourceFile(current) ? current : undefined;
41
+ };
42
+ // 查找最近的函数或块作用域
43
+ const findNearestScope = (node) => {
44
+ let current = node;
45
+ while (current &&
46
+ !isFunctionLikeDeclaration(current) &&
47
+ !ts.isSourceFile(current) &&
48
+ !ts.isBlock(current)) {
49
+ current = current.parent;
50
+ }
51
+ return current;
52
+ };
53
+ // 判断是否是 Promise 静态方法调用
54
+ const isPromiseStaticMethodCall = (node) => {
55
+ if (!ts.isPropertyAccessExpression(node.expression)) {
56
+ return false;
57
+ }
58
+ const object = node.expression.expression;
59
+ if (!ts.isIdentifier(object) || object.text !== 'Promise') {
60
+ return false;
61
+ }
62
+ const methodName = node.expression.name.text;
63
+ return PROMISE_STATIC_METHODS.includes(methodName);
64
+ };
65
+ // 判断是否是数组方法调用
66
+ const isArrayMethodCall = (node, methodNames) => {
67
+ if (!ts.isPropertyAccessExpression(node.expression)) {
68
+ return false;
69
+ }
70
+ const methodName = node.expression.name.text;
71
+ return methodNames.includes(methodName);
72
+ };
73
+ // 检查节点是否在数组方法回调中
74
+ const isInArrayMethodCallback = (node, methodNames = ARRAY_METHODS) => {
75
+ let current = node;
76
+ while (current) {
77
+ if (isFunctionLikeDeclaration(current)) {
78
+ const parent = current.parent;
79
+ if (ts.isCallExpression(parent) && isArrayMethodCall(parent, methodNames)) {
80
+ return true;
81
+ }
82
+ break;
83
+ }
84
+ current = current.parent;
85
+ }
86
+ return false;
87
+ };
88
+ // 检查变量是否在节点中被引用
89
+ const isSymbolReferencedInNode = (node, symbol, typeChecker) => {
90
+ if (ts.isIdentifier(node)) {
91
+ const nodeSymbol = typeChecker.getSymbolAtLocation(node);
92
+ return nodeSymbol === symbol;
93
+ }
94
+ let found = false;
95
+ ts.forEachChild(node, (child) => {
96
+ if (!found && isSymbolReferencedInNode(child, symbol, typeChecker)) {
97
+ found = true;
98
+ }
99
+ });
100
+ return found;
101
+ };
102
+ // 辅助函数:检查节点是否是透明包装(括号、类型断言等)
103
+ const isTransparentWrapper = (node) => {
104
+ return ts.isParenthesizedExpression(node) ||
105
+ ts.isAsExpression(node) ||
106
+ ts.isTypeAssertionExpression(node) ||
107
+ ts.isNonNullExpression(node);
108
+ };
109
+ // 辅助函数:获取去除透明包装后的实际节点
110
+ const unwrapTransparentWrappers = (node) => {
111
+ let current = node;
112
+ while (isTransparentWrapper(current)) {
113
+ if (ts.isParenthesizedExpression(current)) {
114
+ current = current.expression;
115
+ }
116
+ else if (ts.isAsExpression(current)) {
117
+ current = current.expression;
118
+ }
119
+ else if (ts.isTypeAssertionExpression(current)) {
120
+ current = current.expression;
121
+ }
122
+ else if (ts.isNonNullExpression(current)) {
123
+ current = current.expression;
124
+ }
125
+ else {
126
+ break;
127
+ }
128
+ }
129
+ return current;
130
+ };
131
+ /**
132
+ * 检查类型是否是 Promise 类型
133
+ * @param type ts.Type
134
+ * @returns boolean
135
+ */
136
+ const isPromiseType = (type, typeChecker) => {
137
+ // 检查类型符号
138
+ const symbol = type.getSymbol();
139
+ if (symbol) {
140
+ const name = symbol.getName();
141
+ if (name === 'Promise') {
142
+ return true;
143
+ }
144
+ }
145
+ // 检查类型字符串表示
146
+ const typeString = typeChecker.typeToString(type);
147
+ if (typeString.startsWith('Promise<') || typeString === 'Promise') {
148
+ return true;
149
+ }
150
+ // 检查联合类型(例如 Promise<T> | undefined)
151
+ if (type.isUnion()) {
152
+ return type.types.some(t => isPromiseType(t, typeChecker));
153
+ }
154
+ // 检查基类型
155
+ const baseTypes = type.getBaseTypes?.() || [];
156
+ for (const baseType of baseTypes) {
157
+ if (isPromiseType(baseType, typeChecker)) {
158
+ return true;
159
+ }
160
+ }
161
+ return false;
162
+ };
163
+ // 类型守卫:检查节点是否是标识符且匹配指定符号
164
+ const isIdentifierWithSymbol = (node, symbol, typeChecker) => {
165
+ if (!ts.isIdentifier(node)) {
166
+ return false;
167
+ }
168
+ const nodeSymbol = typeChecker.getSymbolAtLocation(node);
169
+ return nodeSymbol === symbol;
170
+ };
171
+ /**
172
+ * 判断文件是否在检查范围内
173
+ * @param fileName 文件完整路径
174
+ * @param customConfig 自定义配置
175
+ * @returns boolean
176
+ */
177
+ const isFileInCheckScope = (pwd, fileName, customConfig) => {
178
+ const patterns = customConfig.context?.filePatterns || ['**/*.ts', '**/*.tsx'];
179
+ const normalizedFileName = path.normalize(path.relative(pwd, fileName)).replace(/\\/g, '/');
180
+ for (const pattern of patterns) {
181
+ if ((0, glob_1.matchGlobPattern)(normalizedFileName, pattern.replace(/\\/g, '/'))) {
182
+ return true;
183
+ }
184
+ }
185
+ return false;
186
+ };
187
+ const verboseLogging = false;
188
+ const log = verboseLogging
189
+ ? (...args) => console.log('[tscBuilder]', ...args)
190
+ : () => { }; // 空函数,避免参数计算开销
191
+ exports.OAK_IGNORE_TAGS = [
192
+ '@oak-ignore',
193
+ '@oak-ignore-asynccontext',
194
+ ];
195
+ // ANSI 颜色代码
196
+ const colors = {
197
+ reset: '\x1b[0m',
198
+ cyan: '\x1b[36m',
199
+ red: '\x1b[91m',
200
+ yellow: '\x1b[93m',
201
+ gray: '\x1b[90m',
202
+ green: '\x1b[92m',
203
+ };
204
+ // 解析命令行参数
205
+ function parseArgs(pargs) {
206
+ const args = pargs.slice(2);
207
+ const options = {
208
+ project: 'tsconfig.json',
209
+ noEmit: false
210
+ };
211
+ for (let i = 0; i < args.length; i++) {
212
+ if (args[i] === '-p' || args[i] === '--project') {
213
+ if (i + 1 < args.length) {
214
+ options.project = args[i + 1];
215
+ break;
216
+ }
217
+ else {
218
+ console.error('error: option \'-p, --project\' argument missing');
219
+ process.exit(1);
220
+ }
221
+ }
222
+ else if (args[i] === '--noEmit') {
223
+ options.noEmit = true;
224
+ }
225
+ }
226
+ return options;
227
+ }
228
+ const getContextLocationText = (pwd, callNode) => {
229
+ const sourceFile = callNode.getSourceFile();
230
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(callNode.getStart());
231
+ // const callText = callNode.getText(sourceFile);
232
+ // return `${callText}@${path.relative(process.cwd(), sourceFile.fileName)}:${line + 1}:${character + 1}`;
233
+ return `${path.relative(pwd, sourceFile.fileName)}:${line + 1}:${character + 1}`;
234
+ };
235
+ function printDiagnostic(pwd, diagnostic, index) {
236
+ const isCustom = 'callChain' in diagnostic;
237
+ if (diagnostic.file && diagnostic.start !== undefined) {
238
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
239
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
240
+ const isError = diagnostic.category === ts.DiagnosticCategory.Error;
241
+ const category = isError ? 'error' : 'warning';
242
+ const categoryColor = isError ? colors.red : colors.yellow;
243
+ // 主要错误信息
244
+ console.log(`\n${colors.cyan}┌─ Issue #${index + 1}${colors.reset}`);
245
+ console.log(`${colors.cyan}│${colors.reset} ${colors.cyan}${path.relative(pwd, diagnostic.file.fileName)}${colors.reset}:${colors.yellow}${line + 1}${colors.reset}:${colors.yellow}${character + 1}${colors.reset}`);
246
+ console.log(`${colors.cyan}│${colors.reset} ${categoryColor}${category}${colors.reset} ${colors.gray}TS${diagnostic.code}${colors.reset}: ${message}`);
247
+ // 显示代码片段
248
+ const sourceFile = diagnostic.file;
249
+ const lineStart = sourceFile.getPositionOfLineAndCharacter(line, 0);
250
+ const lineEnd = sourceFile.getPositionOfLineAndCharacter(line + 1, 0);
251
+ let lineText = sourceFile.text.substring(lineStart, lineEnd).trimEnd();
252
+ // 确保只显示单行内容,去除换行符
253
+ const newlineIndex = lineText.indexOf('\n');
254
+ if (newlineIndex !== -1) {
255
+ lineText = lineText.substring(0, newlineIndex);
256
+ }
257
+ // 限制显示的最大字符数,避免输出过长
258
+ const maxDisplayLength = 100;
259
+ const isTruncated = lineText.length > maxDisplayLength;
260
+ const displayText = isTruncated
261
+ ? lineText.substring(0, maxDisplayLength) + colors.gray + '...' + colors.reset
262
+ : lineText;
263
+ // 计算实际显示的文本长度(不包括颜色代码)
264
+ const actualDisplayLength = Math.min(lineText.length, maxDisplayLength);
265
+ // 调整错误标记的显示位置和长度
266
+ const effectiveCharacter = Math.min(character, actualDisplayLength);
267
+ const maxPossibleLength = actualDisplayLength - effectiveCharacter;
268
+ const effectiveLength = Math.min(Math.max(1, diagnostic.length || 1), maxPossibleLength);
269
+ console.log(`${colors.cyan}│${colors.reset}`);
270
+ console.log(`${colors.cyan}│${colors.reset} ${colors.gray}${line + 1}${colors.reset} │ ${displayText}`);
271
+ console.log(`${colors.cyan}│${colors.reset} ${' '.repeat(String(line + 1).length)}│ ${' '.repeat(effectiveCharacter)}${colors.red}${'~'.repeat(effectiveLength)}${colors.reset}`);
272
+ // 如果是自定义诊断,显示额外信息
273
+ if (isCustom) {
274
+ const customDiag = diagnostic;
275
+ // 显示调用链
276
+ if (customDiag.callChain && customDiag.callChain.length > 1) {
277
+ console.log(`${colors.cyan}│${colors.reset}`);
278
+ console.log(`${colors.cyan}│${colors.reset} ${colors.yellow}调用链:${colors.reset}`);
279
+ customDiag.callChain.forEach((func, idx) => {
280
+ console.log(`${colors.cyan}│${colors.reset} ${colors.gray}→ ${func}${colors.reset}`);
281
+ });
282
+ }
283
+ // 显示实际的context调用位置
284
+ if (customDiag.contextCallNode) {
285
+ console.log(`${colors.cyan}│${colors.reset}`);
286
+ console.log(`${colors.cyan}│${colors.reset} ${colors.red}→ ${getContextLocationText(pwd, customDiag.contextCallNode)}${colors.reset}`);
287
+ }
288
+ // 显示检测原因
289
+ if (customDiag.reason) {
290
+ console.log(`${colors.cyan}│${colors.reset}`);
291
+ console.log(`${colors.cyan}│${colors.reset} ${colors.yellow}检测原因: ${customDiag.reason}${colors.reset}`);
292
+ }
293
+ // 显示详细原因
294
+ if (customDiag.reasonDetails && customDiag.reasonDetails.length > 0) {
295
+ console.log(`${colors.cyan}│${colors.reset}`);
296
+ console.log(`${colors.cyan}│${colors.reset} ${colors.yellow}详细分析:${colors.reset}`);
297
+ customDiag.reasonDetails.forEach(detail => {
298
+ // 根据符号选择颜色
299
+ let color = colors.gray;
300
+ if (detail.includes('✓')) {
301
+ color = colors.green;
302
+ }
303
+ else if (detail.includes('✘')) {
304
+ color = colors.red;
305
+ }
306
+ else if (detail.includes('¡')) {
307
+ color = colors.cyan;
308
+ }
309
+ console.log(`${colors.cyan}│${colors.reset} ${color}${detail}${colors.reset}`);
310
+ });
311
+ }
312
+ }
313
+ console.log(`${colors.cyan}└─${colors.reset}`);
314
+ }
315
+ else {
316
+ const isError = diagnostic.category === ts.DiagnosticCategory.Error;
317
+ const category = isError ? 'error' : 'warning';
318
+ const categoryColor = isError ? colors.red : colors.yellow;
319
+ console.log(`\n${index + 1}. ${categoryColor}${category}${colors.reset} ${colors.gray}TS${diagnostic.code}${colors.reset}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`);
320
+ }
321
+ }
322
+ /**
323
+ * 执行自定义检查
324
+ * @param program ts.Program
325
+ * @param typeChecker ts.TypeChecker
326
+ * @param customConfig 自定义配置
327
+ * @returns CustomDiagnostic[] 自定义诊断列表
328
+ */
329
+ function performCustomChecks(pwd, program, typeChecker, customConfig) {
330
+ const diagnostics = [];
331
+ const enableAsyncContextCheck = customConfig.context?.checkAsyncContext !== false;
332
+ const enableTCheck = customConfig.locale?.checkI18nKeys !== false;
333
+ console.log(`Custom AsyncContext checks are ${enableAsyncContextCheck ? colors.green + 'enabled' + colors.reset : colors.gray + 'disabled' + colors.reset}.`);
334
+ console.log(`Custom i18n key checks are ${enableTCheck ? colors.green + 'enabled' + colors.reset : colors.gray + 'disabled' + colors.reset}.`);
335
+ const checkTemplateLiterals = customConfig?.locale?.checkTemplateLiterals ?? false;
336
+ const warnStringKeys = customConfig?.locale?.warnStringKeys ?? false;
337
+ // 数据结构定义
338
+ const functionsWithContextCalls = new Set();
339
+ const functionDeclarations = new Map();
340
+ const functionCallGraph = new Map(); // 谁调用了谁
341
+ const directContextCalls = new Map(); // 函数内的直接 context 调用
342
+ const allContextCalls = []; // 所有 context 调用点
343
+ // 缓存:函数符号 -> 参数索引 -> 是否被处理
344
+ const functionParameterHandling = new Map();
345
+ // 缓存:变量符号 -> 是否被正确处理
346
+ const variableHandlingCache = new Map();
347
+ // 正在检查的变量集合(防止循环)
348
+ const checkingVariables = new Set();
349
+ const typeCache = new Map();
350
+ // 文件和对应需要检查的调用点缓存
351
+ const callsToCheckByFile = new Map();
352
+ // 忽略注释节点缓存
353
+ const ignoreCommentNodes = new Set();
354
+ // 符号缓存
355
+ const symbolCache = new Map();
356
+ // t函数调用符号缓存
357
+ const tFunctionSymbolCache = new Set();
358
+ const getSymbolCached = (node) => {
359
+ if (symbolCache.has(node)) {
360
+ return symbolCache.get(node);
361
+ }
362
+ const symbol = typeChecker.getSymbolAtLocation(node);
363
+ symbolCache.set(node, symbol);
364
+ return symbol;
365
+ };
366
+ const addDiagnostic = (node, messageText, code, options) => {
367
+ const sourceFile = node.getSourceFile();
368
+ // 构建调用链文本
369
+ let callChain;
370
+ if (options?.callChain && options.callChain.length > 0) {
371
+ callChain = options.callChain.map(symbol => {
372
+ const decl = functionDeclarations.get(symbol);
373
+ if (decl && decl.name && ts.isIdentifier(decl.name)) {
374
+ const file = decl.getSourceFile();
375
+ const { line } = file.getLineAndCharacterOfPosition(decl.getStart());
376
+ return `${symbol.name} (${path.basename(file.fileName)}:${line + 1})`;
377
+ }
378
+ return symbol.name;
379
+ });
380
+ }
381
+ diagnostics.push({
382
+ file: sourceFile,
383
+ start: node.getStart(sourceFile),
384
+ length: node.getWidth(sourceFile),
385
+ messageText,
386
+ category: ts.DiagnosticCategory.Warning,
387
+ code,
388
+ callChain,
389
+ contextCallNode: options?.contextCall,
390
+ reason: options?.reason,
391
+ reasonDetails: options?.reasonDetails
392
+ });
393
+ };
394
+ const shouldReportUnawaitedCall = (callNode, sourceFile) => {
395
+ // 1. 检查忽略注释(现在很快,因为有缓存)
396
+ if (hasIgnoreComment(callNode, sourceFile)) {
397
+ return false;
398
+ }
399
+ // 2. 检查是否 await(通常很快)
400
+ if (isAwaited(callNode)) {
401
+ return false;
402
+ }
403
+ // 3. 检查特定场景(很快)
404
+ if (shouldSkipCheck(callNode)) {
405
+ return false;
406
+ }
407
+ // 4. 最后检查复杂的赋值处理(较慢)
408
+ if (isCallAssignedAndHandled(callNode)) {
409
+ return false;
410
+ }
411
+ return true;
412
+ };
413
+ // 分析为什么调用未被正确处理,返回详细原因
414
+ const analyzeUnhandledReason = (callNode) => {
415
+ const details = [];
416
+ let reason = '未正确处理Promise';
417
+ let suggestedFix = '';
418
+ // 检查是否被 await
419
+ if (!isAwaited(callNode)) {
420
+ details.push('✘ 调用未使用 await 关键字');
421
+ suggestedFix = `await ${callNode.getText()}`;
422
+ }
423
+ else {
424
+ details.push('✓ 调用已使用 await');
425
+ }
426
+ // 检查是否在 return 语句中
427
+ const parent = callNode.parent;
428
+ if (ts.isReturnStatement(parent)) {
429
+ details.push('✓ 在 return 语句中(已传递给调用者)');
430
+ }
431
+ else if (ts.isArrowFunction(parent) && parent.body === callNode) {
432
+ details.push('✓ 作为箭头函数返回值(已传递给调用者)');
433
+ }
434
+ else {
435
+ details.push('✘ 未通过 return 传递给调用者');
436
+ }
437
+ // 检查是否赋值给变量
438
+ if (ts.isVariableDeclaration(parent) && parent.initializer === callNode) {
439
+ const variableDecl = parent;
440
+ if (variableDecl.name && ts.isIdentifier(variableDecl.name)) {
441
+ const variableName = variableDecl.name.text;
442
+ const variableSymbol = typeChecker.getSymbolAtLocation(variableDecl.name);
443
+ if (variableSymbol) {
444
+ details.push(`¡ 赋值给变量: ${variableName}`);
445
+ // 检查变量的各种处理方式
446
+ const scope = findNearestFunctionScope(variableDecl);
447
+ if (scope) {
448
+ const funcLike = scope;
449
+ if (funcLike.body) {
450
+ const check = checkPromiseHandlingWithInstanceOf(funcLike.body, variableSymbol, {
451
+ stopAtFunctionBoundary: true
452
+ });
453
+ if (check.hasAwait) {
454
+ details.push(` ✓ 变量 ${variableName} 后续被 await`);
455
+ }
456
+ else {
457
+ details.push(` ✘ 变量 ${variableName} 未被 await`);
458
+ }
459
+ if (check.hasPromiseMethod) {
460
+ details.push(` ✓ 变量 ${variableName} 调用了 .then/.catch/.finally`);
461
+ }
462
+ else {
463
+ details.push(` ✘ 变量 ${variableName} 未调用 .then/.catch/.finally`);
464
+ }
465
+ if (check.hasPromiseStatic) {
466
+ details.push(` ✓ 变量 ${variableName} 被传入 Promise.all/race 等`);
467
+ }
468
+ else {
469
+ details.push(` ✘ 变量 ${variableName} 未被传入 Promise.all/race 等`);
470
+ }
471
+ if (check.hasReturn) {
472
+ details.push(` ✓ 变量 ${variableName} 被 return`);
473
+ }
474
+ else {
475
+ details.push(` ✘ 变量 ${variableName} 未被 return`);
476
+ }
477
+ if (check.hasInstanceOf) {
478
+ details.push(` ✓ 变量 ${variableName} 在 instanceof Promise 检查后被处理`);
479
+ }
480
+ // 检查是否传递给其他函数
481
+ if (isSymbolPassedToHandlingFunction(variableSymbol, funcLike.body)) {
482
+ details.push(` ✓ 变量 ${variableName} 被传递给会处理Promise的函数`);
483
+ }
484
+ else {
485
+ details.push(` ✘ 变量 ${variableName} 未被传递给会处理Promise的函数`);
486
+ }
487
+ }
488
+ }
489
+ reason = `变量 ${variableName} 未被正确处理`;
490
+ if (!suggestedFix) {
491
+ suggestedFix = `await ${variableName}`;
492
+ }
493
+ }
494
+ }
495
+ }
496
+ else {
497
+ details.push('✘ 未赋值给变量进行后续处理');
498
+ }
499
+ // 检查是否作为参数传递
500
+ if (ts.isCallExpression(parent)) {
501
+ const argIndex = parent.arguments.indexOf(callNode);
502
+ if (argIndex !== -1) {
503
+ details.push(`¡ 作为第 ${argIndex + 1} 个参数传递给函数`);
504
+ if (isCallPassedToHandlingFunction(callNode)) {
505
+ details.push(' ✓ 该函数会正确处理Promise参数');
506
+ }
507
+ else {
508
+ details.push(' ✘ 该函数不会处理Promise参数');
509
+ reason = '传递给不处理Promise的函数';
510
+ }
511
+ }
512
+ }
513
+ // 检查是否在数组方法中
514
+ if (isInArrayMethodCallback(callNode, ARRAY_TRANSFORM_METHODS)) {
515
+ details.push('¡ 在数组转换方法(map/filter/flatMap)的回调中');
516
+ if (isCallInHandledArrayMethod(callNode)) {
517
+ details.push(' ✓ 数组方法的结果被正确处理(如传入Promise.all)');
518
+ }
519
+ else {
520
+ details.push(' ✘ 数组方法的结果未被正确处理');
521
+ reason = '在数组方法中但结果未被处理';
522
+ suggestedFix = '将数组方法结果传入 Promise.all()';
523
+ }
524
+ }
525
+ return { reason, details, suggestedFix };
526
+ };
527
+ const isCallAssignedAndHandled = (callNode) => {
528
+ const parent = callNode.parent;
529
+ if (ts.isVariableDeclaration(parent) && parent.initializer === callNode) {
530
+ if (parent.name && ts.isIdentifier(parent.name)) {
531
+ const variableSymbol = typeChecker.getSymbolAtLocation(parent.name);
532
+ if (variableSymbol) {
533
+ // 如果变量被 return,则在 shouldSkipCheck 中已经处理,这里不需要再检查
534
+ if (isVariableReturned(variableSymbol, parent)) {
535
+ return true; // 被 return 的变量,认为已处理
536
+ }
537
+ // 否则检查是否在作用域内被正确处理
538
+ if (isPromiseProperlyHandled(variableSymbol, parent)) {
539
+ return true;
540
+ }
541
+ }
542
+ }
543
+ }
544
+ // 检查是否作为参数传递给会处理 Promise 的函数
545
+ if (isCallPassedToHandlingFunction(callNode)) {
546
+ return true;
547
+ }
548
+ return false;
549
+ };
550
+ const resolveModuleName = (moduleName, containingFile) => {
551
+ const compilerOptions = program.getCompilerOptions();
552
+ const resolvedModule = ts.resolveModuleName(moduleName, containingFile, compilerOptions, ts.sys);
553
+ return resolvedModule.resolvedModule?.resolvedFileName;
554
+ };
555
+ const pathCache = new Map();
556
+ const normalizeModulePath = (filePath) => {
557
+ if (pathCache.has(filePath)) {
558
+ return pathCache.get(filePath);
559
+ }
560
+ const normalized = filePath
561
+ .replace(/\\/g, '/')
562
+ .replace(/\.(ts|tsx|js|jsx|d\.ts)$/, '');
563
+ pathCache.set(filePath, normalized);
564
+ return normalized;
565
+ };
566
+ const preprocessIgnoreComments = (sourceFile) => {
567
+ const fullText = sourceFile.getFullText();
568
+ // 收集文件中所有的注释及其位置
569
+ const allComments = [];
570
+ const scanComments = (pos, end) => {
571
+ const leading = ts.getLeadingCommentRanges(fullText, pos);
572
+ const trailing = ts.getTrailingCommentRanges(fullText, end);
573
+ if (leading) {
574
+ leading.forEach(comment => {
575
+ const text = fullText.substring(comment.pos, comment.end);
576
+ allComments.push({
577
+ pos: comment.pos,
578
+ end: comment.end,
579
+ text,
580
+ isIgnore: isIgnoreComment(text)
581
+ });
582
+ });
583
+ }
584
+ if (trailing) {
585
+ trailing.forEach(comment => {
586
+ const text = fullText.substring(comment.pos, comment.end);
587
+ allComments.push({
588
+ pos: comment.pos,
589
+ end: comment.end,
590
+ text,
591
+ isIgnore: isIgnoreComment(text)
592
+ });
593
+ });
594
+ }
595
+ };
596
+ // 扫描整个文件的注释
597
+ const scanNode = (node) => {
598
+ scanComments(node.getFullStart(), node.getEnd());
599
+ ts.forEachChild(node, scanNode);
600
+ };
601
+ scanNode(sourceFile);
602
+ // 检查节点是否在忽略注释的影响范围内
603
+ const isNodeCoveredByIgnoreComment = (node) => {
604
+ const nodeStart = node.getStart(sourceFile);
605
+ const nodeEnd = node.getEnd();
606
+ for (const comment of allComments) {
607
+ if (!comment.isIgnore)
608
+ continue;
609
+ // 情况1:注释在节点之前(前导注释)
610
+ // 允许注释和节点之间有少量空白(最多50个字符)
611
+ if (comment.end <= nodeStart && nodeStart - comment.end <= 50) {
612
+ return true;
613
+ }
614
+ // 情况2:注释在节点之后(尾随注释)
615
+ // 允许节点和注释之间有少量空白(最多50个字符)
616
+ if (comment.pos >= nodeEnd && comment.pos - nodeEnd <= 50) {
617
+ return true;
618
+ }
619
+ // 情况3:注释在节点内部(JSX 表达式中的注释)
620
+ if (comment.pos >= nodeStart && comment.end <= nodeEnd) {
621
+ return true;
622
+ }
623
+ }
624
+ return false;
625
+ };
626
+ const visit = (node) => {
627
+ // 检查当前节点是否被忽略注释覆盖
628
+ if (isNodeCoveredByIgnoreComment(node)) {
629
+ markNodeAndChildren(node);
630
+ return;
631
+ }
632
+ // 特殊处理:JSX 表达式
633
+ if (ts.isJsxExpression(node)) {
634
+ // 检查 JSX 表达式内部是否有忽略注释
635
+ if (node.expression && isNodeCoveredByIgnoreComment(node.expression)) {
636
+ markNodeAndChildren(node.expression);
637
+ }
638
+ }
639
+ // 特殊处理:条件表达式(三元运算符)
640
+ if (ts.isConditionalExpression(node)) {
641
+ // 检查 whenTrue 和 whenFalse 分支
642
+ if (isNodeCoveredByIgnoreComment(node.whenTrue)) {
643
+ markNodeAndChildren(node.whenTrue);
644
+ }
645
+ if (isNodeCoveredByIgnoreComment(node.whenFalse)) {
646
+ markNodeAndChildren(node.whenFalse);
647
+ }
648
+ }
649
+ // 特殊处理:二元表达式
650
+ if (ts.isBinaryExpression(node)) {
651
+ // 检查左右操作数
652
+ if (isNodeCoveredByIgnoreComment(node.left)) {
653
+ markNodeAndChildren(node.left);
654
+ }
655
+ if (isNodeCoveredByIgnoreComment(node.right)) {
656
+ markNodeAndChildren(node.right);
657
+ }
658
+ }
659
+ ts.forEachChild(node, visit);
660
+ };
661
+ const markNodeAndChildren = (node) => {
662
+ ignoreCommentNodes.add(node);
663
+ ts.forEachChild(node, markNodeAndChildren);
664
+ };
665
+ visit(sourceFile);
666
+ };
667
+ // 修改 hasIgnoreComment 函数
668
+ const hasIgnoreComment = (node, sourceFile) => {
669
+ // 直接查缓存
670
+ if (ignoreCommentNodes.has(node)) {
671
+ return true;
672
+ }
673
+ // 如果文件开头有忽略注释,整个文件都忽略
674
+ const fileStartComments = ts.getLeadingCommentRanges(sourceFile.getFullText(), 0);
675
+ if (fileStartComments) {
676
+ for (const comment of fileStartComments) {
677
+ const commentText = sourceFile.getFullText().substring(comment.pos, comment.end);
678
+ if (isIgnoreComment(commentText)) {
679
+ return true;
680
+ }
681
+ }
682
+ }
683
+ // 向上查找父节点(最多3层)
684
+ let current = node.parent;
685
+ let depth = 0;
686
+ while (current && depth < 3) {
687
+ if (ignoreCommentNodes.has(current)) {
688
+ return true;
689
+ }
690
+ current = current.parent;
691
+ depth++;
692
+ }
693
+ return false;
694
+ };
695
+ const isIgnoreComment = (commentText) => {
696
+ return exports.OAK_IGNORE_TAGS.some(tag => commentText.includes(tag));
697
+ };
698
+ const isAsyncContextType = (type, modules) => {
699
+ if (typeCache.has(type)) {
700
+ return typeCache.get(type);
701
+ }
702
+ const result = checkIsAsyncContextType(type, modules);
703
+ typeCache.set(type, result);
704
+ return result;
705
+ };
706
+ const checkIsAsyncContextType = (type, modules) => {
707
+ // 检查类型本身
708
+ if (checkTypeSymbol(type, modules)) {
709
+ return true;
710
+ }
711
+ // 检查联合类型(例如 RuntimeCxt = FRC | BRC)
712
+ if (type.isUnion()) {
713
+ return type.types.some(t => isAsyncContextType(t, modules));
714
+ }
715
+ // 检查交叉类型
716
+ if (type.isIntersection()) {
717
+ return type.types.some(t => isAsyncContextType(t, modules));
718
+ }
719
+ // 检查基类型(继承关系)
720
+ const baseTypes = type.getBaseTypes?.() || [];
721
+ for (const baseType of baseTypes) {
722
+ if (isAsyncContextType(baseType, modules)) {
723
+ return true;
724
+ }
725
+ }
726
+ return false;
727
+ };
728
+ const checkTypeSymbol = (type, modules) => {
729
+ const symbol = type.getSymbol();
730
+ if (!symbol) {
731
+ return false;
732
+ }
733
+ const declarations = symbol.getDeclarations();
734
+ if (!declarations || declarations.length === 0) {
735
+ return false;
736
+ }
737
+ for (const declaration of declarations) {
738
+ const sourceFile = declaration.getSourceFile();
739
+ const fileName = sourceFile.fileName;
740
+ const normalizedFileName = normalizeModulePath(fileName);
741
+ // 检查是否来自目标模块
742
+ for (const moduleName of modules) {
743
+ // 直接路径匹配(处理已解析的路径)
744
+ if (normalizedFileName.includes(moduleName.replace(/\\/g, '/'))) {
745
+ return true;
746
+ }
747
+ // 尝试解析模块别名
748
+ const resolvedPath = resolveModuleName(moduleName, sourceFile.fileName);
749
+ if (resolvedPath) {
750
+ const normalizedResolvedPath = normalizeModulePath(resolvedPath);
751
+ if (normalizedFileName === normalizedResolvedPath ||
752
+ normalizedFileName.includes(normalizedResolvedPath)) {
753
+ return true;
754
+ }
755
+ }
756
+ // 检查模块说明符(从 import 语句中获取)
757
+ const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration);
758
+ for (const importDecl of importDeclarations) {
759
+ if (importDecl.moduleSpecifier && ts.isStringLiteral(importDecl.moduleSpecifier)) {
760
+ const importPath = importDecl.moduleSpecifier.text;
761
+ if (importPath === moduleName || importPath.includes(moduleName)) {
762
+ // 检查当前符号是否来自这个 import
763
+ const importClause = importDecl.importClause;
764
+ if (importClause) {
765
+ const importSymbol = typeChecker.getSymbolAtLocation(importDecl.moduleSpecifier);
766
+ if (importSymbol && isSymbolRelated(symbol, importSymbol)) {
767
+ return true;
768
+ }
769
+ }
770
+ }
771
+ }
772
+ }
773
+ }
774
+ }
775
+ return false;
776
+ };
777
+ const isSymbolRelated = (symbol, moduleSymbol) => {
778
+ // 检查符号是否与模块相关
779
+ const exports = typeChecker.getExportsOfModule(moduleSymbol);
780
+ return exports.some(exp => exp === symbol || exp.name === symbol.name);
781
+ };
782
+ /**
783
+ * 检查节点是否被 await 修饰
784
+ * @param node ts.Node
785
+ * @returns boolean
786
+ */
787
+ const isAwaited = (node) => {
788
+ let parent = node.parent;
789
+ // 向上遍历父节点,查找 await 表达式
790
+ while (parent) {
791
+ // 检查是否被 await 修饰(考虑透明包装)
792
+ if (ts.isAwaitExpression(parent)) {
793
+ // 去除 await 表达式中的透明包装层
794
+ const awaitedExpression = unwrapTransparentWrappers(parent.expression);
795
+ // 检查去除包装后是否就是目标节点
796
+ if (awaitedExpression === node) {
797
+ return true;
798
+ }
799
+ // 也检查是否在子树中(处理更复杂的嵌套情况)
800
+ if (isNodeInSubtree(parent.expression, node)) {
801
+ return true;
802
+ }
803
+ }
804
+ // 如果遇到函数边界,停止向上查找
805
+ if (isFunctionLikeDeclaration(parent)) {
806
+ break;
807
+ }
808
+ parent = parent.parent;
809
+ }
810
+ return false;
811
+ };
812
+ const isNodeInSubtree = (root, target) => {
813
+ if (root === target) {
814
+ return true;
815
+ }
816
+ let found = false;
817
+ ts.forEachChild(root, (child) => {
818
+ if (found)
819
+ return;
820
+ if (child === target || isNodeInSubtree(child, target)) {
821
+ found = true;
822
+ }
823
+ });
824
+ return found;
825
+ };
826
+ // 辅助函数:获取函数调用的名称
827
+ const getFunctionCallName = (node, sourceFile) => {
828
+ if (ts.isIdentifier(node.expression)) {
829
+ return node.expression.getText(sourceFile);
830
+ }
831
+ else if (ts.isPropertyAccessExpression(node.expression)) {
832
+ return node.expression.name.getText(sourceFile);
833
+ }
834
+ return node.expression.getText(sourceFile);
835
+ };
836
+ // 辅助函数:获取声明的符号
837
+ const getSymbolOfDeclaration = (declaration) => {
838
+ // 处理有名称的声明(函数声明、方法声明等)
839
+ if ('name' in declaration && declaration.name) {
840
+ return typeChecker.getSymbolAtLocation(declaration.name);
841
+ }
842
+ // 处理箭头函数:尝试从父节点(变量声明)获取符号
843
+ if (isFunctionLikeDeclaration(declaration)) {
844
+ const parent = declaration.parent;
845
+ if (ts.isVariableDeclaration(parent) && parent.name) {
846
+ return typeChecker.getSymbolAtLocation(parent.name);
847
+ }
848
+ // 处理作为属性值的情况
849
+ if (ts.isPropertyAssignment(parent) && parent.name) {
850
+ return typeChecker.getSymbolAtLocation(parent.name);
851
+ }
852
+ }
853
+ return undefined;
854
+ };
855
+ const collectAndCheck = (sourceFile) => {
856
+ let currentFunction;
857
+ const localCallsToCheck = []; // 收集需要检查的调用
858
+ const visit = (node) => {
859
+ // 记录函数声明
860
+ if (isFunctionLikeDeclaration(node)) {
861
+ const symbol = getSymbolOfDeclaration(node);
862
+ if (symbol) {
863
+ functionDeclarations.set(symbol, node);
864
+ const previousFunction = currentFunction;
865
+ currentFunction = symbol;
866
+ ts.forEachChild(node, visit);
867
+ currentFunction = previousFunction;
868
+ return;
869
+ }
870
+ }
871
+ // 处理调用表达式 - 合并原来的两个逻辑
872
+ if (ts.isCallExpression(node)) {
873
+ if (enableAsyncContextCheck) {
874
+ // 检查 context 调用
875
+ if (ts.isPropertyAccessExpression(node.expression)) {
876
+ const objectType = typeChecker.getTypeAtLocation(node.expression.expression);
877
+ const targetModules = customConfig.context?.targetModules ||
878
+ ['@project/context/BackendRuntimeContext'];
879
+ if (isAsyncContextType(objectType, targetModules)) {
880
+ if (!hasIgnoreComment(node, sourceFile)) {
881
+ allContextCalls.push(node);
882
+ if (currentFunction) {
883
+ if (!directContextCalls.has(currentFunction)) {
884
+ directContextCalls.set(currentFunction, []);
885
+ }
886
+ directContextCalls.get(currentFunction).push(node);
887
+ }
888
+ }
889
+ }
890
+ }
891
+ // 记录函数调用关系 + 收集需要检查的调用
892
+ if (currentFunction) {
893
+ const signature = typeChecker.getResolvedSignature(node);
894
+ if (signature) {
895
+ const declaration = signature.getDeclaration();
896
+ if (declaration) {
897
+ const calledSymbol = getSymbolOfDeclaration(declaration);
898
+ if (calledSymbol && calledSymbol !== currentFunction) {
899
+ if (!functionCallGraph.has(currentFunction)) {
900
+ functionCallGraph.set(currentFunction, new Set());
901
+ }
902
+ functionCallGraph.get(currentFunction).add(calledSymbol);
903
+ }
904
+ }
905
+ }
906
+ // 收集可能需要检查的调用(延迟到标记传播后)
907
+ localCallsToCheck.push(node);
908
+ }
909
+ }
910
+ if (enableTCheck && (0, identifier_1.isTCall)(node, typeChecker, customConfig.locale?.tFunctionModules || ['oak-frontend-base'])) {
911
+ tFunctionSymbolCache.add(node);
912
+ }
913
+ }
914
+ ts.forEachChild(node, visit);
915
+ };
916
+ visit(sourceFile);
917
+ // 保存需要检查的调用
918
+ callsToCheckByFile.set(sourceFile, localCallsToCheck);
919
+ };
920
+ // 过滤掉非 Promise 返回的 context 调用
921
+ const filterAsyncContextCalls = () => {
922
+ // 过滤 allContextCalls
923
+ const asyncContextCalls = [];
924
+ for (const callNode of allContextCalls) {
925
+ // 添加忽略检查
926
+ const sourceFile = callNode.getSourceFile();
927
+ if (hasIgnoreComment(callNode, sourceFile)) {
928
+ continue;
929
+ }
930
+ const signature = typeChecker.getResolvedSignature(callNode);
931
+ if (signature) {
932
+ const returnType = typeChecker.getReturnTypeOfSignature(signature);
933
+ if (isPromiseType(returnType, typeChecker)) {
934
+ asyncContextCalls.push(callNode);
935
+ }
936
+ }
937
+ }
938
+ allContextCalls.length = 0;
939
+ allContextCalls.push(...asyncContextCalls);
940
+ // 过滤 directContextCalls
941
+ const newDirectContextCalls = new Map();
942
+ for (const [functionSymbol, calls] of directContextCalls.entries()) {
943
+ const asyncCalls = calls.filter(callNode => {
944
+ // 添加忽略检查
945
+ const sourceFile = callNode.getSourceFile();
946
+ if (hasIgnoreComment(callNode, sourceFile)) {
947
+ return false;
948
+ }
949
+ const signature = typeChecker.getResolvedSignature(callNode);
950
+ if (!signature)
951
+ return false;
952
+ const returnType = typeChecker.getReturnTypeOfSignature(signature);
953
+ return isPromiseType(returnType, typeChecker);
954
+ });
955
+ // 只保留有异步调用的函数
956
+ if (asyncCalls.length > 0) {
957
+ newDirectContextCalls.set(functionSymbol, asyncCalls);
958
+ }
959
+ }
960
+ directContextCalls.clear();
961
+ newDirectContextCalls.forEach((calls, symbol) => {
962
+ directContextCalls.set(symbol, calls);
963
+ });
964
+ };
965
+ const propagateContextMarks = () => {
966
+ // 初始标记:直接包含 context 调用的函数
967
+ const markedFunctions = new Set(directContextCalls.keys());
968
+ // 迭代传播标记
969
+ let changed = true;
970
+ while (changed) {
971
+ changed = false;
972
+ for (const [caller, callees] of functionCallGraph.entries()) {
973
+ if (markedFunctions.has(caller))
974
+ continue;
975
+ // 如果调用了任何标记的函数,则标记当前函数
976
+ for (const callee of callees) {
977
+ if (markedFunctions.has(callee)) {
978
+ markedFunctions.add(caller);
979
+ functionsWithContextCalls.add(caller);
980
+ changed = true;
981
+ break;
982
+ }
983
+ }
984
+ }
985
+ }
986
+ // 更新全局标记
987
+ markedFunctions.forEach(symbol => functionsWithContextCalls.add(symbol));
988
+ };
989
+ // 检查直接的 context 调用
990
+ const checkDirectContextCalls = () => {
991
+ for (const callNode of allContextCalls) {
992
+ if (shouldSkipCheck(callNode))
993
+ continue;
994
+ const signature = typeChecker.getResolvedSignature(callNode);
995
+ if (!signature)
996
+ continue;
997
+ const returnType = typeChecker.getReturnTypeOfSignature(signature);
998
+ if (!isPromiseType(returnType, typeChecker))
999
+ continue;
1000
+ // 检查是否直接 await
1001
+ const sourceFile = callNode.getSourceFile();
1002
+ if (shouldReportUnawaitedCall(callNode, sourceFile)) {
1003
+ const propertyAccess = callNode.expression;
1004
+ const methodName = propertyAccess.name.getText(sourceFile);
1005
+ const objectName = propertyAccess.expression.getText(sourceFile);
1006
+ // 分析具体原因
1007
+ // const analysis = analyzeUnhandledReason(callNode);
1008
+ addDiagnostic(callNode, `未await的context调用可能导致事务不受控: ${objectName}.${methodName}()`, 9100, {
1009
+ contextCall: callNode,
1010
+ // reason: analysis.reason,
1011
+ // reasonDetails: analysis.details
1012
+ });
1013
+ }
1014
+ }
1015
+ };
1016
+ // 检查间接调用(使用缓存的调用列表)
1017
+ const checkIndirectCalls = () => {
1018
+ // 使用之前收集的调用列表,避免重复遍历
1019
+ for (const [sourceFile, callsToCheck] of callsToCheckByFile.entries()) {
1020
+ if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) {
1021
+ continue;
1022
+ }
1023
+ for (const node of callsToCheck) {
1024
+ if (!ts.isCallExpression(node))
1025
+ continue;
1026
+ if (shouldSkipCheck(node))
1027
+ continue;
1028
+ const signature = typeChecker.getResolvedSignature(node);
1029
+ if (!signature)
1030
+ continue;
1031
+ const declaration = signature.getDeclaration();
1032
+ if (!declaration)
1033
+ continue;
1034
+ const symbol = getSymbolOfDeclaration(declaration);
1035
+ if (!symbol)
1036
+ continue;
1037
+ // 检查是否调用了标记的函数
1038
+ if (functionsWithContextCalls.has(symbol)) {
1039
+ const returnType = typeChecker.getReturnTypeOfSignature(signature);
1040
+ if (!isPromiseType(returnType, typeChecker))
1041
+ continue;
1042
+ if (shouldReportUnawaitedCall(node, sourceFile)) {
1043
+ const functionName = getFunctionCallName(node, sourceFile);
1044
+ // 追踪调用链
1045
+ const callChain = traceCallChain(symbol);
1046
+ const contextCall = findContextCallInChain(symbol);
1047
+ // 分析具体原因
1048
+ // const analysis = analyzeUnhandledReason(node);
1049
+ addDiagnostic(node, `未await的函数调用可能导致事务不受控: ${functionName}()`, 9101, {
1050
+ callChain,
1051
+ contextCall,
1052
+ // reason: analysis.reason,
1053
+ // reasonDetails: analysis.details
1054
+ });
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ };
1060
+ // 追踪调用链
1061
+ const traceCallChain = (symbol) => {
1062
+ const chain = [symbol];
1063
+ const visited = new Set();
1064
+ const trace = (currentSymbol) => {
1065
+ if (visited.has(currentSymbol))
1066
+ return false;
1067
+ visited.add(currentSymbol);
1068
+ // 如果直接包含context调用,返回true
1069
+ if (directContextCalls.has(currentSymbol)) {
1070
+ return true;
1071
+ }
1072
+ // 检查调用的函数
1073
+ const callees = functionCallGraph.get(currentSymbol);
1074
+ if (callees) {
1075
+ for (const callee of callees) {
1076
+ if (functionsWithContextCalls.has(callee)) {
1077
+ chain.push(callee);
1078
+ if (trace(callee)) {
1079
+ return true;
1080
+ }
1081
+ chain.pop();
1082
+ }
1083
+ }
1084
+ }
1085
+ return false;
1086
+ };
1087
+ trace(symbol);
1088
+ return chain;
1089
+ };
1090
+ // 找到调用链中的context调用
1091
+ const findContextCallInChain = (symbol) => {
1092
+ const visited = new Set();
1093
+ const find = (currentSymbol) => {
1094
+ if (visited.has(currentSymbol))
1095
+ return undefined;
1096
+ visited.add(currentSymbol);
1097
+ // 如果直接包含context调用,返回第一个
1098
+ const calls = directContextCalls.get(currentSymbol);
1099
+ if (calls && calls.length > 0) {
1100
+ return calls[0];
1101
+ }
1102
+ // 递归查找
1103
+ const callees = functionCallGraph.get(currentSymbol);
1104
+ if (callees) {
1105
+ for (const callee of callees) {
1106
+ if (functionsWithContextCalls.has(callee)) {
1107
+ const result = find(callee);
1108
+ if (result)
1109
+ return result;
1110
+ }
1111
+ }
1112
+ }
1113
+ return undefined;
1114
+ };
1115
+ return find(symbol);
1116
+ };
1117
+ const shouldSkipCheck = (node) => {
1118
+ // 直接在 return 语句中
1119
+ if (ts.isReturnStatement(node.parent)) {
1120
+ if (isInArrayMethodCallback(node)) {
1121
+ return false;
1122
+ }
1123
+ return true;
1124
+ }
1125
+ // 箭头函数的直接返回值
1126
+ if (ts.isArrowFunction(node.parent) && node.parent.body === node) {
1127
+ if (isInArrayMethodCallback(node)) {
1128
+ return false;
1129
+ }
1130
+ return true;
1131
+ }
1132
+ // 检查是否赋值给变量,然后该变量被 return
1133
+ if (ts.isVariableDeclaration(node.parent) && node.parent.initializer === node) {
1134
+ const variableDecl = node.parent;
1135
+ if (variableDecl.name && ts.isIdentifier(variableDecl.name)) {
1136
+ const variableSymbol = typeChecker.getSymbolAtLocation(variableDecl.name);
1137
+ if (variableSymbol && isVariableReturned(variableSymbol, variableDecl)) {
1138
+ return true;
1139
+ }
1140
+ }
1141
+ }
1142
+ return false;
1143
+ };
1144
+ // 检查变量是否在其作用域内被 return
1145
+ const isVariableReturned = (symbol, declarationNode) => {
1146
+ const scope = findNearestFunctionScope(declarationNode);
1147
+ if (!scope) {
1148
+ return false;
1149
+ }
1150
+ let found = false;
1151
+ const visit = (node) => {
1152
+ if (found)
1153
+ return;
1154
+ if (ts.isReturnStatement(node) && node.expression) {
1155
+ if (isSymbolReferencedInNode(node.expression, symbol, typeChecker)) {
1156
+ found = true;
1157
+ return;
1158
+ }
1159
+ }
1160
+ // 不进入嵌套函数
1161
+ if (isFunctionLikeDeclaration(node)) {
1162
+ return;
1163
+ }
1164
+ ts.forEachChild(node, visit);
1165
+ };
1166
+ const funcLike = scope;
1167
+ if (funcLike.body) {
1168
+ visit(funcLike.body);
1169
+ }
1170
+ return found;
1171
+ };
1172
+ // 检查节点中是否包含指定变量
1173
+ const containsVariable = (node, symbol) => {
1174
+ return isSymbolReferencedInNode(node, symbol, typeChecker);
1175
+ };
1176
+ const checkPromiseHandlingInNode = (node, symbol, options = {}) => {
1177
+ const result = {};
1178
+ const visit = (n) => {
1179
+ // 检查 await
1180
+ if (ts.isAwaitExpression(n)) {
1181
+ const expression = unwrapTransparentWrappers(n.expression);
1182
+ if (isIdentifierWithSymbol(expression, symbol, typeChecker)) {
1183
+ result.hasAwait = true;
1184
+ return;
1185
+ }
1186
+ }
1187
+ // 检查 Promise 实例方法
1188
+ if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
1189
+ const object = n.expression.expression;
1190
+ if (ts.isIdentifier(object)) {
1191
+ const objSymbol = getSymbolCached(object);
1192
+ if (objSymbol === symbol) {
1193
+ const methodName = n.expression.name.text;
1194
+ if (PROMISE_METHODS.includes(methodName)) {
1195
+ result.hasPromiseMethod = true;
1196
+ return;
1197
+ }
1198
+ }
1199
+ }
1200
+ // 检查 Promise 静态方法
1201
+ if (isPromiseStaticMethodCall(n)) {
1202
+ for (const arg of n.arguments) {
1203
+ if (isSymbolReferencedInNode(arg, symbol, typeChecker)) {
1204
+ result.hasPromiseStatic = true;
1205
+ return;
1206
+ }
1207
+ }
1208
+ }
1209
+ }
1210
+ // 检查 return
1211
+ if (ts.isReturnStatement(n) && n.expression) {
1212
+ if (isSymbolReferencedInNode(n.expression, symbol, typeChecker)) {
1213
+ result.hasReturn = true;
1214
+ return;
1215
+ }
1216
+ }
1217
+ // 函数边界处理
1218
+ if (options.stopAtFunctionBoundary && isFunctionLikeDeclaration(n)) {
1219
+ return;
1220
+ }
1221
+ ts.forEachChild(n, visit);
1222
+ };
1223
+ visit(node);
1224
+ return result;
1225
+ };
1226
+ const checkPromiseHandlingWithInstanceOf = (node, symbol, options = {}) => {
1227
+ const result = {};
1228
+ const visit = (n) => {
1229
+ // 复用基础检查
1230
+ const baseCheck = checkPromiseHandlingInNode(n, symbol, options);
1231
+ Object.assign(result, baseCheck);
1232
+ // 额外检查 instanceof Promise
1233
+ if (ts.isBinaryExpression(n) &&
1234
+ n.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) {
1235
+ const left = n.left;
1236
+ if (ts.isIdentifier(left)) {
1237
+ const leftSymbol = getSymbolCached(left);
1238
+ if (leftSymbol === symbol) {
1239
+ const right = n.right;
1240
+ if (ts.isIdentifier(right) && right.text === 'Promise') {
1241
+ // 找到 instanceof Promise 所在的 if 语句
1242
+ let ifStatement = n.parent;
1243
+ while (ifStatement && !ts.isIfStatement(ifStatement)) {
1244
+ ifStatement = ifStatement.parent;
1245
+ }
1246
+ if (ifStatement && ts.isIfStatement(ifStatement)) {
1247
+ const thenBlock = ifStatement.thenStatement;
1248
+ if (isPromiseHandledInBlock(thenBlock, symbol)) {
1249
+ result.hasInstanceOf = true;
1250
+ return;
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+ }
1256
+ }
1257
+ if (options.stopAtFunctionBoundary && isFunctionLikeDeclaration(n)) {
1258
+ return;
1259
+ }
1260
+ ts.forEachChild(n, visit);
1261
+ };
1262
+ visit(node);
1263
+ return result;
1264
+ };
1265
+ // 检查符号是否作为参数传递给会处理 Promise 的函数
1266
+ const isSymbolPassedToHandlingFunction = (symbol, scope) => {
1267
+ let found = false;
1268
+ const visit = (node) => {
1269
+ if (found)
1270
+ return;
1271
+ if (ts.isCallExpression(node)) {
1272
+ // 检查直接参数传递
1273
+ for (let i = 0; i < node.arguments.length; i++) {
1274
+ const arg = node.arguments[i];
1275
+ if (ts.isIdentifier(arg)) {
1276
+ const argSymbol = getSymbolCached(arg);
1277
+ if (argSymbol === symbol) {
1278
+ const signature = typeChecker.getResolvedSignature(node);
1279
+ if (signature) {
1280
+ const declaration = signature.getDeclaration();
1281
+ if (declaration) {
1282
+ const calledSymbol = getSymbolOfDeclaration(declaration);
1283
+ if (calledSymbol) {
1284
+ const paramHandling = getFunctionParameterHandling(calledSymbol);
1285
+ if (paramHandling.get(i) === true) {
1286
+ found = true;
1287
+ return;
1288
+ }
1289
+ // 处理剩余参数
1290
+ if ('parameters' in declaration) {
1291
+ const params = declaration.parameters;
1292
+ if (params.length > 0) {
1293
+ const lastParam = params[params.length - 1];
1294
+ if (lastParam?.dotDotDotToken && i >= params.length - 1) {
1295
+ if (paramHandling.get(params.length - 1) === true) {
1296
+ found = true;
1297
+ return;
1298
+ }
1299
+ }
1300
+ }
1301
+ }
1302
+ }
1303
+ }
1304
+ }
1305
+ }
1306
+ }
1307
+ }
1308
+ // 检查回调函数中的处理
1309
+ for (const arg of node.arguments) {
1310
+ if (isFunctionLikeDeclaration(arg)) {
1311
+ if (arg.body && containsVariable(arg.body, symbol)) {
1312
+ const callbackCheck = checkPromiseHandlingInNode(arg.body, symbol, {
1313
+ stopAtFunctionBoundary: true
1314
+ });
1315
+ if (callbackCheck.hasAwait || callbackCheck.hasPromiseMethod ||
1316
+ callbackCheck.hasPromiseStatic || callbackCheck.hasReturn) {
1317
+ found = true;
1318
+ return;
1319
+ }
1320
+ }
1321
+ }
1322
+ }
1323
+ }
1324
+ if (isFunctionLikeDeclaration(node)) {
1325
+ return;
1326
+ }
1327
+ ts.forEachChild(node, visit);
1328
+ };
1329
+ visit(scope);
1330
+ return found;
1331
+ };
1332
+ // 检查调用节点是否在数组方法回调中,且该数组方法的结果被正确处理
1333
+ const isCallInHandledArrayMethod = (callNode) => {
1334
+ let current = callNode;
1335
+ // 向上查找,看是否在数组方法的回调中
1336
+ while (current) {
1337
+ // 跳过 return 语句
1338
+ if (ts.isReturnStatement(current)) {
1339
+ current = current.parent;
1340
+ continue;
1341
+ }
1342
+ // 检查是否是箭头函数或函数表达式
1343
+ if (isFunctionLikeDeclaration(current)) {
1344
+ const functionParent = current.parent;
1345
+ // 检查这个函数是否是数组方法的参数
1346
+ if (ts.isCallExpression(functionParent) &&
1347
+ isArrayMethodCall(functionParent, ARRAY_TRANSFORM_METHODS)) {
1348
+ // 检查这个数组方法调用是否被正确处理
1349
+ return isArrayMethodResultHandled(functionParent);
1350
+ }
1351
+ break;
1352
+ }
1353
+ current = current.parent;
1354
+ }
1355
+ return false;
1356
+ };
1357
+ // 检查数组方法的结果是否被正确处理
1358
+ const isArrayMethodResultHandled = (arrayMethodCall) => {
1359
+ let current = arrayMethodCall;
1360
+ while (current) {
1361
+ const currentParent = current.parent;
1362
+ // 跳过透明包装
1363
+ if (isTransparentWrapper(currentParent)) {
1364
+ current = currentParent;
1365
+ continue;
1366
+ }
1367
+ // 情况1:直接作为 Promise 静态方法的参数
1368
+ if (ts.isCallExpression(currentParent) && isPromiseStaticMethodCall(currentParent)) {
1369
+ return true;
1370
+ }
1371
+ // 情况2:赋值给变量
1372
+ if (ts.isVariableDeclaration(currentParent) &&
1373
+ currentParent.initializer === current) {
1374
+ if (currentParent.name && ts.isIdentifier(currentParent.name)) {
1375
+ const variableSymbol = getSymbolCached(currentParent.name);
1376
+ if (variableSymbol) {
1377
+ return isVariablePassedToPromiseAll(variableSymbol, currentParent);
1378
+ }
1379
+ }
1380
+ }
1381
+ // 情况3:作为属性赋值(暂不处理)
1382
+ if (ts.isPropertyAssignment(currentParent) &&
1383
+ currentParent.initializer === current) {
1384
+ return false;
1385
+ }
1386
+ break;
1387
+ }
1388
+ return false;
1389
+ };
1390
+ // 检查变量是否被传给 Promise.all
1391
+ const isVariablePassedToPromiseAll = (symbol, declarationNode) => {
1392
+ // 使用函数作用域而不是块作用域,以便找到更远的 Promise.all 调用
1393
+ const scope = findNearestFunctionScope(declarationNode);
1394
+ if (!scope) {
1395
+ return false;
1396
+ }
1397
+ let found = false;
1398
+ const visit = (node) => {
1399
+ if (found)
1400
+ return;
1401
+ if (ts.isIfStatement(node)) {
1402
+ const condition = node.expression;
1403
+ if (ts.isBinaryExpression(condition) &&
1404
+ condition.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) {
1405
+ // 检查条件左侧是否引用了目标变量(支持数组元素访问)
1406
+ const left = condition.left;
1407
+ let isTargetVariable = false;
1408
+ if (ts.isIdentifier(left)) {
1409
+ const leftSymbol = getSymbolCached(left);
1410
+ isTargetVariable = leftSymbol === symbol;
1411
+ }
1412
+ else if (ts.isElementAccessExpression(left)) {
1413
+ // 处理 childLegalAuths[0] 这种情况
1414
+ if (ts.isIdentifier(left.expression)) {
1415
+ const arraySymbol = getSymbolCached(left.expression);
1416
+ isTargetVariable = arraySymbol === symbol;
1417
+ }
1418
+ }
1419
+ // 检查条件右侧是否是 Promise
1420
+ const right = condition.right;
1421
+ if (isTargetVariable && ts.isIdentifier(right) && right.text === 'Promise') {
1422
+ // 在 then 分支中递归查找 Promise.all
1423
+ const thenBlock = node.thenStatement;
1424
+ const visitThen = (n) => {
1425
+ if (found)
1426
+ return;
1427
+ if (ts.isCallExpression(n) && isPromiseStaticMethodCall(n)) {
1428
+ for (const arg of n.arguments) {
1429
+ if (ts.isIdentifier(arg)) {
1430
+ const argSymbol = getSymbolCached(arg);
1431
+ if (argSymbol === symbol) {
1432
+ found = true;
1433
+ return;
1434
+ }
1435
+ }
1436
+ if (ts.isArrayLiteralExpression(arg)) {
1437
+ for (const element of arg.elements) {
1438
+ if (ts.isSpreadElement(element) &&
1439
+ ts.isIdentifier(element.expression)) {
1440
+ const spreadSymbol = getSymbolCached(element.expression);
1441
+ if (spreadSymbol === symbol) {
1442
+ found = true;
1443
+ return;
1444
+ }
1445
+ }
1446
+ }
1447
+ }
1448
+ }
1449
+ }
1450
+ ts.forEachChild(n, visitThen);
1451
+ };
1452
+ visitThen(thenBlock);
1453
+ if (found)
1454
+ return;
1455
+ }
1456
+ }
1457
+ }
1458
+ // 检查是否是 Promise 静态方法调用
1459
+ if (ts.isCallExpression(node) && isPromiseStaticMethodCall(node)) {
1460
+ // 检查参数中是否包含该变量
1461
+ for (const arg of node.arguments) {
1462
+ if (ts.isIdentifier(arg)) {
1463
+ const argSymbol = getSymbolCached(arg);
1464
+ if (argSymbol === symbol) {
1465
+ found = true;
1466
+ return;
1467
+ }
1468
+ }
1469
+ // 检查展开运算符:Promise.all([...variable])
1470
+ if (ts.isArrayLiteralExpression(arg)) {
1471
+ for (const element of arg.elements) {
1472
+ if (ts.isSpreadElement(element) &&
1473
+ ts.isIdentifier(element.expression)) {
1474
+ const spreadSymbol = getSymbolCached(element.expression);
1475
+ if (spreadSymbol === symbol) {
1476
+ found = true;
1477
+ return;
1478
+ }
1479
+ }
1480
+ }
1481
+ }
1482
+ }
1483
+ }
1484
+ // 不进入嵌套函数
1485
+ if (node !== scope && isFunctionLikeDeclaration(node)) {
1486
+ return;
1487
+ }
1488
+ ts.forEachChild(node, visit);
1489
+ };
1490
+ visit(scope);
1491
+ return found;
1492
+ };
1493
+ // 检查在代码块中是否处理了 Promise
1494
+ const isPromiseHandledInBlock = (block, symbol) => {
1495
+ const check = checkPromiseHandlingInNode(block, symbol, { stopAtFunctionBoundary: true });
1496
+ return !!(check.hasAwait || check.hasPromiseMethod || check.hasPromiseStatic || check.hasReturn);
1497
+ };
1498
+ // 综合检查:Promise 是否被正确处理
1499
+ const isPromiseProperlyHandled = (symbol, declarationNode) => {
1500
+ // 检查缓存
1501
+ if (variableHandlingCache.has(symbol)) {
1502
+ return variableHandlingCache.get(symbol);
1503
+ }
1504
+ // 防止循环检查
1505
+ if (checkingVariables.has(symbol)) {
1506
+ return false;
1507
+ }
1508
+ checkingVariables.add(symbol);
1509
+ try {
1510
+ const scope = findNearestFunctionScope(declarationNode);
1511
+ if (!scope) {
1512
+ variableHandlingCache.set(symbol, false);
1513
+ return false;
1514
+ }
1515
+ const funcLike = scope;
1516
+ if (!funcLike.body) {
1517
+ variableHandlingCache.set(symbol, false);
1518
+ return false;
1519
+ }
1520
+ // 使用统一的检查函数
1521
+ const check = checkPromiseHandlingWithInstanceOf(funcLike.body, symbol, {
1522
+ stopAtFunctionBoundary: true
1523
+ });
1524
+ // 使用统一的参数传递检查
1525
+ const passedToFunction = isSymbolPassedToHandlingFunction(symbol, funcLike.body);
1526
+ const result = !!(check.hasAwait ||
1527
+ check.hasPromiseMethod ||
1528
+ check.hasPromiseStatic ||
1529
+ check.hasReturn ||
1530
+ check.hasInstanceOf ||
1531
+ passedToFunction);
1532
+ variableHandlingCache.set(symbol, result);
1533
+ return result;
1534
+ }
1535
+ finally {
1536
+ checkingVariables.delete(symbol);
1537
+ }
1538
+ };
1539
+ // 分析函数参数是否在函数体内被正确处理
1540
+ const analyzeParameterHandling = (functionSymbol, declaration) => {
1541
+ const parameterHandling = new Map();
1542
+ if (!declaration.parameters || declaration.parameters.length === 0) {
1543
+ return parameterHandling;
1544
+ }
1545
+ // 获取函数体
1546
+ const body = declaration.body;
1547
+ if (!body) {
1548
+ return parameterHandling;
1549
+ }
1550
+ // 分析每个参数
1551
+ declaration.parameters.forEach((param, index) => {
1552
+ if (!param.name) {
1553
+ parameterHandling.set(index, false);
1554
+ return;
1555
+ }
1556
+ // 处理标识符参数
1557
+ if (ts.isIdentifier(param.name)) {
1558
+ const paramSymbol = typeChecker.getSymbolAtLocation(param.name);
1559
+ if (paramSymbol) {
1560
+ const isHandled = isPromiseProperlyHandled(paramSymbol, body);
1561
+ parameterHandling.set(index, isHandled);
1562
+ return;
1563
+ }
1564
+ }
1565
+ // 处理解构参数 - 保守处理,标记为未处理
1566
+ // TODO: 可以进一步分析解构后的变量是否被处理
1567
+ parameterHandling.set(index, false);
1568
+ });
1569
+ return parameterHandling;
1570
+ };
1571
+ // 获取函数参数处理信息(带缓存)
1572
+ const getFunctionParameterHandling = (functionSymbol) => {
1573
+ if (functionParameterHandling.has(functionSymbol)) {
1574
+ return functionParameterHandling.get(functionSymbol);
1575
+ }
1576
+ const declaration = functionDeclarations.get(functionSymbol);
1577
+ if (!declaration) {
1578
+ return new Map();
1579
+ }
1580
+ const handling = analyzeParameterHandling(functionSymbol, declaration);
1581
+ functionParameterHandling.set(functionSymbol, handling);
1582
+ return handling;
1583
+ };
1584
+ // 检查调用是否作为参数传递给会处理 Promise 的函数
1585
+ const isCallPassedToHandlingFunction = (callNode) => {
1586
+ const parent = callNode.parent;
1587
+ // 检查是否作为函数调用的参数
1588
+ if (ts.isCallExpression(parent)) {
1589
+ const argIndex = parent.arguments.indexOf(callNode);
1590
+ if (argIndex === -1) {
1591
+ return false;
1592
+ }
1593
+ const signature = typeChecker.getResolvedSignature(parent);
1594
+ if (!signature) {
1595
+ return false;
1596
+ }
1597
+ const declaration = signature.getDeclaration();
1598
+ if (!declaration) {
1599
+ return false;
1600
+ }
1601
+ const calledSymbol = getSymbolOfDeclaration(declaration);
1602
+ if (!calledSymbol) {
1603
+ return false;
1604
+ }
1605
+ // 检查剩余参数
1606
+ if ('parameters' in declaration) {
1607
+ const params = declaration.parameters;
1608
+ const lastParam = params[params.length - 1];
1609
+ if (lastParam?.dotDotDotToken && argIndex >= params.length - 1) {
1610
+ const paramHandling = getFunctionParameterHandling(calledSymbol);
1611
+ return paramHandling.get(params.length - 1) === true;
1612
+ }
1613
+ }
1614
+ // 检查该函数是否会处理这个参数位置的 Promise
1615
+ const paramHandling = getFunctionParameterHandling(calledSymbol);
1616
+ return paramHandling.get(argIndex) === true;
1617
+ }
1618
+ // 检查是否作为数组元素传递给 Promise.all 等
1619
+ if (ts.isArrayLiteralExpression(parent)) {
1620
+ const grandParent = parent.parent;
1621
+ if (ts.isCallExpression(grandParent) && isPromiseStaticMethodCall(grandParent)) {
1622
+ return true;
1623
+ }
1624
+ }
1625
+ // 检查是否在数组方法回调中返回,且该数组方法的结果被正确处理
1626
+ return isCallInHandledArrayMethod(callNode);
1627
+ };
1628
+ const checkI18nKeys = (pwd, callSet, program, typeChecker) => {
1629
+ const diagnostics = [];
1630
+ const addDiagnostic = (node, messageText, code, options) => {
1631
+ const sourceFile = node.getSourceFile();
1632
+ diagnostics.push({
1633
+ file: sourceFile,
1634
+ start: node.getStart(sourceFile),
1635
+ length: node.getWidth(sourceFile),
1636
+ messageText,
1637
+ category: ts.DiagnosticCategory.Warning,
1638
+ code,
1639
+ reason: options?.reason,
1640
+ reasonDetails: options?.reasonDetails
1641
+ });
1642
+ };
1643
+ const groupedCalls = new Map(); // 按照文件路径分组,这样方便后面去读取 i18n 文件
1644
+ const commonLocaleCache = {}; // 公共缓存,避免重复读取文件
1645
+ const entityLocaleCache = {}; // 实体缓存,避免重复读取文件
1646
+ const commonLocaleKeyRegex = /^([a-zA-Z0-9_.-]+)::/; // 以语言代码开头,后面跟两个冒号
1647
+ const entityLocaleKeyRegex = /^([a-zA-Z0-9_.-]+):/; // 以实体代码开头,后面跟一个冒号
1648
+ callSet.forEach(callNode => {
1649
+ const sourceFile = callNode.getSourceFile();
1650
+ const filePath = path.normalize(path.relative(pwd, sourceFile.fileName));
1651
+ if (!groupedCalls.has(filePath)) {
1652
+ groupedCalls.set(filePath, []);
1653
+ }
1654
+ groupedCalls.get(filePath).push(callNode);
1655
+ });
1656
+ const getCommonLocaleData = (namespace) => {
1657
+ if (commonLocaleCache.hasOwnProperty(namespace)) {
1658
+ return commonLocaleCache[namespace];
1659
+ }
1660
+ // 尝试加载公共 i18n 文件,在pwd/src/locales/${namespace}/???.json
1661
+ const localeDir = path.join(pwd, 'src', 'locales', namespace);
1662
+ if (fs.existsSync(localeDir) && fs.statSync(localeDir).isDirectory()) {
1663
+ const localeData = {};
1664
+ LOCALE_FILE_NAMES.forEach(fileName => {
1665
+ const filePath = path.join(localeDir, fileName);
1666
+ if (fs.existsSync(filePath)) {
1667
+ try {
1668
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
1669
+ const data = JSON.parse(fileContent);
1670
+ Object.assign(localeData, data);
1671
+ }
1672
+ catch (error) {
1673
+ console.error(`Error reading or parsing common i18n file: ${filePath}`, error);
1674
+ }
1675
+ }
1676
+ });
1677
+ commonLocaleCache[namespace] = localeData;
1678
+ return localeData;
1679
+ }
1680
+ else {
1681
+ commonLocaleCache[namespace] = null;
1682
+ return null;
1683
+ }
1684
+ };
1685
+ const getEntityLocaleData = (entity) => {
1686
+ if (entityLocaleCache.hasOwnProperty(entity)) {
1687
+ return entityLocaleCache[entity];
1688
+ }
1689
+ // 尝试加载实体 i18n 文件,在pwd/src/oak-app-domain/${大学开头entity}/locales/zh_CN.json 这里一定是zh_CN.json
1690
+ const entityDir = path.join(pwd, 'src', 'oak-app-domain', (0, lodash_1.upperFirst)(entity), 'locales');
1691
+ if (fs.existsSync(entityDir) && fs.statSync(entityDir).isDirectory()) {
1692
+ const localeData = {};
1693
+ try {
1694
+ const filePath = path.join(entityDir, 'zh_CN.json');
1695
+ if (fs.existsSync(filePath)) {
1696
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
1697
+ const data = JSON.parse(fileContent);
1698
+ Object.assign(localeData, data);
1699
+ }
1700
+ }
1701
+ catch (error) {
1702
+ console.error(`Error reading or parsing entity i18n file: ${entityDir}`, error);
1703
+ }
1704
+ entityLocaleCache[entity] = localeData;
1705
+ return localeData;
1706
+ }
1707
+ else {
1708
+ entityLocaleCache[entity] = null;
1709
+ return null;
1710
+ }
1711
+ };
1712
+ /**
1713
+ * 递归解析二元表达式(字符串拼接)
1714
+ */
1715
+ const parseBinaryExpression = (expr, typeChecker) => {
1716
+ // 如果是二元表达式且是 + 运算符
1717
+ if (ts.isBinaryExpression(expr) &&
1718
+ expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
1719
+ // 递归解析左右两侧
1720
+ const leftParts = parseBinaryExpression(expr.left, typeChecker);
1721
+ const rightParts = parseBinaryExpression(expr.right, typeChecker);
1722
+ if (leftParts === null || rightParts === null) {
1723
+ return null;
1724
+ }
1725
+ return [...leftParts, ...rightParts];
1726
+ }
1727
+ // 基础情况:单个表达式
1728
+ if (ts.isStringLiteral(expr)) {
1729
+ // 字符串字面量
1730
+ return [{
1731
+ values: [expr.text],
1732
+ isLiteral: true,
1733
+ expression: expr
1734
+ }];
1735
+ }
1736
+ else if (ts.isNoSubstitutionTemplateLiteral(expr)) {
1737
+ // 模板字面量(无替换)
1738
+ return [{
1739
+ values: [expr.text],
1740
+ isLiteral: true,
1741
+ expression: expr
1742
+ }];
1743
+ }
1744
+ else {
1745
+ // 其他表达式(变量、属性访问等)
1746
+ const analysis = analyzeExpressionType(expr, typeChecker);
1747
+ if (analysis.isLiteralUnion) {
1748
+ // 新增:过滤掉需要忽略的字段值
1749
+ const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
1750
+ return [{
1751
+ values: filteredValues.length > 0 ? filteredValues : null,
1752
+ isLiteral: false,
1753
+ expression: expr,
1754
+ analysis
1755
+ }];
1756
+ }
1757
+ else if (analysis.isString) {
1758
+ // string 类型,无法确定
1759
+ return [{
1760
+ values: null,
1761
+ isLiteral: false,
1762
+ expression: expr,
1763
+ analysis
1764
+ }];
1765
+ }
1766
+ else if (analysis.isNullable) {
1767
+ // 可能为 null/undefined
1768
+ return [{
1769
+ values: [''], // 空字符串表示 null/undefined
1770
+ isLiteral: false,
1771
+ expression: expr,
1772
+ analysis
1773
+ }];
1774
+ }
1775
+ else {
1776
+ // 其他类型,无法处理
1777
+ return null;
1778
+ }
1779
+ }
1780
+ };
1781
+ /**
1782
+ * 生成字符串拼接的所有可能组合
1783
+ */
1784
+ const generateConcatenationVariants = (parts) => {
1785
+ // 如果任何部分无法确定,返回 null
1786
+ if (parts.some(part => part.values === null)) {
1787
+ return null;
1788
+ }
1789
+ const results = [];
1790
+ const generate = (index, current) => {
1791
+ if (index >= parts.length) {
1792
+ results.push(current);
1793
+ return;
1794
+ }
1795
+ const part = parts[index];
1796
+ const values = part.values;
1797
+ for (const value of values) {
1798
+ generate(index + 1, current + value);
1799
+ }
1800
+ };
1801
+ generate(0, '');
1802
+ return results;
1803
+ };
1804
+ /**
1805
+ * 检查二元表达式(字符串拼接)的 i18n key
1806
+ */
1807
+ const checkBinaryExpressionKey = (binaryExpr, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString) => {
1808
+ const sourceFile = callNode.getSourceFile();
1809
+ // 解析二元表达式
1810
+ const parts = parseBinaryExpression(binaryExpr, typeChecker);
1811
+ if (parts === null) {
1812
+ // 无法解析
1813
+ addDiagnostic(callNode, `i18n key 使用了复杂的字符串拼接表达式,无法进行检查。`, 9206, {
1814
+ reason: 'ComplexConcatenation',
1815
+ reasonDetails: [
1816
+ `✘ 表达式 ${binaryExpr.getText(sourceFile)} 过于复杂`
1817
+ ]
1818
+ });
1819
+ return;
1820
+ }
1821
+ // 检查是否有无法确定的部分
1822
+ const uncheckableParts = parts.filter(part => part.values === null);
1823
+ const nullableParts = parts.filter(part => part.analysis?.isNullable && !part.analysis?.hasNonNullAssertion);
1824
+ if (uncheckableParts.length > 0) {
1825
+ // 有无法确定的部分
1826
+ const details = [];
1827
+ details.push('¡ 字符串拼接包含无法确定范围的部分:');
1828
+ details.push('');
1829
+ parts.forEach(part => {
1830
+ const exprText = part.expression.getText(sourceFile);
1831
+ if (part.values === null) {
1832
+ details.push(` ✘ ${exprText} - 类型为 string,范围太大`);
1833
+ }
1834
+ else if (part.isLiteral) {
1835
+ details.push(` ✓ "${part.values[0]}" - 字面量`);
1836
+ }
1837
+ else if (part.analysis?.isLiteralUnion) {
1838
+ details.push(` ✓ ${exprText} - 字面量联合类型: ${part.values.map(v => `"${v}"`).join(' | ')}`);
1839
+ }
1840
+ else if (part.analysis?.isNullable) {
1841
+ const hasAssertion = part.analysis.hasNonNullAssertion;
1842
+ details.push(` ${hasAssertion ? '✓' : '✘'} ${exprText} - 可能为 null/undefined${hasAssertion ? ' (有非空断言)' : ''}`);
1843
+ }
1844
+ });
1845
+ addDiagnostic(callNode, `i18n key 使用了字符串拼接,但包含无法确定范围的部分,无法进行完整检查。`, 9203, {
1846
+ reason: 'UncheckableConcatenation',
1847
+ reasonDetails: details
1848
+ });
1849
+ return;
1850
+ }
1851
+ // 生成所有可能的组合
1852
+ const keyVariants = generateConcatenationVariants(parts);
1853
+ if (keyVariants === null) {
1854
+ // 理论上不应该到这里,因为前面已经检查过了
1855
+ return;
1856
+ }
1857
+ // 检查所有可能的 key
1858
+ const missingKeys = [];
1859
+ const foundKeys = [];
1860
+ for (const key of keyVariants) {
1861
+ const result = isKeyExistsInI18nData(i18nData, key);
1862
+ const commonResult = result.exists ? result : checkWithCommonStringExists(i18nData, key);
1863
+ const exists = commonResult.exists;
1864
+ // 如果找到了 key,检查占位符
1865
+ if (exists && commonResult.placeholders.length > 0) {
1866
+ checkSecondArgument(callNode, commonResult.placeholders, addDiagnostic);
1867
+ }
1868
+ if (exists) {
1869
+ foundKeys.push(key);
1870
+ }
1871
+ else {
1872
+ missingKeys.push(key);
1873
+ }
1874
+ }
1875
+ // 如果有 nullable 的情况,需要特别处理
1876
+ if (nullableParts.length > 0) {
1877
+ if (missingKeys.length > 0) {
1878
+ const details = [];
1879
+ details.push(`¡ 字符串拼接包含可能为 null/undefined 的部分`);
1880
+ nullableParts.forEach(part => {
1881
+ const exprText = part.expression.getText(sourceFile);
1882
+ const hasAssertion = part.analysis?.hasNonNullAssertion ? ' (有非空断言)' : '';
1883
+ details.push(` ${part.analysis?.hasNonNullAssertion ? '✓' : '✘'} ${exprText}${hasAssertion}`);
1884
+ });
1885
+ details.push('');
1886
+ details.push('需要检查的 key 变体:');
1887
+ missingKeys.forEach(key => {
1888
+ details.push(` ✘ "${key}" - 未找到`);
1889
+ });
1890
+ foundKeys.forEach(key => {
1891
+ details.push(` ✓ "${key}" - 已找到`);
1892
+ });
1893
+ addDiagnostic(callNode, `i18n key 字符串拼接的某些变体未找到: ${missingKeys.join(', ')}`, 9200, {
1894
+ reason: 'MissingConcatenationVariants',
1895
+ reasonDetails: details
1896
+ });
1897
+ }
1898
+ }
1899
+ else {
1900
+ // 没有 nullable,所有 key 都必须存在
1901
+ if (missingKeys.length > 0) {
1902
+ const details = [];
1903
+ if (keyVariants.length > 1) {
1904
+ details.push(`¡ 字符串拼接有 ${keyVariants.length} 个可能的变体`);
1905
+ details.push('');
1906
+ details.push('拼接部分:');
1907
+ parts.forEach(part => {
1908
+ const exprText = part.expression.getText(sourceFile);
1909
+ if (part.isLiteral) {
1910
+ details.push(` ✓ "${part.values[0]}" - 字面量`);
1911
+ }
1912
+ else if (part.analysis?.isLiteralUnion) {
1913
+ details.push(` ✓ ${exprText} - 可能值: ${part.values.map(v => `"${v}"`).join(' | ')}`);
1914
+ }
1915
+ });
1916
+ details.push('');
1917
+ }
1918
+ missingKeys.forEach(key => {
1919
+ details.push(` ✘ "${key}" - 未找到`);
1920
+ });
1921
+ foundKeys.forEach(key => {
1922
+ details.push(` ✓ "${key}" - 已找到`);
1923
+ });
1924
+ addDiagnostic(callNode, `i18n key 字符串拼接的某些变体未找到: ${missingKeys.join(', ')}`, 9200, {
1925
+ reason: 'MissingConcatenationVariants',
1926
+ reasonDetails: details
1927
+ });
1928
+ }
1929
+ }
1930
+ };
1931
+ /**
1932
+ * 生成模板字符串的所有可能组合
1933
+ */
1934
+ const generateKeyVariants = (head, spans) => {
1935
+ // 如果任何一个 span 的 values 为 null(表示无法确定),返回 null
1936
+ if (spans.some(span => span.values === null)) {
1937
+ return null;
1938
+ }
1939
+ const results = [];
1940
+ const generate = (index, current) => {
1941
+ if (index >= spans.length) {
1942
+ results.push(current);
1943
+ return;
1944
+ }
1945
+ const span = spans[index];
1946
+ const values = span.values;
1947
+ for (const value of values) {
1948
+ generate(index + 1, current + value + span.text);
1949
+ }
1950
+ };
1951
+ generate(0, head);
1952
+ return results;
1953
+ };
1954
+ /**
1955
+ * 检查字段名是否匹配忽略模式
1956
+ */
1957
+ const shouldIgnoreI18nKeyField = (fieldName) => {
1958
+ // 检查精确匹配
1959
+ if (IGNORED_I18N_KEY_PATTERNS.exact.includes(fieldName)) {
1960
+ return true;
1961
+ }
1962
+ // 检查前缀匹配
1963
+ if (IGNORED_I18N_KEY_PATTERNS.startsWith.some(prefix => fieldName.startsWith(prefix))) {
1964
+ return true;
1965
+ }
1966
+ // 检查后缀匹配
1967
+ if (IGNORED_I18N_KEY_PATTERNS.endsWith.some(suffix => fieldName.endsWith(suffix))) {
1968
+ return true;
1969
+ }
1970
+ return false;
1971
+ };
1972
+ /**
1973
+ * 检查模板表达式中的 i18n key
1974
+ */
1975
+ const checkTemplateExpressionKey = (templateExpr, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString) => {
1976
+ const head = templateExpr.head.text;
1977
+ const spans = [];
1978
+ let hasUncheckableSpan = false;
1979
+ let hasNullableSpan = false;
1980
+ const spanAnalyses = [];
1981
+ // 分析每个模板片段
1982
+ for (const span of templateExpr.templateSpans) {
1983
+ const analysis = analyzeExpressionType(span.expression, typeChecker);
1984
+ spanAnalyses.push({ span, analysis });
1985
+ if (analysis.isLiteralUnion) {
1986
+ // 字面量联合类型,可以检查
1987
+ const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
1988
+ spans.push({
1989
+ text: span.literal.text,
1990
+ values: filteredValues.length > 0 ? filteredValues : null // 如果过滤后为空,设为 null
1991
+ });
1992
+ }
1993
+ else if (analysis.isString) {
1994
+ // string 类型,无法检查
1995
+ hasUncheckableSpan = true;
1996
+ spans.push({
1997
+ text: span.literal.text,
1998
+ values: null
1999
+ });
2000
+ }
2001
+ else if (analysis.isNullable) {
2002
+ // 可能为 null/undefined
2003
+ hasNullableSpan = true;
2004
+ spans.push({
2005
+ text: span.literal.text,
2006
+ values: [''] // 空字符串表示 null/undefined 的情况
2007
+ });
2008
+ }
2009
+ else {
2010
+ // 其他类型,尝试获取字符串表示
2011
+ const typeString = typeChecker.typeToString(analysis.originalType);
2012
+ hasUncheckableSpan = true;
2013
+ spans.push({
2014
+ text: span.literal.text,
2015
+ values: null
2016
+ });
2017
+ }
2018
+ }
2019
+ // 生成所有可能的 key 组合
2020
+ const keyVariants = generateKeyVariants(head, spans);
2021
+ if (keyVariants === null) {
2022
+ // 有无法确定的占位符
2023
+ if (hasUncheckableSpan) {
2024
+ const sourceFile = callNode.getSourceFile();
2025
+ const { line } = sourceFile.getLineAndCharacterOfPosition(templateExpr.getStart());
2026
+ addDiagnostic(callNode, `i18n key 使用了模板字符串,但包含无法确定范围的占位符变量(类型为 string),无法进行完整检查。`, 9203, {
2027
+ reason: 'UncheckableTemplate',
2028
+ reasonDetails: spanAnalyses
2029
+ .filter(({ analysis }) => analysis.isString)
2030
+ .map(({ span }) => {
2031
+ const exprText = span.expression.getText(sourceFile);
2032
+ return `✘ 占位符 \${${exprText}} 的类型为 string,范围太大`;
2033
+ })
2034
+ });
2035
+ }
2036
+ return;
2037
+ }
2038
+ // 检查所有可能的 key
2039
+ const missingKeys = [];
2040
+ const foundKeys = [];
2041
+ for (const key of keyVariants) {
2042
+ const result = isKeyExistsInI18nData(i18nData, key);
2043
+ const commonResult = result.exists ? result : checkWithCommonStringExists(i18nData, key);
2044
+ const exists = commonResult.exists;
2045
+ // 如果找到了 key,检查占位符
2046
+ if (exists && commonResult.placeholders.length > 0) {
2047
+ checkSecondArgument(callNode, commonResult.placeholders, addDiagnostic);
2048
+ }
2049
+ if (exists) {
2050
+ foundKeys.push(key);
2051
+ }
2052
+ else {
2053
+ missingKeys.push(key);
2054
+ }
2055
+ }
2056
+ // 如果有 nullable 的情况,需要特别处理
2057
+ if (hasNullableSpan) {
2058
+ const nullableSpans = spanAnalyses.filter(({ analysis }) => analysis.isNullable);
2059
+ if (missingKeys.length > 0) {
2060
+ const sourceFile = callNode.getSourceFile();
2061
+ const details = [];
2062
+ details.push(`¡ 模板字符串包含可能为 null/undefined 的占位符`);
2063
+ nullableSpans.forEach(({ span, analysis }) => {
2064
+ const exprText = span.expression.getText(sourceFile);
2065
+ const hasAssertion = analysis.hasNonNullAssertion ? ' (有非空断言)' : '';
2066
+ details.push(` ${analysis.hasNonNullAssertion ? '✓' : '✘'} \${${exprText}}${hasAssertion}`);
2067
+ });
2068
+ details.push('');
2069
+ details.push('需要检查的 key 变体:');
2070
+ missingKeys.forEach(key => {
2071
+ details.push(` ✘ "${key}" - 未找到`);
2072
+ });
2073
+ foundKeys.forEach(key => {
2074
+ details.push(` ✓ "${key}" - 已找到`);
2075
+ });
2076
+ addDiagnostic(callNode, `i18n key 模板字符串的某些变体未找到: ${missingKeys.join(', ')}`, 9200, {
2077
+ reason: 'MissingTemplateVariants',
2078
+ reasonDetails: details
2079
+ });
2080
+ }
2081
+ }
2082
+ else {
2083
+ // 没有 nullable,所有 key 都必须存在
2084
+ if (missingKeys.length > 0) {
2085
+ const sourceFile = callNode.getSourceFile();
2086
+ const details = [];
2087
+ if (keyVariants.length > 1) {
2088
+ details.push(`¡ 模板字符串有 ${keyVariants.length} 个可能的变体`);
2089
+ details.push('');
2090
+ }
2091
+ missingKeys.forEach(key => {
2092
+ details.push(` ✘ "${key}" - 未找到`);
2093
+ });
2094
+ foundKeys.forEach(key => {
2095
+ details.push(` ✓ "${key}" - 已找到`);
2096
+ });
2097
+ addDiagnostic(callNode, `i18n key 模板字符串的某些变体未找到: ${missingKeys.join(', ')}`, 9200, {
2098
+ reason: 'MissingTemplateVariants',
2099
+ reasonDetails: details
2100
+ });
2101
+ }
2102
+ }
2103
+ };
2104
+ /**
2105
+ * 检查是否存在(不添加诊断,仅返回结果)
2106
+ */
2107
+ const checkWithCommonStringExists = (i18nData, key) => {
2108
+ if (commonLocaleKeyRegex.test(key)) {
2109
+ const parts = commonLocaleKeyRegex.exec(key);
2110
+ if (parts && parts.length >= 2) {
2111
+ const namespace = parts[1];
2112
+ const localeData = getCommonLocaleData(namespace);
2113
+ const actualKey = key.substring(namespace.length + 2);
2114
+ return isKeyExistsInI18nData(localeData, actualKey);
2115
+ }
2116
+ }
2117
+ else if (entityLocaleKeyRegex.test(key)) {
2118
+ const parts = entityLocaleKeyRegex.exec(key);
2119
+ if (parts && parts.length >= 2) {
2120
+ const entity = parts[1];
2121
+ const localeData = getEntityLocaleData(entity);
2122
+ const actualKey = key.substring(entity.length + 1);
2123
+ return isKeyExistsInI18nData(localeData, actualKey);
2124
+ }
2125
+ }
2126
+ else {
2127
+ return isKeyExistsInI18nData(i18nData, key);
2128
+ }
2129
+ return { exists: false, placeholders: [] };
2130
+ };
2131
+ /**
2132
+ * 检查 t 函数的第二个参数是否提供了所需的占位符
2133
+ */
2134
+ const checkSecondArgument = (callNode, placeholders, addDiagnostic) => {
2135
+ if (placeholders.length === 0) {
2136
+ return; // 没有占位符,不需要检查
2137
+ }
2138
+ const args = callNode.arguments;
2139
+ if (args.length < 2) {
2140
+ // 缺少第二个参数
2141
+ addDiagnostic(callNode, `i18n 值包含占位符 ${placeholders.map(p => `%{${p}}`).join(', ')},但未提供第二个参数。`, 9210, {
2142
+ reason: 'MissingSecondArgument',
2143
+ reasonDetails: [
2144
+ `✘ 需要的占位符: ${placeholders.join(', ')}`,
2145
+ '建议: 添加第二个参数对象,如: t("key", { url: "..." })'
2146
+ ]
2147
+ });
2148
+ return;
2149
+ }
2150
+ const secondArg = args[1];
2151
+ // 检查第二个参数是否是对象字面量
2152
+ if (!ts.isObjectLiteralExpression(secondArg)) {
2153
+ addDiagnostic(callNode, `i18n 值包含占位符,但第二个参数不是字面对象,无法检查占位符是否提供。`, 9211, {
2154
+ reason: 'NonLiteralSecondArgument',
2155
+ reasonDetails: [
2156
+ `✘ 第二个参数类型: ${secondArg.getText()}`,
2157
+ `需要的占位符: ${placeholders.join(', ')}`,
2158
+ '建议: 使用对象字面量,如: { url: "..." }'
2159
+ ]
2160
+ });
2161
+ return;
2162
+ }
2163
+ // 提取对象字面量中的属性名
2164
+ const providedKeys = new Set();
2165
+ secondArg.properties.forEach(prop => {
2166
+ if (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) {
2167
+ if (ts.isIdentifier(prop.name)) {
2168
+ providedKeys.add(prop.name.text);
2169
+ }
2170
+ else if (ts.isStringLiteral(prop.name)) {
2171
+ providedKeys.add(prop.name.text);
2172
+ }
2173
+ else if (ts.isComputedPropertyName(prop.name)) {
2174
+ // 计算属性名,无法静态检查
2175
+ // 可以选择跳过或警告
2176
+ }
2177
+ }
2178
+ else if (ts.isSpreadAssignment(prop)) {
2179
+ // 展开运算符,无法静态检查所有属性
2180
+ // 可以选择跳过检查
2181
+ return;
2182
+ }
2183
+ });
2184
+ // 检查是否有展开运算符
2185
+ const hasSpread = secondArg.properties.some(prop => ts.isSpreadAssignment(prop));
2186
+ // 检查缺失的占位符
2187
+ const missingPlaceholders = placeholders.filter(p => !providedKeys.has(p));
2188
+ if (missingPlaceholders.length > 0) {
2189
+ const details = [];
2190
+ if (hasSpread) {
2191
+ details.push('¡ 第二个参数包含展开运算符,可能提供了额外的属性');
2192
+ }
2193
+ details.push('需要的占位符:');
2194
+ placeholders.forEach(p => {
2195
+ if (providedKeys.has(p)) {
2196
+ details.push(` ✓ ${p} - 已提供`);
2197
+ }
2198
+ else {
2199
+ details.push(` ✘ ${p} - 缺失`);
2200
+ }
2201
+ });
2202
+ if (!hasSpread) {
2203
+ addDiagnostic(callNode, `i18n 第二个参数缺少占位符: ${missingPlaceholders.join(', ')}`, 9212, {
2204
+ reason: 'MissingPlaceholders',
2205
+ reasonDetails: details
2206
+ });
2207
+ }
2208
+ else {
2209
+ // 有展开运算符,降级为警告
2210
+ addDiagnostic(callNode, `i18n 第二个参数可能缺少占位符: ${missingPlaceholders.join(', ')} (包含展开运算符,无法完全确定)`, 9213, {
2211
+ reason: 'PossiblyMissingPlaceholders',
2212
+ reasonDetails: details
2213
+ });
2214
+ }
2215
+ }
2216
+ };
2217
+ /**
2218
+ * 检查变量引用的 i18n key
2219
+ */
2220
+ const checkVariableReferenceKey = (identifier, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString) => {
2221
+ const analysis = analyzeExpressionType(identifier, typeChecker);
2222
+ const sourceFile = callNode.getSourceFile();
2223
+ const varName = identifier.getText(sourceFile);
2224
+ if (analysis.isLiteralUnion) {
2225
+ // 字面量联合类型,检查所有可能的值
2226
+ const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
2227
+ // 如果过滤后没有值了,直接返回
2228
+ if (filteredValues.length === 0) {
2229
+ return;
2230
+ }
2231
+ const missingKeys = [];
2232
+ const foundKeys = [];
2233
+ for (const value of analysis.literalValues) {
2234
+ const result = isKeyExistsInI18nData(i18nData, value);
2235
+ const commonResult = result.exists ? result : checkWithCommonStringExists(i18nData, value);
2236
+ const exists = commonResult.exists;
2237
+ if (exists) {
2238
+ foundKeys.push(value);
2239
+ // 检查占位符(对于字面量联合类型,只在所有值都找到时检查)
2240
+ if (commonResult.placeholders.length > 0) {
2241
+ checkSecondArgument(callNode, commonResult.placeholders, addDiagnostic);
2242
+ }
2243
+ }
2244
+ else {
2245
+ missingKeys.push(value);
2246
+ }
2247
+ if (exists) {
2248
+ foundKeys.push(value);
2249
+ }
2250
+ else {
2251
+ missingKeys.push(value);
2252
+ }
2253
+ }
2254
+ if (missingKeys.length > 0) {
2255
+ const details = [];
2256
+ details.push(`¡ 变量 ${varName} 的类型为: ${analysis.literalValues.map(v => `"${v}"`).join(' | ')}`);
2257
+ details.push('');
2258
+ missingKeys.forEach(key => {
2259
+ details.push(` ✘ "${key}" - 未找到`);
2260
+ });
2261
+ foundKeys.forEach(key => {
2262
+ details.push(` ✓ "${key}" - 已找到`);
2263
+ });
2264
+ addDiagnostic(callNode, `变量 ${varName} 的某些可能值在 i18n 中未找到: ${missingKeys.join(', ')}`, 9200, {
2265
+ reason: 'MissingVariableVariants',
2266
+ reasonDetails: details
2267
+ });
2268
+ }
2269
+ }
2270
+ else if (analysis.isNullable) {
2271
+ // 可能为 null/undefined
2272
+ addDiagnostic(callNode, `变量 ${varName} 可能为 null 或 undefined,这可能导致 i18n 查找失败。`, 9204, {
2273
+ reason: 'NullableVariable',
2274
+ reasonDetails: [
2275
+ `✘ 变量 ${varName} 的类型包含 null 或 undefined`,
2276
+ analysis.hasNonNullAssertion
2277
+ ? '✓ 使用了非空断言 (!)'
2278
+ : '建议: 添加非空断言或进行空值检查'
2279
+ ]
2280
+ });
2281
+ }
2282
+ else if (analysis.isString) {
2283
+ // string 类型,无法检查
2284
+ warnStringKeys && addDiagnostic(callNode, `变量 ${varName} 的类型为 string,范围太大无法检查 i18n key 是否存在。`, 9203, {
2285
+ reason: 'UncheckableVariable',
2286
+ reasonDetails: [
2287
+ `✘ 变量 ${varName} 的类型为 string`,
2288
+ '建议: 使用字面量联合类型限制可能的值,如: "key1" | "key2"'
2289
+ ]
2290
+ });
2291
+ }
2292
+ else {
2293
+ // 其他类型
2294
+ const typeString = typeChecker.typeToString(analysis.originalType);
2295
+ addDiagnostic(callNode, `变量 ${varName} 的类型 (${typeString}) 不是有效的 i18n key 类型。`, 9205, {
2296
+ reason: 'InvalidVariableType',
2297
+ reasonDetails: [
2298
+ `✘ 变量 ${varName} 的类型为: ${typeString}`,
2299
+ '期望: string 字面量或字面量联合类型'
2300
+ ]
2301
+ });
2302
+ }
2303
+ };
2304
+ const checkWithCommonString = (i18nData, key, callNode, localePath) => {
2305
+ let result;
2306
+ if (commonLocaleKeyRegex.test(key)) {
2307
+ const parts = commonLocaleKeyRegex.exec(key);
2308
+ if (parts && parts.length >= 2) {
2309
+ const namespace = parts[1];
2310
+ const localeData = getCommonLocaleData(namespace);
2311
+ const actualKey = key.substring(namespace.length + 2);
2312
+ result = isKeyExistsInI18nData(localeData, actualKey);
2313
+ if (result.exists) {
2314
+ // 检查占位符
2315
+ if (result.placeholders.length > 0) {
2316
+ checkSecondArgument(callNode, result.placeholders, addDiagnostic);
2317
+ }
2318
+ return;
2319
+ }
2320
+ else {
2321
+ addDiagnostic(callNode, `i18n key "${key}" not found in public locale files: namespace "${namespace}".`, 9200);
2322
+ return;
2323
+ }
2324
+ }
2325
+ else {
2326
+ addDiagnostic(callNode, `i18n key "${key}" has invalid format.`, 9201, {
2327
+ reason: 'InvalidFormat',
2328
+ reasonDetails: [
2329
+ `Expected format: <namespace>::<key>, e.g., common::title`
2330
+ ]
2331
+ });
2332
+ return;
2333
+ }
2334
+ }
2335
+ else if (entityLocaleKeyRegex.test(key)) {
2336
+ const parts = entityLocaleKeyRegex.exec(key);
2337
+ if (parts && parts.length >= 2) {
2338
+ const entity = parts[1];
2339
+ const localeData = getEntityLocaleData(entity);
2340
+ const actualKey = key.substring(entity.length + 1);
2341
+ result = isKeyExistsInI18nData(localeData, actualKey);
2342
+ if (result.exists) {
2343
+ // 检查占位符
2344
+ if (result.placeholders.length > 0) {
2345
+ checkSecondArgument(callNode, result.placeholders, addDiagnostic);
2346
+ }
2347
+ return;
2348
+ }
2349
+ else {
2350
+ addDiagnostic(callNode, `i18n key "${key}" not found in entity locale files: entity "${entity}".`, 9200);
2351
+ return;
2352
+ }
2353
+ }
2354
+ else {
2355
+ addDiagnostic(callNode, `i18n key "${key}" has invalid format.`, 9201, {
2356
+ reason: 'InvalidFormat',
2357
+ reasonDetails: [
2358
+ `Expected format: <entity>:<key>, e.g., user:attr.name`
2359
+ ]
2360
+ });
2361
+ return;
2362
+ }
2363
+ }
2364
+ else {
2365
+ result = isKeyExistsInI18nData(i18nData, key);
2366
+ if (result.exists) {
2367
+ // 检查占位符
2368
+ if (result.placeholders.length > 0) {
2369
+ checkSecondArgument(callNode, result.placeholders, addDiagnostic);
2370
+ }
2371
+ return;
2372
+ }
2373
+ else {
2374
+ addDiagnostic(callNode, `i18n key "${key}" not found in its locale files: ${localePath}.`, 9200);
2375
+ return;
2376
+ }
2377
+ }
2378
+ };
2379
+ // 逐文件处理
2380
+ groupedCalls.forEach((calls, filePath) => {
2381
+ // 如果这个文件同级目录下没有index.ts, 暂时不做检查
2382
+ const dirPath = path.dirname(path.resolve(pwd, filePath));
2383
+ const localePath = path.join(dirPath, 'locales');
2384
+ const indexTsPath = path.join(dirPath, 'index.ts');
2385
+ if (!fs.existsSync(indexTsPath)) {
2386
+ return;
2387
+ }
2388
+ const i18nData = loadI18nData(localePath);
2389
+ if (!i18nData) {
2390
+ // 全部加入警告
2391
+ calls.forEach(callNode => {
2392
+ // 检查是否有忽略注释
2393
+ const sourceFile = callNode.getSourceFile();
2394
+ if (hasIgnoreComment(callNode, sourceFile)) {
2395
+ return; // 有忽略注释,跳过检查
2396
+ }
2397
+ const args = callNode.arguments;
2398
+ if (args.length === 0) {
2399
+ return; // 没有参数,跳过
2400
+ }
2401
+ const firstArg = args[0];
2402
+ if (ts.isStringLiteral(firstArg)) {
2403
+ const key = firstArg.text;
2404
+ // 检查是否是 entity 或 common 格式
2405
+ if (commonLocaleKeyRegex.test(key) || entityLocaleKeyRegex.test(key)) {
2406
+ // 是 entity/common 格式,使用空对象作为本地 i18n 数据,让 checkWithCommonString 处理
2407
+ checkWithCommonString({}, key, callNode, localePath);
2408
+ }
2409
+ else {
2410
+ // 普通格式的 key,但本地没有 i18n 文件
2411
+ addDiagnostic(callNode, `i18n key "${key}" 无法检查,因为找不到locales文件: ${localePath}。`, 9202);
2412
+ }
2413
+ }
2414
+ else if (ts.isTemplateExpression(firstArg) ||
2415
+ ts.isNoSubstitutionTemplateLiteral(firstArg) ||
2416
+ ts.isIdentifier(firstArg) ||
2417
+ ts.isPropertyAccessExpression(firstArg) ||
2418
+ ts.isElementAccessExpression(firstArg)) {
2419
+ // TODO: 对于非字面量的情况,暂时跳过(因为无法确定是否是 entity/common 格式)
2420
+ // 可以考虑添加更详细的类型分析
2421
+ }
2422
+ });
2423
+ return;
2424
+ }
2425
+ // 逐调用检查
2426
+ calls.forEach(callNode => {
2427
+ // 检查是否有忽略注释
2428
+ const sourceFile = callNode.getSourceFile();
2429
+ if (hasIgnoreComment(callNode, sourceFile)) {
2430
+ return; // 有忽略注释,跳过检查
2431
+ }
2432
+ const args = callNode.arguments;
2433
+ if (args.length === 0) {
2434
+ return; // 没有参数,跳过
2435
+ }
2436
+ const firstArg = args[0];
2437
+ if (ts.isStringLiteral(firstArg)) {
2438
+ // 字符串字面量
2439
+ const key = firstArg.text;
2440
+ checkWithCommonString(i18nData, key, callNode, localePath);
2441
+ }
2442
+ else if (ts.isTemplateExpression(firstArg)) {
2443
+ // 模板字符串
2444
+ checkTemplateLiterals && checkTemplateExpressionKey(firstArg, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString);
2445
+ }
2446
+ else if (ts.isNoSubstitutionTemplateLiteral(firstArg)) {
2447
+ // 无替换的模板字面量 `key`
2448
+ const key = firstArg.text;
2449
+ checkWithCommonString(i18nData, key, callNode, localePath);
2450
+ }
2451
+ else if (ts.isIdentifier(firstArg)) {
2452
+ // 变量引用
2453
+ checkVariableReferenceKey(firstArg, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString);
2454
+ }
2455
+ else if (ts.isBinaryExpression(firstArg) &&
2456
+ firstArg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
2457
+ // 字符串拼接表达式
2458
+ warnStringKeys && checkBinaryExpressionKey(firstArg, callNode, i18nData, localePath, typeChecker, addDiagnostic, checkWithCommonString);
2459
+ }
2460
+ else if (ts.isPropertyAccessExpression(firstArg) || ts.isElementAccessExpression(firstArg)) {
2461
+ // 属性访问或元素访问,尝试分析类型
2462
+ const analysis = analyzeExpressionType(firstArg, typeChecker);
2463
+ const sourceFile = callNode.getSourceFile();
2464
+ const exprText = firstArg.getText(sourceFile);
2465
+ if (analysis.isLiteralUnion) {
2466
+ // 可以检查
2467
+ const filteredValues = analysis.literalValues.filter(value => !shouldIgnoreI18nKeyField(value));
2468
+ // 如果过滤后没有值了,直接返回
2469
+ if (filteredValues.length === 0) {
2470
+ return;
2471
+ }
2472
+ const missingKeys = [];
2473
+ const foundKeys = [];
2474
+ for (const value of analysis.literalValues) {
2475
+ const result = isKeyExistsInI18nData(i18nData, value);
2476
+ const commonResult = result.exists ? result : checkWithCommonStringExists(i18nData, value);
2477
+ const exists = commonResult.exists;
2478
+ if (exists) {
2479
+ foundKeys.push(value);
2480
+ // 检查占位符(对于字面量联合类型,只在所有值都找到时检查)
2481
+ if (commonResult.placeholders.length > 0) {
2482
+ checkSecondArgument(callNode, commonResult.placeholders, addDiagnostic);
2483
+ }
2484
+ }
2485
+ else {
2486
+ missingKeys.push(value);
2487
+ }
2488
+ if (exists) {
2489
+ foundKeys.push(value);
2490
+ }
2491
+ else {
2492
+ missingKeys.push(value);
2493
+ }
2494
+ }
2495
+ if (missingKeys.length > 0) {
2496
+ addDiagnostic(callNode, `表达式 ${exprText} 的某些可能值在 i18n 中未找到: ${missingKeys.join(', ')}`, 9200, {
2497
+ reason: 'MissingExpressionVariants',
2498
+ reasonDetails: [
2499
+ `¡ 表达式 ${exprText} 的类型为: ${analysis.literalValues.map(v => `"${v}"`).join(' | ')}`,
2500
+ '',
2501
+ ...missingKeys.map(key => ` ✘ "${key}" - 未找到`),
2502
+ ...foundKeys.map(key => ` ✓ "${key}" - 已找到`)
2503
+ ]
2504
+ });
2505
+ }
2506
+ }
2507
+ else if (analysis.isString) {
2508
+ addDiagnostic(callNode, `表达式 ${exprText} 的类型为 string,范围太大无法检查。`, 9203, {
2509
+ reason: 'UncheckableExpression',
2510
+ reasonDetails: [`✘ 表达式 ${exprText} 的类型为 string`]
2511
+ });
2512
+ }
2513
+ else if (analysis.isNullable) {
2514
+ addDiagnostic(callNode, `表达式 ${exprText} 可能为 null 或 undefined。`, 9204, {
2515
+ reason: 'NullableExpression',
2516
+ reasonDetails: [`✘ 表达式 ${exprText} 可能为 null 或 undefined`]
2517
+ });
2518
+ }
2519
+ }
2520
+ else {
2521
+ // 其他复杂表达式
2522
+ const sourceFile = callNode.getSourceFile();
2523
+ const exprText = firstArg.getText(sourceFile);
2524
+ addDiagnostic(callNode, `i18n key 参数使用了复杂表达式 (${exprText}),无法进行检查。`, 9206, {
2525
+ reason: 'ComplexExpression',
2526
+ reasonDetails: [`✘ 表达式 ${exprText} 过于复杂,无法确定其值`]
2527
+ });
2528
+ }
2529
+ });
2530
+ });
2531
+ return diagnostics;
2532
+ };
2533
+ // 信息收集
2534
+ for (const sourceFile of program.getSourceFiles()) {
2535
+ if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('node_modules')) {
2536
+ continue;
2537
+ }
2538
+ // 文件名是否符合检查范围
2539
+ if (!isFileInCheckScope(pwd, sourceFile.fileName, customConfig)) {
2540
+ continue;
2541
+ }
2542
+ preprocessIgnoreComments(sourceFile); // 预处理注释
2543
+ collectAndCheck(sourceFile); // 合并后的收集和检查
2544
+ }
2545
+ // 过滤掉非异步的 context 调用
2546
+ filterAsyncContextCalls();
2547
+ // 预分析所有函数的参数处理情况
2548
+ for (const [symbol, declaration] of functionDeclarations.entries()) {
2549
+ getFunctionParameterHandling(symbol);
2550
+ }
2551
+ // 标记传播
2552
+ propagateContextMarks();
2553
+ checkDirectContextCalls(); // 检查直接调用
2554
+ checkIndirectCalls(); // 检查间接调用(使用缓存的调用列表)
2555
+ const i18nDiagnostics = checkI18nKeys(pwd, tFunctionSymbolCache, program, typeChecker);
2556
+ return diagnostics.concat(i18nDiagnostics);
2557
+ }
2558
+ const loadI18nData = (dirPath) => {
2559
+ // 尝试加载 LOCALE_FILE_NAMES 中的文件
2560
+ for (const fileName of LOCALE_FILE_NAMES) {
2561
+ const filePath = path.join(dirPath, fileName);
2562
+ if (fs.existsSync(filePath)) {
2563
+ try {
2564
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
2565
+ const data = JSON.parse(fileContent);
2566
+ return data;
2567
+ }
2568
+ catch (error) {
2569
+ console.error(`Error reading or parsing i18n file: ${filePath}`, error);
2570
+ }
2571
+ }
2572
+ }
2573
+ return null;
2574
+ };
2575
+ /**
2576
+ * 从 i18n 值中提取占位符
2577
+ * 支持格式:%{key} 或 {{key}}
2578
+ */
2579
+ const extractPlaceholders = (value) => {
2580
+ const placeholders = [];
2581
+ // 匹配 %{xxx} 格式
2582
+ const percentPattern = /%\{([^}]+)\}/g;
2583
+ let match;
2584
+ while ((match = percentPattern.exec(value)) !== null) {
2585
+ placeholders.push(match[1]);
2586
+ }
2587
+ // 匹配 {{xxx}} 格式(如果需要)
2588
+ const bracePattern = /\{\{([^}]+)\}\}/g;
2589
+ while ((match = bracePattern.exec(value)) !== null) {
2590
+ if (!placeholders.includes(match[1])) {
2591
+ placeholders.push(match[1]);
2592
+ }
2593
+ }
2594
+ return placeholders;
2595
+ };
2596
+ const isKeyExistsInI18nData = (i18nData, key) => {
2597
+ if (!i18nData) {
2598
+ return { exists: false, placeholders: [] };
2599
+ }
2600
+ const checkPath = (obj, remainingKey) => {
2601
+ if (!obj || typeof obj !== 'object') {
2602
+ return { exists: false, placeholders: [] };
2603
+ }
2604
+ // 如果剩余key完整存在于当前对象
2605
+ if (obj.hasOwnProperty(remainingKey)) {
2606
+ const value = obj[remainingKey];
2607
+ return {
2608
+ exists: true,
2609
+ value: typeof value === 'string' ? value : undefined,
2610
+ placeholders: typeof value === 'string' ? extractPlaceholders(value) : []
2611
+ };
2612
+ }
2613
+ // 尝试所有可能的分割点
2614
+ for (let i = 1; i <= remainingKey.length; i++) {
2615
+ const firstPart = remainingKey.substring(0, i);
2616
+ const restPart = remainingKey.substring(i + 1);
2617
+ if (obj.hasOwnProperty(firstPart) && i < remainingKey.length && remainingKey[i] === '.') {
2618
+ const result = checkPath(obj[firstPart], restPart);
2619
+ if (result.exists) {
2620
+ return result;
2621
+ }
2622
+ }
2623
+ }
2624
+ return { exists: false, placeholders: [] };
2625
+ };
2626
+ return checkPath(i18nData, key);
2627
+ };
2628
+ /**
2629
+ * 分析表达式的类型信息
2630
+ */
2631
+ const analyzeExpressionType = (expr, typeChecker) => {
2632
+ const result = {
2633
+ isLiteralUnion: false,
2634
+ literalValues: [],
2635
+ isString: false,
2636
+ isNullable: false,
2637
+ hasNonNullAssertion: false,
2638
+ originalType: typeChecker.getTypeAtLocation(expr)
2639
+ };
2640
+ // 检查是否有非空断言
2641
+ let actualExpr = expr;
2642
+ if (ts.isNonNullExpression(expr)) {
2643
+ result.hasNonNullAssertion = true;
2644
+ actualExpr = expr.expression;
2645
+ }
2646
+ const type = typeChecker.getTypeAtLocation(actualExpr);
2647
+ // 检查是否是联合类型
2648
+ if (type.isUnion()) {
2649
+ const nonNullTypes = [];
2650
+ for (const subType of type.types) {
2651
+ // 检查是否包含 null 或 undefined
2652
+ if (subType.flags & ts.TypeFlags.Null || subType.flags & ts.TypeFlags.Undefined) {
2653
+ result.isNullable = true;
2654
+ }
2655
+ else {
2656
+ nonNullTypes.push(subType);
2657
+ }
2658
+ }
2659
+ // 如果有非空断言,忽略 nullable
2660
+ if (result.hasNonNullAssertion) {
2661
+ result.isNullable = false;
2662
+ }
2663
+ // 检查非 null 类型是否都是字面量
2664
+ if (nonNullTypes.length > 0) {
2665
+ const allLiterals = nonNullTypes.every(t => t.isStringLiteral() || (t.flags & ts.TypeFlags.StringLiteral));
2666
+ if (allLiterals) {
2667
+ result.isLiteralUnion = true;
2668
+ result.literalValues = nonNullTypes.map(t => {
2669
+ if (t.isStringLiteral()) {
2670
+ return t.value;
2671
+ }
2672
+ return typeChecker.typeToString(t).replace(/['"]/g, '');
2673
+ });
2674
+ }
2675
+ }
2676
+ }
2677
+ else {
2678
+ // 单一类型
2679
+ if (type.flags & ts.TypeFlags.Null || type.flags & ts.TypeFlags.Undefined) {
2680
+ result.isNullable = true;
2681
+ if (result.hasNonNullAssertion) {
2682
+ result.isNullable = false;
2683
+ }
2684
+ }
2685
+ else if (type.isStringLiteral() || (type.flags & ts.TypeFlags.StringLiteral)) {
2686
+ result.isLiteralUnion = true;
2687
+ result.literalValues = [
2688
+ type.isStringLiteral()
2689
+ ? type.value
2690
+ : typeChecker.typeToString(type).replace(/['"]/g, '')
2691
+ ];
2692
+ }
2693
+ else if (type.flags & ts.TypeFlags.String) {
2694
+ result.isString = true;
2695
+ }
2696
+ }
2697
+ return result;
2698
+ };
2699
+ const compile = (pwd, options) => {
2700
+ // 读取 tsconfig.json
2701
+ const configFile = ts.readConfigFile(options.project, ts.sys.readFile);
2702
+ // 读取自定义配置
2703
+ const customConfig = configFile.config.oakBuildChecks || {};
2704
+ if (configFile.error) {
2705
+ console.error(ts.formatDiagnostic(configFile.error, {
2706
+ getCanonicalFileName: (f) => f,
2707
+ getCurrentDirectory: process.cwd,
2708
+ getNewLine: () => '\n'
2709
+ }));
2710
+ process.exit(1);
2711
+ }
2712
+ // 解析配置
2713
+ const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(options.project));
2714
+ if (parsedConfig.errors.length > 0) {
2715
+ parsedConfig.errors.forEach((diagnostic) => {
2716
+ console.error(ts.formatDiagnostic(diagnostic, {
2717
+ getCanonicalFileName: (f) => f,
2718
+ getCurrentDirectory: process.cwd,
2719
+ getNewLine: () => '\n'
2720
+ }));
2721
+ });
2722
+ process.exit(1);
2723
+ }
2724
+ // 创建编译程序
2725
+ let program;
2726
+ if (parsedConfig.options.incremental || parsedConfig.options.composite) {
2727
+ const host = ts.createIncrementalCompilerHost(parsedConfig.options);
2728
+ const incrementalProgram = ts.createIncrementalProgram({
2729
+ rootNames: parsedConfig.fileNames,
2730
+ options: parsedConfig.options,
2731
+ host: host,
2732
+ configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(parsedConfig),
2733
+ });
2734
+ program = incrementalProgram.getProgram();
2735
+ }
2736
+ else {
2737
+ program = ts.createProgram({
2738
+ rootNames: parsedConfig.fileNames,
2739
+ options: parsedConfig.options,
2740
+ });
2741
+ }
2742
+ // 获取类型检查器
2743
+ const typeChecker = program.getTypeChecker();
2744
+ // 执行自定义检查(传入自定义配置)
2745
+ const customDiagnostics = performCustomChecks(pwd, program, typeChecker, customConfig);
2746
+ if (customDiagnostics.length > 0) {
2747
+ // 输出统计
2748
+ console.log(`${colors.cyan}发现 ${customDiagnostics.length} 个潜在问题:${colors.reset}`);
2749
+ // 输出详细信息
2750
+ customDiagnostics.forEach((diagnostic, index) => printDiagnostic(pwd, diagnostic, index));
2751
+ // 输出忽略提示
2752
+ console.log(`\n${colors.yellow}═══════════════════════════════════════════════════════════${colors.reset}`);
2753
+ console.log(`${colors.cyan}如果确定逻辑正确,可以使用以下注释标签来忽略检查:${colors.reset}`);
2754
+ exports.OAK_IGNORE_TAGS.forEach(tag => {
2755
+ console.log(` ${colors.green}// ${tag}${colors.reset}`);
2756
+ });
2757
+ console.log(`${colors.yellow}═══════════════════════════════════════════════════════════${colors.reset}\n`);
2758
+ }
2759
+ let emitResult = { emitSkipped: true, diagnostics: [] };
2760
+ if (!options.noEmit) {
2761
+ // 执行编译
2762
+ emitResult = program.emit();
2763
+ }
2764
+ // 获取诊断信息
2765
+ const allDiagnostics = [
2766
+ ...ts.getPreEmitDiagnostics(program),
2767
+ ...emitResult.diagnostics,
2768
+ ];
2769
+ // 输出诊断信息
2770
+ allDiagnostics.forEach((diagnostic, index) => printDiagnostic(pwd, diagnostic, index));
2771
+ // 输出编译统计
2772
+ const errorCount = allDiagnostics.filter(d => d.category === ts.DiagnosticCategory.Error).length;
2773
+ const warningCount = allDiagnostics.filter(d => d.category === ts.DiagnosticCategory.Warning).length;
2774
+ if (errorCount > 0 || warningCount > 0) {
2775
+ if (allDiagnostics.length > 0) {
2776
+ console.log('');
2777
+ }
2778
+ const parts = [];
2779
+ if (errorCount > 0) {
2780
+ parts.push(`${errorCount} error${errorCount !== 1 ? 's' : ''}`);
2781
+ }
2782
+ if (warningCount > 0) {
2783
+ parts.push(`${warningCount} warning${warningCount !== 1 ? 's' : ''}`);
2784
+ }
2785
+ console.log(`Found ${parts.join(' and ')}.`);
2786
+ }
2787
+ if (errorCount > 0) {
2788
+ console.log(`${colors.red}Compilation failed due to errors.${colors.reset}`);
2789
+ process.exit(1);
2790
+ }
2791
+ console.log(`${colors.green}Compilation completed successfully.${colors.reset}`);
2792
+ };
2793
+ const build = (pwd, args) => {
2794
+ // 执行编译
2795
+ const options = parseArgs(args);
2796
+ let configPath;
2797
+ // 判断参数是目录还是文件
2798
+ if (fs.existsSync(options.project)) {
2799
+ const stat = fs.statSync(options.project);
2800
+ if (stat.isDirectory()) {
2801
+ configPath = path.resolve(options.project, 'tsconfig.json');
2802
+ }
2803
+ else {
2804
+ configPath = path.resolve(options.project);
2805
+ }
2806
+ }
2807
+ else {
2808
+ configPath = path.resolve(pwd, options.project);
2809
+ if (!fs.existsSync(configPath)) {
2810
+ const dirPath = path.resolve(pwd, options.project);
2811
+ if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
2812
+ configPath = path.join(dirPath, 'tsconfig.json');
2813
+ }
2814
+ }
2815
+ }
2816
+ if (!fs.existsSync(configPath)) {
2817
+ console.error(`error TS5058: The specified path does not exist: '${configPath}'.`);
2818
+ process.exit(1);
2819
+ }
2820
+ options.project = configPath;
2821
+ compile(pwd, options);
2822
+ };
2823
+ exports.build = build;