gitlab-ai-review 4.2.1 → 4.2.3
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
|
@@ -252,23 +252,33 @@ export class IncrementalCallChainAnalyzer {
|
|
|
252
252
|
const callChains = [];
|
|
253
253
|
|
|
254
254
|
for (const symbol of changedSymbols.deleted) {
|
|
255
|
-
const chain = this.buildCallChain(symbol);
|
|
255
|
+
const chain = this.buildCallChain(symbol, changedFiles);
|
|
256
256
|
if (chain) {
|
|
257
257
|
callChains.push(chain);
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
for (const symbol of changedSymbols.modified) {
|
|
262
|
-
const chain = this.buildCallChain(symbol);
|
|
262
|
+
const chain = this.buildCallChain(symbol, changedFiles);
|
|
263
263
|
if (chain) {
|
|
264
264
|
callChains.push(chain);
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
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
|
+
|
|
268
278
|
console.log(`✅ 构建了 ${callChains.length} 个调用链`);
|
|
269
279
|
|
|
270
|
-
//
|
|
271
|
-
const codeContext = this.extractCodeContext(callChains);
|
|
280
|
+
// 5. 提取代码上下文
|
|
281
|
+
const codeContext = this.extractCodeContext(callChains, changedFiles);
|
|
272
282
|
|
|
273
283
|
return {
|
|
274
284
|
method: 'typescript-callchain',
|
|
@@ -286,28 +296,46 @@ export class IncrementalCallChainAnalyzer {
|
|
|
286
296
|
/**
|
|
287
297
|
* 为单个符号构建调用链
|
|
288
298
|
*/
|
|
289
|
-
buildCallChain(symbol) {
|
|
299
|
+
buildCallChain(symbol, changedFiles) {
|
|
290
300
|
const symbolName = symbol.name || symbol;
|
|
291
|
-
|
|
301
|
+
const symbolType = symbol.type || 'unknown';
|
|
302
|
+
console.log(` 🔗 构建调用链: ${symbolName} (${symbolType})`);
|
|
292
303
|
|
|
293
304
|
try {
|
|
294
305
|
// 1. 找到符号的所有引用
|
|
295
|
-
const references = this.findAllReferences(symbolName);
|
|
306
|
+
const references = this.findAllReferences(symbolName, symbolType);
|
|
307
|
+
|
|
308
|
+
// 2. 如果是字段,额外查找父类型的使用
|
|
309
|
+
let typeUsages = [];
|
|
310
|
+
if (symbolType === 'field') {
|
|
311
|
+
typeUsages = this.findFieldParentTypeUsages(symbolName, changedFiles);
|
|
312
|
+
}
|
|
296
313
|
|
|
297
|
-
if (references.length === 0) {
|
|
314
|
+
if (references.length === 0 && typeUsages.length === 0) {
|
|
298
315
|
console.log(` ℹ️ 未找到引用`);
|
|
316
|
+
// 对于字段,即使没找到引用,也返回基本信息
|
|
317
|
+
if (symbolType === 'field') {
|
|
318
|
+
return {
|
|
319
|
+
symbol: symbolName,
|
|
320
|
+
type: symbolType,
|
|
321
|
+
usages: [],
|
|
322
|
+
typeUsages: [],
|
|
323
|
+
issues: [],
|
|
324
|
+
note: '字段未被直接引用,但可能在类型定义中使用',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
299
327
|
return null;
|
|
300
328
|
}
|
|
301
329
|
|
|
302
|
-
console.log(` ✅ 找到 ${references.length}
|
|
330
|
+
console.log(` ✅ 找到 ${references.length} 处直接引用, ${typeUsages.length} 处类型使用`);
|
|
303
331
|
|
|
304
|
-
//
|
|
332
|
+
// 3. 分析每个引用点
|
|
305
333
|
const usages = references.map(ref => {
|
|
306
334
|
const usage = {
|
|
307
335
|
file: this.getRelativePath(ref.fileName),
|
|
308
336
|
line: ref.line,
|
|
309
337
|
code: this.getLineContent(ref.fileName, ref.line),
|
|
310
|
-
context: this.getContext(ref.fileName, ref.line,
|
|
338
|
+
context: this.getContext(ref.fileName, ref.line, 5),
|
|
311
339
|
};
|
|
312
340
|
|
|
313
341
|
// 检测问题
|
|
@@ -327,8 +355,9 @@ export class IncrementalCallChainAnalyzer {
|
|
|
327
355
|
|
|
328
356
|
return {
|
|
329
357
|
symbol: symbolName,
|
|
330
|
-
type:
|
|
358
|
+
type: symbolType,
|
|
331
359
|
usages,
|
|
360
|
+
typeUsages, // 新增:类型使用(如 mockData: Courseware[])
|
|
332
361
|
issues: usages.filter(u => u.issue).map(u => u.issue),
|
|
333
362
|
};
|
|
334
363
|
|
|
@@ -341,32 +370,162 @@ export class IncrementalCallChainAnalyzer {
|
|
|
341
370
|
/**
|
|
342
371
|
* 查找符号的所有引用
|
|
343
372
|
*/
|
|
344
|
-
findAllReferences(symbolName) {
|
|
373
|
+
findAllReferences(symbolName, symbolType) {
|
|
345
374
|
const references = [];
|
|
346
375
|
|
|
347
376
|
for (const sourceFile of this.program.getSourceFiles()) {
|
|
348
377
|
if (sourceFile.fileName.includes('node_modules')) continue;
|
|
349
378
|
|
|
350
|
-
|
|
351
|
-
|
|
379
|
+
const content = sourceFile.getFullText();
|
|
380
|
+
|
|
381
|
+
// 根据符号类型使用不同的搜索策略
|
|
382
|
+
if (symbolType === 'field') {
|
|
383
|
+
// 字段:搜索属性访问 obj.fieldName
|
|
384
|
+
const fieldAccessPattern = new RegExp(`\\.${symbolName}\\b`, 'g');
|
|
385
|
+
const objectLiteralPattern = new RegExp(`\\b${symbolName}\\s*:`, 'g');
|
|
352
386
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
|
|
387
|
+
let match;
|
|
388
|
+
while ((match = fieldAccessPattern.exec(content)) !== null) {
|
|
389
|
+
const pos = match.index + 1; // +1 跳过 '.'
|
|
390
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
357
391
|
references.push({
|
|
358
392
|
fileName: sourceFile.fileName,
|
|
359
393
|
line: line + 1,
|
|
360
|
-
node:
|
|
394
|
+
node: this.getNodeAtPosition(sourceFile, pos),
|
|
361
395
|
sourceFile: sourceFile,
|
|
396
|
+
matchType: 'field-access',
|
|
362
397
|
});
|
|
363
398
|
}
|
|
364
|
-
|
|
399
|
+
|
|
400
|
+
while ((match = objectLiteralPattern.exec(content)) !== null) {
|
|
401
|
+
const pos = match.index;
|
|
402
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
403
|
+
references.push({
|
|
404
|
+
fileName: sourceFile.fileName,
|
|
405
|
+
line: line + 1,
|
|
406
|
+
node: this.getNodeAtPosition(sourceFile, pos),
|
|
407
|
+
sourceFile: sourceFile,
|
|
408
|
+
matchType: 'object-literal',
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
// 其他符号:简单的文本匹配
|
|
413
|
+
this.visitNode(sourceFile, (node) => {
|
|
414
|
+
const text = node.getText();
|
|
415
|
+
|
|
416
|
+
if (text === symbolName || text.includes(symbolName)) {
|
|
417
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
418
|
+
|
|
419
|
+
references.push({
|
|
420
|
+
fileName: sourceFile.fileName,
|
|
421
|
+
line: line + 1,
|
|
422
|
+
node: node,
|
|
423
|
+
sourceFile: sourceFile,
|
|
424
|
+
matchType: 'text-match',
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
365
429
|
}
|
|
366
430
|
|
|
367
431
|
return references;
|
|
368
432
|
}
|
|
369
433
|
|
|
434
|
+
/**
|
|
435
|
+
* 查找字段所属接口/类型的使用处
|
|
436
|
+
*/
|
|
437
|
+
findFieldParentTypeUsages(fieldName, changedFiles) {
|
|
438
|
+
const typeUsages = [];
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
// 1. 找到字段所在的接口/类型定义
|
|
442
|
+
const parentTypes = this.findFieldParentTypes(fieldName, changedFiles);
|
|
443
|
+
|
|
444
|
+
// 2. 对每个父类型,查找其使用处
|
|
445
|
+
for (const parentType of parentTypes) {
|
|
446
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
447
|
+
if (sourceFile.fileName.includes('node_modules')) continue;
|
|
448
|
+
|
|
449
|
+
const content = sourceFile.getFullText();
|
|
450
|
+
|
|
451
|
+
// 查找类型注解:const x: ParentType
|
|
452
|
+
const typeAnnotationPattern = new RegExp(`:\\s*${parentType}\\b`, 'g');
|
|
453
|
+
let match;
|
|
454
|
+
|
|
455
|
+
while ((match = typeAnnotationPattern.exec(content)) !== null) {
|
|
456
|
+
const pos = match.index;
|
|
457
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
458
|
+
|
|
459
|
+
typeUsages.push({
|
|
460
|
+
file: this.getRelativePath(sourceFile.fileName),
|
|
461
|
+
line: line + 1,
|
|
462
|
+
parentType: parentType,
|
|
463
|
+
context: this.getContext(sourceFile.fileName, line + 1, 8),
|
|
464
|
+
note: `使用了包含字段 ${fieldName} 的类型 ${parentType}`,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
console.log(` 📦 找到 ${typeUsages.length} 处使用父类型 ${parentTypes.join(', ')}`);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.warn(` ⚠️ 查找父类型使用失败:`, error.message);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return typeUsages;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 查找字段所属的接口/类型名称
|
|
480
|
+
*/
|
|
481
|
+
findFieldParentTypes(fieldName, changedFiles) {
|
|
482
|
+
const parentTypes = [];
|
|
483
|
+
|
|
484
|
+
for (const changedFile of changedFiles) {
|
|
485
|
+
const fullPath = path.join(this.projectPath, changedFile);
|
|
486
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
487
|
+
|
|
488
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
489
|
+
|
|
490
|
+
// 简单的正则:查找包含该字段的接口/类型
|
|
491
|
+
const interfacePattern = new RegExp(
|
|
492
|
+
`(?:export\\s+)?interface\\s+(\\w+)\\s*{[^}]*\\b${fieldName}\\s*[?]?\\s*:`,
|
|
493
|
+
'g'
|
|
494
|
+
);
|
|
495
|
+
const typePattern = new RegExp(
|
|
496
|
+
`(?:export\\s+)?type\\s+(\\w+)\\s*=\\s*{[^}]*\\b${fieldName}\\s*[?]?\\s*:`,
|
|
497
|
+
'g'
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
let match;
|
|
501
|
+
while ((match = interfacePattern.exec(content)) !== null) {
|
|
502
|
+
parentTypes.push(match[1]);
|
|
503
|
+
}
|
|
504
|
+
while ((match = typePattern.exec(content)) !== null) {
|
|
505
|
+
parentTypes.push(match[1]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return [...new Set(parentTypes)];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* 获取指定位置的 AST 节点
|
|
514
|
+
*/
|
|
515
|
+
getNodeAtPosition(sourceFile, position) {
|
|
516
|
+
let foundNode = sourceFile;
|
|
517
|
+
|
|
518
|
+
const visit = (node) => {
|
|
519
|
+
if (node.pos <= position && position < node.end) {
|
|
520
|
+
foundNode = node;
|
|
521
|
+
ts.forEachChild(node, visit);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
visit(sourceFile);
|
|
526
|
+
return foundNode;
|
|
527
|
+
}
|
|
528
|
+
|
|
370
529
|
/**
|
|
371
530
|
* 遍历 AST 节点
|
|
372
531
|
*/
|
|
@@ -448,9 +607,10 @@ export class IncrementalCallChainAnalyzer {
|
|
|
448
607
|
/**
|
|
449
608
|
* 提取代码上下文
|
|
450
609
|
*/
|
|
451
|
-
extractCodeContext(callChains) {
|
|
610
|
+
extractCodeContext(callChains, changedFiles) {
|
|
452
611
|
const filesMap = new Map();
|
|
453
612
|
|
|
613
|
+
// 1. 提取直接引用
|
|
454
614
|
for (const chain of callChains) {
|
|
455
615
|
for (const usage of chain.usages) {
|
|
456
616
|
if (!filesMap.has(usage.file)) {
|
|
@@ -467,8 +627,62 @@ export class IncrementalCallChainAnalyzer {
|
|
|
467
627
|
code: usage.context,
|
|
468
628
|
issue: usage.issue,
|
|
469
629
|
dataFlow: usage.dataFlow,
|
|
630
|
+
type: 'direct-reference',
|
|
470
631
|
});
|
|
471
632
|
}
|
|
633
|
+
|
|
634
|
+
// 2. 提取类型使用(针对字段)
|
|
635
|
+
if (chain.typeUsages && chain.typeUsages.length > 0) {
|
|
636
|
+
for (const typeUsage of chain.typeUsages) {
|
|
637
|
+
if (!filesMap.has(typeUsage.file)) {
|
|
638
|
+
filesMap.set(typeUsage.file, {
|
|
639
|
+
path: typeUsage.file,
|
|
640
|
+
snippets: [],
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const fileInfo = filesMap.get(typeUsage.file);
|
|
645
|
+
fileInfo.snippets.push({
|
|
646
|
+
symbol: chain.symbol,
|
|
647
|
+
line: typeUsage.line,
|
|
648
|
+
code: typeUsage.context,
|
|
649
|
+
note: typeUsage.note,
|
|
650
|
+
parentType: typeUsage.parentType,
|
|
651
|
+
type: 'type-usage',
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// 3. 始终包含变更文件本身的完整内容
|
|
658
|
+
for (const changedFile of changedFiles) {
|
|
659
|
+
const relativePath = changedFile.replace(this.projectPath, '').replace(/^[\/\\]/, '');
|
|
660
|
+
|
|
661
|
+
if (!filesMap.has(relativePath)) {
|
|
662
|
+
filesMap.set(relativePath, {
|
|
663
|
+
path: relativePath,
|
|
664
|
+
snippets: [],
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const fileInfo = filesMap.get(relativePath);
|
|
669
|
+
|
|
670
|
+
// 添加文件定义片段
|
|
671
|
+
try {
|
|
672
|
+
const fullPath = path.join(this.projectPath, changedFile);
|
|
673
|
+
if (fs.existsSync(fullPath)) {
|
|
674
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
675
|
+
fileInfo.snippets.push({
|
|
676
|
+
symbol: '(文件定义)',
|
|
677
|
+
line: 1,
|
|
678
|
+
code: content,
|
|
679
|
+
type: 'file-definition',
|
|
680
|
+
note: '这是变更文件的完整内容,用于理解接口和类型定义',
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
} catch (error) {
|
|
684
|
+
console.warn(` ⚠️ 无法读取变更文件:`, error.message);
|
|
685
|
+
}
|
|
472
686
|
}
|
|
473
687
|
|
|
474
688
|
return {
|
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "gitlab-ai-review",
|
|
3
|
-
"version": "4.2.
|
|
4
|
-
"description": "GitLab AI Review SDK with TypeScript Call Chain Analysis - 支持 TypeScript 增量调用链分析、影响分析、删除符号检测、注释代码识别、文件内部冲突检查、智能文件过滤的智能代码审查工具",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"gitlab-ai-review": "./cli.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"test": "node test-info.js",
|
|
12
|
-
"test:info": "node test-info.js"
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"index.js",
|
|
16
|
-
"cli.js",
|
|
17
|
-
"lib/",
|
|
18
|
-
"test-info.js",
|
|
19
|
-
"README.md"
|
|
20
|
-
],
|
|
21
|
-
"keywords": [
|
|
22
|
-
"gitlab",
|
|
23
|
-
"ai",
|
|
24
|
-
"review",
|
|
25
|
-
"code-review",
|
|
26
|
-
"sdk",
|
|
27
|
-
"ci-cd",
|
|
28
|
-
"impact-analysis",
|
|
29
|
-
"static-analysis",
|
|
30
|
-
"merge-request",
|
|
31
|
-
"automation"
|
|
32
|
-
],
|
|
33
|
-
"author": "dengjiang1",
|
|
34
|
-
"license": "MIT",
|
|
35
|
-
"repository": {
|
|
36
|
-
"type": "git",
|
|
37
|
-
"url": "https://github.com/dengjiang1/gitlab-ai-review.git"
|
|
38
|
-
},
|
|
39
|
-
"bugs": {
|
|
40
|
-
"url": "https://github.com/dengjiang1/gitlab-ai-review/issues"
|
|
41
|
-
},
|
|
42
|
-
"homepage": "https://github.com/dengjiang1/gitlab-ai-review#readme",
|
|
43
|
-
"engines": {
|
|
44
|
-
"node": ">=18.0.0"
|
|
45
|
-
},
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"openai": "^4.73.0",
|
|
48
|
-
"typescript": "^5.3.3"
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "gitlab-ai-review",
|
|
3
|
+
"version": "4.2.3",
|
|
4
|
+
"description": "GitLab AI Review SDK with TypeScript Call Chain Analysis - 支持 TypeScript 增量调用链分析、影响分析、删除符号检测、注释代码识别、文件内部冲突检查、智能文件过滤的智能代码审查工具",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"gitlab-ai-review": "./cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node test-info.js",
|
|
12
|
+
"test:info": "node test-info.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.js",
|
|
16
|
+
"cli.js",
|
|
17
|
+
"lib/",
|
|
18
|
+
"test-info.js",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"gitlab",
|
|
23
|
+
"ai",
|
|
24
|
+
"review",
|
|
25
|
+
"code-review",
|
|
26
|
+
"sdk",
|
|
27
|
+
"ci-cd",
|
|
28
|
+
"impact-analysis",
|
|
29
|
+
"static-analysis",
|
|
30
|
+
"merge-request",
|
|
31
|
+
"automation"
|
|
32
|
+
],
|
|
33
|
+
"author": "dengjiang1",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/dengjiang1/gitlab-ai-review.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/dengjiang1/gitlab-ai-review/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/dengjiang1/gitlab-ai-review#readme",
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"openai": "^4.73.0",
|
|
48
|
+
"typescript": "^5.3.3"
|
|
49
|
+
}
|
|
50
|
+
}
|