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