gitlab-ai-review 4.2.1 → 4.2.2
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/lib/impact-analyzer.js +700 -700
- package/lib/incremental-callchain-analyzer.js +236 -22
- package/package.json +50 -50
package/lib/impact-analyzer.js
CHANGED
|
@@ -1,700 +1,700 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 影响分析器 - 分析代码变更对其他文件的影响
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { IncrementalCallChainAnalyzer } from './incremental-callchain-analyzer.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 从文件内容中提取导入/导出信息
|
|
9
|
-
* @param {string} content - 文件内容
|
|
10
|
-
* @param {string} fileName - 文件名
|
|
11
|
-
* @returns {Object} 导入导出信息
|
|
12
|
-
*/
|
|
13
|
-
export function extractImportsExports(content, fileName) {
|
|
14
|
-
const imports = [];
|
|
15
|
-
const exports = [];
|
|
16
|
-
|
|
17
|
-
// JavaScript/TypeScript 导入
|
|
18
|
-
const importRegex = /import\s+(?:{([^}]+)}|(\w+)|\*\s+as\s+(\w+))\s+from\s+['"]([^'"]+)['"]/g;
|
|
19
|
-
let match;
|
|
20
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
21
|
-
const namedImports = match[1] ? match[1].split(',').map(s => s.trim()) : [];
|
|
22
|
-
const defaultImport = match[2];
|
|
23
|
-
const namespaceImport = match[3];
|
|
24
|
-
const from = match[4];
|
|
25
|
-
|
|
26
|
-
imports.push({
|
|
27
|
-
type: match[1] ? 'named' : match[2] ? 'default' : 'namespace',
|
|
28
|
-
names: namedImports,
|
|
29
|
-
default: defaultImport,
|
|
30
|
-
namespace: namespaceImport,
|
|
31
|
-
from: from,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// 导出
|
|
36
|
-
const exportRegex = /export\s+(?:default\s+)?(?:class|function|const|let|var|interface|type|enum)\s+(\w+)/g;
|
|
37
|
-
while ((match = exportRegex.exec(content)) !== null) {
|
|
38
|
-
exports.push({
|
|
39
|
-
name: match[1],
|
|
40
|
-
line: content.substring(0, match.index).split('\n').length,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Vue 文件特殊处理
|
|
45
|
-
if (fileName.endsWith('.vue')) {
|
|
46
|
-
// 提取组件名
|
|
47
|
-
const componentNameMatch = content.match(/export\s+default\s+{[\s\S]*?name:\s*['"](\w+)['"]/);
|
|
48
|
-
if (componentNameMatch) {
|
|
49
|
-
exports.push({
|
|
50
|
-
name: componentNameMatch[1],
|
|
51
|
-
type: 'vue-component',
|
|
52
|
-
line: content.substring(0, componentNameMatch.index).split('\n').length,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return { imports, exports };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 从 diff 中提取变更的函数/类/组件名称
|
|
62
|
-
* @param {string} diff - diff 内容
|
|
63
|
-
* @param {string} fileName - 文件名
|
|
64
|
-
* @returns {Object} 变更的符号列表 { added: [], deleted: [], modified: [], commented: [] }
|
|
65
|
-
*/
|
|
66
|
-
export function extractChangedSymbols(diff, fileName) {
|
|
67
|
-
if (!diff) return { added: [], deleted: [], modified: [], commented: [] };
|
|
68
|
-
|
|
69
|
-
const addedSymbols = [];
|
|
70
|
-
const deletedSymbols = [];
|
|
71
|
-
const commentedSymbols = []; // 新增:被注释掉的符号
|
|
72
|
-
const lines = diff.split('\n');
|
|
73
|
-
|
|
74
|
-
// 匹配函数、类、常量定义
|
|
75
|
-
const definitionPatterns = [
|
|
76
|
-
/(?:export\s+)?(?:function|const|let|var)\s+(\w+)/, // 函数/变量
|
|
77
|
-
/(?:export\s+)?class\s+(\w+)/, // 类
|
|
78
|
-
/(?:export\s+)?interface\s+(\w+)/, // 接口
|
|
79
|
-
/(?:export\s+)?type\s+(\w+)/, // 类型
|
|
80
|
-
/(?:export\s+)?enum\s+(\w+)/, // 枚举
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
// 匹配接口/类型的字段定义
|
|
84
|
-
const fieldPatterns = [
|
|
85
|
-
/^\s*(\w+)\s*[?]?\s*:\s*/, // 接口字段:fieldName: type 或 fieldName?: type
|
|
86
|
-
/^\s*(\w+)\s*[=]/, // 对象属性:fieldName = value
|
|
87
|
-
];
|
|
88
|
-
|
|
89
|
-
lines.forEach((line, index) => {
|
|
90
|
-
const isAddition = line.startsWith('+') && !line.startsWith('+++');
|
|
91
|
-
const isDeletion = line.startsWith('-') && !line.startsWith('---');
|
|
92
|
-
|
|
93
|
-
if (isAddition || isDeletion) {
|
|
94
|
-
const cleanLine = line.substring(1); // 移除 +/- 前缀
|
|
95
|
-
|
|
96
|
-
// 检查是否是"注释掉代码"的情况
|
|
97
|
-
if (isAddition && isDeletion) {
|
|
98
|
-
// 这种情况不会发生在单行中
|
|
99
|
-
} else if (isAddition) {
|
|
100
|
-
// 检查新增的行是否是注释掉的代码
|
|
101
|
-
const trimmedLine = cleanLine.trim();
|
|
102
|
-
|
|
103
|
-
// 判断是否是注释行
|
|
104
|
-
const isComment = trimmedLine.startsWith('//') ||
|
|
105
|
-
trimmedLine.startsWith('/*') ||
|
|
106
|
-
trimmedLine.startsWith('*');
|
|
107
|
-
|
|
108
|
-
if (isComment) {
|
|
109
|
-
// 提取注释后的代码
|
|
110
|
-
const codeAfterComment = trimmedLine.replace(/^\/\/\s*/, '')
|
|
111
|
-
.replace(/^\/\*\s*/, '')
|
|
112
|
-
.replace(/^[\*\s]*/, '');
|
|
113
|
-
|
|
114
|
-
// 检查注释后的内容是否包含定义
|
|
115
|
-
definitionPatterns.forEach(pattern => {
|
|
116
|
-
const match = codeAfterComment.match(pattern);
|
|
117
|
-
if (match) {
|
|
118
|
-
commentedSymbols.push({
|
|
119
|
-
name: match[1],
|
|
120
|
-
type: 'commented',
|
|
121
|
-
line: index + 1,
|
|
122
|
-
originalCode: codeAfterComment,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
} else {
|
|
127
|
-
// 正常的新增代码
|
|
128
|
-
definitionPatterns.forEach(pattern => {
|
|
129
|
-
const match = cleanLine.match(pattern);
|
|
130
|
-
if (match) {
|
|
131
|
-
addedSymbols.push({
|
|
132
|
-
name: match[1],
|
|
133
|
-
type: 'definition',
|
|
134
|
-
line: index + 1,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// 匹配接口/类型字段
|
|
140
|
-
fieldPatterns.forEach(pattern => {
|
|
141
|
-
const match = cleanLine.match(pattern);
|
|
142
|
-
if (match) {
|
|
143
|
-
const fieldName = match[1];
|
|
144
|
-
// 过滤掉常见关键字
|
|
145
|
-
if (!['if', 'else', 'for', 'while', 'return', 'const', 'let', 'var', 'function', 'class'].includes(fieldName)) {
|
|
146
|
-
addedSymbols.push({
|
|
147
|
-
name: fieldName,
|
|
148
|
-
type: 'field',
|
|
149
|
-
line: index + 1,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// 匹配函数调用
|
|
156
|
-
const callPattern = /(\w+)\s*\(/g;
|
|
157
|
-
let callMatch;
|
|
158
|
-
while ((callMatch = callPattern.exec(cleanLine)) !== null) {
|
|
159
|
-
addedSymbols.push({
|
|
160
|
-
name: callMatch[1],
|
|
161
|
-
type: 'usage',
|
|
162
|
-
line: index + 1,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} else if (isDeletion) {
|
|
167
|
-
// 删除的代码
|
|
168
|
-
definitionPatterns.forEach(pattern => {
|
|
169
|
-
const match = cleanLine.match(pattern);
|
|
170
|
-
if (match) {
|
|
171
|
-
deletedSymbols.push({
|
|
172
|
-
name: match[1],
|
|
173
|
-
type: 'definition',
|
|
174
|
-
line: index + 1,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// 匹配被删除的接口/类型字段
|
|
180
|
-
fieldPatterns.forEach(pattern => {
|
|
181
|
-
const match = cleanLine.match(pattern);
|
|
182
|
-
if (match) {
|
|
183
|
-
const fieldName = match[1];
|
|
184
|
-
// 过滤掉常见关键字
|
|
185
|
-
if (!['if', 'else', 'for', 'while', 'return', 'const', 'let', 'var', 'function', 'class'].includes(fieldName)) {
|
|
186
|
-
deletedSymbols.push({
|
|
187
|
-
name: fieldName,
|
|
188
|
-
type: 'field',
|
|
189
|
-
line: index + 1,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// 检测"注释掉"的模式:删除了代码,然后添加了注释版本
|
|
199
|
-
// 对于每个被注释的符号,检查是否有对应的删除
|
|
200
|
-
const commentedAsDeleted = [];
|
|
201
|
-
commentedSymbols.forEach(commented => {
|
|
202
|
-
const wasDeleted = deletedSymbols.find(del => del.name === commented.name);
|
|
203
|
-
if (wasDeleted) {
|
|
204
|
-
// 这个符号被注释掉了(相当于删除)
|
|
205
|
-
commentedAsDeleted.push({
|
|
206
|
-
name: commented.name,
|
|
207
|
-
type: 'commented-out',
|
|
208
|
-
line: commented.line,
|
|
209
|
-
wasDeleted: true,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// 将"被注释掉"的符号也算作删除
|
|
215
|
-
const allDeleted = [...deletedSymbols, ...commentedAsDeleted];
|
|
216
|
-
|
|
217
|
-
// 找出被修改的符号(既被删除又被添加,但不是注释掉)
|
|
218
|
-
const modifiedSymbols = [];
|
|
219
|
-
const deletedNames = new Set(deletedSymbols.map(s => s.name));
|
|
220
|
-
const addedNames = new Set(addedSymbols.map(s => s.name));
|
|
221
|
-
const commentedNames = new Set(commentedAsDeleted.map(s => s.name));
|
|
222
|
-
|
|
223
|
-
deletedNames.forEach(name => {
|
|
224
|
-
if (addedNames.has(name) && !commentedNames.has(name)) {
|
|
225
|
-
modifiedSymbols.push({ name, type: 'modified' });
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
added: addedSymbols,
|
|
231
|
-
deleted: allDeleted, // 包含注释掉的符号
|
|
232
|
-
modified: modifiedSymbols,
|
|
233
|
-
commented: commentedAsDeleted, // 单独记录被注释掉的符号
|
|
234
|
-
all: [...addedSymbols, ...allDeleted], // 所有变更的符号
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* 检查一行代码中的符号使用是否可能有局部定义
|
|
240
|
-
* @param {string} line - 代码行
|
|
241
|
-
* @param {string} symbol - 符号名
|
|
242
|
-
* @returns {boolean} 是否可能有局部定义
|
|
243
|
-
*/
|
|
244
|
-
function hasLocalDefinition(line, symbol) {
|
|
245
|
-
const trimmedLine = line.trim();
|
|
246
|
-
|
|
247
|
-
// 检查是否是局部变量定义
|
|
248
|
-
const varPatterns = [
|
|
249
|
-
new RegExp(`(const|let|var)\\s+${symbol}\\s*[=:]`), // const symbol = ...
|
|
250
|
-
new RegExp(`(const|let|var)\\s*{[^}]*\\b${symbol}\\b[^}]*}`), // const { symbol } = ...
|
|
251
|
-
new RegExp(`function\\s+\\w+\\([^)]*\\b${symbol}\\b[^)]*\\)`), // function foo(symbol)
|
|
252
|
-
new RegExp(`\\([^)]*\\b${symbol}\\b[^)]*\\)\\s*=>`), // (symbol) => ...
|
|
253
|
-
new RegExp(`\\b${symbol}\\s*:`), // symbol: in object or parameter
|
|
254
|
-
];
|
|
255
|
-
|
|
256
|
-
return varPatterns.some(pattern => pattern.test(trimmedLine));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* 检查一行代码中是否有导入语句
|
|
261
|
-
* @param {string} line - 代码行
|
|
262
|
-
* @param {string} symbol - 符号名
|
|
263
|
-
* @returns {boolean} 是否有导入
|
|
264
|
-
*/
|
|
265
|
-
function hasImportStatement(line, symbol) {
|
|
266
|
-
const trimmedLine = line.trim();
|
|
267
|
-
|
|
268
|
-
// 检查各种导入模式
|
|
269
|
-
const importPatterns = [
|
|
270
|
-
new RegExp(`import\\s+${symbol}\\s+from`), // import symbol from ...
|
|
271
|
-
new RegExp(`import\\s*{[^}]*\\b${symbol}\\b[^}]*}\\s+from`), // import { symbol } from ...
|
|
272
|
-
new RegExp(`import\\s*\\*\\s+as\\s+${symbol}\\s+from`), // import * as symbol from ...
|
|
273
|
-
new RegExp(`require\\([^)]*\\)\\.${symbol}`), // require(...).symbol
|
|
274
|
-
new RegExp(`const\\s+${symbol}\\s*=\\s*require`), // const symbol = require(...)
|
|
275
|
-
];
|
|
276
|
-
|
|
277
|
-
return importPatterns.some(pattern => pattern.test(trimmedLine));
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* 检查文件内部是否使用了指定的符号
|
|
282
|
-
* @param {string} content - 文件内容
|
|
283
|
-
* @param {Array} symbols - 要检查的符号列表(通常是被删除的符号)
|
|
284
|
-
* @returns {Array} 文件内部使用情况
|
|
285
|
-
*/
|
|
286
|
-
export function checkInternalUsage(content, symbols) {
|
|
287
|
-
if (!content || !symbols || symbols.length === 0) {
|
|
288
|
-
return [];
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const usages = [];
|
|
292
|
-
const lines = content.split('\n');
|
|
293
|
-
|
|
294
|
-
symbols.forEach(symbol => {
|
|
295
|
-
const symbolName = symbol.name || symbol;
|
|
296
|
-
const regex = new RegExp(`\\b${symbolName}\\b`, 'g');
|
|
297
|
-
|
|
298
|
-
lines.forEach((line, index) => {
|
|
299
|
-
// 跳过定义行(避免匹配定义本身)
|
|
300
|
-
if (line.match(/(?:export\s+)?(?:function|const|let|var|class)\s+/) &&
|
|
301
|
-
line.includes(symbolName)) {
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (regex.test(line)) {
|
|
306
|
-
usages.push({
|
|
307
|
-
symbol: symbolName,
|
|
308
|
-
lineNumber: index + 1,
|
|
309
|
-
line: line.trim(),
|
|
310
|
-
context: {
|
|
311
|
-
before: lines.slice(Math.max(0, index - 2), index).map(l => l.trim()),
|
|
312
|
-
after: lines.slice(index + 1, index + 3).map(l => l.trim()),
|
|
313
|
-
},
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
return usages;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* 搜索项目中使用了指定符号的文件
|
|
324
|
-
* @param {Object} gitlabClient - GitLab 客户端
|
|
325
|
-
* @param {string} projectId - 项目 ID
|
|
326
|
-
* @param {string} ref - 分支名
|
|
327
|
-
* @param {Array} symbols - 要搜索的符号列表
|
|
328
|
-
* @returns {Promise<Array>} 使用了这些符号的文件列表
|
|
329
|
-
*/
|
|
330
|
-
export async function searchSymbolUsage(gitlabClient, projectId, ref, symbols) {
|
|
331
|
-
if (!symbols || symbols.length === 0) {
|
|
332
|
-
return [];
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const affectedFiles = new Map();
|
|
336
|
-
|
|
337
|
-
// 对每个符号进行搜索
|
|
338
|
-
for (const symbol of symbols) {
|
|
339
|
-
try {
|
|
340
|
-
const symbolName = symbol.name || symbol;
|
|
341
|
-
// 使用 GitLab Search API 搜索代码
|
|
342
|
-
const searchResults = await gitlabClient.searchInProject(
|
|
343
|
-
projectId,
|
|
344
|
-
symbolName,
|
|
345
|
-
ref
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
if (searchResults && searchResults.length > 0) {
|
|
349
|
-
searchResults.forEach(result => {
|
|
350
|
-
const key = result.path || result.filename;
|
|
351
|
-
if (!affectedFiles.has(key)) {
|
|
352
|
-
affectedFiles.set(key, {
|
|
353
|
-
path: key,
|
|
354
|
-
symbols: [],
|
|
355
|
-
lines: [],
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const fileInfo = affectedFiles.get(key);
|
|
360
|
-
fileInfo.symbols.push(symbolName);
|
|
361
|
-
if (result.startline) {
|
|
362
|
-
fileInfo.lines.push(result.startline);
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
} catch (error) {
|
|
367
|
-
console.warn(`搜索符号 ${symbol.name || symbol} 失败:`, error.message);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return Array.from(affectedFiles.values());
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* 提取文件中特定符号周围的代码片段
|
|
376
|
-
* @param {string} content - 文件内容
|
|
377
|
-
* @param {string} symbolName - 符号名称
|
|
378
|
-
* @param {number} contextLines - 上下文行数
|
|
379
|
-
* @returns {Array} 代码片段数组
|
|
380
|
-
*/
|
|
381
|
-
export function extractCodeSnippets(content, symbolName, contextLines = 5) {
|
|
382
|
-
const lines = content.split('\n');
|
|
383
|
-
const snippets = [];
|
|
384
|
-
|
|
385
|
-
lines.forEach((line, index) => {
|
|
386
|
-
if (line.includes(symbolName)) {
|
|
387
|
-
const start = Math.max(0, index - contextLines);
|
|
388
|
-
const end = Math.min(lines.length, index + contextLines + 1);
|
|
389
|
-
|
|
390
|
-
snippets.push({
|
|
391
|
-
lineNumber: index + 1,
|
|
392
|
-
snippet: lines.slice(start, end).join('\n'),
|
|
393
|
-
startLine: start + 1,
|
|
394
|
-
endLine: end,
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
return snippets;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* 获取变更文件的函数/类签名
|
|
404
|
-
* @param {string} content - 文件内容
|
|
405
|
-
* @param {Array} changedSymbols - 变更的符号列表
|
|
406
|
-
* @returns {Array} 函数签名列表
|
|
407
|
-
*/
|
|
408
|
-
export function extractSignatures(content, changedSymbols) {
|
|
409
|
-
const signatures = [];
|
|
410
|
-
const lines = content.split('\n');
|
|
411
|
-
|
|
412
|
-
changedSymbols.forEach(symbol => {
|
|
413
|
-
// 找到定义该符号的行
|
|
414
|
-
const definitionLineIndex = lines.findIndex(line =>
|
|
415
|
-
line.includes(`function ${symbol.name}`) ||
|
|
416
|
-
line.includes(`const ${symbol.name}`) ||
|
|
417
|
-
line.includes(`class ${symbol.name}`) ||
|
|
418
|
-
line.includes(`interface ${symbol.name}`)
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
if (definitionLineIndex !== -1) {
|
|
422
|
-
// 提取函数签名(可能跨多行)
|
|
423
|
-
let signature = lines[definitionLineIndex];
|
|
424
|
-
let lineIndex = definitionLineIndex;
|
|
425
|
-
|
|
426
|
-
// 如果没有找到完整的签名(例如没有 { 或 =>),继续往下找
|
|
427
|
-
while (lineIndex < lines.length - 1 &&
|
|
428
|
-
!signature.includes('{') &&
|
|
429
|
-
!signature.includes('=>') &&
|
|
430
|
-
!signature.includes(';')) {
|
|
431
|
-
lineIndex++;
|
|
432
|
-
signature += '\n' + lines[lineIndex];
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
signatures.push({
|
|
436
|
-
name: symbol.name,
|
|
437
|
-
signature: signature.trim(),
|
|
438
|
-
lineNumber: definitionLineIndex + 1,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
return signatures;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* 使用增量调用链分析(TypeScript Compiler API)
|
|
448
|
-
* @param {Array} allChanges - 所有变更
|
|
449
|
-
* @returns {Promise<Object>} 调用链分析结果
|
|
450
|
-
*/
|
|
451
|
-
export async function analyzeWithCallChain(allChanges) {
|
|
452
|
-
try {
|
|
453
|
-
const analyzer = new IncrementalCallChainAnalyzer();
|
|
454
|
-
|
|
455
|
-
if (!analyzer.isAvailable()) {
|
|
456
|
-
console.log('ℹ️ TypeScript 调用链分析不可用');
|
|
457
|
-
return null;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// 提取所有变更的文件
|
|
461
|
-
const changedFiles = allChanges.map(c => c.new_path || c.old_path);
|
|
462
|
-
|
|
463
|
-
// 提取所有变更的符号
|
|
464
|
-
const allChangedSymbols = { added: [], deleted: [], modified: [], all: [] };
|
|
465
|
-
for (const change of allChanges) {
|
|
466
|
-
const fileName = change.new_path || change.old_path;
|
|
467
|
-
const diff = change.diff;
|
|
468
|
-
const symbols = extractChangedSymbols(diff, fileName);
|
|
469
|
-
|
|
470
|
-
allChangedSymbols.added.push(...symbols.added);
|
|
471
|
-
allChangedSymbols.deleted.push(...symbols.deleted);
|
|
472
|
-
allChangedSymbols.modified.push(...symbols.modified);
|
|
473
|
-
allChangedSymbols.all.push(...symbols.all);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// 执行增量调用链分析
|
|
477
|
-
const result = await analyzer.analyzeImpact(changedFiles, allChangedSymbols);
|
|
478
|
-
|
|
479
|
-
return result;
|
|
480
|
-
} catch (error) {
|
|
481
|
-
console.error('❌ 调用链分析失败:', error.message);
|
|
482
|
-
return null;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* 分析代码变更的完整影响
|
|
488
|
-
* @param {Object} options - 配置选项
|
|
489
|
-
* @param {Object} options.gitlabClient - GitLab 客户端
|
|
490
|
-
* @param {string} options.projectId - 项目 ID
|
|
491
|
-
* @param {string} options.ref - 分支名
|
|
492
|
-
* @param {Object} options.change - 代码变更对象
|
|
493
|
-
* @param {number} options.maxAffectedFiles - 最多分析的受影响文件数量
|
|
494
|
-
* @param {Object} options.callChainResult - 调用链分析结果(可选)
|
|
495
|
-
* @returns {Promise<Object>} 影响分析结果
|
|
496
|
-
*/
|
|
497
|
-
export async function analyzeImpact(options) {
|
|
498
|
-
const {
|
|
499
|
-
gitlabClient,
|
|
500
|
-
projectId,
|
|
501
|
-
ref,
|
|
502
|
-
change,
|
|
503
|
-
maxAffectedFiles = 10,
|
|
504
|
-
callChainResult = null,
|
|
505
|
-
} = options;
|
|
506
|
-
|
|
507
|
-
const fileName = change.new_path || change.old_path;
|
|
508
|
-
const diff = change.diff;
|
|
509
|
-
|
|
510
|
-
console.log(`\n🔍 分析文件 ${fileName} 的影响...`);
|
|
511
|
-
|
|
512
|
-
try {
|
|
513
|
-
// 1. 提取变更的符号(包括新增、删除、修改)
|
|
514
|
-
const changedSymbols = extractChangedSymbols(diff, fileName);
|
|
515
|
-
const totalSymbols = changedSymbols.all.length;
|
|
516
|
-
|
|
517
|
-
console.log(` 发现变更符号: 新增 ${changedSymbols.added.length}, 删除 ${changedSymbols.deleted.length}, 修改 ${changedSymbols.modified.length}`);
|
|
518
|
-
|
|
519
|
-
if (totalSymbols === 0) {
|
|
520
|
-
return {
|
|
521
|
-
fileName,
|
|
522
|
-
changedSymbols,
|
|
523
|
-
fileContent: null,
|
|
524
|
-
internalUsage: [],
|
|
525
|
-
affectedFiles: [],
|
|
526
|
-
signatures: [],
|
|
527
|
-
callChain: null,
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// 2. 获取变更文件的完整内容
|
|
532
|
-
let fileContent = null;
|
|
533
|
-
let signatures = [];
|
|
534
|
-
let internalUsage = [];
|
|
535
|
-
|
|
536
|
-
try {
|
|
537
|
-
fileContent = await gitlabClient.getProjectFile(projectId, fileName, ref);
|
|
538
|
-
if (fileContent) {
|
|
539
|
-
// 提取函数签名(针对新增和修改的符号)
|
|
540
|
-
const symbolsForSignature = [...changedSymbols.added, ...changedSymbols.modified];
|
|
541
|
-
if (symbolsForSignature.length > 0) {
|
|
542
|
-
signatures = extractSignatures(fileContent, symbolsForSignature);
|
|
543
|
-
console.log(` 提取了 ${signatures.length} 个函数签名`);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// 检查文件内部是否使用了被删除的符号
|
|
547
|
-
if (changedSymbols.deleted.length > 0) {
|
|
548
|
-
internalUsage = checkInternalUsage(fileContent, changedSymbols.deleted);
|
|
549
|
-
if (internalUsage.length > 0) {
|
|
550
|
-
console.log(` ⚠️ 文件内部有 ${internalUsage.length} 处使用了被删除的符号`);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
} catch (error) {
|
|
555
|
-
console.warn(` 无法获取文件内容:`, error.message);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// 3. 优先使用调用链结果,否则降级到 GitLab Search
|
|
559
|
-
let affectedFiles = [];
|
|
560
|
-
let callChainInfo = null;
|
|
561
|
-
|
|
562
|
-
if (callChainResult && callChainResult.callChains) {
|
|
563
|
-
// 使用 TypeScript 调用链分析结果
|
|
564
|
-
console.log(` 📊 使用 TypeScript 调用链分析结果`);
|
|
565
|
-
|
|
566
|
-
// 过滤出当前文件相关的调用链
|
|
567
|
-
const relevantChains = callChainResult.callChains.filter(chain => {
|
|
568
|
-
const symbolNames = changedSymbols.all.map(s => s.name || s);
|
|
569
|
-
return symbolNames.includes(chain.symbol);
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
if (relevantChains.length > 0) {
|
|
573
|
-
console.log(` ✅ 找到 ${relevantChains.length} 个相关调用链`);
|
|
574
|
-
|
|
575
|
-
callChainInfo = {
|
|
576
|
-
method: 'typescript-callchain',
|
|
577
|
-
chains: relevantChains,
|
|
578
|
-
summary: callChainResult.codeContext?.summary,
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
// 将调用链转换为 affectedFiles 格式(用于兼容现有代码)
|
|
582
|
-
const filesMap = new Map();
|
|
583
|
-
|
|
584
|
-
for (const chain of relevantChains) {
|
|
585
|
-
for (const usage of chain.usages) {
|
|
586
|
-
if (usage.file === fileName) continue; // 跳过当前文件
|
|
587
|
-
|
|
588
|
-
if (!filesMap.has(usage.file)) {
|
|
589
|
-
filesMap.set(usage.file, {
|
|
590
|
-
path: usage.file,
|
|
591
|
-
symbols: [],
|
|
592
|
-
lines: [],
|
|
593
|
-
snippets: [],
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const fileInfo = filesMap.get(usage.file);
|
|
598
|
-
fileInfo.symbols.push(chain.symbol);
|
|
599
|
-
fileInfo.lines.push(usage.line);
|
|
600
|
-
fileInfo.snippets.push({
|
|
601
|
-
lineNumber: usage.line,
|
|
602
|
-
snippet: usage.context,
|
|
603
|
-
issue: usage.issue,
|
|
604
|
-
dataFlow: usage.dataFlow,
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
affectedFiles = Array.from(filesMap.values());
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// 4. 如果没有调用链结果,使用传统的 GitLab Search
|
|
614
|
-
if (affectedFiles.length === 0) {
|
|
615
|
-
console.log(` 🔍 使用 GitLab Search API 搜索`);
|
|
616
|
-
|
|
617
|
-
const symbolsToSearch = [
|
|
618
|
-
...changedSymbols.deleted.filter(s => s.type === 'definition'),
|
|
619
|
-
...changedSymbols.modified,
|
|
620
|
-
...changedSymbols.added.filter(s => s.type === 'definition'),
|
|
621
|
-
];
|
|
622
|
-
|
|
623
|
-
const searchResults = await searchSymbolUsage(
|
|
624
|
-
gitlabClient,
|
|
625
|
-
projectId,
|
|
626
|
-
ref,
|
|
627
|
-
symbolsToSearch
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
// 过滤掉当前文件本身
|
|
631
|
-
const externalAffectedFiles = searchResults.filter(f => f.path !== fileName);
|
|
632
|
-
|
|
633
|
-
console.log(` 找到 ${externalAffectedFiles.length} 个其他文件可能受影响`);
|
|
634
|
-
|
|
635
|
-
// 限制数量并获取代码片段
|
|
636
|
-
const limitedFiles = externalAffectedFiles.slice(0, maxAffectedFiles);
|
|
637
|
-
affectedFiles = await Promise.all(
|
|
638
|
-
limitedFiles.map(async (file) => {
|
|
639
|
-
try {
|
|
640
|
-
const content = await gitlabClient.getProjectFile(projectId, file.path, ref);
|
|
641
|
-
|
|
642
|
-
if (content) {
|
|
643
|
-
const snippets = [];
|
|
644
|
-
file.symbols.forEach(symbol => {
|
|
645
|
-
const symbolSnippets = extractCodeSnippets(content, symbol, 3);
|
|
646
|
-
snippets.push(...symbolSnippets);
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
return {
|
|
650
|
-
...file,
|
|
651
|
-
snippets: snippets.slice(0, 3), // 每个文件最多 3 个片段
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return file;
|
|
656
|
-
} catch (error) {
|
|
657
|
-
console.warn(` 无法获取文件 ${file.path} 的内容:`, error.message);
|
|
658
|
-
return file;
|
|
659
|
-
}
|
|
660
|
-
})
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
return {
|
|
665
|
-
fileName,
|
|
666
|
-
changedSymbols,
|
|
667
|
-
fileContent, // 完整文件内容
|
|
668
|
-
internalUsage, // 文件内部使用情况
|
|
669
|
-
signatures,
|
|
670
|
-
affectedFiles,
|
|
671
|
-
totalAffectedFiles: affectedFiles.length,
|
|
672
|
-
callChain: callChainInfo, // 新增:调用链信息
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
} catch (error) {
|
|
676
|
-
console.error(` 影响分析失败:`, error.message);
|
|
677
|
-
return {
|
|
678
|
-
fileName,
|
|
679
|
-
error: error.message,
|
|
680
|
-
changedSymbols: { added: [], deleted: [], modified: [], all: [] },
|
|
681
|
-
fileContent: null,
|
|
682
|
-
internalUsage: [],
|
|
683
|
-
affectedFiles: [],
|
|
684
|
-
signatures: [],
|
|
685
|
-
callChain: null,
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
export default {
|
|
691
|
-
extractImportsExports,
|
|
692
|
-
extractChangedSymbols,
|
|
693
|
-
checkInternalUsage,
|
|
694
|
-
searchSymbolUsage,
|
|
695
|
-
extractCodeSnippets,
|
|
696
|
-
extractSignatures,
|
|
697
|
-
analyzeImpact,
|
|
698
|
-
analyzeWithCallChain,
|
|
699
|
-
};
|
|
700
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 影响分析器 - 分析代码变更对其他文件的影响
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IncrementalCallChainAnalyzer } from './incremental-callchain-analyzer.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 从文件内容中提取导入/导出信息
|
|
9
|
+
* @param {string} content - 文件内容
|
|
10
|
+
* @param {string} fileName - 文件名
|
|
11
|
+
* @returns {Object} 导入导出信息
|
|
12
|
+
*/
|
|
13
|
+
export function extractImportsExports(content, fileName) {
|
|
14
|
+
const imports = [];
|
|
15
|
+
const exports = [];
|
|
16
|
+
|
|
17
|
+
// JavaScript/TypeScript 导入
|
|
18
|
+
const importRegex = /import\s+(?:{([^}]+)}|(\w+)|\*\s+as\s+(\w+))\s+from\s+['"]([^'"]+)['"]/g;
|
|
19
|
+
let match;
|
|
20
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
21
|
+
const namedImports = match[1] ? match[1].split(',').map(s => s.trim()) : [];
|
|
22
|
+
const defaultImport = match[2];
|
|
23
|
+
const namespaceImport = match[3];
|
|
24
|
+
const from = match[4];
|
|
25
|
+
|
|
26
|
+
imports.push({
|
|
27
|
+
type: match[1] ? 'named' : match[2] ? 'default' : 'namespace',
|
|
28
|
+
names: namedImports,
|
|
29
|
+
default: defaultImport,
|
|
30
|
+
namespace: namespaceImport,
|
|
31
|
+
from: from,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 导出
|
|
36
|
+
const exportRegex = /export\s+(?:default\s+)?(?:class|function|const|let|var|interface|type|enum)\s+(\w+)/g;
|
|
37
|
+
while ((match = exportRegex.exec(content)) !== null) {
|
|
38
|
+
exports.push({
|
|
39
|
+
name: match[1],
|
|
40
|
+
line: content.substring(0, match.index).split('\n').length,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Vue 文件特殊处理
|
|
45
|
+
if (fileName.endsWith('.vue')) {
|
|
46
|
+
// 提取组件名
|
|
47
|
+
const componentNameMatch = content.match(/export\s+default\s+{[\s\S]*?name:\s*['"](\w+)['"]/);
|
|
48
|
+
if (componentNameMatch) {
|
|
49
|
+
exports.push({
|
|
50
|
+
name: componentNameMatch[1],
|
|
51
|
+
type: 'vue-component',
|
|
52
|
+
line: content.substring(0, componentNameMatch.index).split('\n').length,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { imports, exports };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 从 diff 中提取变更的函数/类/组件名称
|
|
62
|
+
* @param {string} diff - diff 内容
|
|
63
|
+
* @param {string} fileName - 文件名
|
|
64
|
+
* @returns {Object} 变更的符号列表 { added: [], deleted: [], modified: [], commented: [] }
|
|
65
|
+
*/
|
|
66
|
+
export function extractChangedSymbols(diff, fileName) {
|
|
67
|
+
if (!diff) return { added: [], deleted: [], modified: [], commented: [] };
|
|
68
|
+
|
|
69
|
+
const addedSymbols = [];
|
|
70
|
+
const deletedSymbols = [];
|
|
71
|
+
const commentedSymbols = []; // 新增:被注释掉的符号
|
|
72
|
+
const lines = diff.split('\n');
|
|
73
|
+
|
|
74
|
+
// 匹配函数、类、常量定义
|
|
75
|
+
const definitionPatterns = [
|
|
76
|
+
/(?:export\s+)?(?:function|const|let|var)\s+(\w+)/, // 函数/变量
|
|
77
|
+
/(?:export\s+)?class\s+(\w+)/, // 类
|
|
78
|
+
/(?:export\s+)?interface\s+(\w+)/, // 接口
|
|
79
|
+
/(?:export\s+)?type\s+(\w+)/, // 类型
|
|
80
|
+
/(?:export\s+)?enum\s+(\w+)/, // 枚举
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// 匹配接口/类型的字段定义
|
|
84
|
+
const fieldPatterns = [
|
|
85
|
+
/^\s*(\w+)\s*[?]?\s*:\s*/, // 接口字段:fieldName: type 或 fieldName?: type
|
|
86
|
+
/^\s*(\w+)\s*[=]/, // 对象属性:fieldName = value
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
lines.forEach((line, index) => {
|
|
90
|
+
const isAddition = line.startsWith('+') && !line.startsWith('+++');
|
|
91
|
+
const isDeletion = line.startsWith('-') && !line.startsWith('---');
|
|
92
|
+
|
|
93
|
+
if (isAddition || isDeletion) {
|
|
94
|
+
const cleanLine = line.substring(1); // 移除 +/- 前缀
|
|
95
|
+
|
|
96
|
+
// 检查是否是"注释掉代码"的情况
|
|
97
|
+
if (isAddition && isDeletion) {
|
|
98
|
+
// 这种情况不会发生在单行中
|
|
99
|
+
} else if (isAddition) {
|
|
100
|
+
// 检查新增的行是否是注释掉的代码
|
|
101
|
+
const trimmedLine = cleanLine.trim();
|
|
102
|
+
|
|
103
|
+
// 判断是否是注释行
|
|
104
|
+
const isComment = trimmedLine.startsWith('//') ||
|
|
105
|
+
trimmedLine.startsWith('/*') ||
|
|
106
|
+
trimmedLine.startsWith('*');
|
|
107
|
+
|
|
108
|
+
if (isComment) {
|
|
109
|
+
// 提取注释后的代码
|
|
110
|
+
const codeAfterComment = trimmedLine.replace(/^\/\/\s*/, '')
|
|
111
|
+
.replace(/^\/\*\s*/, '')
|
|
112
|
+
.replace(/^[\*\s]*/, '');
|
|
113
|
+
|
|
114
|
+
// 检查注释后的内容是否包含定义
|
|
115
|
+
definitionPatterns.forEach(pattern => {
|
|
116
|
+
const match = codeAfterComment.match(pattern);
|
|
117
|
+
if (match) {
|
|
118
|
+
commentedSymbols.push({
|
|
119
|
+
name: match[1],
|
|
120
|
+
type: 'commented',
|
|
121
|
+
line: index + 1,
|
|
122
|
+
originalCode: codeAfterComment,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
// 正常的新增代码
|
|
128
|
+
definitionPatterns.forEach(pattern => {
|
|
129
|
+
const match = cleanLine.match(pattern);
|
|
130
|
+
if (match) {
|
|
131
|
+
addedSymbols.push({
|
|
132
|
+
name: match[1],
|
|
133
|
+
type: 'definition',
|
|
134
|
+
line: index + 1,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// 匹配接口/类型字段
|
|
140
|
+
fieldPatterns.forEach(pattern => {
|
|
141
|
+
const match = cleanLine.match(pattern);
|
|
142
|
+
if (match) {
|
|
143
|
+
const fieldName = match[1];
|
|
144
|
+
// 过滤掉常见关键字
|
|
145
|
+
if (!['if', 'else', 'for', 'while', 'return', 'const', 'let', 'var', 'function', 'class'].includes(fieldName)) {
|
|
146
|
+
addedSymbols.push({
|
|
147
|
+
name: fieldName,
|
|
148
|
+
type: 'field',
|
|
149
|
+
line: index + 1,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// 匹配函数调用
|
|
156
|
+
const callPattern = /(\w+)\s*\(/g;
|
|
157
|
+
let callMatch;
|
|
158
|
+
while ((callMatch = callPattern.exec(cleanLine)) !== null) {
|
|
159
|
+
addedSymbols.push({
|
|
160
|
+
name: callMatch[1],
|
|
161
|
+
type: 'usage',
|
|
162
|
+
line: index + 1,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else if (isDeletion) {
|
|
167
|
+
// 删除的代码
|
|
168
|
+
definitionPatterns.forEach(pattern => {
|
|
169
|
+
const match = cleanLine.match(pattern);
|
|
170
|
+
if (match) {
|
|
171
|
+
deletedSymbols.push({
|
|
172
|
+
name: match[1],
|
|
173
|
+
type: 'definition',
|
|
174
|
+
line: index + 1,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// 匹配被删除的接口/类型字段
|
|
180
|
+
fieldPatterns.forEach(pattern => {
|
|
181
|
+
const match = cleanLine.match(pattern);
|
|
182
|
+
if (match) {
|
|
183
|
+
const fieldName = match[1];
|
|
184
|
+
// 过滤掉常见关键字
|
|
185
|
+
if (!['if', 'else', 'for', 'while', 'return', 'const', 'let', 'var', 'function', 'class'].includes(fieldName)) {
|
|
186
|
+
deletedSymbols.push({
|
|
187
|
+
name: fieldName,
|
|
188
|
+
type: 'field',
|
|
189
|
+
line: index + 1,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// 检测"注释掉"的模式:删除了代码,然后添加了注释版本
|
|
199
|
+
// 对于每个被注释的符号,检查是否有对应的删除
|
|
200
|
+
const commentedAsDeleted = [];
|
|
201
|
+
commentedSymbols.forEach(commented => {
|
|
202
|
+
const wasDeleted = deletedSymbols.find(del => del.name === commented.name);
|
|
203
|
+
if (wasDeleted) {
|
|
204
|
+
// 这个符号被注释掉了(相当于删除)
|
|
205
|
+
commentedAsDeleted.push({
|
|
206
|
+
name: commented.name,
|
|
207
|
+
type: 'commented-out',
|
|
208
|
+
line: commented.line,
|
|
209
|
+
wasDeleted: true,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// 将"被注释掉"的符号也算作删除
|
|
215
|
+
const allDeleted = [...deletedSymbols, ...commentedAsDeleted];
|
|
216
|
+
|
|
217
|
+
// 找出被修改的符号(既被删除又被添加,但不是注释掉)
|
|
218
|
+
const modifiedSymbols = [];
|
|
219
|
+
const deletedNames = new Set(deletedSymbols.map(s => s.name));
|
|
220
|
+
const addedNames = new Set(addedSymbols.map(s => s.name));
|
|
221
|
+
const commentedNames = new Set(commentedAsDeleted.map(s => s.name));
|
|
222
|
+
|
|
223
|
+
deletedNames.forEach(name => {
|
|
224
|
+
if (addedNames.has(name) && !commentedNames.has(name)) {
|
|
225
|
+
modifiedSymbols.push({ name, type: 'modified' });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
added: addedSymbols,
|
|
231
|
+
deleted: allDeleted, // 包含注释掉的符号
|
|
232
|
+
modified: modifiedSymbols,
|
|
233
|
+
commented: commentedAsDeleted, // 单独记录被注释掉的符号
|
|
234
|
+
all: [...addedSymbols, ...allDeleted], // 所有变更的符号
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 检查一行代码中的符号使用是否可能有局部定义
|
|
240
|
+
* @param {string} line - 代码行
|
|
241
|
+
* @param {string} symbol - 符号名
|
|
242
|
+
* @returns {boolean} 是否可能有局部定义
|
|
243
|
+
*/
|
|
244
|
+
function hasLocalDefinition(line, symbol) {
|
|
245
|
+
const trimmedLine = line.trim();
|
|
246
|
+
|
|
247
|
+
// 检查是否是局部变量定义
|
|
248
|
+
const varPatterns = [
|
|
249
|
+
new RegExp(`(const|let|var)\\s+${symbol}\\s*[=:]`), // const symbol = ...
|
|
250
|
+
new RegExp(`(const|let|var)\\s*{[^}]*\\b${symbol}\\b[^}]*}`), // const { symbol } = ...
|
|
251
|
+
new RegExp(`function\\s+\\w+\\([^)]*\\b${symbol}\\b[^)]*\\)`), // function foo(symbol)
|
|
252
|
+
new RegExp(`\\([^)]*\\b${symbol}\\b[^)]*\\)\\s*=>`), // (symbol) => ...
|
|
253
|
+
new RegExp(`\\b${symbol}\\s*:`), // symbol: in object or parameter
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
return varPatterns.some(pattern => pattern.test(trimmedLine));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 检查一行代码中是否有导入语句
|
|
261
|
+
* @param {string} line - 代码行
|
|
262
|
+
* @param {string} symbol - 符号名
|
|
263
|
+
* @returns {boolean} 是否有导入
|
|
264
|
+
*/
|
|
265
|
+
function hasImportStatement(line, symbol) {
|
|
266
|
+
const trimmedLine = line.trim();
|
|
267
|
+
|
|
268
|
+
// 检查各种导入模式
|
|
269
|
+
const importPatterns = [
|
|
270
|
+
new RegExp(`import\\s+${symbol}\\s+from`), // import symbol from ...
|
|
271
|
+
new RegExp(`import\\s*{[^}]*\\b${symbol}\\b[^}]*}\\s+from`), // import { symbol } from ...
|
|
272
|
+
new RegExp(`import\\s*\\*\\s+as\\s+${symbol}\\s+from`), // import * as symbol from ...
|
|
273
|
+
new RegExp(`require\\([^)]*\\)\\.${symbol}`), // require(...).symbol
|
|
274
|
+
new RegExp(`const\\s+${symbol}\\s*=\\s*require`), // const symbol = require(...)
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
return importPatterns.some(pattern => pattern.test(trimmedLine));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 检查文件内部是否使用了指定的符号
|
|
282
|
+
* @param {string} content - 文件内容
|
|
283
|
+
* @param {Array} symbols - 要检查的符号列表(通常是被删除的符号)
|
|
284
|
+
* @returns {Array} 文件内部使用情况
|
|
285
|
+
*/
|
|
286
|
+
export function checkInternalUsage(content, symbols) {
|
|
287
|
+
if (!content || !symbols || symbols.length === 0) {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const usages = [];
|
|
292
|
+
const lines = content.split('\n');
|
|
293
|
+
|
|
294
|
+
symbols.forEach(symbol => {
|
|
295
|
+
const symbolName = symbol.name || symbol;
|
|
296
|
+
const regex = new RegExp(`\\b${symbolName}\\b`, 'g');
|
|
297
|
+
|
|
298
|
+
lines.forEach((line, index) => {
|
|
299
|
+
// 跳过定义行(避免匹配定义本身)
|
|
300
|
+
if (line.match(/(?:export\s+)?(?:function|const|let|var|class)\s+/) &&
|
|
301
|
+
line.includes(symbolName)) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (regex.test(line)) {
|
|
306
|
+
usages.push({
|
|
307
|
+
symbol: symbolName,
|
|
308
|
+
lineNumber: index + 1,
|
|
309
|
+
line: line.trim(),
|
|
310
|
+
context: {
|
|
311
|
+
before: lines.slice(Math.max(0, index - 2), index).map(l => l.trim()),
|
|
312
|
+
after: lines.slice(index + 1, index + 3).map(l => l.trim()),
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return usages;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 搜索项目中使用了指定符号的文件
|
|
324
|
+
* @param {Object} gitlabClient - GitLab 客户端
|
|
325
|
+
* @param {string} projectId - 项目 ID
|
|
326
|
+
* @param {string} ref - 分支名
|
|
327
|
+
* @param {Array} symbols - 要搜索的符号列表
|
|
328
|
+
* @returns {Promise<Array>} 使用了这些符号的文件列表
|
|
329
|
+
*/
|
|
330
|
+
export async function searchSymbolUsage(gitlabClient, projectId, ref, symbols) {
|
|
331
|
+
if (!symbols || symbols.length === 0) {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const affectedFiles = new Map();
|
|
336
|
+
|
|
337
|
+
// 对每个符号进行搜索
|
|
338
|
+
for (const symbol of symbols) {
|
|
339
|
+
try {
|
|
340
|
+
const symbolName = symbol.name || symbol;
|
|
341
|
+
// 使用 GitLab Search API 搜索代码
|
|
342
|
+
const searchResults = await gitlabClient.searchInProject(
|
|
343
|
+
projectId,
|
|
344
|
+
symbolName,
|
|
345
|
+
ref
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (searchResults && searchResults.length > 0) {
|
|
349
|
+
searchResults.forEach(result => {
|
|
350
|
+
const key = result.path || result.filename;
|
|
351
|
+
if (!affectedFiles.has(key)) {
|
|
352
|
+
affectedFiles.set(key, {
|
|
353
|
+
path: key,
|
|
354
|
+
symbols: [],
|
|
355
|
+
lines: [],
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const fileInfo = affectedFiles.get(key);
|
|
360
|
+
fileInfo.symbols.push(symbolName);
|
|
361
|
+
if (result.startline) {
|
|
362
|
+
fileInfo.lines.push(result.startline);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.warn(`搜索符号 ${symbol.name || symbol} 失败:`, error.message);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return Array.from(affectedFiles.values());
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 提取文件中特定符号周围的代码片段
|
|
376
|
+
* @param {string} content - 文件内容
|
|
377
|
+
* @param {string} symbolName - 符号名称
|
|
378
|
+
* @param {number} contextLines - 上下文行数
|
|
379
|
+
* @returns {Array} 代码片段数组
|
|
380
|
+
*/
|
|
381
|
+
export function extractCodeSnippets(content, symbolName, contextLines = 5) {
|
|
382
|
+
const lines = content.split('\n');
|
|
383
|
+
const snippets = [];
|
|
384
|
+
|
|
385
|
+
lines.forEach((line, index) => {
|
|
386
|
+
if (line.includes(symbolName)) {
|
|
387
|
+
const start = Math.max(0, index - contextLines);
|
|
388
|
+
const end = Math.min(lines.length, index + contextLines + 1);
|
|
389
|
+
|
|
390
|
+
snippets.push({
|
|
391
|
+
lineNumber: index + 1,
|
|
392
|
+
snippet: lines.slice(start, end).join('\n'),
|
|
393
|
+
startLine: start + 1,
|
|
394
|
+
endLine: end,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
return snippets;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 获取变更文件的函数/类签名
|
|
404
|
+
* @param {string} content - 文件内容
|
|
405
|
+
* @param {Array} changedSymbols - 变更的符号列表
|
|
406
|
+
* @returns {Array} 函数签名列表
|
|
407
|
+
*/
|
|
408
|
+
export function extractSignatures(content, changedSymbols) {
|
|
409
|
+
const signatures = [];
|
|
410
|
+
const lines = content.split('\n');
|
|
411
|
+
|
|
412
|
+
changedSymbols.forEach(symbol => {
|
|
413
|
+
// 找到定义该符号的行
|
|
414
|
+
const definitionLineIndex = lines.findIndex(line =>
|
|
415
|
+
line.includes(`function ${symbol.name}`) ||
|
|
416
|
+
line.includes(`const ${symbol.name}`) ||
|
|
417
|
+
line.includes(`class ${symbol.name}`) ||
|
|
418
|
+
line.includes(`interface ${symbol.name}`)
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
if (definitionLineIndex !== -1) {
|
|
422
|
+
// 提取函数签名(可能跨多行)
|
|
423
|
+
let signature = lines[definitionLineIndex];
|
|
424
|
+
let lineIndex = definitionLineIndex;
|
|
425
|
+
|
|
426
|
+
// 如果没有找到完整的签名(例如没有 { 或 =>),继续往下找
|
|
427
|
+
while (lineIndex < lines.length - 1 &&
|
|
428
|
+
!signature.includes('{') &&
|
|
429
|
+
!signature.includes('=>') &&
|
|
430
|
+
!signature.includes(';')) {
|
|
431
|
+
lineIndex++;
|
|
432
|
+
signature += '\n' + lines[lineIndex];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
signatures.push({
|
|
436
|
+
name: symbol.name,
|
|
437
|
+
signature: signature.trim(),
|
|
438
|
+
lineNumber: definitionLineIndex + 1,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return signatures;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* 使用增量调用链分析(TypeScript Compiler API)
|
|
448
|
+
* @param {Array} allChanges - 所有变更
|
|
449
|
+
* @returns {Promise<Object>} 调用链分析结果
|
|
450
|
+
*/
|
|
451
|
+
export async function analyzeWithCallChain(allChanges) {
|
|
452
|
+
try {
|
|
453
|
+
const analyzer = new IncrementalCallChainAnalyzer();
|
|
454
|
+
|
|
455
|
+
if (!analyzer.isAvailable()) {
|
|
456
|
+
console.log('ℹ️ TypeScript 调用链分析不可用');
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 提取所有变更的文件
|
|
461
|
+
const changedFiles = allChanges.map(c => c.new_path || c.old_path);
|
|
462
|
+
|
|
463
|
+
// 提取所有变更的符号
|
|
464
|
+
const allChangedSymbols = { added: [], deleted: [], modified: [], all: [] };
|
|
465
|
+
for (const change of allChanges) {
|
|
466
|
+
const fileName = change.new_path || change.old_path;
|
|
467
|
+
const diff = change.diff;
|
|
468
|
+
const symbols = extractChangedSymbols(diff, fileName);
|
|
469
|
+
|
|
470
|
+
allChangedSymbols.added.push(...symbols.added);
|
|
471
|
+
allChangedSymbols.deleted.push(...symbols.deleted);
|
|
472
|
+
allChangedSymbols.modified.push(...symbols.modified);
|
|
473
|
+
allChangedSymbols.all.push(...symbols.all);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 执行增量调用链分析
|
|
477
|
+
const result = await analyzer.analyzeImpact(changedFiles, allChangedSymbols);
|
|
478
|
+
|
|
479
|
+
return result;
|
|
480
|
+
} catch (error) {
|
|
481
|
+
console.error('❌ 调用链分析失败:', error.message);
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* 分析代码变更的完整影响
|
|
488
|
+
* @param {Object} options - 配置选项
|
|
489
|
+
* @param {Object} options.gitlabClient - GitLab 客户端
|
|
490
|
+
* @param {string} options.projectId - 项目 ID
|
|
491
|
+
* @param {string} options.ref - 分支名
|
|
492
|
+
* @param {Object} options.change - 代码变更对象
|
|
493
|
+
* @param {number} options.maxAffectedFiles - 最多分析的受影响文件数量
|
|
494
|
+
* @param {Object} options.callChainResult - 调用链分析结果(可选)
|
|
495
|
+
* @returns {Promise<Object>} 影响分析结果
|
|
496
|
+
*/
|
|
497
|
+
export async function analyzeImpact(options) {
|
|
498
|
+
const {
|
|
499
|
+
gitlabClient,
|
|
500
|
+
projectId,
|
|
501
|
+
ref,
|
|
502
|
+
change,
|
|
503
|
+
maxAffectedFiles = 10,
|
|
504
|
+
callChainResult = null,
|
|
505
|
+
} = options;
|
|
506
|
+
|
|
507
|
+
const fileName = change.new_path || change.old_path;
|
|
508
|
+
const diff = change.diff;
|
|
509
|
+
|
|
510
|
+
console.log(`\n🔍 分析文件 ${fileName} 的影响...`);
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
// 1. 提取变更的符号(包括新增、删除、修改)
|
|
514
|
+
const changedSymbols = extractChangedSymbols(diff, fileName);
|
|
515
|
+
const totalSymbols = changedSymbols.all.length;
|
|
516
|
+
|
|
517
|
+
console.log(` 发现变更符号: 新增 ${changedSymbols.added.length}, 删除 ${changedSymbols.deleted.length}, 修改 ${changedSymbols.modified.length}`);
|
|
518
|
+
|
|
519
|
+
if (totalSymbols === 0) {
|
|
520
|
+
return {
|
|
521
|
+
fileName,
|
|
522
|
+
changedSymbols,
|
|
523
|
+
fileContent: null,
|
|
524
|
+
internalUsage: [],
|
|
525
|
+
affectedFiles: [],
|
|
526
|
+
signatures: [],
|
|
527
|
+
callChain: null,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// 2. 获取变更文件的完整内容
|
|
532
|
+
let fileContent = null;
|
|
533
|
+
let signatures = [];
|
|
534
|
+
let internalUsage = [];
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
fileContent = await gitlabClient.getProjectFile(projectId, fileName, ref);
|
|
538
|
+
if (fileContent) {
|
|
539
|
+
// 提取函数签名(针对新增和修改的符号)
|
|
540
|
+
const symbolsForSignature = [...changedSymbols.added, ...changedSymbols.modified];
|
|
541
|
+
if (symbolsForSignature.length > 0) {
|
|
542
|
+
signatures = extractSignatures(fileContent, symbolsForSignature);
|
|
543
|
+
console.log(` 提取了 ${signatures.length} 个函数签名`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 检查文件内部是否使用了被删除的符号
|
|
547
|
+
if (changedSymbols.deleted.length > 0) {
|
|
548
|
+
internalUsage = checkInternalUsage(fileContent, changedSymbols.deleted);
|
|
549
|
+
if (internalUsage.length > 0) {
|
|
550
|
+
console.log(` ⚠️ 文件内部有 ${internalUsage.length} 处使用了被删除的符号`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} catch (error) {
|
|
555
|
+
console.warn(` 无法获取文件内容:`, error.message);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 3. 优先使用调用链结果,否则降级到 GitLab Search
|
|
559
|
+
let affectedFiles = [];
|
|
560
|
+
let callChainInfo = null;
|
|
561
|
+
|
|
562
|
+
if (callChainResult && callChainResult.callChains) {
|
|
563
|
+
// 使用 TypeScript 调用链分析结果
|
|
564
|
+
console.log(` 📊 使用 TypeScript 调用链分析结果`);
|
|
565
|
+
|
|
566
|
+
// 过滤出当前文件相关的调用链
|
|
567
|
+
const relevantChains = callChainResult.callChains.filter(chain => {
|
|
568
|
+
const symbolNames = changedSymbols.all.map(s => s.name || s);
|
|
569
|
+
return symbolNames.includes(chain.symbol);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
if (relevantChains.length > 0) {
|
|
573
|
+
console.log(` ✅ 找到 ${relevantChains.length} 个相关调用链`);
|
|
574
|
+
|
|
575
|
+
callChainInfo = {
|
|
576
|
+
method: 'typescript-callchain',
|
|
577
|
+
chains: relevantChains,
|
|
578
|
+
summary: callChainResult.codeContext?.summary,
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// 将调用链转换为 affectedFiles 格式(用于兼容现有代码)
|
|
582
|
+
const filesMap = new Map();
|
|
583
|
+
|
|
584
|
+
for (const chain of relevantChains) {
|
|
585
|
+
for (const usage of chain.usages) {
|
|
586
|
+
if (usage.file === fileName) continue; // 跳过当前文件
|
|
587
|
+
|
|
588
|
+
if (!filesMap.has(usage.file)) {
|
|
589
|
+
filesMap.set(usage.file, {
|
|
590
|
+
path: usage.file,
|
|
591
|
+
symbols: [],
|
|
592
|
+
lines: [],
|
|
593
|
+
snippets: [],
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const fileInfo = filesMap.get(usage.file);
|
|
598
|
+
fileInfo.symbols.push(chain.symbol);
|
|
599
|
+
fileInfo.lines.push(usage.line);
|
|
600
|
+
fileInfo.snippets.push({
|
|
601
|
+
lineNumber: usage.line,
|
|
602
|
+
snippet: usage.context,
|
|
603
|
+
issue: usage.issue,
|
|
604
|
+
dataFlow: usage.dataFlow,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
affectedFiles = Array.from(filesMap.values());
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 4. 如果没有调用链结果,使用传统的 GitLab Search
|
|
614
|
+
if (affectedFiles.length === 0) {
|
|
615
|
+
console.log(` 🔍 使用 GitLab Search API 搜索`);
|
|
616
|
+
|
|
617
|
+
const symbolsToSearch = [
|
|
618
|
+
...changedSymbols.deleted.filter(s => s.type === 'definition'),
|
|
619
|
+
...changedSymbols.modified,
|
|
620
|
+
...changedSymbols.added.filter(s => s.type === 'definition'),
|
|
621
|
+
];
|
|
622
|
+
|
|
623
|
+
const searchResults = await searchSymbolUsage(
|
|
624
|
+
gitlabClient,
|
|
625
|
+
projectId,
|
|
626
|
+
ref,
|
|
627
|
+
symbolsToSearch
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// 过滤掉当前文件本身
|
|
631
|
+
const externalAffectedFiles = searchResults.filter(f => f.path !== fileName);
|
|
632
|
+
|
|
633
|
+
console.log(` 找到 ${externalAffectedFiles.length} 个其他文件可能受影响`);
|
|
634
|
+
|
|
635
|
+
// 限制数量并获取代码片段
|
|
636
|
+
const limitedFiles = externalAffectedFiles.slice(0, maxAffectedFiles);
|
|
637
|
+
affectedFiles = await Promise.all(
|
|
638
|
+
limitedFiles.map(async (file) => {
|
|
639
|
+
try {
|
|
640
|
+
const content = await gitlabClient.getProjectFile(projectId, file.path, ref);
|
|
641
|
+
|
|
642
|
+
if (content) {
|
|
643
|
+
const snippets = [];
|
|
644
|
+
file.symbols.forEach(symbol => {
|
|
645
|
+
const symbolSnippets = extractCodeSnippets(content, symbol, 3);
|
|
646
|
+
snippets.push(...symbolSnippets);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
...file,
|
|
651
|
+
snippets: snippets.slice(0, 3), // 每个文件最多 3 个片段
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return file;
|
|
656
|
+
} catch (error) {
|
|
657
|
+
console.warn(` 无法获取文件 ${file.path} 的内容:`, error.message);
|
|
658
|
+
return file;
|
|
659
|
+
}
|
|
660
|
+
})
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
fileName,
|
|
666
|
+
changedSymbols,
|
|
667
|
+
fileContent, // 完整文件内容
|
|
668
|
+
internalUsage, // 文件内部使用情况
|
|
669
|
+
signatures,
|
|
670
|
+
affectedFiles,
|
|
671
|
+
totalAffectedFiles: affectedFiles.length,
|
|
672
|
+
callChain: callChainInfo, // 新增:调用链信息
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
} catch (error) {
|
|
676
|
+
console.error(` 影响分析失败:`, error.message);
|
|
677
|
+
return {
|
|
678
|
+
fileName,
|
|
679
|
+
error: error.message,
|
|
680
|
+
changedSymbols: { added: [], deleted: [], modified: [], all: [] },
|
|
681
|
+
fileContent: null,
|
|
682
|
+
internalUsage: [],
|
|
683
|
+
affectedFiles: [],
|
|
684
|
+
signatures: [],
|
|
685
|
+
callChain: null,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export default {
|
|
691
|
+
extractImportsExports,
|
|
692
|
+
extractChangedSymbols,
|
|
693
|
+
checkInternalUsage,
|
|
694
|
+
searchSymbolUsage,
|
|
695
|
+
extractCodeSnippets,
|
|
696
|
+
extractSignatures,
|
|
697
|
+
analyzeImpact,
|
|
698
|
+
analyzeWithCallChain,
|
|
699
|
+
};
|
|
700
|
+
|