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,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PropertyAccessTracker - 属性访问追踪器
|
|
3
|
+
*
|
|
4
|
+
* 追踪函数返回值的属性访问,帮助理解类型传播
|
|
5
|
+
*/
|
|
6
|
+
import * as ts from 'typescript';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
export class PropertyAccessTracker {
|
|
9
|
+
program;
|
|
10
|
+
checker;
|
|
11
|
+
projectRoot;
|
|
12
|
+
constructor(projectRoot, tsConfigPath) {
|
|
13
|
+
this.projectRoot = projectRoot;
|
|
14
|
+
const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
|
|
15
|
+
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(tsConfigPath));
|
|
16
|
+
this.program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options);
|
|
17
|
+
this.checker = this.program.getTypeChecker();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 分析函数调用的属性访问链
|
|
21
|
+
*/
|
|
22
|
+
analyzeFunctionCalls(functionName, inFiles) {
|
|
23
|
+
const results = [];
|
|
24
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
25
|
+
const filePath = sourceFile.fileName;
|
|
26
|
+
if (filePath.includes('node_modules') || filePath.includes('.d.ts'))
|
|
27
|
+
continue;
|
|
28
|
+
// 如果指定了文件列表,只分析这些文件
|
|
29
|
+
if (inFiles && inFiles.length > 0) {
|
|
30
|
+
const basename = filePath.replace(/^.*[/\\]/, '');
|
|
31
|
+
if (!inFiles.some(f => filePath.includes(f) || f.includes(basename))) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const analysis = this.analyzeFile(sourceFile, functionName);
|
|
36
|
+
if (analysis) {
|
|
37
|
+
results.push(...analysis);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 分析单个文件
|
|
44
|
+
*/
|
|
45
|
+
analyzeFile(sourceFile, functionName) {
|
|
46
|
+
const results = [];
|
|
47
|
+
const visit = (node) => {
|
|
48
|
+
// 找函数调用
|
|
49
|
+
if (ts.isCallExpression(node)) {
|
|
50
|
+
const expr = node.expression;
|
|
51
|
+
if (ts.isIdentifier(expr) && expr.text === functionName) {
|
|
52
|
+
const callSite = this.analyzeCallSite(node, sourceFile, functionName);
|
|
53
|
+
if (callSite) {
|
|
54
|
+
results.push(callSite);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
ts.forEachChild(node, visit);
|
|
59
|
+
};
|
|
60
|
+
visit(sourceFile);
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 分析调用点
|
|
65
|
+
*/
|
|
66
|
+
analyzeCallSite(callExpr, sourceFile, functionName) {
|
|
67
|
+
const line = ts.getLineAndCharacterOfPosition(sourceFile, callExpr.getStart()).line + 1;
|
|
68
|
+
// 获取调用行的完整代码上下文
|
|
69
|
+
const sourceText = sourceFile.getFullText();
|
|
70
|
+
const lineStarts = sourceFile.getLineStarts();
|
|
71
|
+
const lineStart = lineStarts[line - 1];
|
|
72
|
+
const lineEnd = lineStarts[line] || sourceText.length;
|
|
73
|
+
const codeContext = sourceText.slice(lineStart, lineEnd).trim();
|
|
74
|
+
// 检查是否在 Promise.all 中
|
|
75
|
+
let inPromiseAll = false;
|
|
76
|
+
let arrayIndex = -1;
|
|
77
|
+
let parent = callExpr.parent;
|
|
78
|
+
if (ts.isArrayLiteralExpression(parent)) {
|
|
79
|
+
const elements = parent.elements;
|
|
80
|
+
arrayIndex = elements.indexOf(callExpr);
|
|
81
|
+
const grandParent = parent.parent;
|
|
82
|
+
if (ts.isCallExpression(grandParent) && ts.isIdentifier(grandParent.expression)) {
|
|
83
|
+
if (grandParent.expression.text === 'Promise.all') {
|
|
84
|
+
inPromiseAll = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// 获取调用表达式的文本
|
|
89
|
+
const callText = callExpr.getText().slice(0, 50);
|
|
90
|
+
// 查找返回值被赋值给的变量
|
|
91
|
+
let assignedTo;
|
|
92
|
+
let current = callExpr;
|
|
93
|
+
while (current.parent) {
|
|
94
|
+
const parent = current.parent;
|
|
95
|
+
// 直接赋值: const x = call()
|
|
96
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
97
|
+
assignedTo = parent.name.text;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
// 数组解构: const [a, b] = await Promise.all([call()])
|
|
101
|
+
if (ts.isArrayLiteralExpression(parent)) {
|
|
102
|
+
const elements = parent.elements;
|
|
103
|
+
const idx = elements.indexOf(current);
|
|
104
|
+
if (idx >= 0) {
|
|
105
|
+
// parent is ArrayLiteralExpression, go up to find Promise.all CallExpression
|
|
106
|
+
const arrayLiteralParent = parent.parent;
|
|
107
|
+
if (!ts.isCallExpression(arrayLiteralParent))
|
|
108
|
+
continue;
|
|
109
|
+
// Check if it's Promise.all - handle both Promise.all and Promise.all<any>
|
|
110
|
+
let isPromiseAll = false;
|
|
111
|
+
const callExpr = arrayLiteralParent.expression;
|
|
112
|
+
if (ts.isIdentifier(callExpr) && callExpr.text === 'Promise.all') {
|
|
113
|
+
isPromiseAll = true;
|
|
114
|
+
}
|
|
115
|
+
else if (ts.isPropertyAccessExpression(callExpr)) {
|
|
116
|
+
// Handle Promise.all<any> where expression is PropertyAccessExpression
|
|
117
|
+
const propExpr = callExpr.expression;
|
|
118
|
+
const propName = callExpr.name.text;
|
|
119
|
+
if (ts.isIdentifier(propExpr) && propExpr.text === 'Promise' && propName === 'all') {
|
|
120
|
+
isPromiseAll = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!isPromiseAll)
|
|
124
|
+
continue;
|
|
125
|
+
// Now go up from Promise.all CallExpression to find AwaitExpression
|
|
126
|
+
const promiseAllParent = arrayLiteralParent.parent;
|
|
127
|
+
if (!ts.isAwaitExpression(promiseAllParent))
|
|
128
|
+
continue;
|
|
129
|
+
// Finally find VariableDeclaration
|
|
130
|
+
const varDeclParent = promiseAllParent.parent;
|
|
131
|
+
if (!ts.isVariableDeclaration(varDeclParent))
|
|
132
|
+
continue;
|
|
133
|
+
// Check if name is array binding pattern (const [a, b] = ...)
|
|
134
|
+
if (!ts.isArrayBindingPattern(varDeclParent.name))
|
|
135
|
+
continue;
|
|
136
|
+
const bindingPattern = varDeclParent.name;
|
|
137
|
+
const bindingElements = bindingPattern.elements;
|
|
138
|
+
if (bindingElements[idx] && !ts.isOmittedExpression(bindingElements[idx])) {
|
|
139
|
+
const bindingElement = bindingElements[idx];
|
|
140
|
+
if (ts.isIdentifier(bindingElement.name)) {
|
|
141
|
+
assignedTo = bindingElement.name.text;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
current = parent;
|
|
148
|
+
}
|
|
149
|
+
// 查找后续的属性访问
|
|
150
|
+
const propertyAccesses = assignedTo
|
|
151
|
+
? this.findPropertyAccesses(sourceFile, assignedTo, callExpr)
|
|
152
|
+
: [];
|
|
153
|
+
const analysis = {
|
|
154
|
+
functionName: functionName, // Use the parameter
|
|
155
|
+
file: sourceFile.fileName,
|
|
156
|
+
line,
|
|
157
|
+
callExpression: callText,
|
|
158
|
+
returnedTo: assignedTo,
|
|
159
|
+
codeContext,
|
|
160
|
+
propertyAccesses,
|
|
161
|
+
};
|
|
162
|
+
return analysis;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 查找变量被访问的属性链
|
|
166
|
+
*/
|
|
167
|
+
findPropertyAccesses(sourceFile, variableName, afterNode) {
|
|
168
|
+
const accesses = [];
|
|
169
|
+
// 获取源代码文本
|
|
170
|
+
const sourceText = sourceFile.getFullText();
|
|
171
|
+
const lineStarts = sourceFile.getLineStarts();
|
|
172
|
+
// 查找所有对 variableName 的属性访问
|
|
173
|
+
const visit = (node) => {
|
|
174
|
+
// 属性访问: variable.xxx.yyy
|
|
175
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
176
|
+
const expr = node.expression;
|
|
177
|
+
const propLine = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart()).line + 1;
|
|
178
|
+
// 获取该行的完整代码
|
|
179
|
+
const lineStart = lineStarts[propLine - 1];
|
|
180
|
+
const lineEnd = lineStarts[propLine] || sourceText.length;
|
|
181
|
+
const lineContext = sourceText.slice(lineStart, lineEnd).trim();
|
|
182
|
+
// 检查是否是标识符
|
|
183
|
+
if (ts.isIdentifier(expr)) {
|
|
184
|
+
if (expr.text === variableName) {
|
|
185
|
+
// 构建属性访问链
|
|
186
|
+
const chain = this.buildPropertyChain(node);
|
|
187
|
+
accesses.push({
|
|
188
|
+
file: sourceFile.fileName,
|
|
189
|
+
line: propLine,
|
|
190
|
+
variableName,
|
|
191
|
+
accessChain: chain,
|
|
192
|
+
fullExpression: node.getText().slice(0, 80),
|
|
193
|
+
codeContext: lineContext,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// 检查链式访问: a.b.c 中的 a
|
|
198
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
199
|
+
const baseExpr = this.getBaseIdentifier(expr);
|
|
200
|
+
if (baseExpr && baseExpr.text === variableName) {
|
|
201
|
+
const chain = this.buildPropertyChain(node);
|
|
202
|
+
accesses.push({
|
|
203
|
+
file: sourceFile.fileName,
|
|
204
|
+
line: propLine,
|
|
205
|
+
variableName,
|
|
206
|
+
accessChain: chain,
|
|
207
|
+
fullExpression: node.getText().slice(0, 80),
|
|
208
|
+
codeContext: lineContext,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
ts.forEachChild(node, visit);
|
|
214
|
+
};
|
|
215
|
+
visit(sourceFile);
|
|
216
|
+
return accesses.filter(a => a.line > ts.getLineAndCharacterOfPosition(sourceFile, afterNode.getStart()).line);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 获取属性访问的基标识符
|
|
220
|
+
*/
|
|
221
|
+
getBaseIdentifier(node) {
|
|
222
|
+
let current = node;
|
|
223
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
224
|
+
current = current.expression;
|
|
225
|
+
}
|
|
226
|
+
return ts.isIdentifier(current) ? current : null;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* 构建属性访问链
|
|
230
|
+
*/
|
|
231
|
+
buildPropertyChain(node) {
|
|
232
|
+
const chain = [];
|
|
233
|
+
let current = node;
|
|
234
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
235
|
+
chain.unshift(current.name.text);
|
|
236
|
+
current = current.expression;
|
|
237
|
+
}
|
|
238
|
+
return chain;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* 生成报告
|
|
242
|
+
*/
|
|
243
|
+
generateReport(analyses) {
|
|
244
|
+
const lines = [];
|
|
245
|
+
lines.push('═══════════════════════════════════════════════════════════════');
|
|
246
|
+
lines.push(' PROPERTY ACCESS ANALYSIS ');
|
|
247
|
+
lines.push('═══════════════════════════════════════════════════════════════');
|
|
248
|
+
lines.push('');
|
|
249
|
+
if (analyses.length === 0) {
|
|
250
|
+
lines.push('No call sites found.');
|
|
251
|
+
return lines.join('\n');
|
|
252
|
+
}
|
|
253
|
+
for (const analysis of analyses) {
|
|
254
|
+
lines.push(`📍 ${path.basename(analysis.file)}:${analysis.line}`);
|
|
255
|
+
lines.push(` Call: ${analysis.callExpression}`);
|
|
256
|
+
if (analysis.returnedTo) {
|
|
257
|
+
lines.push(` → Returns to: ${analysis.returnedTo}`);
|
|
258
|
+
}
|
|
259
|
+
if (analysis.propertyAccesses.length > 0) {
|
|
260
|
+
lines.push(' Properties accessed:');
|
|
261
|
+
for (const access of analysis.propertyAccesses) {
|
|
262
|
+
lines.push(` • ${access.accessChain.join('.')}`);
|
|
263
|
+
lines.push(` Line ${access.line}: ${access.fullExpression}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
lines.push('');
|
|
267
|
+
}
|
|
268
|
+
// 汇总
|
|
269
|
+
const allProperties = analyses.flatMap(a => a.propertyAccesses.map(p => p.accessChain.join('.')));
|
|
270
|
+
const propertyCounts = new Map();
|
|
271
|
+
for (const prop of allProperties) {
|
|
272
|
+
propertyCounts.set(prop, (propertyCounts.get(prop) || 0) + 1);
|
|
273
|
+
}
|
|
274
|
+
lines.push('─── Property Access Summary ────────────────────────────────');
|
|
275
|
+
const sorted = [...propertyCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
276
|
+
for (const [prop, count] of sorted.slice(0, 10)) {
|
|
277
|
+
lines.push(` ${prop}: ${count} occurrence(s)`);
|
|
278
|
+
}
|
|
279
|
+
return lines.join('\n');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SymbolAnalyzer - 符号级分析器
|
|
3
|
+
*
|
|
4
|
+
* 使用 ts-morph 的 findReferences() 追踪符号的所有引用
|
|
5
|
+
* 比 import 追踪强大得多
|
|
6
|
+
*/
|
|
7
|
+
import { Project, Node, SourceFile } from 'ts-morph';
|
|
8
|
+
export interface SymbolLocation {
|
|
9
|
+
file: string;
|
|
10
|
+
line: number;
|
|
11
|
+
column: number;
|
|
12
|
+
node: Node;
|
|
13
|
+
nodeKind: string;
|
|
14
|
+
context: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SymbolInfo {
|
|
17
|
+
name: string;
|
|
18
|
+
kind: 'function' | 'class' | 'interface' | 'type' | 'variable' | 'enum' | 'property' | 'method' | 'parameter' | 'unknown';
|
|
19
|
+
file: string;
|
|
20
|
+
line: number;
|
|
21
|
+
declaration: Node;
|
|
22
|
+
exports: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface ReferenceInfo {
|
|
25
|
+
symbol: SymbolInfo;
|
|
26
|
+
location: SymbolLocation;
|
|
27
|
+
referenceType: 'definition' | 'import' | 'call' | 'type' | 'assign' | 'property' | 'export' | 'extend' | 'implement' | 'decorate' | 'parameter' | 'generic';
|
|
28
|
+
impactLevel: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class SymbolAnalyzer {
|
|
31
|
+
private project;
|
|
32
|
+
private projectRoot;
|
|
33
|
+
private cache;
|
|
34
|
+
private referenceCache;
|
|
35
|
+
constructor(projectRoot: string, tsConfigPath?: string);
|
|
36
|
+
/**
|
|
37
|
+
* 添加源文件
|
|
38
|
+
*/
|
|
39
|
+
addSourceFiles(patterns: string[]): void;
|
|
40
|
+
/**
|
|
41
|
+
* 递归扫描目录获取文件列表
|
|
42
|
+
*/
|
|
43
|
+
private scanDirectory;
|
|
44
|
+
/**
|
|
45
|
+
* 自动发现并添加所有源文件
|
|
46
|
+
*/
|
|
47
|
+
discoverSourceFiles(includeTests?: boolean): void;
|
|
48
|
+
/**
|
|
49
|
+
* 获取项目
|
|
50
|
+
*/
|
|
51
|
+
getProject(): Project;
|
|
52
|
+
/**
|
|
53
|
+
* 获取所有源文件
|
|
54
|
+
*/
|
|
55
|
+
getSourceFiles(): SourceFile[];
|
|
56
|
+
/**
|
|
57
|
+
* 查找文件的主要导出符号
|
|
58
|
+
* 返回默认导出或第一个命名导出
|
|
59
|
+
*/
|
|
60
|
+
findMainExport(filePath: string): SymbolInfo | null;
|
|
61
|
+
/**
|
|
62
|
+
* 获取节点对应的符号类型
|
|
63
|
+
*/
|
|
64
|
+
private getNodeKind;
|
|
65
|
+
/**
|
|
66
|
+
* 查找符号信息
|
|
67
|
+
*/
|
|
68
|
+
findSymbol(symbolName: string, inFile?: string): SymbolInfo | null;
|
|
69
|
+
/**
|
|
70
|
+
* 查找符号的所有引用
|
|
71
|
+
* 使用 TypeScript Language Service 的 findReferences API 实现真正的符号级追踪
|
|
72
|
+
*/
|
|
73
|
+
findAllReferences(symbolName: string, inFile?: string): ReferenceInfo[];
|
|
74
|
+
/**
|
|
75
|
+
* 回退方案:使用文本匹配查找引用
|
|
76
|
+
*/
|
|
77
|
+
private findAllReferencesFallback;
|
|
78
|
+
/**
|
|
79
|
+
* 根据节点分类引用类型
|
|
80
|
+
*/
|
|
81
|
+
private classifyReferenceByNode;
|
|
82
|
+
/**
|
|
83
|
+
* 分类引用类型
|
|
84
|
+
*/
|
|
85
|
+
private classifyReference;
|
|
86
|
+
/**
|
|
87
|
+
* 获取节点上下文描述
|
|
88
|
+
*/
|
|
89
|
+
private getNodeContext;
|
|
90
|
+
/**
|
|
91
|
+
* 创建符号信息
|
|
92
|
+
*/
|
|
93
|
+
private createSymbolInfo;
|
|
94
|
+
/**
|
|
95
|
+
* 去重引用
|
|
96
|
+
*/
|
|
97
|
+
private deduplicateReferences;
|
|
98
|
+
/**
|
|
99
|
+
* 分析改动的影响范围(递归深度分析)
|
|
100
|
+
*/
|
|
101
|
+
analyzeImpact(symbolName: string, changeType?: 'modify' | 'delete' | 'rename', inFile?: string, maxDepth?: number): {
|
|
102
|
+
symbol: SymbolInfo | null;
|
|
103
|
+
references: ReferenceInfo[];
|
|
104
|
+
callGraph: Map<string, ReferenceInfo[]>;
|
|
105
|
+
typeDependencies: ReferenceInfo[];
|
|
106
|
+
exportDependents: ReferenceInfo[];
|
|
107
|
+
downstreamChain: DownstreamNode[];
|
|
108
|
+
impactScore: number;
|
|
109
|
+
riskLevel: 'low' | 'medium' | 'high' | 'critical';
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* 下游节点
|
|
113
|
+
*/
|
|
114
|
+
private downstreamNodes;
|
|
115
|
+
/**
|
|
116
|
+
* 递归收集下游引用
|
|
117
|
+
*/
|
|
118
|
+
private collectDownstreamReferences;
|
|
119
|
+
/**
|
|
120
|
+
* 构建下游链路
|
|
121
|
+
*/
|
|
122
|
+
private buildDownstreamChain;
|
|
123
|
+
/**
|
|
124
|
+
* 分类文件
|
|
125
|
+
*/
|
|
126
|
+
private categorizeFile;
|
|
127
|
+
}
|
|
128
|
+
export interface DownstreamNode {
|
|
129
|
+
depth: number;
|
|
130
|
+
file: string;
|
|
131
|
+
fileName: string;
|
|
132
|
+
type: string;
|
|
133
|
+
callSites: Array<{
|
|
134
|
+
line: number;
|
|
135
|
+
calledFunction: string;
|
|
136
|
+
context: string;
|
|
137
|
+
}>;
|
|
138
|
+
references: ReferenceInfo[];
|
|
139
|
+
}
|