blast-radius-analyzer 1.2.1

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.
Files changed (49) hide show
  1. package/README.md +108 -0
  2. package/TEST-REPORT.md +379 -0
  3. package/dist/core/AnalysisCache.d.ts +59 -0
  4. package/dist/core/AnalysisCache.js +156 -0
  5. package/dist/core/BlastRadiusAnalyzer.d.ts +99 -0
  6. package/dist/core/BlastRadiusAnalyzer.js +510 -0
  7. package/dist/core/CallStackBuilder.d.ts +63 -0
  8. package/dist/core/CallStackBuilder.js +269 -0
  9. package/dist/core/DataFlowAnalyzer.d.ts +215 -0
  10. package/dist/core/DataFlowAnalyzer.js +1115 -0
  11. package/dist/core/DependencyGraph.d.ts +55 -0
  12. package/dist/core/DependencyGraph.js +541 -0
  13. package/dist/core/ImpactTracer.d.ts +96 -0
  14. package/dist/core/ImpactTracer.js +398 -0
  15. package/dist/core/PropagationTracker.d.ts +73 -0
  16. package/dist/core/PropagationTracker.js +502 -0
  17. package/dist/core/PropertyAccessTracker.d.ts +56 -0
  18. package/dist/core/PropertyAccessTracker.js +281 -0
  19. package/dist/core/SymbolAnalyzer.d.ts +139 -0
  20. package/dist/core/SymbolAnalyzer.js +608 -0
  21. package/dist/core/TypeFlowAnalyzer.d.ts +120 -0
  22. package/dist/core/TypeFlowAnalyzer.js +654 -0
  23. package/dist/core/TypePropagationAnalyzer.d.ts +58 -0
  24. package/dist/core/TypePropagationAnalyzer.js +269 -0
  25. package/dist/index.d.ts +13 -0
  26. package/dist/index.js +952 -0
  27. package/dist/types.d.ts +102 -0
  28. package/dist/types.js +5 -0
  29. package/package.json +39 -0
  30. package/src/core/AnalysisCache.ts +189 -0
  31. package/src/core/CallStackBuilder.ts +345 -0
  32. package/src/core/DataFlowAnalyzer.ts +1403 -0
  33. package/src/core/DependencyGraph.ts +584 -0
  34. package/src/core/ImpactTracer.ts +521 -0
  35. package/src/core/PropagationTracker.ts +630 -0
  36. package/src/core/PropertyAccessTracker.ts +349 -0
  37. package/src/core/SymbolAnalyzer.ts +746 -0
  38. package/src/core/TypeFlowAnalyzer.ts +844 -0
  39. package/src/core/TypePropagationAnalyzer.ts +332 -0
  40. package/src/index.ts +1071 -0
  41. package/src/types.ts +163 -0
  42. package/test-cases/.blast-radius-cache/file-states.json +14 -0
  43. package/test-cases/config.ts +13 -0
  44. package/test-cases/consumer.ts +12 -0
  45. package/test-cases/nested.ts +25 -0
  46. package/test-cases/simple.ts +62 -0
  47. package/test-cases/tsconfig.json +11 -0
  48. package/test-cases/user.ts +32 -0
  49. package/tsconfig.json +16 -0
