smart-review 1.0.1 → 1.0.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/reviewer.js CHANGED
@@ -9,6 +9,7 @@ import { GitDiffParser } from './utils/git-diff-parser.js';
9
9
  import { logger } from './utils/logger.js';
10
10
  import { DEFAULT_CONFIG, BATCH_CONSTANTS } from './utils/constants.js';
11
11
  import { ConcurrencyLimiter } from './utils/concurrency-limiter.js';
12
+ import { t } from './utils/i18n.js';
12
13
 
13
14
  const execAsync = promisify(exec);
14
15
 
@@ -16,8 +17,8 @@ export class CodeReviewer {
16
17
  constructor(config, rules) {
17
18
  this.config = config;
18
19
  this.rules = rules;
19
- // 传递 reviewDir 给 AI 客户端用于读取自定义提示词目录 .smart-review/ai-rules
20
- this.aiClient = config.ai?.enabled ? new AIClient({ ...config.ai, reviewDir: config.reviewDir }) : null;
20
+ // 传递 reviewDir 与 locale 给 AI 客户端用于读取自定义提示词目录与国际化
21
+ this.aiClient = config.ai?.enabled ? new AIClient({ ...config.ai, reviewDir: config.reviewDir, locale: config.locale }) : null;
21
22
  this.issues = [];
22
23
  this.aiRan = false;
23
24
 
@@ -61,42 +62,42 @@ export class CodeReviewer {
61
62
 
62
63
  async reviewStagedFiles() {
63
64
  try {
64
- logger.progress('开始审查暂存区代码...');
65
+ logger.progress(t(this.config, 'review_staged_start'));
65
66
 
66
67
  // 检查是否启用git diff增量审查模式
67
68
  const reviewOnlyChanges = this.config.ai?.reviewOnlyChanges || false;
68
69
 
69
70
  if (reviewOnlyChanges) {
70
- logger.info('🔍 使用Git Diff增量审查模式 - 仅审查变动内容');
71
+ logger.info(t(this.config, 'using_diff_mode'));
71
72
  return await this.reviewStagedDiff();
72
73
  } else {
73
- logger.info('📁 使用全文件审查模式');
74
+ logger.info(t(this.config, 'using_full_mode'));
74
75
  const stagedFiles = await this.getStagedFiles();
75
76
 
76
77
  if (stagedFiles.length === 0) {
77
- logger.info('📭 暂存区没有文件需要审查');
78
+ logger.info(t(this.config, 'no_staged_files'));
78
79
  return this.generateResult();
79
80
  }
80
- logger.info(`📁 发现 ${stagedFiles.length} 个文件需要审查`);
81
+ logger.info(t(this.config, 'found_staged_files_n', { count: stagedFiles.length }));
81
82
 
82
83
  await this.reviewFilesBatchAware(stagedFiles);
83
84
  return this.generateResult();
84
85
  }
85
86
  } catch (error) {
86
- logger.error('审查过程出错:', error);
87
+ logger.error(t(this.config, 'review_error', { error: error?.message || String(error) }));
87
88
  throw error;
88
89
  }
89
90
  }
90
91
 
91
92
  async reviewSpecificFiles(filePaths) {
92
- logger.progress(`开始审查指定文件: ${filePaths.join(', ')}`);
93
+ logger.progress(t(this.config, 'review_specific_start', { files: filePaths.join(', ') }));
93
94
  const fullPaths = [];
94
95
  for (const filePath of filePaths) {
95
96
  const fullPath = path.isAbsolute(filePath) ? filePath : path.join(this.config.projectRoot, filePath);
96
97
  if (fs.existsSync(fullPath)) {
97
98
  fullPaths.push(fullPath);
98
99
  } else {
99
- logger.warn(`文件不存在: ${fullPath}`);
100
+ logger.warn(t(this.config, 'file_not_exists', { file: fullPath }));
100
101
  }
101
102
  }
102
103
  await this.reviewFilesBatchAware(fullPaths);
@@ -114,7 +115,7 @@ export class CodeReviewer {
114
115
  .map(file => path.resolve(this.config.projectRoot, file))
115
116
  .filter(file => this.isReviewableFile(file));
116
117
  } catch (error) {
117
- logger.error('获取暂存区文件失败:', error);
118
+ logger.error(t(this.config, 'get_staged_failed', { error: error?.message || String(error) }));
118
119
  return [];
119
120
  }
120
121
  }
@@ -124,7 +125,7 @@ export class CodeReviewer {
124
125
  */
125
126
  async reviewStagedDiff() {
126
127
  try {
127
- logger.info('🔍 启动Git Diff增量审查模式...');
128
+ logger.info(t(this.config, 'start_git_diff_mode'));
128
129
 
129
130
  const contextMergeLines = this.config.ai?.contextMergeLines || 10;
130
131
  const diffParser = new GitDiffParser(this.config.projectRoot, contextMergeLines, this.config);
@@ -133,11 +134,10 @@ export class CodeReviewer {
133
134
  const diffReviewData = await diffParser.getStagedDiffReviewData();
134
135
 
135
136
  if (diffReviewData.length === 0) {
136
- logger.info('📝 暂存区无变更内容,跳过审查');
137
+ logger.info(t(this.config, 'no_changes_skip'));
137
138
  return this.generateResult();
138
139
  }
139
-
140
- logger.info(`📊 发现 ${diffReviewData.length} 个变更文件,开始增量审查...`);
140
+ logger.info(t(this.config, 'found_changed_files_n', { count: diffReviewData.length }));
141
141
 
142
142
  // 第一阶段:对所有文件进行静态规则检查,检测阻断风险
143
143
  const riskLevels = this.config.riskLevels || {};
@@ -149,16 +149,22 @@ export class CodeReviewer {
149
149
  const globalIndex = i + 1;
150
150
  const filePath = path.resolve(this.config.projectRoot, fileData.filePath);
151
151
  if (!this.isReviewableFile(filePath)) {
152
- logger.info(`文件已跳过审查: ${filePath} (文件类型被忽略)`);
152
+ logger.info(t(this.config, 'file_skipped_by_type', { path: filePath }));
153
153
  continue;
154
154
  }
155
- logger.progress(`[${globalIndex}/${diffReviewData.length}] 审查文件: ${fileData.filePath} (新增${fileData.totalAddedLines}行, ${fileData.segments.length}个分段)`);
155
+ logger.progress(t(this.config, 'review_file_progress', {
156
+ index: globalIndex,
157
+ total: diffReviewData.length,
158
+ file: fileData.filePath,
159
+ added: fileData.totalAddedLines,
160
+ segments: fileData.segments.length
161
+ }));
156
162
  // 应用静态规则检查
157
163
  const staticIssues = await this.applyStaticRulesToDiff(fileData, filePath);
158
164
  this.issues.push(...staticIssues);
159
165
 
160
166
  if (staticIssues.length > 0) {
161
- logger.debug(`静态规则发现 ${staticIssues.length} 个问题`);
167
+ logger.debug(t(this.config, 'static_rules_found_n_dbg', { count: staticIssues.length }));
162
168
  }
163
169
 
164
170
  // 检查是否有阻断等级问题
@@ -169,7 +175,7 @@ export class CodeReviewer {
169
175
 
170
176
  if (blockedIssues.length > 0) {
171
177
  const levelsText = [...new Set(blockedIssues.map(i => i.risk))].join(', ');
172
- logger.error(`发现阻断等级风险 (${levelsText}),跳过AI分析`);
178
+ logger.error(t(this.config, 'blocking_risk_detected_skip_ai', { levels: levelsText }));
173
179
  hasBlockingIssues = true;
174
180
  } else {
175
181
  // 没有阻断问题的文件可以进行AI分析
@@ -179,13 +185,13 @@ export class CodeReviewer {
179
185
 
180
186
  // 如果发现阻断等级问题,终止整个审查流程
181
187
  if (hasBlockingIssues) {
182
- logger.error('发现阻断等级风险,终止审查流程');
188
+ logger.error(t(this.config, 'blocking_risk_terminate'));
183
189
  return this.generateResult();
184
190
  }
185
191
 
186
192
  // 第二阶段:对通过静态检查的文件进行AI分析
187
193
  if (aiEligibleFiles.length > 0 && this.aiClient) {
188
- logger.info(`🤖 开始AI智能分析 ${aiEligibleFiles.length} 个文件...`);
194
+ logger.info(t(this.config, 'ai_start_n_files', { count: aiEligibleFiles.length }));
189
195
 
190
196
  // 使用并发处理AI分析
191
197
  const concurrency = this.config.concurrency || 3;
@@ -204,7 +210,7 @@ export class CodeReviewer {
204
210
  // 并发处理当前批次的文件
205
211
  const promises = batch.map((fileData, index) => {
206
212
  const globalIndex = batchIndex * concurrency + index + 1;
207
- logger.debug(` 🤖 [${globalIndex}/${aiEligibleFiles.length}] AI分析: ${fileData.filePath}`);
213
+ logger.debug(t(this.config, 'ai_file_progress_dbg', { index: globalIndex, total: aiEligibleFiles.length, file: fileData.filePath }));
208
214
  return this.performAIDiffAnalysis(fileData);
209
215
  });
210
216
 
@@ -212,11 +218,11 @@ export class CodeReviewer {
212
218
  }
213
219
  }
214
220
 
215
- logger.success('✨ Git Diff增量审查完成');
221
+ logger.success(t(this.config, 'git_diff_done'));
216
222
 
217
223
  return this.generateResult();
218
224
  } catch (error) {
219
- logger.error('Git Diff审查过程出错:', error);
225
+ logger.error(t(this.config, 'git_diff_error', { error: error?.message || String(error) }));
220
226
  throw error;
221
227
  }
222
228
  }
@@ -229,18 +235,18 @@ export class CodeReviewer {
229
235
  const filePath = path.resolve(this.config.projectRoot, fileData.filePath);
230
236
 
231
237
  if (!this.isReviewableFile(filePath)) {
232
- logger.info(`文件已跳过审查: ${filePath} (文件类型被忽略)`);
238
+ logger.info(t(this.config, 'file_skipped_by_type', { path: filePath }));
233
239
  return;
234
240
  }
235
241
 
236
242
  try {
237
243
  // 1. 对新增代码应用静态规则
238
- logger.debug(`应用静态规则检查...`);
244
+ logger.debug(t(this.config, 'apply_static_rules_dbg'));
239
245
  const staticIssues = await this.applyStaticRulesToDiff(fileData, filePath);
240
246
  this.issues.push(...staticIssues);
241
247
 
242
248
  if (staticIssues.length > 0) {
243
- logger.debug(`静态规则发现 ${staticIssues.length} 个问题`);
249
+ logger.debug(t(this.config, 'static_rules_found_n_dbg', { count: staticIssues.length }));
244
250
  }
245
251
 
246
252
  // 2. 本地规则门槛判定
@@ -252,23 +258,23 @@ export class CodeReviewer {
252
258
 
253
259
  if (blockedIssues.length > 0) {
254
260
  const levelsText = [...new Set(blockedIssues.map(i => i.risk))].join(', ');
255
- logger.error(`发现阻断等级风险 (${levelsText}),跳过AI分析`);
261
+ logger.error(t(this.config, 'blocking_risk_detected_skip_ai', { levels: levelsText }));
256
262
  } else {
257
263
  // 3. 应用AI分析(如果启用)
258
264
  if (this.aiClient && this.shouldUseAI(filePath, fileData.fullContent)) {
259
- logger.debug(`启动AI智能分析...`);
265
+ logger.debug(t(this.config, 'ai_start_progress_note'));
260
266
  const aiIssues = await this.aiClient.analyzeDiffFile(fileData, { staticIssues });
261
267
  this.issues.push(...aiIssues);
262
268
  this.aiRan = true;
263
269
 
264
270
  if (aiIssues.length > 0) {
265
- logger.debug(`AI分析发现 ${aiIssues.length} 个问题`);
271
+ logger.debug(t(this.config, 'ai_issues_found_n_dbg', { count: aiIssues.length }));
266
272
  }
267
273
  }
268
274
  }
269
275
 
270
276
  } catch (error) {
271
- logger.error(`审查文件变更失败 ${filePath}:`, error);
277
+ logger.error(t(this.config, 'review_file_failed', { file: filePath, error: error?.message || String(error) }));
272
278
  }
273
279
  }
274
280
 
@@ -282,17 +288,17 @@ export class CodeReviewer {
282
288
 
283
289
  try {
284
290
  if (this.aiClient && this.shouldUseAI(filePath, fileData.fullContent)) {
285
- logger.debug(`启动AI智能分析...`);
291
+ logger.debug(t(this.config, 'ai_start_progress_note'));
286
292
  const aiIssues = await this.aiClient.analyzeDiffFile(fileData, { staticIssues });
287
293
  this.issues.push(...aiIssues);
288
294
  this.aiRan = true;
289
295
 
290
296
  if (aiIssues.length > 0) {
291
- logger.debug(`AI分析发现 ${aiIssues.length} 个问题`);
297
+ logger.debug(t(this.config, 'ai_issues_found_n_dbg', { count: aiIssues.length }));
292
298
  }
293
299
  }
294
300
  } catch (error) {
295
- logger.error(`AI分析文件变更失败 ${filePath}:`, error);
301
+ logger.error(t(this.config, 'ai_diff_failed', { path: filePath, error: error?.message || String(error) }));
296
302
  }
297
303
  }
298
304
 
@@ -359,7 +365,61 @@ export class CodeReviewer {
359
365
  const regex = this.getCachedRegex(rule.pattern, rule.flags || 'gm');
360
366
  let match;
361
367
  const reportedSnippets = new Set();
362
-
368
+ const requiresAbsent = Array.isArray(rule.requiresAbsent) ? rule.requiresAbsent : null;
369
+ const isPairable = !!(requiresAbsent && requiresAbsent.some(s => /\(/.test(String(s))));
370
+ const escapeRegExp = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
371
+ const findAssignedHandleBefore = (src, mIndex) => {
372
+ const lineStart = src.lastIndexOf('\n', Math.max(0, mIndex - 1)) + 1;
373
+ const left = src.slice(lineStart, mIndex);
374
+ const assignMatch = /([A-Za-z_][\w$]*(?:\.[A-Za-z_][\w$]*)*)\s*(?:=|:=)\s*$/.exec(left);
375
+ if (assignMatch) return assignMatch[1];
376
+ return null;
377
+ };
378
+ const isEffectiveIndex = (idx) => {
379
+ if (this.isIndexInRanges(idx, commentRanges)) return false;
380
+ if (this.isIndexInRanges(idx, disableRanges.suppressRanges || [])) return false;
381
+ return true;
382
+ };
383
+ const buildPairedCleanupRegexes = (rxStr, handle, flags) => {
384
+ const raw = String(rxStr);
385
+ if (!/(\(|\\\()/.test(raw)) return [];
386
+ const escapedHandle = escapeRegExp(handle);
387
+ const fFlags = flags || 'gm';
388
+ const out = [];
389
+ // 1) 函数调用样式:fnName( handle )
390
+ try {
391
+ const withHandle = raw.replace(/\\\(/, `\\(\\s*${escapedHandle}\\s*`);
392
+ const patternStr = withHandle.endsWith(')') || /\\\)\s*$/.test(withHandle) ? withHandle : `${withHandle}\\)`;
393
+ out.push(new RegExp(patternStr, fFlags));
394
+ } catch (_) {}
395
+ // 2) 句柄方法样式:handle.fnName(...)
396
+ try {
397
+ const fnMatch = raw.match(/([A-Za-z_][A-Za-z0-9_.$]*)\s*(?:\\\(|\()/);
398
+ if (fnMatch) {
399
+ const fn = escapeRegExp(fnMatch[1]);
400
+ out.push(new RegExp(`${escapedHandle}\\s*\\.\\s*${fn}\\s*\\(`, fFlags));
401
+ }
402
+ } catch (_) {}
403
+ return out;
404
+ };
405
+
406
+ // 非配对规则:若 requiresAbsent 存在且在新增内容中命中有效清理,则跳过该规则在此段
407
+ if (!isPairable && requiresAbsent && requiresAbsent.length > 0) {
408
+ let hasCleanup = false;
409
+ for (const rxStr of requiresAbsent) {
410
+ try {
411
+ const rx = this.getCachedRegex(rxStr, rule.flags || 'gm');
412
+ let cm;
413
+ while ((cm = rx.exec(addedLinesContent)) !== null) {
414
+ if (isEffectiveIndex(cm.index)) { hasCleanup = true; break; }
415
+ }
416
+ } catch (_) {}
417
+ if (hasCleanup) break;
418
+ }
419
+ if (hasCleanup) {
420
+ continue;
421
+ }
422
+ }
363
423
  while ((match = regex.exec(addedLinesContent)) !== null) {
364
424
  // 检查匹配位置是否在注释中
365
425
  if (this.isIndexInRanges(match.index, commentRanges)) {
@@ -371,6 +431,29 @@ export class CodeReviewer {
371
431
  continue;
372
432
  }
373
433
 
434
+ // 当规则的 requiresAbsent 是“函数调用样式”时:对每个匹配执行句柄级清理配对
435
+ if (isPairable && requiresAbsent && requiresAbsent.length > 0) {
436
+ const handle = findAssignedHandleBefore(addedLinesContent, match.index);
437
+ let cleaned = false;
438
+ if (handle) {
439
+ for (const rxStr of requiresAbsent) {
440
+ const pairedRegexes = buildPairedCleanupRegexes(rxStr, handle, rule.flags);
441
+ if (!pairedRegexes || pairedRegexes.length === 0) continue;
442
+ for (const pairedRx of pairedRegexes) {
443
+ let cm;
444
+ while ((cm = pairedRx.exec(addedLinesContent)) !== null) {
445
+ if (isEffectiveIndex(cm.index)) { cleaned = true; break; }
446
+ }
447
+ if (cleaned) break;
448
+ }
449
+ if (cleaned) break;
450
+ }
451
+ }
452
+ if (cleaned) {
453
+ continue; // 已对同一句柄执行清理,不报警
454
+ }
455
+ }
456
+
374
457
  const snippetText = (match[0] || '').substring(0, 200); // 限制片段长度
375
458
 
376
459
  if (reportedSnippets.has(snippetText)) {
@@ -399,7 +482,7 @@ export class CodeReviewer {
399
482
  }
400
483
  }
401
484
  } catch (error) {
402
- logger.warn(`规则 ${rule.id} 在diff模式下执行失败:`, error.message);
485
+ logger.warn(t(this.config, 'rule_diff_exec_failed_warn', { id: rule.id, error: error?.message || String(error) }));
403
486
  }
404
487
  }
405
488
  }
@@ -426,33 +509,54 @@ export class CodeReviewer {
426
509
  return addedLines;
427
510
  }
428
511
 
429
- /**
430
- * 从段落中提取新增行内容和行号映射
431
- * @param {Object} segment 代码段
432
- * @returns {Object} { addedLinesContent: string, lineMapping: Array }
433
- */
434
- extractAddedLinesFromSegment(segment) {
435
- const lines = segment.content.split('\n');
436
- const addedLines = [];
437
- const lineMapping = []; // 映射:新内容行号 -> 原始段落行号
438
-
439
- for (let i = 0; i < lines.length; i++) {
440
- const line = lines[i];
441
- // 只提取新增行(以+开头的行)
442
- if (line.startsWith('+')) {
443
- // 移除+前缀,保留实际代码内容
444
- addedLines.push(line.substring(1));
445
- lineMapping.push(i + 1); // 记录在原始段落中的行号(1-based)
446
- }
447
- }
448
-
449
- return {
450
- addedLinesContent: addedLines.join('\n'),
451
- lineMapping
452
- };
453
- }
512
+ /**
513
+ * 从段落中提取新增行内容和行号映射
514
+ * @param {Object} segment 代码段
515
+ * @returns {Object} { addedLinesContent: string, lineMapping: Array, originalAddedLinesContent: string }
516
+ */
517
+ extractAddedLinesFromSegment(segment) {
518
+ const lines = segment.content.split('\n');
519
+ const addedLinesOriginal = [];
520
+ const addedLinesNormalized = [];
521
+ const lineMapping = []; // 新内容行号(1-based) -> 原文件行号
522
+ const bracketRe = /^\s*\[\s*(\d+)\s*\]\s*/; // 解析类似 "[2067] " 的前缀
523
+ let plusIdx = 0; // 计数第几条新增行
524
+
525
+ for (let i = 0; i < lines.length; i++) {
526
+ const line = lines[i];
527
+ // 只提取新增行(以+开头的行)
528
+ if (line.startsWith('+')) {
529
+ const withoutPlus = line.substring(1);
530
+ addedLinesOriginal.push(withoutPlus);
531
+
532
+ // 优先使用 segment.addedLineNumbers 提供的真实文件行号
533
+ let actualLine = Array.isArray(segment.addedLineNumbers) && plusIdx < segment.addedLineNumbers.length
534
+ ? segment.addedLineNumbers[plusIdx]
535
+ : undefined;
536
+ // 若未提供,则尝试从 [行号] 前缀中解析
537
+ if (!Number.isFinite(actualLine)) {
538
+ const m = withoutPlus.match(bracketRe);
539
+ if (m) actualLine = parseInt(m[1], 10);
540
+ }
541
+ // 兜底:用段起始行近似(不理想,但避免 undefined)
542
+ if (!Number.isFinite(actualLine)) {
543
+ actualLine = Number.isFinite(segment?.startLine) ? (segment.startLine + plusIdx) : undefined;
544
+ }
545
+ lineMapping.push(actualLine);
454
546
 
547
+ // 生成用于匹配的规范化行:移除 [行号] 前缀
548
+ const normalized = withoutPlus.replace(bracketRe, '');
549
+ addedLinesNormalized.push(normalized);
550
+ plusIdx++;
551
+ }
552
+ }
455
553
 
554
+ return {
555
+ addedLinesContent: addedLinesNormalized.join('\n'),
556
+ lineMapping,
557
+ originalAddedLinesContent: addedLinesOriginal.join('\n')
558
+ };
559
+ }
456
560
 
457
561
  // 批量感知:优先批量AI分析(方案A),同时保留静态规则与阻断判定
458
562
  async reviewFilesBatchAware(filePaths) {
@@ -463,11 +567,11 @@ export class CodeReviewer {
463
567
 
464
568
  for (const file of filePaths) {
465
569
  if (!this.isReviewableFile(file)) {
466
- logger.info(`文件已跳过审查: ${file} (文件类型被忽略)`);
570
+ logger.info(t(this.config, 'file_skipped_by_type', { path: file }));
467
571
  continue;
468
572
  }
469
573
  const relativePath = path.relative(this.config.projectRoot, file);
470
- logger.debug(`审查文件: ${relativePath}`);
574
+ logger.debug(t(this.config, 'review_file_dbg', { file: relativePath }));
471
575
 
472
576
  try {
473
577
  const content = await this.getFileContent(file);
@@ -493,19 +597,19 @@ export class CodeReviewer {
493
597
  aiEligible.push({ filePath: file, content, staticIssues: contextStatic });
494
598
  }
495
599
  } catch (error) {
496
- logger.error(`审查文件失败 ${file}:`, error);
600
+ logger.error(t(this.config, 'review_file_failed', { file, error: error?.message || String(error) }));
497
601
  }
498
602
  }
499
603
  // 若任意文件存在阻断等级问题,直接返回(跳过AI)
500
604
  if (anyBlocking) {
501
- logger.info('本地规则存在阻断等级风险,跳过所有文件的AI分析。');
605
+ logger.info(t(this.config, 'blocking_risk_skip_all_ai_info'));
502
606
  return;
503
607
  }
504
608
 
505
609
  if (aiEligible.length === 0) return;
506
610
 
507
611
  // 使用增量式分析器进行分析(默认行为)
508
- logger.progress('使用增量式分析器进行分析...');
612
+ logger.progress(t(this.config, 'using_incremental_analyzer_progress'));
509
613
  await this.reviewFilesWithIncrementalAnalyzer(aiEligible);
510
614
  }
511
615
 
@@ -515,7 +619,7 @@ export class CodeReviewer {
515
619
  await this.reviewFilesWithSmartBatching(aiEligible);
516
620
 
517
621
  } catch (error) {
518
- logger.error(`增量式分析器失败: ${error.message}`);
622
+ logger.error(t(this.config, 'incremental_analyzer_failed_error', { error: error?.message || String(error) }));
519
623
  throw error;
520
624
  }
521
625
  }
@@ -533,7 +637,7 @@ export class CodeReviewer {
533
637
  chunkOverlapLines: batchCfg.chunkOverlapLines || 5
534
638
  });
535
639
 
536
- logger.progress('开始AI智能分析,根据文件大小耗时不同,请耐心等待...');
640
+ logger.progress(t(this.config, 'ai_smart_analysis_start_progress'));
537
641
  const batchResult = smartBatching.createSmartBatches(aiEligible);
538
642
 
539
643
  if (this.useConcurrency && this.aiClientPool) {
@@ -547,10 +651,10 @@ export class CodeReviewer {
547
651
 
548
652
  this.aiRan = true;
549
653
  } catch (error) {
550
- logger.error('AI智能批量分析过程出错:', error);
654
+ logger.error(t(this.config, 'smart_batching_process_error', { error: error?.message || String(error) }));
551
655
 
552
656
  // 回退到原有的简单分批方式
553
- logger.progress('回退到简单分析方式...');
657
+ logger.progress(t(this.config, 'fallback_simple_analysis_progress'));
554
658
  try {
555
659
  const max = Number(batchCfg.maxFilesPerBatch || 20);
556
660
  const batches = [];
@@ -558,18 +662,18 @@ export class CodeReviewer {
558
662
  batches.push(aiEligible.slice(i, i + max));
559
663
  }
560
664
 
561
- logger.info(`开始AI分析,共${batches.length}批文件`);
665
+ logger.info(t(this.config, 'ai_analysis_start_batches', { count: batches.length }));
562
666
 
563
667
  for (let i = 0; i < batches.length; i++) {
564
668
  const batch = batches[i];
565
- logger.info(`批次 ${i + 1}/${batches.length}: 分析${batch.length}个文件`);
669
+ logger.info(t(this.config, 'batch_info_header', { index: i + 1, total: batches.length, files: batch.length }));
566
670
  const aiIssues = await this.aiClient.analyzeFilesBatch(batch);
567
671
  this.issues.push(...aiIssues);
568
- logger.success(`批次 ${i + 1}/${batches.length} 完成`);
672
+ logger.success(t(this.config, 'batch_done_success', { index: i + 1, total: batches.length }));
569
673
  }
570
674
  this.aiRan = true;
571
675
  } catch (fallbackError) {
572
- logger.error('回退分析也失败:', fallbackError);
676
+ logger.error(t(this.config, 'fallback_analysis_failed', { error: fallbackError?.message || String(fallbackError) }));
573
677
  }
574
678
  }
575
679
  }
@@ -582,8 +686,8 @@ export class CodeReviewer {
582
686
  async processBatchesConcurrently(batches, smartBatching) {
583
687
  // 只有当批次数量大于1且并发数量大于1时才显示并发处理日志
584
688
  if (batches.length > 1 && this.concurrency > 1) {
585
- logger.info(`启用并发处理,并发数: ${this.concurrency}`);
586
- logger.info(`使用并发模式处理 ${batches.length} 个批次`);
689
+ logger.info(t(this.config, 'start_concurrency_enabled_info', { concurrency: this.concurrency }));
690
+ logger.info(t(this.config, 'start_concurrency_dbg', { count: batches.length }));
587
691
  }
588
692
 
589
693
  // 创建进度跟踪器
@@ -600,7 +704,7 @@ export class CodeReviewer {
600
704
  progressTracker.completed++;
601
705
  } else if (status === 'failed') {
602
706
  progressTracker.failed++;
603
- logger.warn(`批次 ${batchIndex + 1} 处理失败: ${error?.message || '未知错误'}`);
707
+ logger.warn(t(this.config, 'concurrent_group_failed', { index: batchIndex + 1, error: error?.message || String(error) }));
604
708
  }
605
709
  };
606
710
 
@@ -613,10 +717,10 @@ export class CodeReviewer {
613
717
 
614
718
  const elapsed = ((totalDurationMs) / 1000).toFixed(1);
615
719
  const totalIssues = allIssues.length;
616
- logger.success(`AI分析完成,发现${totalIssues}个问题,共耗时:${elapsed}秒`);
720
+ logger.success(t(this.config, 'ai_analysis_done_summary', { totalIssues, elapsed }));
617
721
 
618
722
  } catch (error) {
619
- logger.error(`并发处理过程中发生错误: ${error.message}`);
723
+ logger.error(t(this.config, 'concurrent_processing_error', { error: error?.message || String(error) }));
620
724
  throw error;
621
725
  }
622
726
  }
@@ -627,7 +731,7 @@ export class CodeReviewer {
627
731
  * @param {SmartBatching} smartBatching - 智能分批实例
628
732
  */
629
733
  async processBatchesSerially(batches, smartBatching) {
630
- logger.info(`使用串行模式处理 ${batches.length} 个批次`);
734
+ logger.info(t(this.config, 'serial_process_batches_info', { count: batches.length }));
631
735
  const startTime = Date.now();
632
736
 
633
737
  for (let i = 0; i < batches.length; i++) {
@@ -637,21 +741,21 @@ export class CodeReviewer {
637
741
  // 判断批次类型并输出相应信息
638
742
  if (batch.isLargeFileSegment) {
639
743
  // 大文件批次 - 显示文件路径和总段数
640
- logger.info(`批次 ${i + 1}/${batches.length}: 分析 ${batch.segmentedFile} 文件,共${batch.totalSegments}段`);
744
+ logger.info(t(this.config, 'serial_large_file_batch_info', { index: i + 1, total: batches.length, file: batch.segmentedFile, segments: batch.totalSegments }));
641
745
  } else {
642
746
  // 小文件批次 - 列出所有文件
643
747
  const fileList = batch.items.map(item => item.filePath).join(',');
644
- logger.info(`批次 ${i + 1}/${batches.length}: 分析 ${fileList} 文件`);
748
+ logger.info(t(this.config, 'serial_small_file_batch_info', { index: i + 1, total: batches.length, files: fileList }));
645
749
  }
646
750
 
647
751
  const aiIssues = await this.aiClient.analyzeSmartBatch(formattedBatch, batch);
648
752
  this.issues.push(...aiIssues);
649
753
 
650
- logger.success(`批次 ${i + 1}/${batches.length} 完成`);
754
+ logger.success(t(this.config, 'batch_done_success', { index: i + 1, total: batches.length }));
651
755
  }
652
756
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
653
757
  const totalIssues = this.issues.length;
654
- logger.success(`AI分析完成,发现${totalIssues}个问题,共耗时:${elapsed}秒`);
758
+ logger.success(t(this.config, 'ai_analysis_done_summary', { totalIssues, elapsed }));
655
759
  }
656
760
 
657
761
  async getFileContent(filePath) {
@@ -670,7 +774,7 @@ export class CodeReviewer {
670
774
  return await this.readFileStream(filePath);
671
775
  }
672
776
  } catch (error) {
673
- logger.error(`❌ 读取文件内容失败 ${filePath}:`, error);
777
+ logger.error(t(this.config, 'read_file_failed', { file: filePath, error: error?.message || String(error) }));
674
778
  return null;
675
779
  }
676
780
  }
@@ -692,7 +796,7 @@ export class CodeReviewer {
692
796
  }
693
797
 
694
798
  // 对于大文件,使用流式读取
695
- logger.debug(`使用流式读取大文件: ${filePath} (${fileSizeKB.toFixed(1)}KB)`);
799
+ logger.debug(t(this.config, 'stream_read_large_file_dbg', { file: filePath, sizeKB: fileSizeKB.toFixed(1) }));
696
800
 
697
801
  const chunks = [];
698
802
  const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
@@ -720,8 +824,10 @@ export class CodeReviewer {
720
824
  let skippedByDirectives = 0;
721
825
  for (const rule of this.rules) {
722
826
  try {
723
- // 可选:当文件中存在指定清理/反证模式时,跳过该规则以降低误报
724
- if (Array.isArray(rule.requiresAbsent) && rule.requiresAbsent.length > 0 && typeof rule.pattern !== 'function') {
827
+ // 保留通用 requiresAbsent 跳过,但当 requiresAbsent 明显是“函数调用样式”时,改为逐匹配的配对校验
828
+ const requiresAbsent = Array.isArray(rule.requiresAbsent) ? rule.requiresAbsent : null;
829
+ const isPairable = !!(requiresAbsent && requiresAbsent.some(s => /\(/.test(String(s))));
830
+ if (!isPairable && requiresAbsent && requiresAbsent.length > 0 && typeof rule.pattern !== 'function') {
725
831
  const hasCleanup = rule.requiresAbsent.some(rxStr => {
726
832
  try {
727
833
  const rx = this.getCachedRegex(rxStr, rule.flags || 'gm');
@@ -730,7 +836,7 @@ export class CodeReviewer {
730
836
  return false;
731
837
  }
732
838
  });
733
- if (hasCleanup) continue; // 文件已存在对应清理逻辑,跳过此规则
839
+ if (hasCleanup) continue;
734
840
  }
735
841
 
736
842
  // 简化:pattern 支持函数。若返回片段字符串或字符串数组,则直接使用该片段作为结果;
@@ -772,6 +878,44 @@ export class CodeReviewer {
772
878
  // 记录该规则已在哪些代码片段上报过,避免重复片段
773
879
  const reportedSnippets = new Set();
774
880
 
881
+ const escapeRegExp = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
882
+ const findAssignedHandleBefore = (src, mIndex) => {
883
+ const lineStart = src.lastIndexOf('\n', Math.max(0, mIndex - 1)) + 1;
884
+ const left = src.slice(lineStart, mIndex);
885
+ // 通用赋值检测:匹配“标识符或属性” + “= 或 :=”结尾(覆盖多语言常见语法)
886
+ const assignMatch = /([A-Za-z_][\w$]*(?:\.[A-Za-z_][\w$]*)*)\s*(?:=|:=)\s*$/.exec(left);
887
+ if (assignMatch) return assignMatch[1];
888
+ return null;
889
+ };
890
+ const isEffectiveIndex = (idx) => {
891
+ if (this.isIndexInRanges(idx, commentRanges)) return false;
892
+ if (this.isIndexInRanges(idx, disable.suppressRanges || [])) return false;
893
+ return true;
894
+ };
895
+ const buildPairedCleanupRegexes = (rxStr, handle, flags) => {
896
+ const raw = String(rxStr);
897
+ // 仅当 requiresAbsent 为“函数调用样式”时构建带句柄的清理匹配
898
+ if (!/(\(|\\\()/.test(raw)) return [];
899
+ const escapedHandle = escapeRegExp(handle);
900
+ const fFlags = flags || 'gm';
901
+ const out = [];
902
+ // 1) 函数调用样式:fnName( handle )
903
+ try {
904
+ const withHandle = raw.replace(/\\\(/, `\\(\\s*${escapedHandle}\\s*`);
905
+ const patternStr = withHandle.endsWith(')') || /\\\)\s*$/.test(withHandle) ? withHandle : `${withHandle}\\)`;
906
+ out.push(new RegExp(patternStr, fFlags));
907
+ } catch (_) {}
908
+ // 2) 句柄方法样式:handle.fnName( ... )
909
+ try {
910
+ const fnMatch = raw.match(/([A-Za-z_][A-Za-z0-9_.$]*)\s*(?:\\\(|\()/);
911
+ if (fnMatch) {
912
+ const fn = escapeRegExp(fnMatch[1]);
913
+ out.push(new RegExp(`${escapedHandle}\\s*\\.\\s*${fn}\\s*\\(`, fFlags));
914
+ }
915
+ } catch (_) {}
916
+ return out;
917
+ };
918
+
775
919
  while ((match = regex.exec(content)) !== null) {
776
920
  // 若匹配位置在注释中,跳过
777
921
  if (this.isIndexInRanges(match.index, commentRanges)) {
@@ -783,6 +927,32 @@ export class CodeReviewer {
783
927
  skippedByDirectives++;
784
928
  continue;
785
929
  }
930
+ // 当规则的 requiresAbsent 为“函数调用样式”时:逐匹配校验是否存在针对同一句柄的清理
931
+ if (isPairable && requiresAbsent && requiresAbsent.length > 0) {
932
+ const handle = findAssignedHandleBefore(content, match.index);
933
+ let cleaned = false;
934
+ if (handle) {
935
+ for (const rxStr of requiresAbsent) {
936
+ const pairedRegexes = buildPairedCleanupRegexes(rxStr, handle, rule.flags);
937
+ if (!pairedRegexes || pairedRegexes.length === 0) continue;
938
+ for (const pairedRx of pairedRegexes) {
939
+ let cm;
940
+ while ((cm = pairedRx.exec(content)) !== null) {
941
+ if (isEffectiveIndex(cm.index)) { cleaned = true; break; }
942
+ }
943
+ if (cleaned) break;
944
+ }
945
+ if (cleaned) break;
946
+ }
947
+ } else {
948
+ // 未保存返回句柄则视为未清理风险(无法对应清理目标)
949
+ cleaned = false;
950
+ }
951
+ if (cleaned) {
952
+ continue; // 已对同一句柄执行清理,不报警
953
+ }
954
+ }
955
+
786
956
  const lineNumber = this.getLineNumber(content, match.index);
787
957
  const snippetText = (match[0] || '').substring(0, BATCH_CONSTANTS.MAX_SNIPPET_LENGTH);
788
958
 
@@ -804,15 +974,15 @@ export class CodeReviewer {
804
974
  });
805
975
  }
806
976
  } catch (error) {
807
- logger.warn(`规则 ${rule.id} 执行失败:`, error.message);
977
+ logger.warn(t(this.config, 'rule_exec_failed_warn', { id: rule.id, error: error?.message || String(error) }));
808
978
  }
809
979
  }
810
980
 
811
981
  if (skippedByComments > 0) {
812
- logger.debug(`注释代码已跳过审查(${skippedByComments}条匹配)`);
982
+ logger.debug(t(this.config, 'skip_by_comments_dbg', { count: skippedByComments }));
813
983
  }
814
984
  if (skippedByDirectives > 0) {
815
- logger.debug(`指令禁用范围已跳过审查(${skippedByDirectives}条匹配)`);
985
+ logger.debug(t(this.config, 'skip_by_directives_dbg', { count: skippedByDirectives }));
816
986
  }
817
987
 
818
988
  return issues;
@@ -876,7 +1046,7 @@ export class CodeReviewer {
876
1046
  if (!shouldInclude) {
877
1047
  return {
878
1048
  reviewable: false,
879
- reason: `文件扩展名 ${ext} 不在支持列表中`,
1049
+ reason: t(this.config, 'file_ext_not_supported_reason', { ext }),
880
1050
  matchedPattern: null
881
1051
  };
882
1052
  }
@@ -897,7 +1067,7 @@ export class CodeReviewer {
897
1067
  if (originalPattern === normalized || originalPattern === relativePath || originalPattern === basename) {
898
1068
  return {
899
1069
  reviewable: false,
900
- reason: '匹配精确模式',
1070
+ reason: t(this.config, 'file_reason_exact_match'),
901
1071
  matchedPattern: originalPattern
902
1072
  };
903
1073
  }
@@ -911,7 +1081,7 @@ export class CodeReviewer {
911
1081
  if (normalizedMatch || relativeMatch) {
912
1082
  return {
913
1083
  reviewable: false,
914
- reason: '匹配正则表达式',
1084
+ reason: t(this.config, 'file_reason_regex_match'),
915
1085
  matchedPattern: originalPattern
916
1086
  };
917
1087
  }
@@ -925,7 +1095,7 @@ export class CodeReviewer {
925
1095
  if (this.matchPattern(normalized, patternStr) || this.matchPattern(relativePath, patternStr)) {
926
1096
  return {
927
1097
  reviewable: false,
928
- reason: '匹配glob模式',
1098
+ reason: t(this.config, 'file_reason_glob_match'),
929
1099
  matchedPattern: originalPattern
930
1100
  };
931
1101
  }
@@ -1014,7 +1184,7 @@ export class CodeReviewer {
1014
1184
 
1015
1185
  // 检查文件大小限制
1016
1186
  if (content.length > (this.config.ai?.maxFileSizeKB || DEFAULT_CONFIG.MAX_FILE_SIZE_KB) * 1024) {
1017
- logger.info(`跳过AI分析大文件: ${filePath}`);
1187
+ logger.info(t(this.config, 'skip_ai_large_file', { file: filePath }));
1018
1188
  return false;
1019
1189
  }
1020
1190