gitlab-ai-review 4.2.4 → 6.3.9
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 +36 -2
- package/cli.js +118 -32
- package/index.js +741 -335
- package/lib/ai-client.js +145 -61
- package/lib/config.js +798 -11
- package/lib/diff-parser.js +143 -44
- package/lib/document-loader.js +329 -0
- package/lib/export-analyzer.js +384 -0
- package/lib/gitlab-client.js +588 -7
- package/lib/prompt-tools.js +241 -453
- package/package.json +52 -50
- package/lib/impact-analyzer.js +0 -700
- package/lib/incremental-callchain-analyzer.js +0 -811
|
@@ -1,811 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 增量调用链分析器 - 基于 TypeScript Compiler API
|
|
3
|
-
* 只分析 diff 中变更的符号及其调用链,避免全量分析
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import ts from 'typescript';
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
|
-
|
|
11
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
-
const __dirname = path.dirname(__filename);
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 增量调用链分析器
|
|
16
|
-
*/
|
|
17
|
-
export class IncrementalCallChainAnalyzer {
|
|
18
|
-
constructor(projectPath) {
|
|
19
|
-
this.projectPath = projectPath || process.env.CI_PROJECT_DIR || process.cwd();
|
|
20
|
-
this.program = null;
|
|
21
|
-
this.checker = null;
|
|
22
|
-
this.sourceFilesCache = new Map();
|
|
23
|
-
|
|
24
|
-
console.log(`📁 项目路径: ${this.projectPath}`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 检查 TypeScript 环境是否可用
|
|
29
|
-
*/
|
|
30
|
-
isAvailable() {
|
|
31
|
-
try {
|
|
32
|
-
// 检查项目路径是否存在
|
|
33
|
-
if (!fs.existsSync(this.projectPath)) {
|
|
34
|
-
console.warn(`⚠️ 项目路径不存在: ${this.projectPath}`);
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// 检查是否有 TypeScript 文件
|
|
39
|
-
const hasTS = this.hasTypeScriptFiles();
|
|
40
|
-
if (!hasTS) {
|
|
41
|
-
console.warn('⚠️ 项目中没有 TypeScript 文件');
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return true;
|
|
46
|
-
} catch (error) {
|
|
47
|
-
console.warn('⚠️ TypeScript 环境检查失败:', error.message);
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 检查项目是否有 TypeScript 文件
|
|
54
|
-
*/
|
|
55
|
-
hasTypeScriptFiles() {
|
|
56
|
-
const extensions = ['.ts', '.tsx'];
|
|
57
|
-
|
|
58
|
-
const checkDir = (dir, depth = 0) => {
|
|
59
|
-
if (depth > 3) return false; // 限制深度
|
|
60
|
-
if (dir.includes('node_modules')) return false;
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
64
|
-
|
|
65
|
-
for (const entry of entries) {
|
|
66
|
-
if (entry.isFile()) {
|
|
67
|
-
const ext = path.extname(entry.name);
|
|
68
|
-
if (extensions.includes(ext)) {
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
} else if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
72
|
-
const subDir = path.join(dir, entry.name);
|
|
73
|
-
if (checkDir(subDir, depth + 1)) {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
} catch (error) {
|
|
79
|
-
// 忽略权限错误
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return false;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
return checkDir(this.projectPath);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 获取项目中受影响的文件列表(只分析必要的文件)
|
|
90
|
-
*/
|
|
91
|
-
getAffectedFiles(changedFiles) {
|
|
92
|
-
const files = new Set();
|
|
93
|
-
|
|
94
|
-
// 1. 添加变更文件本身
|
|
95
|
-
changedFiles.forEach(file => {
|
|
96
|
-
const fullPath = path.join(this.projectPath, file);
|
|
97
|
-
if (fs.existsSync(fullPath)) {
|
|
98
|
-
files.add(fullPath);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// 2. 查找导入这些文件的文件(简单的文本搜索)
|
|
103
|
-
const allProjectFiles = this.getAllProjectFiles();
|
|
104
|
-
|
|
105
|
-
for (const changedFile of changedFiles) {
|
|
106
|
-
const baseName = path.basename(changedFile, path.extname(changedFile));
|
|
107
|
-
const dirName = path.dirname(changedFile);
|
|
108
|
-
|
|
109
|
-
for (const projectFile of allProjectFiles) {
|
|
110
|
-
try {
|
|
111
|
-
const content = fs.readFileSync(projectFile, 'utf8');
|
|
112
|
-
|
|
113
|
-
// 检查是否导入了变更的文件
|
|
114
|
-
const importPatterns = [
|
|
115
|
-
new RegExp(`from\\s+['"]([^'"]*${baseName}[^'"]*)['"]`, 'g'),
|
|
116
|
-
new RegExp(`import\\s+['"]([^'"]*${baseName}[^'"]*)['"]`, 'g'),
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
const hasImport = importPatterns.some(pattern => pattern.test(content));
|
|
120
|
-
|
|
121
|
-
if (hasImport) {
|
|
122
|
-
files.add(projectFile);
|
|
123
|
-
}
|
|
124
|
-
} catch (error) {
|
|
125
|
-
// 忽略读取错误
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
console.log(`📂 需要分析的文件数: ${files.size}`);
|
|
131
|
-
return Array.from(files);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* 获取所有项目文件
|
|
136
|
-
*/
|
|
137
|
-
getAllProjectFiles() {
|
|
138
|
-
const files = [];
|
|
139
|
-
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
140
|
-
|
|
141
|
-
const scanDir = (dir, depth = 0) => {
|
|
142
|
-
if (depth > 5) return; // 限制深度
|
|
143
|
-
if (dir.includes('node_modules')) return;
|
|
144
|
-
if (dir.includes('dist')) return;
|
|
145
|
-
if (dir.includes('build')) return;
|
|
146
|
-
if (dir.includes('.git')) return;
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
150
|
-
|
|
151
|
-
for (const entry of entries) {
|
|
152
|
-
if (entry.name.startsWith('.')) continue;
|
|
153
|
-
|
|
154
|
-
const fullPath = path.join(dir, entry.name);
|
|
155
|
-
|
|
156
|
-
if (entry.isFile()) {
|
|
157
|
-
const ext = path.extname(entry.name);
|
|
158
|
-
if (extensions.includes(ext)) {
|
|
159
|
-
files.push(fullPath);
|
|
160
|
-
}
|
|
161
|
-
} else if (entry.isDirectory()) {
|
|
162
|
-
scanDir(fullPath, depth + 1);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
} catch (error) {
|
|
166
|
-
// 忽略权限错误
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
scanDir(this.projectPath);
|
|
171
|
-
return files;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* 创建 TypeScript 程序(只解析必要的文件)
|
|
176
|
-
*/
|
|
177
|
-
createProgram(files) {
|
|
178
|
-
console.log(`🔨 创建 TypeScript 程序...`);
|
|
179
|
-
const startTime = Date.now();
|
|
180
|
-
|
|
181
|
-
// 查找 tsconfig.json
|
|
182
|
-
let configPath = path.join(this.projectPath, 'tsconfig.json');
|
|
183
|
-
if (!fs.existsSync(configPath)) {
|
|
184
|
-
configPath = path.join(this.projectPath, 'tsconfig.app.json');
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
let compilerOptions = {
|
|
188
|
-
target: ts.ScriptTarget.ES2020,
|
|
189
|
-
module: ts.ModuleKind.ESNext,
|
|
190
|
-
jsx: ts.JsxEmit.React,
|
|
191
|
-
skipLibCheck: true,
|
|
192
|
-
skipDefaultLibCheck: true,
|
|
193
|
-
noEmit: true,
|
|
194
|
-
allowJs: true,
|
|
195
|
-
esModuleInterop: true,
|
|
196
|
-
resolveJsonModule: true,
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
// 尝试加载 tsconfig
|
|
200
|
-
if (fs.existsSync(configPath)) {
|
|
201
|
-
try {
|
|
202
|
-
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
203
|
-
const parsedConfig = ts.parseJsonConfigFileContent(
|
|
204
|
-
configFile.config,
|
|
205
|
-
ts.sys,
|
|
206
|
-
this.projectPath
|
|
207
|
-
);
|
|
208
|
-
compilerOptions = { ...compilerOptions, ...parsedConfig.options };
|
|
209
|
-
console.log(` ✅ 加载了 tsconfig: ${path.basename(configPath)}`);
|
|
210
|
-
} catch (error) {
|
|
211
|
-
console.warn(` ⚠️ 加载 tsconfig 失败,使用默认配置`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 创建程序
|
|
216
|
-
this.program = ts.createProgram(files, compilerOptions);
|
|
217
|
-
this.checker = this.program.getTypeChecker();
|
|
218
|
-
|
|
219
|
-
const duration = Date.now() - startTime;
|
|
220
|
-
console.log(`✅ TypeScript 程序创建完成 (${duration}ms)`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* 分析变更的影响(核心方法)
|
|
225
|
-
*/
|
|
226
|
-
async analyzeImpact(changedFiles, changedSymbols) {
|
|
227
|
-
console.log(`\n🔍 开始增量调用链分析...`);
|
|
228
|
-
|
|
229
|
-
if (!this.isAvailable()) {
|
|
230
|
-
console.log('⚠️ TypeScript 环境不可用,跳过调用链分析');
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
// 1. 获取受影响的文件
|
|
236
|
-
const affectedFiles = this.getAffectedFiles(changedFiles);
|
|
237
|
-
|
|
238
|
-
if (affectedFiles.length === 0) {
|
|
239
|
-
console.log('⚠️ 没有找到受影响的文件');
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (affectedFiles.length > 50) {
|
|
244
|
-
console.log(`⚠️ 受影响的文件过多 (${affectedFiles.length}),跳过 TypeScript 分析`);
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// 2. 创建 TypeScript 程序
|
|
249
|
-
this.createProgram(affectedFiles);
|
|
250
|
-
|
|
251
|
-
// 3. 为每个变更的符号构建调用链
|
|
252
|
-
const callChains = [];
|
|
253
|
-
|
|
254
|
-
for (const symbol of changedSymbols.deleted) {
|
|
255
|
-
const chain = this.buildCallChain(symbol, changedFiles);
|
|
256
|
-
if (chain) {
|
|
257
|
-
callChains.push(chain);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
for (const symbol of changedSymbols.modified) {
|
|
262
|
-
const chain = this.buildCallChain(symbol, changedFiles);
|
|
263
|
-
if (chain) {
|
|
264
|
-
callChains.push(chain);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// 4. 新增:即使没有直接引用,也提取相关的类型使用
|
|
269
|
-
for (const symbol of changedSymbols.added) {
|
|
270
|
-
if (symbol.type === 'field') {
|
|
271
|
-
const chain = this.buildCallChain(symbol, changedFiles);
|
|
272
|
-
if (chain) {
|
|
273
|
-
callChains.push(chain);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
console.log(`✅ 构建了 ${callChains.length} 个调用链`);
|
|
279
|
-
|
|
280
|
-
// 输出调用链摘要
|
|
281
|
-
if (callChains.length > 0) {
|
|
282
|
-
console.log(`\n📊 调用链摘要:`);
|
|
283
|
-
callChains.forEach((chain, idx) => {
|
|
284
|
-
const totalRefs = (chain.usages?.length || 0) + (chain.typeUsages?.length || 0);
|
|
285
|
-
const issueCount = chain.issues?.length || 0;
|
|
286
|
-
const issueInfo = issueCount > 0 ? ` ⚠️ ${issueCount} 个问题` : '';
|
|
287
|
-
console.log(` ${idx + 1}. ${chain.symbol} (${chain.type}): ${totalRefs} 处引用${issueInfo}`);
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// 5. 提取代码上下文
|
|
292
|
-
const codeContext = this.extractCodeContext(callChains, changedFiles);
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
method: 'typescript-callchain',
|
|
296
|
-
callChains,
|
|
297
|
-
codeContext,
|
|
298
|
-
filesAnalyzed: affectedFiles.length,
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
} catch (error) {
|
|
302
|
-
console.error('❌ 调用链分析失败:', error.message);
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* 为单个符号构建调用链
|
|
309
|
-
*/
|
|
310
|
-
buildCallChain(symbol, changedFiles) {
|
|
311
|
-
const symbolName = symbol.name || symbol;
|
|
312
|
-
const symbolType = symbol.type || 'unknown';
|
|
313
|
-
console.log(` 🔗 构建调用链: ${symbolName} (${symbolType})`);
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
// 1. 找到符号的所有引用
|
|
317
|
-
const references = this.findAllReferences(symbolName, symbolType);
|
|
318
|
-
|
|
319
|
-
// 2. 如果是字段,额外查找父类型的使用
|
|
320
|
-
let typeUsages = [];
|
|
321
|
-
if (symbolType === 'field') {
|
|
322
|
-
typeUsages = this.findFieldParentTypeUsages(symbolName, changedFiles);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (references.length === 0 && typeUsages.length === 0) {
|
|
326
|
-
console.log(` ℹ️ 未找到引用`);
|
|
327
|
-
// 对于字段,即使没找到引用,也返回基本信息
|
|
328
|
-
if (symbolType === 'field') {
|
|
329
|
-
return {
|
|
330
|
-
symbol: symbolName,
|
|
331
|
-
type: symbolType,
|
|
332
|
-
usages: [],
|
|
333
|
-
typeUsages: [],
|
|
334
|
-
issues: [],
|
|
335
|
-
note: '字段未被直接引用,但可能在类型定义中使用',
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
console.log(` ✅ 找到 ${references.length} 处直接引用, ${typeUsages.length} 处类型使用`);
|
|
342
|
-
|
|
343
|
-
// 输出类型使用的详细位置
|
|
344
|
-
if (typeUsages.length > 0) {
|
|
345
|
-
console.log(` 📦 类型使用位置:`);
|
|
346
|
-
typeUsages.slice(0, 3).forEach((typeUsage, idx) => {
|
|
347
|
-
console.log(` ${idx + 1}. ${typeUsage.file}:${typeUsage.line} - ${typeUsage.parentType}`);
|
|
348
|
-
});
|
|
349
|
-
if (typeUsages.length > 3) {
|
|
350
|
-
console.log(` ... 还有 ${typeUsages.length - 3} 处`);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// 3. 分析每个引用点
|
|
355
|
-
const usages = references.map(ref => {
|
|
356
|
-
const usage = {
|
|
357
|
-
file: this.getRelativePath(ref.fileName),
|
|
358
|
-
line: ref.line,
|
|
359
|
-
code: this.getLineContent(ref.fileName, ref.line),
|
|
360
|
-
context: this.getContext(ref.fileName, ref.line, 5),
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
// 检测问题
|
|
364
|
-
const issue = this.detectIssue(ref, symbol);
|
|
365
|
-
if (issue) {
|
|
366
|
-
usage.issue = issue;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// 追踪数据流
|
|
370
|
-
const dataFlow = this.traceDataFlow(ref.node);
|
|
371
|
-
if (dataFlow.length > 0) {
|
|
372
|
-
usage.dataFlow = dataFlow;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return usage;
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
const issues = usages.filter(u => u.issue).map(u => u.issue);
|
|
379
|
-
|
|
380
|
-
// 输出直接引用的详细信息
|
|
381
|
-
if (references.length > 0) {
|
|
382
|
-
console.log(` 🔍 直接引用位置:`);
|
|
383
|
-
usages.slice(0, 3).forEach((usage, idx) => {
|
|
384
|
-
const issueInfo = usage.issue ? ` ⚠️ ${usage.issue.type}` : '';
|
|
385
|
-
const dataFlowInfo = usage.dataFlow && usage.dataFlow.length > 0
|
|
386
|
-
? ` → ${usage.dataFlow[usage.dataFlow.length - 1]}`
|
|
387
|
-
: '';
|
|
388
|
-
console.log(` ${idx + 1}. ${usage.file}:${usage.line}${issueInfo}${dataFlowInfo}`);
|
|
389
|
-
});
|
|
390
|
-
if (usages.length > 3) {
|
|
391
|
-
console.log(` ... 还有 ${usages.length - 3} 处`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// 输出检测到的问题
|
|
396
|
-
if (issues.length > 0) {
|
|
397
|
-
console.log(` 🚨 检测到 ${issues.length} 个问题:`);
|
|
398
|
-
issues.forEach((issue, idx) => {
|
|
399
|
-
console.log(` ${idx + 1}. [${issue.severity}] ${issue.message}`);
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return {
|
|
404
|
-
symbol: symbolName,
|
|
405
|
-
type: symbolType,
|
|
406
|
-
usages,
|
|
407
|
-
typeUsages, // 新增:类型使用(如 mockData: Courseware[])
|
|
408
|
-
issues,
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
} catch (error) {
|
|
412
|
-
console.warn(` ⚠️ 构建调用链失败:`, error.message);
|
|
413
|
-
return null;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* 查找符号的所有引用
|
|
419
|
-
*/
|
|
420
|
-
findAllReferences(symbolName, symbolType) {
|
|
421
|
-
const references = [];
|
|
422
|
-
|
|
423
|
-
for (const sourceFile of this.program.getSourceFiles()) {
|
|
424
|
-
if (sourceFile.fileName.includes('node_modules')) continue;
|
|
425
|
-
|
|
426
|
-
const content = sourceFile.getFullText();
|
|
427
|
-
|
|
428
|
-
// 根据符号类型使用不同的搜索策略
|
|
429
|
-
if (symbolType === 'field') {
|
|
430
|
-
// 字段:搜索属性访问 obj.fieldName
|
|
431
|
-
const fieldAccessPattern = new RegExp(`\\.${symbolName}\\b`, 'g');
|
|
432
|
-
const objectLiteralPattern = new RegExp(`\\b${symbolName}\\s*:`, 'g');
|
|
433
|
-
|
|
434
|
-
let match;
|
|
435
|
-
while ((match = fieldAccessPattern.exec(content)) !== null) {
|
|
436
|
-
const pos = match.index + 1; // +1 跳过 '.'
|
|
437
|
-
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
438
|
-
references.push({
|
|
439
|
-
fileName: sourceFile.fileName,
|
|
440
|
-
line: line + 1,
|
|
441
|
-
node: this.getNodeAtPosition(sourceFile, pos),
|
|
442
|
-
sourceFile: sourceFile,
|
|
443
|
-
matchType: 'field-access',
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
while ((match = objectLiteralPattern.exec(content)) !== null) {
|
|
448
|
-
const pos = match.index;
|
|
449
|
-
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
450
|
-
references.push({
|
|
451
|
-
fileName: sourceFile.fileName,
|
|
452
|
-
line: line + 1,
|
|
453
|
-
node: this.getNodeAtPosition(sourceFile, pos),
|
|
454
|
-
sourceFile: sourceFile,
|
|
455
|
-
matchType: 'object-literal',
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
} else {
|
|
459
|
-
// 其他符号:简单的文本匹配
|
|
460
|
-
this.visitNode(sourceFile, (node) => {
|
|
461
|
-
const text = node.getText();
|
|
462
|
-
|
|
463
|
-
if (text === symbolName || text.includes(symbolName)) {
|
|
464
|
-
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
465
|
-
|
|
466
|
-
references.push({
|
|
467
|
-
fileName: sourceFile.fileName,
|
|
468
|
-
line: line + 1,
|
|
469
|
-
node: node,
|
|
470
|
-
sourceFile: sourceFile,
|
|
471
|
-
matchType: 'text-match',
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return references;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* 查找字段所属接口/类型的使用处
|
|
483
|
-
*/
|
|
484
|
-
findFieldParentTypeUsages(fieldName, changedFiles) {
|
|
485
|
-
const typeUsages = [];
|
|
486
|
-
|
|
487
|
-
try {
|
|
488
|
-
// 1. 找到字段所在的接口/类型定义
|
|
489
|
-
const parentTypes = this.findFieldParentTypes(fieldName, changedFiles);
|
|
490
|
-
|
|
491
|
-
// 2. 对每个父类型,查找其使用处
|
|
492
|
-
for (const parentType of parentTypes) {
|
|
493
|
-
for (const sourceFile of this.program.getSourceFiles()) {
|
|
494
|
-
if (sourceFile.fileName.includes('node_modules')) continue;
|
|
495
|
-
|
|
496
|
-
const content = sourceFile.getFullText();
|
|
497
|
-
|
|
498
|
-
// 查找类型注解:const x: ParentType
|
|
499
|
-
const typeAnnotationPattern = new RegExp(`:\\s*${parentType}\\b`, 'g');
|
|
500
|
-
let match;
|
|
501
|
-
|
|
502
|
-
while ((match = typeAnnotationPattern.exec(content)) !== null) {
|
|
503
|
-
const pos = match.index;
|
|
504
|
-
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
505
|
-
|
|
506
|
-
typeUsages.push({
|
|
507
|
-
file: this.getRelativePath(sourceFile.fileName),
|
|
508
|
-
line: line + 1,
|
|
509
|
-
parentType: parentType,
|
|
510
|
-
context: this.getContext(sourceFile.fileName, line + 1, 8),
|
|
511
|
-
note: `使用了包含字段 ${fieldName} 的类型 ${parentType}`,
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
console.log(` 📦 找到 ${typeUsages.length} 处使用父类型 ${parentTypes.join(', ')}`);
|
|
518
|
-
} catch (error) {
|
|
519
|
-
console.warn(` ⚠️ 查找父类型使用失败:`, error.message);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
return typeUsages;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* 查找字段所属的接口/类型名称
|
|
527
|
-
*/
|
|
528
|
-
findFieldParentTypes(fieldName, changedFiles) {
|
|
529
|
-
const parentTypes = [];
|
|
530
|
-
|
|
531
|
-
for (const changedFile of changedFiles) {
|
|
532
|
-
const fullPath = path.join(this.projectPath, changedFile);
|
|
533
|
-
if (!fs.existsSync(fullPath)) continue;
|
|
534
|
-
|
|
535
|
-
const content = fs.readFileSync(fullPath, 'utf8');
|
|
536
|
-
|
|
537
|
-
// 简单的正则:查找包含该字段的接口/类型
|
|
538
|
-
const interfacePattern = new RegExp(
|
|
539
|
-
`(?:export\\s+)?interface\\s+(\\w+)\\s*{[^}]*\\b${fieldName}\\s*[?]?\\s*:`,
|
|
540
|
-
'g'
|
|
541
|
-
);
|
|
542
|
-
const typePattern = new RegExp(
|
|
543
|
-
`(?:export\\s+)?type\\s+(\\w+)\\s*=\\s*{[^}]*\\b${fieldName}\\s*[?]?\\s*:`,
|
|
544
|
-
'g'
|
|
545
|
-
);
|
|
546
|
-
|
|
547
|
-
let match;
|
|
548
|
-
while ((match = interfacePattern.exec(content)) !== null) {
|
|
549
|
-
parentTypes.push(match[1]);
|
|
550
|
-
}
|
|
551
|
-
while ((match = typePattern.exec(content)) !== null) {
|
|
552
|
-
parentTypes.push(match[1]);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
return [...new Set(parentTypes)];
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* 获取指定位置的 AST 节点
|
|
561
|
-
*/
|
|
562
|
-
getNodeAtPosition(sourceFile, position) {
|
|
563
|
-
let foundNode = sourceFile;
|
|
564
|
-
|
|
565
|
-
const visit = (node) => {
|
|
566
|
-
if (node.pos <= position && position < node.end) {
|
|
567
|
-
foundNode = node;
|
|
568
|
-
ts.forEachChild(node, visit);
|
|
569
|
-
}
|
|
570
|
-
};
|
|
571
|
-
|
|
572
|
-
visit(sourceFile);
|
|
573
|
-
return foundNode;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* 遍历 AST 节点
|
|
578
|
-
*/
|
|
579
|
-
visitNode(node, callback) {
|
|
580
|
-
callback(node);
|
|
581
|
-
ts.forEachChild(node, (child) => this.visitNode(child, callback));
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* 追踪数据流
|
|
586
|
-
*/
|
|
587
|
-
traceDataFlow(node) {
|
|
588
|
-
const flow = [];
|
|
589
|
-
let current = node;
|
|
590
|
-
let depth = 0;
|
|
591
|
-
|
|
592
|
-
while (current && depth < 5) {
|
|
593
|
-
if (ts.isVariableDeclaration(current)) {
|
|
594
|
-
const name = current.name.getText();
|
|
595
|
-
flow.unshift(`变量: ${name}`);
|
|
596
|
-
} else if (ts.isFunctionDeclaration(current)) {
|
|
597
|
-
const name = current.name?.getText() || 'anonymous';
|
|
598
|
-
flow.unshift(`函数: ${name}`);
|
|
599
|
-
} else if (ts.isPropertyAssignment(current)) {
|
|
600
|
-
const name = current.name.getText();
|
|
601
|
-
flow.unshift(`属性: ${name}`);
|
|
602
|
-
} else if (ts.isCallExpression(current)) {
|
|
603
|
-
const expr = current.expression.getText();
|
|
604
|
-
flow.unshift(`调用: ${expr}()`);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
current = current.parent;
|
|
608
|
-
depth++;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
return flow;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* 检测问题
|
|
616
|
-
*/
|
|
617
|
-
detectIssue(reference, symbol) {
|
|
618
|
-
const line = this.getLineContent(reference.fileName, reference.line);
|
|
619
|
-
const symbolName = symbol.name || symbol;
|
|
620
|
-
|
|
621
|
-
// 检测:访问被删除/重命名的符号
|
|
622
|
-
if (symbol.type === 'definition' && line.includes(symbolName)) {
|
|
623
|
-
// 检查是否是导入语句
|
|
624
|
-
if (line.includes('import') && line.includes(symbolName)) {
|
|
625
|
-
return {
|
|
626
|
-
type: 'import-deleted-symbol',
|
|
627
|
-
severity: 'error',
|
|
628
|
-
message: `导入了被删除的符号: ${symbolName}`,
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// 检查是否是函数调用
|
|
633
|
-
if (line.includes(`${symbolName}(`)) {
|
|
634
|
-
return {
|
|
635
|
-
type: 'call-deleted-function',
|
|
636
|
-
severity: 'error',
|
|
637
|
-
message: `调用了被删除的函数: ${symbolName}()`,
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// 检查是否是类型引用
|
|
642
|
-
if (line.includes(`: ${symbolName}`) || line.includes(`<${symbolName}>`)) {
|
|
643
|
-
return {
|
|
644
|
-
type: 'reference-deleted-type',
|
|
645
|
-
severity: 'error',
|
|
646
|
-
message: `引用了被删除的类型: ${symbolName}`,
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return null;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* 提取代码上下文
|
|
656
|
-
*/
|
|
657
|
-
extractCodeContext(callChains, changedFiles) {
|
|
658
|
-
const filesMap = new Map();
|
|
659
|
-
|
|
660
|
-
// 1. 提取直接引用
|
|
661
|
-
for (const chain of callChains) {
|
|
662
|
-
for (const usage of chain.usages) {
|
|
663
|
-
if (!filesMap.has(usage.file)) {
|
|
664
|
-
filesMap.set(usage.file, {
|
|
665
|
-
path: usage.file,
|
|
666
|
-
snippets: [],
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const fileInfo = filesMap.get(usage.file);
|
|
671
|
-
fileInfo.snippets.push({
|
|
672
|
-
symbol: chain.symbol,
|
|
673
|
-
line: usage.line,
|
|
674
|
-
code: usage.context,
|
|
675
|
-
issue: usage.issue,
|
|
676
|
-
dataFlow: usage.dataFlow,
|
|
677
|
-
type: 'direct-reference',
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// 2. 提取类型使用(针对字段)
|
|
682
|
-
if (chain.typeUsages && chain.typeUsages.length > 0) {
|
|
683
|
-
for (const typeUsage of chain.typeUsages) {
|
|
684
|
-
if (!filesMap.has(typeUsage.file)) {
|
|
685
|
-
filesMap.set(typeUsage.file, {
|
|
686
|
-
path: typeUsage.file,
|
|
687
|
-
snippets: [],
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const fileInfo = filesMap.get(typeUsage.file);
|
|
692
|
-
fileInfo.snippets.push({
|
|
693
|
-
symbol: chain.symbol,
|
|
694
|
-
line: typeUsage.line,
|
|
695
|
-
code: typeUsage.context,
|
|
696
|
-
note: typeUsage.note,
|
|
697
|
-
parentType: typeUsage.parentType,
|
|
698
|
-
type: 'type-usage',
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// 3. 始终包含变更文件本身的完整内容
|
|
705
|
-
for (const changedFile of changedFiles) {
|
|
706
|
-
const relativePath = changedFile.replace(this.projectPath, '').replace(/^[\/\\]/, '');
|
|
707
|
-
|
|
708
|
-
if (!filesMap.has(relativePath)) {
|
|
709
|
-
filesMap.set(relativePath, {
|
|
710
|
-
path: relativePath,
|
|
711
|
-
snippets: [],
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const fileInfo = filesMap.get(relativePath);
|
|
716
|
-
|
|
717
|
-
// 添加文件定义片段
|
|
718
|
-
try {
|
|
719
|
-
const fullPath = path.join(this.projectPath, changedFile);
|
|
720
|
-
if (fs.existsSync(fullPath)) {
|
|
721
|
-
const content = fs.readFileSync(fullPath, 'utf8');
|
|
722
|
-
fileInfo.snippets.push({
|
|
723
|
-
symbol: '(文件定义)',
|
|
724
|
-
line: 1,
|
|
725
|
-
code: content,
|
|
726
|
-
type: 'file-definition',
|
|
727
|
-
note: '这是变更文件的完整内容,用于理解接口和类型定义',
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
} catch (error) {
|
|
731
|
-
console.warn(` ⚠️ 无法读取变更文件:`, error.message);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
return {
|
|
736
|
-
files: Array.from(filesMap.values()),
|
|
737
|
-
totalFiles: filesMap.size,
|
|
738
|
-
totalIssues: callChains.flatMap(c => c.issues).length,
|
|
739
|
-
summary: this.buildSummary(callChains),
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* 构建摘要
|
|
745
|
-
*/
|
|
746
|
-
buildSummary(callChains) {
|
|
747
|
-
const summary = {
|
|
748
|
-
totalSymbols: callChains.length,
|
|
749
|
-
totalReferences: callChains.reduce((sum, c) => sum + c.usages.length, 0),
|
|
750
|
-
totalIssues: callChains.reduce((sum, c) => sum + c.issues.length, 0),
|
|
751
|
-
issuesByType: {},
|
|
752
|
-
};
|
|
753
|
-
|
|
754
|
-
// 统计问题类型
|
|
755
|
-
for (const chain of callChains) {
|
|
756
|
-
for (const issue of chain.issues) {
|
|
757
|
-
const type = issue.type;
|
|
758
|
-
summary.issuesByType[type] = (summary.issuesByType[type] || 0) + 1;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
return summary;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
/**
|
|
766
|
-
* 获取相对路径
|
|
767
|
-
*/
|
|
768
|
-
getRelativePath(filePath) {
|
|
769
|
-
return path.relative(this.projectPath, filePath);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/**
|
|
773
|
-
* 获取行内容
|
|
774
|
-
*/
|
|
775
|
-
getLineContent(fileName, lineNumber) {
|
|
776
|
-
try {
|
|
777
|
-
if (!this.sourceFilesCache.has(fileName)) {
|
|
778
|
-
const content = fs.readFileSync(fileName, 'utf8');
|
|
779
|
-
this.sourceFilesCache.set(fileName, content.split('\n'));
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const lines = this.sourceFilesCache.get(fileName);
|
|
783
|
-
return lines[lineNumber - 1] || '';
|
|
784
|
-
} catch (error) {
|
|
785
|
-
return '';
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* 获取上下文(前后几行)
|
|
791
|
-
*/
|
|
792
|
-
getContext(fileName, lineNumber, contextLines = 3) {
|
|
793
|
-
try {
|
|
794
|
-
if (!this.sourceFilesCache.has(fileName)) {
|
|
795
|
-
const content = fs.readFileSync(fileName, 'utf8');
|
|
796
|
-
this.sourceFilesCache.set(fileName, content.split('\n'));
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const lines = this.sourceFilesCache.get(fileName);
|
|
800
|
-
const start = Math.max(0, lineNumber - contextLines - 1);
|
|
801
|
-
const end = Math.min(lines.length, lineNumber + contextLines);
|
|
802
|
-
|
|
803
|
-
return lines.slice(start, end).join('\n');
|
|
804
|
-
} catch (error) {
|
|
805
|
-
return '';
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
export default IncrementalCallChainAnalyzer;
|
|
811
|
-
|