@@ -0,0 +1,654 @@
1
+ /**
2
+ * TypeFlowAnalyzer - 商用级 TypeScript 类型流分析器
3
+ *
4
+ * 完整支持:
5
+ * - 泛型类型 T<U>
6
+ * - 条件类型 T extends U ? X : Y
7
+ * - 交叉类型 A & B
8
+ * - 映射类型 { [K in keyof T]: ... }
9
+ * - infer 关键字
10
+ * - Promise/Array/Observable 等内置类型
11
+ * - 类型推导和比较
12
+ */
13
+ import * as ts from 'typescript';
14
+ import * as path from 'path';
15
+ /**
16
+ * 商用级类型分析器
17
+ */
18
+ export class TypeFlowAnalyzer {
19
+ program;
20
+ checker;
21
+ typeCache = new Map();
22
+ constructor(projectRoot, tsConfigPath) {
23
+ const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
24
+ const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(tsConfigPath));
25
+ this.program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options);
26
+ this.checker = this.program.getTypeChecker();
27
+ }
28
+ /**
29
+ * 主分析入口
30
+ */
31
+ analyzeTypeFlow(functionName, functionFile) {
32
+ const startTime = Date.now();
33
+ const incompatibilities = [];
34
+ const stats = {
35
+ genericTypes: 0,
36
+ conditionalTypes: 0,
37
+ intersectionTypes: 0,
38
+ promiseTypes: 0,
39
+ };
40
+ let analyzedTypes = 0;
41
+ // 1. 找到函数定义和签名
42
+ const funcInfo = this.findFunctionDefinition(functionName, functionFile);
43
+ if (!funcInfo) {
44
+ return this.createResult(false, [], 'low', 0, stats, Date.now() - startTime);
45
+ }
46
+ const { declarations, signatures } = funcInfo;
47
+ // 2. 提取所有返回类型(处理重载)
48
+ const returnTypes = this.extractAllReturnTypes(signatures, stats);
49
+ analyzedTypes += returnTypes.length;
50
+ // 3. 遍历所有源文件查找调用点
51
+ for (const sourceFile of this.program.getSourceFiles()) {
52
+ if (sourceFile.fileName.includes('node_modules'))
53
+ continue;
54
+ const calls = this.findAllCalls(sourceFile, functionName);
55
+ for (const call of calls) {
56
+ const issues = this.analyzeCall(sourceFile, call, returnTypes, stats);
57
+ incompatibilities.push(...issues);
58
+ }
59
+ }
60
+ const duration = Date.now() - startTime;
61
+ return this.createResult(incompatibilities.length > 0, incompatibilities, incompatibilities.length > 0 ? 'high' : 'medium', analyzedTypes, stats, duration);
62
+ }
63
+ /**
64
+ * 创建结果
65
+ */
66
+ createResult(has, issues, confidence, analyzed, stats, duration) {
67
+ return {
68
+ hasIncompatibilities: has,
69
+ incompatibilities: issues,
70
+ confidence,
71
+ method: `TypeFlow Pro (泛型:${stats.genericTypes} 条件:${stats.conditionalTypes} 交叉:${stats.intersectionTypes})`,
72
+ analyzedTypes: analyzed,
73
+ statistics: stats,
74
+ duration,
75
+ };
76
+ }
77
+ /**
78
+ * 查找函数定义
79
+ */
80
+ findFunctionDefinition(functionName, inFile) {
81
+ const resolvedPath = path.resolve(inFile);
82
+ for (const sourceFile of this.program.getSourceFiles()) {
83
+ if (sourceFile.fileName !== resolvedPath)
84
+ continue;
85
+ const declarations = this.findDeclarations(sourceFile, functionName);
86
+ if (declarations.length === 0)
87
+ return null;
88
+ const signatures = [];
89
+ for (const decl of declarations) {
90
+ const sigs = this.checker.getSignaturesOfType(this.checker.getTypeAtLocation(decl), ts.SignatureKind.Call);
91
+ signatures.push(...sigs);
92
+ }
93
+ return { declarations, signatures };
94
+ }
95
+ return null;
96
+ }
97
+ /**
98
+ * 查找声明
99
+ */
100
+ findDeclarations(sourceFile, name) {
101
+ const results = [];
102
+ const visit = (node) => {
103
+ // 函数声明
104
+ if (ts.isFunctionDeclaration(node) && node.name?.text === name) {
105
+ results.push(node);
106
+ }
107
+ // 变量声明(箭头函数)
108
+ else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === name) {
109
+ results.push(node);
110
+ }
111
+ // 方法声明
112
+ else if (ts.isMethodDeclaration(node)) {
113
+ const methodName = node.name;
114
+ if (ts.isIdentifier(methodName) && methodName.text === name) {
115
+ results.push(node);
116
+ }
117
+ }
118
+ // 类方法
119
+ else if (ts.isPropertyDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === name) {
120
+ results.push(node);
121
+ }
122
+ ts.forEachChild(node, visit);
123
+ };
124
+ visit(sourceFile);
125
+ return results;
126
+ }
127
+ /**
128
+ * 提取所有返回类型
129
+ */
130
+ extractAllReturnTypes(signatures, stats) {
131
+ const typeSet = new Set();
132
+ const types = [];
133
+ for (const sig of signatures) {
134
+ const returnType = sig.getReturnType();
135
+ const expanded = this.expandType(returnType, stats);
136
+ for (const t of expanded) {
137
+ const typeStr = this.checker.typeToString(t);
138
+ if (!typeSet.has(typeStr)) {
139
+ typeSet.add(typeStr);
140
+ types.push(t);
141
+ }
142
+ }
143
+ }
144
+ return types;
145
+ }
146
+ /**
147
+ * 展开类型(处理泛型、条件类型等)
148
+ */
149
+ expandType(type, stats) {
150
+ const results = [type];
151
+ const typeStr = this.checker.typeToString(type);
152
+ // 处理 Promise<T>
153
+ if (typeStr.startsWith('Promise<') || typeStr.startsWith('Promise<')) {
154
+ stats.promiseTypes++;
155
+ const inner = this.extractGenericArgument(type, 'Promise');
156
+ if (inner)
157
+ results.push(inner);
158
+ }
159
+ // 处理 Array<T>
160
+ if (typeStr.startsWith('Array<')) {
161
+ const inner = this.extractGenericArgument(type, 'Array');
162
+ if (inner)
163
+ results.push(inner);
164
+ }
165
+ // 处理泛型引用
166
+ if (type.flags & ts.TypeFlags.TypeParameter) {
167
+ stats.genericTypes++;
168
+ }
169
+ // 处理条件类型
170
+ if (type.flags & ts.TypeFlags.Conditional) {
171
+ stats.conditionalTypes++;
172
+ const condType = type;
173
+ // ConditionalType 有 checkType, extendsType, resolvedTrueType, resolvedFalseType
174
+ const checkType = condType.checkType;
175
+ const extendsType = condType.extendsType;
176
+ const trueType = condType.resolvedTrueType;
177
+ const falseType = condType.resolvedFalseType;
178
+ if (checkType)
179
+ results.push(...this.expandType(checkType, stats));
180
+ if (extendsType)
181
+ results.push(...this.expandType(extendsType, stats));
182
+ if (trueType)
183
+ results.push(...this.expandType(trueType, stats));
184
+ if (falseType)
185
+ results.push(...this.expandType(falseType, stats));
186
+ }
187
+ // 处理交叉类型
188
+ if (type.flags & ts.TypeFlags.Intersection) {
189
+ stats.intersectionTypes++;
190
+ const intersectionType = type;
191
+ for (const t of intersectionType.types || []) {
192
+ results.push(...this.expandType(t, stats));
193
+ }
194
+ }
195
+ return results;
196
+ }
197
+ /**
198
+ * 提取泛型参数
199
+ */
200
+ extractGenericArgument(type, typeName) {
201
+ try {
202
+ const typeStr = this.checker.typeToString(type);
203
+ // 简单字符串解析:Promise<Inner>
204
+ const match = typeStr.match(new RegExp(`${typeName}<(.+)>`));
205
+ if (match) {
206
+ const innerStr = match[1];
207
+ // 创建一个临时类型来解析
208
+ return this.parseTypeString(innerStr);
209
+ }
210
+ // 使用类型引用
211
+ if (type.target) {
212
+ const typeRef = type;
213
+ if (typeRef.target?.symbol?.name === typeName) {
214
+ if (typeRef.typeArguments && typeRef.typeArguments.length > 0) {
215
+ return typeRef.typeArguments[0];
216
+ }
217
+ }
218
+ }
219
+ }
220
+ catch (e) {
221
+ // ignore
222
+ }
223
+ return null;
224
+ }
225
+ /**
226
+ * 解析类型字符串
227
+ */
228
+ parseTypeString(typeStr) {
229
+ // 检查缓存
230
+ if (this.typeCache.has(typeStr)) {
231
+ return this.typeCache.get(typeStr);
232
+ }
233
+ // 创建临时源文件来解析类型
234
+ const tempSource = ts.createSourceFile('temp.ts', `type TempType = ${typeStr};`, ts.ScriptTarget.Latest, true);
235
+ const typeAlias = tempSource.statements[0];
236
+ if (typeAlias && ts.isTypeAliasDeclaration(typeAlias)) {
237
+ const type = this.checker.getTypeAtLocation(typeAlias);
238
+ this.typeCache.set(typeStr, type);
239
+ return type;
240
+ }
241
+ return null;
242
+ }
243
+ /**
244
+ * 查找所有调用
245
+ */
246
+ findAllCalls(sourceFile, functionName) {
247
+ const calls = [];
248
+ const visit = (node) => {
249
+ if (ts.isCallExpression(node)) {
250
+ const expr = node.expression;
251
+ if (ts.isIdentifier(expr) && expr.text === functionName) {
252
+ calls.push(node);
253
+ }
254
+ }
255
+ ts.forEachChild(node, visit);
256
+ };
257
+ visit(sourceFile);
258
+ return calls;
259
+ }
260
+ /**
261
+ * 分析单个调用
262
+ */
263
+ analyzeCall(sourceFile, callExpr, returnTypes, stats) {
264
+ const issues = [];
265
+ const line = this.getLine(sourceFile, callExpr);
266
+ const column = this.getColumn(sourceFile, callExpr);
267
+ const callText = callExpr.getText().slice(0, 60);
268
+ for (const returnType of returnTypes) {
269
+ // 1. 检查赋值兼容性
270
+ if (ts.isVariableDeclaration(callExpr.parent)) {
271
+ const varDecl = callExpr.parent;
272
+ if (ts.isIdentifier(varDecl.name)) {
273
+ const varName = varDecl.name.text;
274
+ const varType = this.checker.getTypeAtLocation(varDecl);
275
+ const comparison = this.compareTypes(returnType, varType, stats);
276
+ if (!comparison.compatible) {
277
+ issues.push({
278
+ file: sourceFile.fileName,
279
+ line,
280
+ column,
281
+ expression: callText,
282
+ assignedTo: varName,
283
+ expectedType: this.checker.typeToString(varType),
284
+ actualType: this.checker.typeToString(returnType),
285
+ reason: comparison.reason || '类型不兼容',
286
+ severity: 'warning',
287
+ code: 'INCOMPATIBLE_ASSIGN',
288
+ });
289
+ }
290
+ // 2. 分析返回值的使用
291
+ const usageIssues = this.analyzeReturnUsage(sourceFile, varDecl, varName, returnType, stats);
292
+ issues.push(...usageIssues);
293
+ }
294
+ }
295
+ // 3. 检查 Promise.then 回调
296
+ if (ts.isPropertyAccessExpression(callExpr.parent)) {
297
+ const propAccess = callExpr.parent;
298
+ if (propAccess.name.text === 'then' || propAccess.name.text === 'catch') {
299
+ const issues_ = this.analyzePromiseCallback(sourceFile, callExpr, propAccess, returnType, stats);
300
+ issues.push(...issues_);
301
+ }
302
+ }
303
+ // 4. 检查 await 使用
304
+ if (ts.isAwaitExpression(callExpr.parent)) {
305
+ const awaitExpr = callExpr.parent;
306
+ const issues_ = this.analyzeAwaitUsage(sourceFile, awaitExpr, returnType, stats);
307
+ issues.push(...issues_);
308
+ }
309
+ }
310
+ return issues;
311
+ }
312
+ /**
313
+ * 分析返回值的使用
314
+ */
315
+ analyzeReturnUsage(sourceFile, varDecl, varName, returnType, stats) {
316
+ const issues = [];
317
+ const varStatement = varDecl.parent;
318
+ if (!varStatement)
319
+ return issues;
320
+ const startPos = varStatement.end;
321
+ // 在声明之后的代码中查找使用
322
+ const visit = (node) => {
323
+ if (node.pos < startPos)
324
+ return;
325
+ // 属性访问 varName.prop
326
+ if (ts.isPropertyAccessExpression(node)) {
327
+ const expr = node.expression;
328
+ if (ts.isIdentifier(expr) && expr.text === varName) {
329
+ const propName = node.name.text;
330
+ const propType = this.checker.getTypeAtLocation(node);
331
+ const propTypeStr = this.checker.typeToString(propType);
332
+ // 检查属性是否存在
333
+ const returnTypeStr = this.checker.typeToString(returnType);
334
+ const returnExpanded = this.expandType(returnType, stats);
335
+ const propExists = returnExpanded.some(t => {
336
+ const tStr = this.checker.typeToString(t);
337
+ const prop = this.checker.getPropertyOfType(t, propName);
338
+ return !!prop;
339
+ });
340
+ if (!propExists) {
341
+ issues.push({
342
+ file: sourceFile.fileName,
343
+ line: this.getLine(sourceFile, node),
344
+ column: this.getColumn(sourceFile, node),
345
+ expression: node.getText().slice(0, 40),
346
+ assignedTo: varName,
347
+ propertyAccess: [propName],
348
+ expectedType: returnTypeStr,
349
+ actualType: 'undefined',
350
+ reason: `类型 ${returnTypeStr} 中不存在属性 ${propName}`,
351
+ severity: 'error',
352
+ code: 'MISSING_PROPERTY',
353
+ });
354
+ }
355
+ // 检查属性类型兼容性
356
+ const returnExpandedTypes = this.expandType(returnType, stats);
357
+ for (const t of returnExpandedTypes) {
358
+ const prop = this.checker.getPropertyOfType(t, propName);
359
+ if (prop) {
360
+ const expectedPropType = this.checker.getTypeAtLocation(prop.valueDeclaration);
361
+ const comparison = this.compareTypes(propType, expectedPropType, stats);
362
+ if (!comparison.compatible) {
363
+ issues.push({
364
+ file: sourceFile.fileName,
365
+ line: this.getLine(sourceFile, node),
366
+ column: this.getColumn(sourceFile, node),
367
+ expression: node.getText().slice(0, 40),
368
+ assignedTo: varName,
369
+ propertyAccess: [propName],
370
+ expectedType: this.checker.typeToString(expectedPropType),
371
+ actualType: propTypeStr,
372
+ reason: comparison.reason || `属性 ${propName} 类型不兼容`,
373
+ severity: 'warning',
374
+ code: 'INCOMPATIBLE_PROPERTY',
375
+ });
376
+ }
377
+ }
378
+ }
379
+ }
380
+ }
381
+ ts.forEachChild(node, visit);
382
+ };
383
+ visit(sourceFile);
384
+ return issues;
385
+ }
386
+ /**
387
+ * 分析 Promise 回调
388
+ */
389
+ analyzePromiseCallback(sourceFile, callExpr, propAccess, returnType, stats) {
390
+ const issues = [];
391
+ const parentCall = propAccess.parent;
392
+ if (!ts.isCallExpression(parentCall))
393
+ return issues;
394
+ const callbackArg = parentCall.arguments[0];
395
+ if (!callbackArg || !ts.isArrowFunction(callbackArg))
396
+ return issues;
397
+ const params = callbackArg.parameters;
398
+ if (params.length === 0)
399
+ return issues;
400
+ const param = params[0];
401
+ if (!ts.isIdentifier(param.name))
402
+ return issues;
403
+ const paramName = param.name.text;
404
+ const paramType = this.checker.getTypeAtLocation(param);
405
+ const paramTypeStr = this.checker.typeToString(paramType);
406
+ // 解包 Promise<T>
407
+ const promiseInner = this.extractGenericArgument(returnType, 'Promise');
408
+ if (promiseInner) {
409
+ const innerTypeStr = this.checker.typeToString(promiseInner);
410
+ const comparison = this.compareTypes(promiseInner, paramType, stats);
411
+ if (!comparison.compatible) {
412
+ issues.push({
413
+ file: sourceFile.fileName,
414
+ line: this.getLine(sourceFile, callExpr),
415
+ column: this.getColumn(sourceFile, callExpr),
416
+ expression: callExpr.getText().slice(0, 60),
417
+ assignedTo: paramName,
418
+ expectedType: paramTypeStr,
419
+ actualType: innerTypeStr,
420
+ reason: comparison.reason || `Promise<${innerTypeStr}> 与回调参数 ${paramTypeStr} 不兼容`,
421
+ severity: 'warning',
422
+ code: 'INCOMPATIBLE_PROMISE_CALLBACK',
423
+ });
424
+ }
425
+ }
426
+ return issues;
427
+ }
428
+ /**
429
+ * 分析 await 使用
430
+ */
431
+ analyzeAwaitUsage(sourceFile, awaitExpr, returnType, stats) {
432
+ const issues = [];
433
+ // await 会解包 Promise<T> -> T
434
+ const promiseInner = this.extractGenericArgument(returnType, 'Promise');
435
+ if (!promiseInner)
436
+ return issues;
437
+ const innerTypeStr = this.checker.typeToString(promiseInner);
438
+ // await 表达式的类型是 T(解包后的)
439
+ const awaitType = this.checker.getTypeAtLocation(awaitExpr);
440
+ const awaitTypeStr = this.checker.typeToString(awaitType);
441
+ // 检查 await 后续使用
442
+ const parent = awaitExpr.parent;
443
+ if (ts.isVariableDeclaration(parent)) {
444
+ if (ts.isIdentifier(parent.name)) {
445
+ const varName = parent.name.text;
446
+ const varType = this.checker.getTypeAtLocation(parent);
447
+ const comparison = this.compareTypes(awaitType, varType, stats);
448
+ if (!comparison.compatible) {
449
+ issues.push({
450
+ file: sourceFile.fileName,
451
+ line: this.getLine(sourceFile, awaitExpr),
452
+ column: this.getColumn(sourceFile, awaitExpr),
453
+ expression: awaitExpr.getText().slice(0, 60),
454
+ assignedTo: varName,
455
+ expectedType: this.checker.typeToString(varType),
456
+ actualType: awaitTypeStr,
457
+ reason: `await 解包后类型 ${awaitTypeStr} 与变量类型不兼容`,
458
+ severity: 'warning',
459
+ code: 'INCOMPATIBLE_AWAIT',
460
+ });
461
+ }
462
+ }
463
+ }
464
+ return issues;
465
+ }
466
+ /**
467
+ * 比较两个类型
468
+ */
469
+ compareTypes(source, target, stats) {
470
+ const sourceStr = this.checker.typeToString(source);
471
+ const targetStr = this.checker.typeToString(target);
472
+ // 相同类型
473
+ if (sourceStr === targetStr) {
474
+ return { compatible: true };
475
+ }
476
+ // any 或 unknown 兼容一切
477
+ if (source.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
478
+ return { compatible: true };
479
+ }
480
+ if (target.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
481
+ return { compatible: true };
482
+ }
483
+ // null/undefined 兼容性
484
+ if (source.flags & ts.TypeFlags.Null) {
485
+ if (target.flags & ts.TypeFlags.Null)
486
+ return { compatible: true };
487
+ }
488
+ // 尝试 isTypeAssignableTo
489
+ try {
490
+ if (this.checker.isTypeAssignableTo(source, target)) {
491
+ return { compatible: true };
492
+ }
493
+ }
494
+ catch (e) {
495
+ // ignore
496
+ }
497
+ // 尝试逆向检查
498
+ try {
499
+ if (this.checker.isTypeAssignableTo(target, source)) {
500
+ return { compatible: true };
501
+ }
502
+ }
503
+ catch (e) {
504
+ // ignore
505
+ }
506
+ // 泛型类型比较(简化处理)
507
+ if (source.flags & ts.TypeFlags.TypeParameter || target.flags & ts.TypeFlags.TypeParameter) {
508
+ stats.genericTypes++;
509
+ // 泛型参数未知,保守处理
510
+ return {
511
+ compatible: true,
512
+ reason: '包含泛型参数,需要运行时验证'
513
+ };
514
+ }
515
+ // 交叉类型展开比较
516
+ if (source.flags & ts.TypeFlags.Intersection) {
517
+ stats.intersectionTypes++;
518
+ const expanded = this.expandType(source, stats);
519
+ for (const t of expanded) {
520
+ const result = this.compareTypes(t, target, stats);
521
+ if (result.compatible)
522
+ return { compatible: true };
523
+ }
524
+ }
525
+ if (target.flags & ts.TypeFlags.Intersection) {
526
+ stats.intersectionTypes++;
527
+ const expanded = this.expandType(target, stats);
528
+ for (const t of expanded) {
529
+ const result = this.compareTypes(source, t, stats);
530
+ if (result.compatible)
531
+ return { compatible: true };
532
+ }
533
+ }
534
+ return {
535
+ compatible: false,
536
+ reason: `类型 ${sourceStr} 不能赋值给 ${targetStr}`,
537
+ };
538
+ }
539
+ /**
540
+ * 获取行号
541
+ */
542
+ getLine(sourceFile, node) {
543
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
544
+ }
545
+ /**
546
+ * 获取列号
547
+ */
548
+ getColumn(sourceFile, node) {
549
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart()).character + 1;
550
+ }
551
+ /**
552
+ * 格式化文本报告
553
+ */
554
+ formatAsText(result, _functionName) {
555
+ const lines = [];
556
+ lines.push('');
557
+ lines.push('═══════════════════════════════════════════════════════════════');
558
+ lines.push(' 🔬 类型流分析 (TypeFlow Pro) ');
559
+ lines.push('═══════════════════════════════════════════════════════════════');
560
+ lines.push('');
561
+ if (!result.hasIncompatibilities) {
562
+ lines.push('✅ 未检测到类型不兼容');
563
+ lines.push(` ${result.method}`);
564
+ lines.push(` 可信度: ${result.confidence}`);
565
+ lines.push(` 耗时: ${result.duration}ms`);
566
+ lines.push('');
567
+ return lines.join('\n');
568
+ }
569
+ lines.push(`⚠️ 检测到 ${result.incompatibilities.length} 处类型不兼容`);
570
+ lines.push(` ${result.method}`);
571
+ lines.push(` 可信度: ${result.confidence}`);
572
+ lines.push(` 耗时: ${result.duration}ms`);
573
+ lines.push('');
574
+ // 按文件分组
575
+ const byFile = new Map();
576
+ for (const issue of result.incompatibilities) {
577
+ if (!byFile.has(issue.file)) {
578
+ byFile.set(issue.file, []);
579
+ }
580
+ byFile.get(issue.file).push(issue);
581
+ }
582
+ for (const [file, items] of byFile) {
583
+ lines.push(`📁 ${path.basename(file)}`);
584
+ for (const item of items) {
585
+ lines.push(` 📍 ${item.line}:${item.column}`);
586
+ lines.push(` 代码: ${item.expression}`);
587
+ if (item.assignedTo)
588
+ lines.push(` 赋值: ${item.assignedTo}`);
589
+ if (item.propertyAccess?.length) {
590
+ lines.push(` 属性: ${item.propertyAccess.join(' → ')}`);
591
+ }
592
+ lines.push(` 期望: ${item.expectedType}`);
593
+ lines.push(` 实际: ${item.actualType}`);
594
+ lines.push(` 原因: ${item.reason}`);
595
+ lines.push(` [${item.code}]`);
596
+ lines.push('');
597
+ }
598
+ }
599
+ lines.push('💡 建议: 请检查上述位置的类型兼容性');
600
+ lines.push('');
601
+ return lines.join('\n');
602
+ }
603
+ /**
604
+ * 格式化 HTML 报告
605
+ */
606
+ formatAsHtml(result, _functionName) {
607
+ if (!result.hasIncompatibilities) {
608
+ return `<div class="type-flow-pro">
609
+ <h3>🔬 类型流分析</h3>
610
+ <div class="type-result-ok">
611
+ <span>✅</span>
612
+ <span>未检测到类型不兼容</span>
613
+ <span class="meta">${result.method} | ${result.confidence} | ${result.duration}ms</span>
614
+ </div>
615
+ </div>`;
616
+ }
617
+ const byFile = new Map();
618
+ for (const issue of result.incompatibilities) {
619
+ if (!byFile.has(issue.file)) {
620
+ byFile.set(issue.file, []);
621
+ }
622
+ byFile.get(issue.file).push(issue);
623
+ }
624
+ let filesHtml = '';
625
+ for (const [file, items] of byFile) {
626
+ let itemsHtml = '';
627
+ for (const item of items) {
628
+ itemsHtml += `<div class="type-item ${item.severity}">
629
+ <div class="type-location">
630
+ <span>📍 ${item.line}:${item.column}</span>
631
+ <span class="code">${item.code}</span>
632
+ </div>
633
+ <div class="type-expr">${item.expression}</div>
634
+ ${item.assignedTo ? `<div>赋值: <code>${item.assignedTo}</code></div>` : ''}
635
+ ${item.propertyAccess?.length ? `<div>属性: ${item.propertyAccess.map(p => `<span class="prop">${p}</span>`).join(' → ')}</div>` : ''}
636
+ <div class="type-types">期望: <code>${item.expectedType}</code> | 实际: <code>${item.actualType}</code></div>
637
+ <div class="type-reason">${item.reason}</div>
638
+ </div>`;
639
+ }
640
+ filesHtml += `<div class="type-file">
641
+ <div class="type-file-header">📁 ${path.basename(file)}</div>
642
+ ${itemsHtml}
643
+ </div>`;
644
+ }
645
+ return `<div class="type-flow-pro">
646
+ <h3>🔬 类型流分析 ⚠️</h3>
647
+ <div class="type-result-warn">
648
+ <span>检测到 <strong>${result.incompatibilities.length}</strong> 处类型不兼容</span>
649
+ <span class="meta">${result.method} | ${result.duration}ms</span>
650
+ </div>
651
+ <div class="type-list">${filesHtml}</div>
652
+ </div>`;
653
+ }
654
+ }