gitlab-ai-review 4.1.4 → 4.2.0
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/index.js +22 -2
- package/lib/impact-analyzer.js +151 -45
- package/lib/incremental-callchain-analyzer.js +550 -0
- package/lib/prompt-tools.js +77 -10
- package/package.json +4 -3
package/index.js
CHANGED
|
@@ -458,6 +458,7 @@ export class GitLabAIReview {
|
|
|
458
458
|
* @param {number} options.maxFiles - 最大审查文件数量(默认不限制)
|
|
459
459
|
* @param {number} options.maxAffectedFiles - 每个文件最多分析的受影响文件数量(默认 10)
|
|
460
460
|
* @param {boolean} options.enableImpactAnalysis - 是否启用影响分析(默认 true)
|
|
461
|
+
* @param {boolean} options.useCallChainAnalysis - 是否使用 TypeScript 调用链分析(默认 true)
|
|
461
462
|
* @returns {Promise<Array>} 评论结果数组
|
|
462
463
|
*/
|
|
463
464
|
async reviewWithImpactAnalysis(options = {}) {
|
|
@@ -467,7 +468,8 @@ export class GitLabAIReview {
|
|
|
467
468
|
const {
|
|
468
469
|
maxFiles = Infinity,
|
|
469
470
|
maxAffectedFiles = 10,
|
|
470
|
-
enableImpactAnalysis = true
|
|
471
|
+
enableImpactAnalysis = true,
|
|
472
|
+
useCallChainAnalysis = true
|
|
471
473
|
} = options;
|
|
472
474
|
|
|
473
475
|
const allChanges = await this.getMergeRequestChanges();
|
|
@@ -479,6 +481,23 @@ export class GitLabAIReview {
|
|
|
479
481
|
const filesToReview = maxFiles === Infinity ? changes.length : Math.min(maxFiles, changes.length);
|
|
480
482
|
console.log(`共 ${changes.length} 个文件需要审查${maxFiles === Infinity ? '(不限制数量)' : `(最多审查 ${maxFiles} 个)`}(已过滤 ${allChanges.length - changes.length} 个文件)`);
|
|
481
483
|
console.log(`影响分析: ${enableImpactAnalysis ? '已启用' : '已禁用'}`);
|
|
484
|
+
console.log(`调用链分析: ${useCallChainAnalysis ? '已启用(TypeScript)' : '已禁用(使用 GitLab Search)'}`);
|
|
485
|
+
|
|
486
|
+
// 🎯 新增:如果启用了调用链分析,先对所有变更进行一次性 TypeScript 分析
|
|
487
|
+
let callChainResult = null;
|
|
488
|
+
if (enableImpactAnalysis && useCallChainAnalysis) {
|
|
489
|
+
console.log('\n🔗 正在进行增量调用链分析(TypeScript Compiler API)...');
|
|
490
|
+
callChainResult = await ImpactAnalyzer.analyzeWithCallChain(changes.slice(0, filesToReview));
|
|
491
|
+
|
|
492
|
+
if (callChainResult) {
|
|
493
|
+
console.log(`✅ 调用链分析完成:`);
|
|
494
|
+
console.log(` - 分析文件数: ${callChainResult.filesAnalyzed}`);
|
|
495
|
+
console.log(` - 构建调用链: ${callChainResult.callChains?.length || 0} 个`);
|
|
496
|
+
console.log(` - 发现问题: ${callChainResult.codeContext?.totalIssues || 0} 个`);
|
|
497
|
+
} else {
|
|
498
|
+
console.log('ℹ️ 调用链分析不可用,将降级使用 GitLab Search API');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
482
501
|
|
|
483
502
|
for (const change of changes.slice(0, filesToReview)) {
|
|
484
503
|
const fileName = change.new_path || change.old_path;
|
|
@@ -497,7 +516,7 @@ export class GitLabAIReview {
|
|
|
497
516
|
|
|
498
517
|
console.log(` 发现 ${meaningfulChanges.length} 个有意义的变更`);
|
|
499
518
|
|
|
500
|
-
//
|
|
519
|
+
// 影响分析(传入调用链结果)
|
|
501
520
|
let impactAnalysis = null;
|
|
502
521
|
if (enableImpactAnalysis) {
|
|
503
522
|
impactAnalysis = await ImpactAnalyzer.analyzeImpact({
|
|
@@ -506,6 +525,7 @@ export class GitLabAIReview {
|
|
|
506
525
|
ref: ref,
|
|
507
526
|
change: change,
|
|
508
527
|
maxAffectedFiles: maxAffectedFiles,
|
|
528
|
+
callChainResult: callChainResult, // 🎯 传入调用链结果
|
|
509
529
|
});
|
|
510
530
|
}
|
|
511
531
|
|
package/lib/impact-analyzer.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* 影响分析器 - 分析代码变更对其他文件的影响
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { IncrementalCallChainAnalyzer } from './incremental-callchain-analyzer.js';
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* 从文件内容中提取导入/导出信息
|
|
7
9
|
* @param {string} content - 文件内容
|
|
@@ -403,6 +405,46 @@ export function extractSignatures(content, changedSymbols) {
|
|
|
403
405
|
return signatures;
|
|
404
406
|
}
|
|
405
407
|
|
|
408
|
+
/**
|
|
409
|
+
* 使用增量调用链分析(TypeScript Compiler API)
|
|
410
|
+
* @param {Array} allChanges - 所有变更
|
|
411
|
+
* @returns {Promise<Object>} 调用链分析结果
|
|
412
|
+
*/
|
|
413
|
+
export async function analyzeWithCallChain(allChanges) {
|
|
414
|
+
try {
|
|
415
|
+
const analyzer = new IncrementalCallChainAnalyzer();
|
|
416
|
+
|
|
417
|
+
if (!analyzer.isAvailable()) {
|
|
418
|
+
console.log('ℹ️ TypeScript 调用链分析不可用');
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 提取所有变更的文件
|
|
423
|
+
const changedFiles = allChanges.map(c => c.new_path || c.old_path);
|
|
424
|
+
|
|
425
|
+
// 提取所有变更的符号
|
|
426
|
+
const allChangedSymbols = { added: [], deleted: [], modified: [], all: [] };
|
|
427
|
+
for (const change of allChanges) {
|
|
428
|
+
const fileName = change.new_path || change.old_path;
|
|
429
|
+
const diff = change.diff;
|
|
430
|
+
const symbols = extractChangedSymbols(diff, fileName);
|
|
431
|
+
|
|
432
|
+
allChangedSymbols.added.push(...symbols.added);
|
|
433
|
+
allChangedSymbols.deleted.push(...symbols.deleted);
|
|
434
|
+
allChangedSymbols.modified.push(...symbols.modified);
|
|
435
|
+
allChangedSymbols.all.push(...symbols.all);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 执行增量调用链分析
|
|
439
|
+
const result = await analyzer.analyzeImpact(changedFiles, allChangedSymbols);
|
|
440
|
+
|
|
441
|
+
return result;
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.error('❌ 调用链分析失败:', error.message);
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
406
448
|
/**
|
|
407
449
|
* 分析代码变更的完整影响
|
|
408
450
|
* @param {Object} options - 配置选项
|
|
@@ -411,6 +453,7 @@ export function extractSignatures(content, changedSymbols) {
|
|
|
411
453
|
* @param {string} options.ref - 分支名
|
|
412
454
|
* @param {Object} options.change - 代码变更对象
|
|
413
455
|
* @param {number} options.maxAffectedFiles - 最多分析的受影响文件数量
|
|
456
|
+
* @param {Object} options.callChainResult - 调用链分析结果(可选)
|
|
414
457
|
* @returns {Promise<Object>} 影响分析结果
|
|
415
458
|
*/
|
|
416
459
|
export async function analyzeImpact(options) {
|
|
@@ -420,6 +463,7 @@ export async function analyzeImpact(options) {
|
|
|
420
463
|
ref,
|
|
421
464
|
change,
|
|
422
465
|
maxAffectedFiles = 10,
|
|
466
|
+
callChainResult = null,
|
|
423
467
|
} = options;
|
|
424
468
|
|
|
425
469
|
const fileName = change.new_path || change.old_path;
|
|
@@ -442,6 +486,7 @@ export async function analyzeImpact(options) {
|
|
|
442
486
|
internalUsage: [],
|
|
443
487
|
affectedFiles: [],
|
|
444
488
|
signatures: [],
|
|
489
|
+
callChain: null,
|
|
445
490
|
};
|
|
446
491
|
}
|
|
447
492
|
|
|
@@ -472,53 +517,111 @@ export async function analyzeImpact(options) {
|
|
|
472
517
|
console.warn(` 无法获取文件内容:`, error.message);
|
|
473
518
|
}
|
|
474
519
|
|
|
475
|
-
// 3.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
...changedSymbols.deleted.filter(s => s.type === 'definition'),
|
|
479
|
-
...changedSymbols.modified,
|
|
480
|
-
...changedSymbols.added.filter(s => s.type === 'definition'),
|
|
481
|
-
];
|
|
482
|
-
|
|
483
|
-
const affectedFiles = await searchSymbolUsage(
|
|
484
|
-
gitlabClient,
|
|
485
|
-
projectId,
|
|
486
|
-
ref,
|
|
487
|
-
symbolsToSearch
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
// 过滤掉当前文件本身
|
|
491
|
-
const externalAffectedFiles = affectedFiles.filter(f => f.path !== fileName);
|
|
492
|
-
|
|
493
|
-
console.log(` 找到 ${externalAffectedFiles.length} 个其他文件可能受影响`);
|
|
520
|
+
// 3. 优先使用调用链结果,否则降级到 GitLab Search
|
|
521
|
+
let affectedFiles = [];
|
|
522
|
+
let callChainInfo = null;
|
|
494
523
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
524
|
+
if (callChainResult && callChainResult.callChains) {
|
|
525
|
+
// 使用 TypeScript 调用链分析结果
|
|
526
|
+
console.log(` 📊 使用 TypeScript 调用链分析结果`);
|
|
527
|
+
|
|
528
|
+
// 过滤出当前文件相关的调用链
|
|
529
|
+
const relevantChains = callChainResult.callChains.filter(chain => {
|
|
530
|
+
const symbolNames = changedSymbols.all.map(s => s.name || s);
|
|
531
|
+
return symbolNames.includes(chain.symbol);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
if (relevantChains.length > 0) {
|
|
535
|
+
console.log(` ✅ 找到 ${relevantChains.length} 个相关调用链`);
|
|
536
|
+
|
|
537
|
+
callChainInfo = {
|
|
538
|
+
method: 'typescript-callchain',
|
|
539
|
+
chains: relevantChains,
|
|
540
|
+
summary: callChainResult.codeContext?.summary,
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// 将调用链转换为 affectedFiles 格式(用于兼容现有代码)
|
|
544
|
+
const filesMap = new Map();
|
|
545
|
+
|
|
546
|
+
for (const chain of relevantChains) {
|
|
547
|
+
for (const usage of chain.usages) {
|
|
548
|
+
if (usage.file === fileName) continue; // 跳过当前文件
|
|
508
549
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
550
|
+
if (!filesMap.has(usage.file)) {
|
|
551
|
+
filesMap.set(usage.file, {
|
|
552
|
+
path: usage.file,
|
|
553
|
+
symbols: [],
|
|
554
|
+
lines: [],
|
|
555
|
+
snippets: [],
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const fileInfo = filesMap.get(usage.file);
|
|
560
|
+
fileInfo.symbols.push(chain.symbol);
|
|
561
|
+
fileInfo.lines.push(usage.line);
|
|
562
|
+
fileInfo.snippets.push({
|
|
563
|
+
lineNumber: usage.line,
|
|
564
|
+
snippet: usage.context,
|
|
565
|
+
issue: usage.issue,
|
|
566
|
+
dataFlow: usage.dataFlow,
|
|
567
|
+
});
|
|
513
568
|
}
|
|
514
|
-
|
|
515
|
-
return file;
|
|
516
|
-
} catch (error) {
|
|
517
|
-
console.warn(` 无法获取文件 ${file.path} 的内容:`, error.message);
|
|
518
|
-
return file;
|
|
519
569
|
}
|
|
520
|
-
|
|
521
|
-
|
|
570
|
+
|
|
571
|
+
affectedFiles = Array.from(filesMap.values());
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 4. 如果没有调用链结果,使用传统的 GitLab Search
|
|
576
|
+
if (affectedFiles.length === 0) {
|
|
577
|
+
console.log(` 🔍 使用 GitLab Search API 搜索`);
|
|
578
|
+
|
|
579
|
+
const symbolsToSearch = [
|
|
580
|
+
...changedSymbols.deleted.filter(s => s.type === 'definition'),
|
|
581
|
+
...changedSymbols.modified,
|
|
582
|
+
...changedSymbols.added.filter(s => s.type === 'definition'),
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
const searchResults = await searchSymbolUsage(
|
|
586
|
+
gitlabClient,
|
|
587
|
+
projectId,
|
|
588
|
+
ref,
|
|
589
|
+
symbolsToSearch
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
// 过滤掉当前文件本身
|
|
593
|
+
const externalAffectedFiles = searchResults.filter(f => f.path !== fileName);
|
|
594
|
+
|
|
595
|
+
console.log(` 找到 ${externalAffectedFiles.length} 个其他文件可能受影响`);
|
|
596
|
+
|
|
597
|
+
// 限制数量并获取代码片段
|
|
598
|
+
const limitedFiles = externalAffectedFiles.slice(0, maxAffectedFiles);
|
|
599
|
+
affectedFiles = await Promise.all(
|
|
600
|
+
limitedFiles.map(async (file) => {
|
|
601
|
+
try {
|
|
602
|
+
const content = await gitlabClient.getProjectFile(projectId, file.path, ref);
|
|
603
|
+
|
|
604
|
+
if (content) {
|
|
605
|
+
const snippets = [];
|
|
606
|
+
file.symbols.forEach(symbol => {
|
|
607
|
+
const symbolSnippets = extractCodeSnippets(content, symbol, 3);
|
|
608
|
+
snippets.push(...symbolSnippets);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
...file,
|
|
613
|
+
snippets: snippets.slice(0, 3), // 每个文件最多 3 个片段
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return file;
|
|
618
|
+
} catch (error) {
|
|
619
|
+
console.warn(` 无法获取文件 ${file.path} 的内容:`, error.message);
|
|
620
|
+
return file;
|
|
621
|
+
}
|
|
622
|
+
})
|
|
623
|
+
);
|
|
624
|
+
}
|
|
522
625
|
|
|
523
626
|
return {
|
|
524
627
|
fileName,
|
|
@@ -526,8 +629,9 @@ export async function analyzeImpact(options) {
|
|
|
526
629
|
fileContent, // 完整文件内容
|
|
527
630
|
internalUsage, // 文件内部使用情况
|
|
528
631
|
signatures,
|
|
529
|
-
affectedFiles
|
|
530
|
-
totalAffectedFiles:
|
|
632
|
+
affectedFiles,
|
|
633
|
+
totalAffectedFiles: affectedFiles.length,
|
|
634
|
+
callChain: callChainInfo, // 新增:调用链信息
|
|
531
635
|
};
|
|
532
636
|
|
|
533
637
|
} catch (error) {
|
|
@@ -540,6 +644,7 @@ export async function analyzeImpact(options) {
|
|
|
540
644
|
internalUsage: [],
|
|
541
645
|
affectedFiles: [],
|
|
542
646
|
signatures: [],
|
|
647
|
+
callChain: null,
|
|
543
648
|
};
|
|
544
649
|
}
|
|
545
650
|
}
|
|
@@ -552,5 +657,6 @@ export default {
|
|
|
552
657
|
extractCodeSnippets,
|
|
553
658
|
extractSignatures,
|
|
554
659
|
analyzeImpact,
|
|
660
|
+
analyzeWithCallChain,
|
|
555
661
|
};
|
|
556
662
|
|
|
@@ -0,0 +1,550 @@
|
|
|
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);
|
|
256
|
+
if (chain) {
|
|
257
|
+
callChains.push(chain);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const symbol of changedSymbols.modified) {
|
|
262
|
+
const chain = this.buildCallChain(symbol);
|
|
263
|
+
if (chain) {
|
|
264
|
+
callChains.push(chain);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log(`✅ 构建了 ${callChains.length} 个调用链`);
|
|
269
|
+
|
|
270
|
+
// 4. 提取代码上下文
|
|
271
|
+
const codeContext = this.extractCodeContext(callChains);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
method: 'typescript-callchain',
|
|
275
|
+
callChains,
|
|
276
|
+
codeContext,
|
|
277
|
+
filesAnalyzed: affectedFiles.length,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error('❌ 调用链分析失败:', error.message);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 为单个符号构建调用链
|
|
288
|
+
*/
|
|
289
|
+
buildCallChain(symbol) {
|
|
290
|
+
const symbolName = symbol.name || symbol;
|
|
291
|
+
console.log(` 🔗 构建调用链: ${symbolName}`);
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
// 1. 找到符号的所有引用
|
|
295
|
+
const references = this.findAllReferences(symbolName);
|
|
296
|
+
|
|
297
|
+
if (references.length === 0) {
|
|
298
|
+
console.log(` ℹ️ 未找到引用`);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log(` ✅ 找到 ${references.length} 处引用`);
|
|
303
|
+
|
|
304
|
+
// 2. 分析每个引用点
|
|
305
|
+
const usages = references.map(ref => {
|
|
306
|
+
const usage = {
|
|
307
|
+
file: this.getRelativePath(ref.fileName),
|
|
308
|
+
line: ref.line,
|
|
309
|
+
code: this.getLineContent(ref.fileName, ref.line),
|
|
310
|
+
context: this.getContext(ref.fileName, ref.line, 3),
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// 检测问题
|
|
314
|
+
const issue = this.detectIssue(ref, symbol);
|
|
315
|
+
if (issue) {
|
|
316
|
+
usage.issue = issue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 追踪数据流
|
|
320
|
+
const dataFlow = this.traceDataFlow(ref.node);
|
|
321
|
+
if (dataFlow.length > 0) {
|
|
322
|
+
usage.dataFlow = dataFlow;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return usage;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
symbol: symbolName,
|
|
330
|
+
type: symbol.type || 'unknown',
|
|
331
|
+
usages,
|
|
332
|
+
issues: usages.filter(u => u.issue).map(u => u.issue),
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.warn(` ⚠️ 构建调用链失败:`, error.message);
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 查找符号的所有引用
|
|
343
|
+
*/
|
|
344
|
+
findAllReferences(symbolName) {
|
|
345
|
+
const references = [];
|
|
346
|
+
|
|
347
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
348
|
+
if (sourceFile.fileName.includes('node_modules')) continue;
|
|
349
|
+
|
|
350
|
+
this.visitNode(sourceFile, (node) => {
|
|
351
|
+
const text = node.getText();
|
|
352
|
+
|
|
353
|
+
// 简单的文本匹配(比完整的符号解析快很多)
|
|
354
|
+
if (text === symbolName || text.includes(symbolName)) {
|
|
355
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
356
|
+
|
|
357
|
+
references.push({
|
|
358
|
+
fileName: sourceFile.fileName,
|
|
359
|
+
line: line + 1,
|
|
360
|
+
node: node,
|
|
361
|
+
sourceFile: sourceFile,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return references;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 遍历 AST 节点
|
|
372
|
+
*/
|
|
373
|
+
visitNode(node, callback) {
|
|
374
|
+
callback(node);
|
|
375
|
+
ts.forEachChild(node, (child) => this.visitNode(child, callback));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 追踪数据流
|
|
380
|
+
*/
|
|
381
|
+
traceDataFlow(node) {
|
|
382
|
+
const flow = [];
|
|
383
|
+
let current = node;
|
|
384
|
+
let depth = 0;
|
|
385
|
+
|
|
386
|
+
while (current && depth < 5) {
|
|
387
|
+
if (ts.isVariableDeclaration(current)) {
|
|
388
|
+
const name = current.name.getText();
|
|
389
|
+
flow.unshift(`变量: ${name}`);
|
|
390
|
+
} else if (ts.isFunctionDeclaration(current)) {
|
|
391
|
+
const name = current.name?.getText() || 'anonymous';
|
|
392
|
+
flow.unshift(`函数: ${name}`);
|
|
393
|
+
} else if (ts.isPropertyAssignment(current)) {
|
|
394
|
+
const name = current.name.getText();
|
|
395
|
+
flow.unshift(`属性: ${name}`);
|
|
396
|
+
} else if (ts.isCallExpression(current)) {
|
|
397
|
+
const expr = current.expression.getText();
|
|
398
|
+
flow.unshift(`调用: ${expr}()`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
current = current.parent;
|
|
402
|
+
depth++;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return flow;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 检测问题
|
|
410
|
+
*/
|
|
411
|
+
detectIssue(reference, symbol) {
|
|
412
|
+
const line = this.getLineContent(reference.fileName, reference.line);
|
|
413
|
+
const symbolName = symbol.name || symbol;
|
|
414
|
+
|
|
415
|
+
// 检测:访问被删除/重命名的符号
|
|
416
|
+
if (symbol.type === 'definition' && line.includes(symbolName)) {
|
|
417
|
+
// 检查是否是导入语句
|
|
418
|
+
if (line.includes('import') && line.includes(symbolName)) {
|
|
419
|
+
return {
|
|
420
|
+
type: 'import-deleted-symbol',
|
|
421
|
+
severity: 'error',
|
|
422
|
+
message: `导入了被删除的符号: ${symbolName}`,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 检查是否是函数调用
|
|
427
|
+
if (line.includes(`${symbolName}(`)) {
|
|
428
|
+
return {
|
|
429
|
+
type: 'call-deleted-function',
|
|
430
|
+
severity: 'error',
|
|
431
|
+
message: `调用了被删除的函数: ${symbolName}()`,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 检查是否是类型引用
|
|
436
|
+
if (line.includes(`: ${symbolName}`) || line.includes(`<${symbolName}>`)) {
|
|
437
|
+
return {
|
|
438
|
+
type: 'reference-deleted-type',
|
|
439
|
+
severity: 'error',
|
|
440
|
+
message: `引用了被删除的类型: ${symbolName}`,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 提取代码上下文
|
|
450
|
+
*/
|
|
451
|
+
extractCodeContext(callChains) {
|
|
452
|
+
const filesMap = new Map();
|
|
453
|
+
|
|
454
|
+
for (const chain of callChains) {
|
|
455
|
+
for (const usage of chain.usages) {
|
|
456
|
+
if (!filesMap.has(usage.file)) {
|
|
457
|
+
filesMap.set(usage.file, {
|
|
458
|
+
path: usage.file,
|
|
459
|
+
snippets: [],
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const fileInfo = filesMap.get(usage.file);
|
|
464
|
+
fileInfo.snippets.push({
|
|
465
|
+
symbol: chain.symbol,
|
|
466
|
+
line: usage.line,
|
|
467
|
+
code: usage.context,
|
|
468
|
+
issue: usage.issue,
|
|
469
|
+
dataFlow: usage.dataFlow,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
files: Array.from(filesMap.values()),
|
|
476
|
+
totalFiles: filesMap.size,
|
|
477
|
+
totalIssues: callChains.flatMap(c => c.issues).length,
|
|
478
|
+
summary: this.buildSummary(callChains),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* 构建摘要
|
|
484
|
+
*/
|
|
485
|
+
buildSummary(callChains) {
|
|
486
|
+
const summary = {
|
|
487
|
+
totalSymbols: callChains.length,
|
|
488
|
+
totalReferences: callChains.reduce((sum, c) => sum + c.usages.length, 0),
|
|
489
|
+
totalIssues: callChains.reduce((sum, c) => sum + c.issues.length, 0),
|
|
490
|
+
issuesByType: {},
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// 统计问题类型
|
|
494
|
+
for (const chain of callChains) {
|
|
495
|
+
for (const issue of chain.issues) {
|
|
496
|
+
const type = issue.type;
|
|
497
|
+
summary.issuesByType[type] = (summary.issuesByType[type] || 0) + 1;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return summary;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* 获取相对路径
|
|
506
|
+
*/
|
|
507
|
+
getRelativePath(filePath) {
|
|
508
|
+
return path.relative(this.projectPath, filePath);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* 获取行内容
|
|
513
|
+
*/
|
|
514
|
+
getLineContent(fileName, lineNumber) {
|
|
515
|
+
try {
|
|
516
|
+
if (!this.sourceFilesCache.has(fileName)) {
|
|
517
|
+
const content = fs.readFileSync(fileName, 'utf8');
|
|
518
|
+
this.sourceFilesCache.set(fileName, content.split('\n'));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const lines = this.sourceFilesCache.get(fileName);
|
|
522
|
+
return lines[lineNumber - 1] || '';
|
|
523
|
+
} catch (error) {
|
|
524
|
+
return '';
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* 获取上下文(前后几行)
|
|
530
|
+
*/
|
|
531
|
+
getContext(fileName, lineNumber, contextLines = 3) {
|
|
532
|
+
try {
|
|
533
|
+
if (!this.sourceFilesCache.has(fileName)) {
|
|
534
|
+
const content = fs.readFileSync(fileName, 'utf8');
|
|
535
|
+
this.sourceFilesCache.set(fileName, content.split('\n'));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const lines = this.sourceFilesCache.get(fileName);
|
|
539
|
+
const start = Math.max(0, lineNumber - contextLines - 1);
|
|
540
|
+
const end = Math.min(lines.length, lineNumber + contextLines);
|
|
541
|
+
|
|
542
|
+
return lines.slice(start, end).join('\n');
|
|
543
|
+
} catch (error) {
|
|
544
|
+
return '';
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export default IncrementalCallChainAnalyzer;
|
|
550
|
+
|
package/lib/prompt-tools.js
CHANGED
|
@@ -242,13 +242,80 @@ export function buildFileReviewWithImpactPrompt(fileName, meaningfulChanges, imp
|
|
|
242
242
|
prompt += `\n`;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
//
|
|
246
|
-
if (impactAnalysis.
|
|
245
|
+
// 🎯 优先显示 TypeScript 调用链信息
|
|
246
|
+
if (impactAnalysis.callChain && impactAnalysis.callChain.chains) {
|
|
247
|
+
prompt += `**🔗 TypeScript 调用链分析**:\n`;
|
|
248
|
+
prompt += `使用了 TypeScript Compiler API 进行精确的类型分析和调用链追踪\n\n`;
|
|
249
|
+
|
|
250
|
+
impactAnalysis.callChain.chains.forEach((chain, chainIndex) => {
|
|
251
|
+
prompt += `### 调用链 ${chainIndex + 1}: \`${chain.symbol}\`\n`;
|
|
252
|
+
|
|
253
|
+
// 显示问题(如果有)
|
|
254
|
+
if (chain.issues && chain.issues.length > 0) {
|
|
255
|
+
prompt += `**🚨 检测到的问题**:\n`;
|
|
256
|
+
chain.issues.forEach(issue => {
|
|
257
|
+
prompt += `- [${issue.severity}] ${issue.message}\n`;
|
|
258
|
+
});
|
|
259
|
+
prompt += `\n`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 显示使用处
|
|
263
|
+
if (chain.usages && chain.usages.length > 0) {
|
|
264
|
+
prompt += `**使用位置** (${chain.usages.length} 处):\n\n`;
|
|
265
|
+
|
|
266
|
+
chain.usages.slice(0, 5).forEach((usage, usageIndex) => {
|
|
267
|
+
prompt += `${usageIndex + 1}. **${usage.file}:${usage.line}**\n`;
|
|
268
|
+
|
|
269
|
+
// 显示数据流(如果有)
|
|
270
|
+
if (usage.dataFlow && usage.dataFlow.length > 0) {
|
|
271
|
+
prompt += ` 数据流: ${usage.dataFlow.join(' → ')}\n`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 显示问题(如果有)
|
|
275
|
+
if (usage.issue) {
|
|
276
|
+
prompt += ` ⚠️ **问题**: ${usage.issue.message}\n`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 显示代码上下文
|
|
280
|
+
if (usage.context) {
|
|
281
|
+
prompt += `\n\`\`\`typescript\n${usage.context}\n\`\`\`\n`;
|
|
282
|
+
}
|
|
283
|
+
prompt += `\n`;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
if (chain.usages.length > 5) {
|
|
287
|
+
prompt += `... 还有 ${chain.usages.length - 5} 处使用\n\n`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// 显示摘要
|
|
293
|
+
if (impactAnalysis.callChain.summary) {
|
|
294
|
+
const summary = impactAnalysis.callChain.summary;
|
|
295
|
+
prompt += `**📊 调用链分析摘要**:\n`;
|
|
296
|
+
prompt += `- 分析的符号: ${summary.totalSymbols || 0} 个\n`;
|
|
297
|
+
prompt += `- 找到的引用: ${summary.totalReferences || 0} 处\n`;
|
|
298
|
+
prompt += `- 检测到的问题: ${summary.totalIssues || 0} 个\n`;
|
|
299
|
+
if (summary.issuesByType && Object.keys(summary.issuesByType).length > 0) {
|
|
300
|
+
prompt += `- 问题类型分布:\n`;
|
|
301
|
+
Object.entries(summary.issuesByType).forEach(([type, count]) => {
|
|
302
|
+
prompt += ` - ${type}: ${count}\n`;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
prompt += `\n`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
prompt += `**✅ 这是精确的类型分析结果,可以直接信任!**\n\n`;
|
|
309
|
+
prompt += `---\n\n`;
|
|
310
|
+
}
|
|
311
|
+
// 如果没有调用链,显示传统的 GitLab Search 结果
|
|
312
|
+
else if (impactAnalysis.affectedFiles && impactAnalysis.affectedFiles.length > 0) {
|
|
247
313
|
prompt += `**其他受影响的文件** (${impactAnalysis.totalAffectedFiles || impactAnalysis.affectedFiles.length} 个`;
|
|
248
314
|
if (impactAnalysis.totalAffectedFiles > impactAnalysis.affectedFiles.length) {
|
|
249
315
|
prompt += `,显示前 ${impactAnalysis.affectedFiles.length} 个`;
|
|
250
316
|
}
|
|
251
|
-
prompt += `):\n
|
|
317
|
+
prompt += `):\n`;
|
|
318
|
+
prompt += `⚠️ 使用 GitLab Search API 搜索(文本匹配,可能不够精确)\n\n`;
|
|
252
319
|
|
|
253
320
|
impactAnalysis.affectedFiles.forEach((file, index) => {
|
|
254
321
|
prompt += `### ${index + 1}. ${file.path}\n`;
|
|
@@ -267,14 +334,14 @@ export function buildFileReviewWithImpactPrompt(fileName, meaningfulChanges, imp
|
|
|
267
334
|
});
|
|
268
335
|
|
|
269
336
|
prompt += `---\n\n`;
|
|
337
|
+
|
|
338
|
+
prompt += `**🔍 跨文件影响分析提醒:**\n`;
|
|
339
|
+
prompt += `上述"受影响的文件"是通过文本搜索找到的,可能存在以下情况:\n`;
|
|
340
|
+
prompt += `1. **局部定义**:受影响的文件中可能有同名的局部变量、函数参数、类方法等\n`;
|
|
341
|
+
prompt += `2. **重新导入**:受影响的文件可能从其他模块导入了同名符号\n`;
|
|
342
|
+
prompt += `3. **命名空间**:可能在不同的命名空间或作用域中\n`;
|
|
343
|
+
prompt += `\n**请仔细检查代码片段,只有确认是引用被删除的符号时,才报告为影响!**\n\n`;
|
|
270
344
|
}
|
|
271
|
-
|
|
272
|
-
prompt += `**🔍 跨文件影响分析提醒:**\n`;
|
|
273
|
-
prompt += `上述"受影响的文件"是通过文本搜索找到的,可能存在以下情况:\n`;
|
|
274
|
-
prompt += `1. **局部定义**:受影响的文件中可能有同名的局部变量、函数参数、类方法等\n`;
|
|
275
|
-
prompt += `2. **重新导入**:受影响的文件可能从其他模块导入了同名符号\n`;
|
|
276
|
-
prompt += `3. **命名空间**:可能在不同的命名空间或作用域中\n`;
|
|
277
|
-
prompt += `\n**请仔细检查代码片段,只有确认是引用被删除的符号时,才报告为影响!**\n\n`;
|
|
278
345
|
}
|
|
279
346
|
|
|
280
347
|
// 添加代码变更
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitlab-ai-review",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "GitLab AI Review SDK with
|
|
3
|
+
"version": "4.2.0",
|
|
4
|
+
"description": "GitLab AI Review SDK with TypeScript Call Chain Analysis - 支持 TypeScript 增量调用链分析、影响分析、删除符号检测、注释代码识别、文件内部冲突检查、智能文件过滤的智能代码审查工具",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"node": ">=18.0.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"openai": "^4.73.0"
|
|
47
|
+
"openai": "^4.73.0",
|
|
48
|
+
"typescript": "^5.3.3"
|
|
48
49
|
}
|
|
49
50
|
}
|