gitlab-ai-review 4.2.0 → 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.
@@ -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
- // 4. 提取代码上下文
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
- console.log(` 🔗 构建调用链: ${symbolName}`);
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
- // 2. 分析每个引用点
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, 3),
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: symbol.type || 'unknown',
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
- this.visitNode(sourceFile, (node) => {
351
- const text = node.getText();
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
- if (text === symbolName || text.includes(symbolName)) {
355
- const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
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: 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.0",
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.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
+ }