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,608 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SymbolAnalyzer - 符号级分析器
|
|
3
|
+
*
|
|
4
|
+
* 使用 ts-morph 的 findReferences() 追踪符号的所有引用
|
|
5
|
+
* 比 import 追踪强大得多
|
|
6
|
+
*/
|
|
7
|
+
import { Project, SyntaxKind, } from 'ts-morph';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
export class SymbolAnalyzer {
|
|
11
|
+
project;
|
|
12
|
+
projectRoot;
|
|
13
|
+
cache = new Map();
|
|
14
|
+
referenceCache = new Map();
|
|
15
|
+
constructor(projectRoot, tsConfigPath) {
|
|
16
|
+
this.projectRoot = projectRoot;
|
|
17
|
+
this.project = new Project({
|
|
18
|
+
tsConfigFilePath: tsConfigPath ?? `${projectRoot}/tsconfig.json`,
|
|
19
|
+
skipAddingFilesFromTsConfig: true,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 添加源文件
|
|
24
|
+
*/
|
|
25
|
+
addSourceFiles(patterns) {
|
|
26
|
+
for (const pattern of patterns) {
|
|
27
|
+
const fullPattern = pattern.startsWith('/')
|
|
28
|
+
? pattern
|
|
29
|
+
: `${this.projectRoot}/${pattern}`;
|
|
30
|
+
this.project.addSourceFilesAtPaths(fullPattern);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 递归扫描目录获取文件列表
|
|
35
|
+
*/
|
|
36
|
+
scanDirectory(dir, extensions, maxDepth = 10, currentDepth = 0) {
|
|
37
|
+
if (currentDepth > maxDepth)
|
|
38
|
+
return [];
|
|
39
|
+
const files = [];
|
|
40
|
+
try {
|
|
41
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const fullPath = path.join(dir, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
// 跳过 node_modules 和隐藏目录
|
|
46
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
47
|
+
continue;
|
|
48
|
+
files.push(...this.scanDirectory(fullPath, extensions, maxDepth, currentDepth + 1));
|
|
49
|
+
}
|
|
50
|
+
else if (entry.isFile()) {
|
|
51
|
+
const ext = path.extname(entry.name);
|
|
52
|
+
if (extensions.includes(ext)) {
|
|
53
|
+
files.push(fullPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
// 忽略无法读取的目录
|
|
60
|
+
}
|
|
61
|
+
return files;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 自动发现并添加所有源文件
|
|
65
|
+
*/
|
|
66
|
+
discoverSourceFiles(includeTests = false) {
|
|
67
|
+
// 大项目保护阈值
|
|
68
|
+
const MAX_FILES = 500;
|
|
69
|
+
const extensions = includeTests
|
|
70
|
+
? ['.ts', '.tsx', '.js', '.jsx']
|
|
71
|
+
: ['.ts', '.tsx'];
|
|
72
|
+
// 扫描目录获取所有匹配的文件
|
|
73
|
+
const allFiles = this.scanDirectory(this.projectRoot, extensions);
|
|
74
|
+
// 过滤 node_modules(虽然scanDirectory已经跳过,但双重保险)
|
|
75
|
+
const filteredFiles = allFiles.filter(f => !f.includes('node_modules'));
|
|
76
|
+
// 如果文件数量超过阈值,优先保留非测试文件
|
|
77
|
+
if (filteredFiles.length > MAX_FILES) {
|
|
78
|
+
console.warn(`⚠️ 警告: 检测到 ${filteredFiles.length} 个源文件(超过 ${MAX_FILES} 限制)`);
|
|
79
|
+
if (includeTests) {
|
|
80
|
+
console.warn(' 策略: 优先保留源代码文件,限制测试文件数量');
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.warn(` 策略: 限制总文件数量为 ${MAX_FILES}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// 按优先级排序:源代码 > 测试代码
|
|
87
|
+
const sorted = filteredFiles.sort((a, b) => {
|
|
88
|
+
if (!includeTests)
|
|
89
|
+
return 0;
|
|
90
|
+
const aIsTest = a.includes('.test.') || a.includes('.spec.') || a.includes('__tests__');
|
|
91
|
+
const bIsTest = b.includes('.test.') || b.includes('.spec.') || b.includes('__tests__');
|
|
92
|
+
if (aIsTest && !bIsTest)
|
|
93
|
+
return 1;
|
|
94
|
+
if (!aIsTest && bIsTest)
|
|
95
|
+
return -1;
|
|
96
|
+
return 0;
|
|
97
|
+
});
|
|
98
|
+
// 限制文件数量
|
|
99
|
+
const limitedFiles = sorted.slice(0, MAX_FILES);
|
|
100
|
+
// 添加文件到项目
|
|
101
|
+
let addedCount = 0;
|
|
102
|
+
for (const file of limitedFiles) {
|
|
103
|
+
try {
|
|
104
|
+
this.project.addSourceFileAtPath(file);
|
|
105
|
+
addedCount++;
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
// 忽略添加失败的文件
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
console.log(` 已加载 ${addedCount} 个源文件`);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 获取项目
|
|
115
|
+
*/
|
|
116
|
+
getProject() {
|
|
117
|
+
return this.project;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 获取所有源文件
|
|
121
|
+
*/
|
|
122
|
+
getSourceFiles() {
|
|
123
|
+
return this.project.getSourceFiles();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 查找文件的主要导出符号
|
|
127
|
+
* 返回默认导出或第一个命名导出
|
|
128
|
+
*/
|
|
129
|
+
findMainExport(filePath) {
|
|
130
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
131
|
+
if (!sourceFile)
|
|
132
|
+
return null;
|
|
133
|
+
// 获取所有导出声明
|
|
134
|
+
const exported = sourceFile.getExportedDeclarations();
|
|
135
|
+
if (exported.size === 0)
|
|
136
|
+
return null;
|
|
137
|
+
// 优先查找 default 导出
|
|
138
|
+
if (exported.has('default')) {
|
|
139
|
+
const decls = exported.get('default');
|
|
140
|
+
if (decls.length > 0) {
|
|
141
|
+
const decl = decls[0];
|
|
142
|
+
return this.createSymbolInfo('default', this.getNodeKind(decl), decl);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 否则返回第一个导出
|
|
146
|
+
const firstEntry = exported.entries().next().value;
|
|
147
|
+
if (firstEntry) {
|
|
148
|
+
const [name, decls] = firstEntry;
|
|
149
|
+
if (decls.length > 0) {
|
|
150
|
+
return this.createSymbolInfo(name, this.getNodeKind(decls[0]), decls[0]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 获取节点对应的符号类型
|
|
157
|
+
*/
|
|
158
|
+
getNodeKind(node) {
|
|
159
|
+
const kind = node.getKind();
|
|
160
|
+
switch (kind) {
|
|
161
|
+
case SyntaxKind.FunctionDeclaration: return 'function';
|
|
162
|
+
case SyntaxKind.ClassDeclaration: return 'class';
|
|
163
|
+
case SyntaxKind.InterfaceDeclaration: return 'interface';
|
|
164
|
+
case SyntaxKind.TypeAliasDeclaration: return 'type';
|
|
165
|
+
case SyntaxKind.VariableDeclaration: return 'variable';
|
|
166
|
+
case SyntaxKind.EnumDeclaration: return 'enum';
|
|
167
|
+
default: return 'unknown';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 查找符号信息
|
|
172
|
+
*/
|
|
173
|
+
findSymbol(symbolName, inFile) {
|
|
174
|
+
const sourceFiles = inFile
|
|
175
|
+
? [this.project.getSourceFile(inFile)].filter(Boolean)
|
|
176
|
+
: this.project.getSourceFiles();
|
|
177
|
+
for (const sourceFile of sourceFiles) {
|
|
178
|
+
// 查找函数
|
|
179
|
+
const funcs = sourceFile.getFunctions();
|
|
180
|
+
for (const func of funcs) {
|
|
181
|
+
const name = func.getName();
|
|
182
|
+
if (name && name === symbolName) {
|
|
183
|
+
return this.createSymbolInfo(name, 'function', func);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// 查找类
|
|
187
|
+
const classes = sourceFile.getClasses();
|
|
188
|
+
for (const cls of classes) {
|
|
189
|
+
const name = cls.getName();
|
|
190
|
+
if (name && name === symbolName) {
|
|
191
|
+
return this.createSymbolInfo(name, 'class', cls);
|
|
192
|
+
}
|
|
193
|
+
// 检查静态属性和方法
|
|
194
|
+
for (const prop of cls.getStaticProperties()) {
|
|
195
|
+
const propName = prop.getName();
|
|
196
|
+
if (propName && propName === symbolName) {
|
|
197
|
+
return this.createSymbolInfo(propName, 'property', prop);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const method of cls.getStaticMethods()) {
|
|
201
|
+
const methodName = method.getName();
|
|
202
|
+
if (methodName && methodName === symbolName) {
|
|
203
|
+
return this.createSymbolInfo(methodName, 'method', method);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// 查找接口
|
|
208
|
+
const interfaces = sourceFile.getInterfaces();
|
|
209
|
+
for (const int of interfaces) {
|
|
210
|
+
const name = int.getName();
|
|
211
|
+
if (name && name === symbolName) {
|
|
212
|
+
return this.createSymbolInfo(name, 'interface', int);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// 查找类型别名
|
|
216
|
+
const types = sourceFile.getTypeAliases();
|
|
217
|
+
for (const type of types) {
|
|
218
|
+
const name = type.getName();
|
|
219
|
+
if (name && name === symbolName) {
|
|
220
|
+
return this.createSymbolInfo(name, 'type', type);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// 查找变量
|
|
224
|
+
const vars = sourceFile.getVariableDeclarations();
|
|
225
|
+
for (const v of vars) {
|
|
226
|
+
const name = v.getName();
|
|
227
|
+
if (name && name === symbolName) {
|
|
228
|
+
return this.createSymbolInfo(name, 'variable', v);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 查找符号的所有引用
|
|
236
|
+
* 使用 TypeScript Language Service 的 findReferences API 实现真正的符号级追踪
|
|
237
|
+
*/
|
|
238
|
+
findAllReferences(symbolName, inFile) {
|
|
239
|
+
const cacheKey = `${inFile ?? 'global'}:${symbolName}`;
|
|
240
|
+
if (this.referenceCache.has(cacheKey)) {
|
|
241
|
+
return this.referenceCache.get(cacheKey);
|
|
242
|
+
}
|
|
243
|
+
const references = [];
|
|
244
|
+
// 先找到符号定义
|
|
245
|
+
const symbolInfo = this.findSymbol(symbolName, inFile);
|
|
246
|
+
if (!symbolInfo) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
// 找到声明节点
|
|
250
|
+
const declarationNode = symbolInfo.declaration;
|
|
251
|
+
// 使用 Language Service 的 findReferences(传入 Node,不是位置)
|
|
252
|
+
const languageService = this.project.getLanguageService();
|
|
253
|
+
try {
|
|
254
|
+
const refs = languageService.findReferences(declarationNode);
|
|
255
|
+
for (const refSymbol of refs || []) {
|
|
256
|
+
const nodeRefs = refSymbol.getReferences();
|
|
257
|
+
for (const entry of nodeRefs || []) {
|
|
258
|
+
const compilerObj = entry.compilerObject;
|
|
259
|
+
const filePath = compilerObj.fileName;
|
|
260
|
+
// 跳过 node_modules
|
|
261
|
+
if (filePath.includes('node_modules')) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
// 获取源文件计算行号
|
|
265
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
266
|
+
if (!sourceFile) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const startPos = compilerObj.textSpan.start;
|
|
270
|
+
const { line, column } = sourceFile.getLineAndColumnAtPos(startPos);
|
|
271
|
+
// 获取标识符文本
|
|
272
|
+
const node = sourceFile.getDescendantAtPos(startPos);
|
|
273
|
+
references.push({
|
|
274
|
+
symbol: symbolInfo,
|
|
275
|
+
location: {
|
|
276
|
+
file: filePath,
|
|
277
|
+
line,
|
|
278
|
+
column,
|
|
279
|
+
node: node || sourceFile,
|
|
280
|
+
nodeKind: node?.getKindName() || 'Unknown',
|
|
281
|
+
context: compilerObj.isDefinition ? 'Definition' : compilerObj.isWriteAccess ? 'Write' : 'Read',
|
|
282
|
+
},
|
|
283
|
+
referenceType: compilerObj.isDefinition ? 'definition' : this.classifyReferenceByNode(node),
|
|
284
|
+
impactLevel: 0,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
// 如果 findReferences 失败,回退到遍历方式
|
|
291
|
+
console.warn('findReferences failed, falling back to text search:', error);
|
|
292
|
+
return this.findAllReferencesFallback(symbolName, inFile, symbolInfo);
|
|
293
|
+
}
|
|
294
|
+
// 去重
|
|
295
|
+
const unique = this.deduplicateReferences(references);
|
|
296
|
+
this.referenceCache.set(cacheKey, unique);
|
|
297
|
+
return unique;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* 回退方案:使用文本匹配查找引用
|
|
301
|
+
*/
|
|
302
|
+
findAllReferencesFallback(symbolName, inFile, symbolInfo) {
|
|
303
|
+
const references = [];
|
|
304
|
+
const sourceFiles = inFile
|
|
305
|
+
? [this.project.getSourceFile(inFile)].filter(Boolean)
|
|
306
|
+
: this.project.getSourceFiles();
|
|
307
|
+
for (const sourceFile of sourceFiles) {
|
|
308
|
+
sourceFile.forEachDescendant((node) => {
|
|
309
|
+
if (node.getKind() !== SyntaxKind.Identifier)
|
|
310
|
+
return;
|
|
311
|
+
if (node.getText() !== symbolName)
|
|
312
|
+
return;
|
|
313
|
+
if (symbolInfo && node.getStart() === symbolInfo.declaration.getStart())
|
|
314
|
+
return;
|
|
315
|
+
const startPos = node.getStart();
|
|
316
|
+
const { line, column } = sourceFile.getLineAndColumnAtPos(startPos);
|
|
317
|
+
references.push({
|
|
318
|
+
symbol: symbolInfo,
|
|
319
|
+
location: {
|
|
320
|
+
file: sourceFile.getFilePath(),
|
|
321
|
+
line,
|
|
322
|
+
column,
|
|
323
|
+
node,
|
|
324
|
+
nodeKind: SyntaxKind[node.getKind()],
|
|
325
|
+
context: this.getNodeContext(node),
|
|
326
|
+
},
|
|
327
|
+
referenceType: this.classifyReference(node, symbolName),
|
|
328
|
+
impactLevel: 0,
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return references;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* 根据节点分类引用类型
|
|
336
|
+
*/
|
|
337
|
+
classifyReferenceByNode(node) {
|
|
338
|
+
if (!node)
|
|
339
|
+
return 'import';
|
|
340
|
+
const parent = node.getParent();
|
|
341
|
+
if (!parent)
|
|
342
|
+
return 'import';
|
|
343
|
+
switch (parent.getKind()) {
|
|
344
|
+
case SyntaxKind.CallExpression:
|
|
345
|
+
return 'call';
|
|
346
|
+
case SyntaxKind.TypeReference:
|
|
347
|
+
return 'type';
|
|
348
|
+
case SyntaxKind.PropertyAccessExpression:
|
|
349
|
+
return 'property';
|
|
350
|
+
case SyntaxKind.ExportAssignment:
|
|
351
|
+
return 'export';
|
|
352
|
+
default:
|
|
353
|
+
return 'import';
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* 分类引用类型
|
|
358
|
+
*/
|
|
359
|
+
classifyReference(node, symbolName) {
|
|
360
|
+
const parent = node.getParent();
|
|
361
|
+
if (parent?.getKind() === SyntaxKind.CallExpression) {
|
|
362
|
+
if (parent.getExpression() === node) {
|
|
363
|
+
return 'call';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (parent?.getKind() === SyntaxKind.TypeReference) {
|
|
367
|
+
return 'type';
|
|
368
|
+
}
|
|
369
|
+
if (parent?.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
370
|
+
return 'property';
|
|
371
|
+
}
|
|
372
|
+
if (parent?.getKind() === SyntaxKind.ExportAssignment) {
|
|
373
|
+
return 'export';
|
|
374
|
+
}
|
|
375
|
+
if (parent?.getKind() === SyntaxKind.HeritageClause) {
|
|
376
|
+
const parentParent = parent.getParent();
|
|
377
|
+
if (parentParent?.getKind() === SyntaxKind.ClassDeclaration) {
|
|
378
|
+
return 'extend';
|
|
379
|
+
}
|
|
380
|
+
if (parentParent?.getKind() === SyntaxKind.InterfaceDeclaration) {
|
|
381
|
+
return 'extend';
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (parent?.getKind() === SyntaxKind.PropertyDeclaration) {
|
|
385
|
+
return 'property';
|
|
386
|
+
}
|
|
387
|
+
if (node.getKind() === SyntaxKind.FunctionDeclaration) {
|
|
388
|
+
return 'definition';
|
|
389
|
+
}
|
|
390
|
+
if (node.getKind() === SyntaxKind.ClassDeclaration) {
|
|
391
|
+
return 'definition';
|
|
392
|
+
}
|
|
393
|
+
if (node.getKind() === SyntaxKind.InterfaceDeclaration) {
|
|
394
|
+
return 'definition';
|
|
395
|
+
}
|
|
396
|
+
if (node.getKind() === SyntaxKind.TypeAliasDeclaration) {
|
|
397
|
+
return 'definition';
|
|
398
|
+
}
|
|
399
|
+
if (node.getKind() === SyntaxKind.VariableDeclaration) {
|
|
400
|
+
return 'assign';
|
|
401
|
+
}
|
|
402
|
+
return 'import';
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* 获取节点上下文描述
|
|
406
|
+
*/
|
|
407
|
+
getNodeContext(node) {
|
|
408
|
+
const text = node.getText().slice(0, 50);
|
|
409
|
+
const parent = node.getParent();
|
|
410
|
+
if (parent?.getKind() === SyntaxKind.CallExpression) {
|
|
411
|
+
const call = parent;
|
|
412
|
+
if (call.getExpression() === node) {
|
|
413
|
+
return `Called as ${text}(...)`;
|
|
414
|
+
}
|
|
415
|
+
return `Passed to ${call.getExpression().getText()}(${text})`;
|
|
416
|
+
}
|
|
417
|
+
if (parent?.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
418
|
+
return `Accessed as ${text}`;
|
|
419
|
+
}
|
|
420
|
+
if (parent?.getKind() === SyntaxKind.VariableDeclaration) {
|
|
421
|
+
return `Assigned to variable ${parent.getName()}`;
|
|
422
|
+
}
|
|
423
|
+
if (parent?.getKind() === SyntaxKind.TypeReference) {
|
|
424
|
+
return `Used as type ${text}`;
|
|
425
|
+
}
|
|
426
|
+
return text;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* 创建符号信息
|
|
430
|
+
*/
|
|
431
|
+
createSymbolInfo(name, kind, node) {
|
|
432
|
+
const sourceFile = node.getSourceFile();
|
|
433
|
+
const startPos = node.getStart();
|
|
434
|
+
const { line, column } = sourceFile.getLineAndColumnAtPos(startPos);
|
|
435
|
+
const exports = [];
|
|
436
|
+
if (sourceFile.getExportedDeclarations().has(name)) {
|
|
437
|
+
exports.push(name);
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
name,
|
|
441
|
+
kind,
|
|
442
|
+
file: sourceFile.getFilePath(),
|
|
443
|
+
line,
|
|
444
|
+
declaration: node,
|
|
445
|
+
exports,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* 去重引用
|
|
450
|
+
*/
|
|
451
|
+
deduplicateReferences(refs) {
|
|
452
|
+
const seen = new Set();
|
|
453
|
+
return refs.filter((ref) => {
|
|
454
|
+
const key = `${ref.location.file}:${ref.location.line}:${ref.location.column}:${ref.symbol.name}`;
|
|
455
|
+
if (seen.has(key))
|
|
456
|
+
return false;
|
|
457
|
+
seen.add(key);
|
|
458
|
+
return true;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* 分析改动的影响范围(递归深度分析)
|
|
463
|
+
*/
|
|
464
|
+
analyzeImpact(symbolName, changeType = 'modify', inFile, maxDepth = 10) {
|
|
465
|
+
const symbol = this.findSymbol(symbolName, inFile);
|
|
466
|
+
// 递归收集所有下游依赖
|
|
467
|
+
const allRefs = new Map();
|
|
468
|
+
this.collectDownstreamReferences(symbolName, inFile, allRefs, 0, maxDepth);
|
|
469
|
+
const refs = Array.from(allRefs.values());
|
|
470
|
+
// 分类引用
|
|
471
|
+
const callGraph = new Map();
|
|
472
|
+
const typeDependencies = [];
|
|
473
|
+
const exportDependents = [];
|
|
474
|
+
for (const ref of refs) {
|
|
475
|
+
if (ref.referenceType === 'call') {
|
|
476
|
+
const funcName = ref.symbol.name;
|
|
477
|
+
if (!callGraph.has(funcName)) {
|
|
478
|
+
callGraph.set(funcName, []);
|
|
479
|
+
}
|
|
480
|
+
callGraph.get(funcName).push(ref);
|
|
481
|
+
}
|
|
482
|
+
if (ref.referenceType === 'type') {
|
|
483
|
+
typeDependencies.push(ref);
|
|
484
|
+
}
|
|
485
|
+
if (ref.referenceType === 'export') {
|
|
486
|
+
exportDependents.push(ref);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// 构建下游链路
|
|
490
|
+
const downstreamChain = this.buildDownstreamChain(symbolName, refs);
|
|
491
|
+
// 计算影响分数
|
|
492
|
+
let impactScore = 0;
|
|
493
|
+
impactScore += refs.filter(r => r.referenceType === 'call').length * 10;
|
|
494
|
+
impactScore += refs.filter(r => r.referenceType === 'type').length * 5;
|
|
495
|
+
impactScore += refs.filter(r => r.referenceType === 'extend').length * 15;
|
|
496
|
+
impactScore += refs.filter(r => r.referenceType === 'implement').length * 15;
|
|
497
|
+
impactScore += exportDependents.length * 20;
|
|
498
|
+
// 风险等级
|
|
499
|
+
let riskLevel = 'low';
|
|
500
|
+
if (symbol?.kind === 'class' || symbol?.kind === 'interface')
|
|
501
|
+
riskLevel = 'medium';
|
|
502
|
+
if (changeType === 'delete')
|
|
503
|
+
riskLevel = 'high';
|
|
504
|
+
if (symbol?.kind === 'interface' && changeType === 'delete')
|
|
505
|
+
riskLevel = 'critical';
|
|
506
|
+
if (impactScore > 100)
|
|
507
|
+
riskLevel = 'high';
|
|
508
|
+
if (impactScore > 200)
|
|
509
|
+
riskLevel = 'critical';
|
|
510
|
+
return {
|
|
511
|
+
symbol,
|
|
512
|
+
references: refs,
|
|
513
|
+
callGraph,
|
|
514
|
+
typeDependencies,
|
|
515
|
+
exportDependents,
|
|
516
|
+
downstreamChain,
|
|
517
|
+
impactScore,
|
|
518
|
+
riskLevel,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* 下游节点
|
|
523
|
+
*/
|
|
524
|
+
downstreamNodes = new Map();
|
|
525
|
+
/**
|
|
526
|
+
* 递归收集下游引用
|
|
527
|
+
*/
|
|
528
|
+
collectDownstreamReferences(symbolName, inFile, collected, depth, maxDepth) {
|
|
529
|
+
if (depth > maxDepth)
|
|
530
|
+
return;
|
|
531
|
+
const refs = this.findAllReferences(symbolName, inFile);
|
|
532
|
+
for (const ref of refs) {
|
|
533
|
+
const key = `${ref.location.file}:${ref.location.line}:${ref.symbol.name}`;
|
|
534
|
+
if (!collected.has(key)) {
|
|
535
|
+
collected.set(key, ref);
|
|
536
|
+
// 如果是调用,继续递归追踪下游
|
|
537
|
+
if (ref.referenceType === 'call' && depth < maxDepth) {
|
|
538
|
+
this.collectDownstreamReferences(ref.symbol.name, ref.location.file, collected, depth + 1, maxDepth);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* 构建下游链路
|
|
545
|
+
*/
|
|
546
|
+
buildDownstreamChain(symbolName, refs) {
|
|
547
|
+
const chain = [];
|
|
548
|
+
const processedFiles = new Set();
|
|
549
|
+
// 按深度和文件组织
|
|
550
|
+
const levelMap = new Map();
|
|
551
|
+
for (const ref of refs) {
|
|
552
|
+
const depth = ref.impactLevel || 0;
|
|
553
|
+
if (!levelMap.has(depth)) {
|
|
554
|
+
levelMap.set(depth, new Map());
|
|
555
|
+
}
|
|
556
|
+
const fileKey = ref.location.file;
|
|
557
|
+
if (!levelMap.get(depth).has(fileKey)) {
|
|
558
|
+
levelMap.get(depth).set(fileKey, {
|
|
559
|
+
depth,
|
|
560
|
+
file: fileKey,
|
|
561
|
+
fileName: fileKey.split('/').pop() || fileKey,
|
|
562
|
+
type: this.categorizeFile(fileKey),
|
|
563
|
+
callSites: [],
|
|
564
|
+
references: [],
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
const node = levelMap.get(depth).get(fileKey);
|
|
568
|
+
node.references.push(ref);
|
|
569
|
+
if (ref.referenceType === 'call') {
|
|
570
|
+
node.callSites.push({
|
|
571
|
+
line: ref.location.line,
|
|
572
|
+
calledFunction: ref.symbol.name,
|
|
573
|
+
context: ref.location.context,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// 转换为数组并按深度排序
|
|
578
|
+
const levels = Array.from(levelMap.entries()).sort((a, b) => a[0] - b[0]);
|
|
579
|
+
for (const [depth, files] of levels) {
|
|
580
|
+
chain.push(...Array.from(files.values()));
|
|
581
|
+
}
|
|
582
|
+
return chain;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* 分类文件
|
|
586
|
+
*/
|
|
587
|
+
categorizeFile(filePath) {
|
|
588
|
+
if (filePath.includes('/api/'))
|
|
589
|
+
return 'API Layer';
|
|
590
|
+
if (filePath.includes('/components/'))
|
|
591
|
+
return 'Component';
|
|
592
|
+
if (filePath.includes('/pages/') || filePath.includes('/views/'))
|
|
593
|
+
return 'Page';
|
|
594
|
+
if (filePath.includes('/hooks/'))
|
|
595
|
+
return 'Hook';
|
|
596
|
+
if (filePath.includes('/store/') || filePath.includes('/redux') || filePath.includes('/mobx'))
|
|
597
|
+
return 'State Management';
|
|
598
|
+
if (filePath.includes('/context') || filePath.includes('/Context'))
|
|
599
|
+
return 'Context';
|
|
600
|
+
if (filePath.includes('/utils/'))
|
|
601
|
+
return 'Utility';
|
|
602
|
+
if (filePath.includes('/types/'))
|
|
603
|
+
return 'Type Definition';
|
|
604
|
+
if (filePath.includes('/services/'))
|
|
605
|
+
return 'Service';
|
|
606
|
+
return 'Other';
|
|
607
|
+
}
|
|
608
|
+
}
|