smart-review 1.0.1 → 1.0.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/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
 
@@ -320,6 +326,17 @@ export class CodeReviewer {
320
326
  const commentRanges = this.getCachedCommentRanges(addedLinesContent, ext);
321
327
 
322
328
  for (const rule of this.rules) {
329
+ // 扩展名过滤:若规则声明了 extensions,则仅在匹配的扩展上生效
330
+ const ruleExts = Array.isArray(rule.extensions) ? rule.extensions : null;
331
+ if (ruleExts && ruleExts.length > 0) {
332
+ const normalized = ruleExts.map((e) => {
333
+ const s = String(e).trim().toLowerCase();
334
+ return s.startsWith('.') ? s : `.${s}`;
335
+ });
336
+ if (!normalized.includes(ext)) {
337
+ continue;
338
+ }
339
+ }
323
340
  try {
324
341
  // 函数类型规则处理
325
342
  if (typeof rule.pattern === 'function') {
@@ -359,7 +376,61 @@ export class CodeReviewer {
359
376
  const regex = this.getCachedRegex(rule.pattern, rule.flags || 'gm');
360
377
  let match;
361
378
  const reportedSnippets = new Set();
362
-
379
+ const requiresAbsent = Array.isArray(rule.requiresAbsent) ? rule.requiresAbsent : null;
380
+ const isPairable = !!(requiresAbsent && requiresAbsent.some(s => /\(/.test(String(s))));
381
+ const escapeRegExp = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
382
+ const findAssignedHandleBefore = (src, mIndex) => {
383
+ const lineStart = src.lastIndexOf('\n', Math.max(0, mIndex - 1)) + 1;
384
+ const left = src.slice(lineStart, mIndex);
385
+ const assignMatch = /([A-Za-z_][\w$]*(?:\.[A-Za-z_][\w$]*)*)\s*(?:=|:=)\s*$/.exec(left);
386
+ if (assignMatch) return assignMatch[1];
387
+ return null;
388
+ };
389
+ const isEffectiveIndex = (idx) => {
390
+ if (this.isIndexInRanges(idx, commentRanges)) return false;
391
+ if (this.isIndexInRanges(idx, disableRanges.suppressRanges || [])) return false;
392
+ return true;
393
+ };
394
+ const buildPairedCleanupRegexes = (rxStr, handle, flags) => {
395
+ const raw = String(rxStr);
396
+ if (!/(\(|\\\()/.test(raw)) return [];
397
+ const escapedHandle = escapeRegExp(handle);
398
+ const fFlags = flags || 'gm';
399
+ const out = [];
400
+ // 1) 函数调用样式:fnName( handle )
401
+ try {
402
+ const withHandle = raw.replace(/\\\(/, `\\(\\s*${escapedHandle}\\s*`);
403
+ const patternStr = withHandle.endsWith(')') || /\\\)\s*$/.test(withHandle) ? withHandle : `${withHandle}\\)`;
404
+ out.push(new RegExp(patternStr, fFlags));
405
+ } catch (_) {}
406
+ // 2) 句柄方法样式:handle.fnName(...)
407
+ try {
408
+ const fnMatch = raw.match(/([A-Za-z_][A-Za-z0-9_.$]*)\s*(?:\\\(|\()/);
409
+ if (fnMatch) {
410
+ const fn = escapeRegExp(fnMatch[1]);
411
+ out.push(new RegExp(`${escapedHandle}\\s*\\.\\s*${fn}\\s*\\(`, fFlags));
412
+ }
413
+ } catch (_) {}
414
+ return out;
415
+ };
416
+
417
+ // 非配对规则:若 requiresAbsent 存在且在新增内容中命中有效清理,则跳过该规则在此段
418
+ if (!isPairable && requiresAbsent && requiresAbsent.length > 0) {
419
+ let hasCleanup = false;
420
+ for (const rxStr of requiresAbsent) {
421
+ try {
422
+ const rx = this.getCachedRegex(rxStr, rule.flags || 'gm');
423
+ let cm;
424
+ while ((cm = rx.exec(addedLinesContent)) !== null) {
425
+ if (isEffectiveIndex(cm.index)) { hasCleanup = true; break; }
426
+ }
427
+ } catch (_) {}
428
+ if (hasCleanup) break;
429
+ }
430
+ if (hasCleanup) {
431
+ continue;
432
+ }
433
+ }
363
434
  while ((match = regex.exec(addedLinesContent)) !== null) {
364
435
  // 检查匹配位置是否在注释中
365
436
  if (this.isIndexInRanges(match.index, commentRanges)) {
@@ -371,6 +442,29 @@ export class CodeReviewer {
371
442
  continue;
372
443
  }
373
444
 
445
+ // 当规则的 requiresAbsent 是“函数调用样式”时:对每个匹配执行句柄级清理配对
446
+ if (isPairable && requiresAbsent && requiresAbsent.length > 0) {
447
+ const handle = findAssignedHandleBefore(addedLinesContent, match.index);
448
+ let cleaned = false;
449
+ if (handle) {
450
+ for (const rxStr of requiresAbsent) {
451
+ const pairedRegexes = buildPairedCleanupRegexes(rxStr, handle, rule.flags);
452
+ if (!pairedRegexes || pairedRegexes.length === 0) continue;
453
+ for (const pairedRx of pairedRegexes) {
454
+ let cm;
455
+ while ((cm = pairedRx.exec(addedLinesContent)) !== null) {
456
+ if (isEffectiveIndex(cm.index)) { cleaned = true; break; }
457
+ }
458
+ if (cleaned) break;
459
+ }
460
+ if (cleaned) break;
461
+ }
462
+ }
463
+ if (cleaned) {
464
+ continue; // 已对同一句柄执行清理,不报警
465
+ }
466
+ }
467
+
374
468
  const snippetText = (match[0] || '').substring(0, 200); // 限制片段长度
375
469
 
376
470
  if (reportedSnippets.has(snippetText)) {
@@ -399,7 +493,7 @@ export class CodeReviewer {
399
493
  }
400
494
  }
401
495
  } catch (error) {
402
- logger.warn(`规则 ${rule.id} 在diff模式下执行失败:`, error.message);
496
+ logger.warn(t(this.config, 'rule_diff_exec_failed_warn', { id: rule.id, error: error?.message || String(error) }));
403
497
  }
404
498
  }
405
499
  }
@@ -426,33 +520,54 @@ export class CodeReviewer {
426
520
  return addedLines;
427
521
  }
428
522
 
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
- }
523
+ /**
524
+ * 从段落中提取新增行内容和行号映射
525
+ * @param {Object} segment 代码段
526
+ * @returns {Object} { addedLinesContent: string, lineMapping: Array, originalAddedLinesContent: string }
527
+ */
528
+ extractAddedLinesFromSegment(segment) {
529
+ const lines = segment.content.split('\n');
530
+ const addedLinesOriginal = [];
531
+ const addedLinesNormalized = [];
532
+ const lineMapping = []; // 新内容行号(1-based) -> 原文件行号
533
+ const bracketRe = /^\s*\[\s*(\d+)\s*\]\s*/; // 解析类似 "[2067] " 的前缀
534
+ let plusIdx = 0; // 计数第几条新增行
535
+
536
+ for (let i = 0; i < lines.length; i++) {
537
+ const line = lines[i];
538
+ // 只提取新增行(以+开头的行)
539
+ if (line.startsWith('+')) {
540
+ const withoutPlus = line.substring(1);
541
+ addedLinesOriginal.push(withoutPlus);
542
+
543
+ // 优先使用 segment.addedLineNumbers 提供的真实文件行号
544
+ let actualLine = Array.isArray(segment.addedLineNumbers) && plusIdx < segment.addedLineNumbers.length
545
+ ? segment.addedLineNumbers[plusIdx]
546
+ : undefined;
547
+ // 若未提供,则尝试从 [行号] 前缀中解析
548
+ if (!Number.isFinite(actualLine)) {
549
+ const m = withoutPlus.match(bracketRe);
550
+ if (m) actualLine = parseInt(m[1], 10);
551
+ }
552
+ // 兜底:用段起始行近似(不理想,但避免 undefined)
553
+ if (!Number.isFinite(actualLine)) {
554
+ actualLine = Number.isFinite(segment?.startLine) ? (segment.startLine + plusIdx) : undefined;
555
+ }
556
+ lineMapping.push(actualLine);
454
557
 
558
+ // 生成用于匹配的规范化行:移除 [行号] 前缀
559
+ const normalized = withoutPlus.replace(bracketRe, '');
560
+ addedLinesNormalized.push(normalized);
561
+ plusIdx++;
562
+ }
563
+ }
455
564
 
565
+ return {
566
+ addedLinesContent: addedLinesNormalized.join('\n'),
567
+ lineMapping,
568
+ originalAddedLinesContent: addedLinesOriginal.join('\n')
569
+ };
570
+ }
456
571
 
457
572
  // 批量感知:优先批量AI分析(方案A),同时保留静态规则与阻断判定
458
573
  async reviewFilesBatchAware(filePaths) {
@@ -463,11 +578,11 @@ export class CodeReviewer {
463
578
 
464
579
  for (const file of filePaths) {
465
580
  if (!this.isReviewableFile(file)) {
466
- logger.info(`文件已跳过审查: ${file} (文件类型被忽略)`);
581
+ logger.info(t(this.config, 'file_skipped_by_type', { path: file }));
467
582
  continue;
468
583
  }
469
584
  const relativePath = path.relative(this.config.projectRoot, file);
470
- logger.debug(`审查文件: ${relativePath}`);
585
+ logger.debug(t(this.config, 'review_file_dbg', { file: relativePath }));
471
586
 
472
587
  try {
473
588
  const content = await this.getFileContent(file);
@@ -493,19 +608,19 @@ export class CodeReviewer {
493
608
  aiEligible.push({ filePath: file, content, staticIssues: contextStatic });
494
609
  }
495
610
  } catch (error) {
496
- logger.error(`审查文件失败 ${file}:`, error);
611
+ logger.error(t(this.config, 'review_file_failed', { file, error: error?.message || String(error) }));
497
612
  }
498
613
  }
499
614
  // 若任意文件存在阻断等级问题,直接返回(跳过AI)
500
615
  if (anyBlocking) {
501
- logger.info('本地规则存在阻断等级风险,跳过所有文件的AI分析。');
616
+ logger.info(t(this.config, 'blocking_risk_skip_all_ai_info'));
502
617
  return;
503
618
  }
504
619
 
505
620
  if (aiEligible.length === 0) return;
506
621
 
507
622
  // 使用增量式分析器进行分析(默认行为)
508
- logger.progress('使用增量式分析器进行分析...');
623
+ logger.progress(t(this.config, 'using_incremental_analyzer_progress'));
509
624
  await this.reviewFilesWithIncrementalAnalyzer(aiEligible);
510
625
  }
511
626
 
@@ -515,7 +630,7 @@ export class CodeReviewer {
515
630
  await this.reviewFilesWithSmartBatching(aiEligible);
516
631
 
517
632
  } catch (error) {
518
- logger.error(`增量式分析器失败: ${error.message}`);
633
+ logger.error(t(this.config, 'incremental_analyzer_failed_error', { error: error?.message || String(error) }));
519
634
  throw error;
520
635
  }
521
636
  }
@@ -533,7 +648,7 @@ export class CodeReviewer {
533
648
  chunkOverlapLines: batchCfg.chunkOverlapLines || 5
534
649
  });
535
650
 
536
- logger.progress('开始AI智能分析,根据文件大小耗时不同,请耐心等待...');
651
+ logger.progress(t(this.config, 'ai_smart_analysis_start_progress'));
537
652
  const batchResult = smartBatching.createSmartBatches(aiEligible);
538
653
 
539
654
  if (this.useConcurrency && this.aiClientPool) {
@@ -547,10 +662,10 @@ export class CodeReviewer {
547
662
 
548
663
  this.aiRan = true;
549
664
  } catch (error) {
550
- logger.error('AI智能批量分析过程出错:', error);
665
+ logger.error(t(this.config, 'smart_batching_process_error', { error: error?.message || String(error) }));
551
666
 
552
667
  // 回退到原有的简单分批方式
553
- logger.progress('回退到简单分析方式...');
668
+ logger.progress(t(this.config, 'fallback_simple_analysis_progress'));
554
669
  try {
555
670
  const max = Number(batchCfg.maxFilesPerBatch || 20);
556
671
  const batches = [];
@@ -558,18 +673,18 @@ export class CodeReviewer {
558
673
  batches.push(aiEligible.slice(i, i + max));
559
674
  }
560
675
 
561
- logger.info(`开始AI分析,共${batches.length}批文件`);
676
+ logger.info(t(this.config, 'ai_analysis_start_batches', { count: batches.length }));
562
677
 
563
678
  for (let i = 0; i < batches.length; i++) {
564
679
  const batch = batches[i];
565
- logger.info(`批次 ${i + 1}/${batches.length}: 分析${batch.length}个文件`);
680
+ logger.info(t(this.config, 'batch_info_header', { index: i + 1, total: batches.length, files: batch.length }));
566
681
  const aiIssues = await this.aiClient.analyzeFilesBatch(batch);
567
682
  this.issues.push(...aiIssues);
568
- logger.success(`批次 ${i + 1}/${batches.length} 完成`);
683
+ logger.success(t(this.config, 'batch_done_success', { index: i + 1, total: batches.length }));
569
684
  }
570
685
  this.aiRan = true;
571
686
  } catch (fallbackError) {
572
- logger.error('回退分析也失败:', fallbackError);
687
+ logger.error(t(this.config, 'fallback_analysis_failed', { error: fallbackError?.message || String(fallbackError) }));
573
688
  }
574
689
  }
575
690
  }
@@ -582,8 +697,8 @@ export class CodeReviewer {
582
697
  async processBatchesConcurrently(batches, smartBatching) {
583
698
  // 只有当批次数量大于1且并发数量大于1时才显示并发处理日志
584
699
  if (batches.length > 1 && this.concurrency > 1) {
585
- logger.info(`启用并发处理,并发数: ${this.concurrency}`);
586
- logger.info(`使用并发模式处理 ${batches.length} 个批次`);
700
+ logger.info(t(this.config, 'start_concurrency_enabled_info', { concurrency: this.concurrency }));
701
+ logger.info(t(this.config, 'start_concurrency_dbg', { count: batches.length }));
587
702
  }
588
703
 
589
704
  // 创建进度跟踪器
@@ -600,7 +715,7 @@ export class CodeReviewer {
600
715
  progressTracker.completed++;
601
716
  } else if (status === 'failed') {
602
717
  progressTracker.failed++;
603
- logger.warn(`批次 ${batchIndex + 1} 处理失败: ${error?.message || '未知错误'}`);
718
+ logger.warn(t(this.config, 'concurrent_group_failed', { index: batchIndex + 1, error: error?.message || String(error) }));
604
719
  }
605
720
  };
606
721
 
@@ -613,10 +728,10 @@ export class CodeReviewer {
613
728
 
614
729
  const elapsed = ((totalDurationMs) / 1000).toFixed(1);
615
730
  const totalIssues = allIssues.length;
616
- logger.success(`AI分析完成,发现${totalIssues}个问题,共耗时:${elapsed}秒`);
731
+ logger.success(t(this.config, 'ai_analysis_done_summary', { totalIssues, elapsed }));
617
732
 
618
733
  } catch (error) {
619
- logger.error(`并发处理过程中发生错误: ${error.message}`);
734
+ logger.error(t(this.config, 'concurrent_processing_error', { error: error?.message || String(error) }));
620
735
  throw error;
621
736
  }
622
737
  }
@@ -627,7 +742,7 @@ export class CodeReviewer {
627
742
  * @param {SmartBatching} smartBatching - 智能分批实例
628
743
  */
629
744
  async processBatchesSerially(batches, smartBatching) {
630
- logger.info(`使用串行模式处理 ${batches.length} 个批次`);
745
+ logger.info(t(this.config, 'serial_process_batches_info', { count: batches.length }));
631
746
  const startTime = Date.now();
632
747
 
633
748
  for (let i = 0; i < batches.length; i++) {
@@ -637,21 +752,21 @@ export class CodeReviewer {
637
752
  // 判断批次类型并输出相应信息
638
753
  if (batch.isLargeFileSegment) {
639
754
  // 大文件批次 - 显示文件路径和总段数
640
- logger.info(`批次 ${i + 1}/${batches.length}: 分析 ${batch.segmentedFile} 文件,共${batch.totalSegments}段`);
755
+ logger.info(t(this.config, 'serial_large_file_batch_info', { index: i + 1, total: batches.length, file: batch.segmentedFile, segments: batch.totalSegments }));
641
756
  } else {
642
757
  // 小文件批次 - 列出所有文件
643
758
  const fileList = batch.items.map(item => item.filePath).join(',');
644
- logger.info(`批次 ${i + 1}/${batches.length}: 分析 ${fileList} 文件`);
759
+ logger.info(t(this.config, 'serial_small_file_batch_info', { index: i + 1, total: batches.length, files: fileList }));
645
760
  }
646
761
 
647
762
  const aiIssues = await this.aiClient.analyzeSmartBatch(formattedBatch, batch);
648
763
  this.issues.push(...aiIssues);
649
764
 
650
- logger.success(`批次 ${i + 1}/${batches.length} 完成`);
765
+ logger.success(t(this.config, 'batch_done_success', { index: i + 1, total: batches.length }));
651
766
  }
652
767
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
653
768
  const totalIssues = this.issues.length;
654
- logger.success(`AI分析完成,发现${totalIssues}个问题,共耗时:${elapsed}秒`);
769
+ logger.success(t(this.config, 'ai_analysis_done_summary', { totalIssues, elapsed }));
655
770
  }
656
771
 
657
772
  async getFileContent(filePath) {
@@ -670,7 +785,7 @@ export class CodeReviewer {
670
785
  return await this.readFileStream(filePath);
671
786
  }
672
787
  } catch (error) {
673
- logger.error(`❌ 读取文件内容失败 ${filePath}:`, error);
788
+ logger.error(t(this.config, 'read_file_failed', { file: filePath, error: error?.message || String(error) }));
674
789
  return null;
675
790
  }
676
791
  }
@@ -692,7 +807,7 @@ export class CodeReviewer {
692
807
  }
693
808
 
694
809
  // 对于大文件,使用流式读取
695
- logger.debug(`使用流式读取大文件: ${filePath} (${fileSizeKB.toFixed(1)}KB)`);
810
+ logger.debug(t(this.config, 'stream_read_large_file_dbg', { file: filePath, sizeKB: fileSizeKB.toFixed(1) }));
696
811
 
697
812
  const chunks = [];
698
813
  const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
@@ -719,9 +834,22 @@ export class CodeReviewer {
719
834
  let skippedByComments = 0;
720
835
  let skippedByDirectives = 0;
721
836
  for (const rule of this.rules) {
837
+ // 扩展名过滤:若规则声明了 extensions,则仅在匹配的扩展上生效
838
+ const ruleExts = Array.isArray(rule.extensions) ? rule.extensions : null;
839
+ if (ruleExts && ruleExts.length > 0) {
840
+ const normalized = ruleExts.map((e) => {
841
+ const s = String(e).trim().toLowerCase();
842
+ return s.startsWith('.') ? s : `.${s}`;
843
+ });
844
+ if (!normalized.includes(ext)) {
845
+ continue;
846
+ }
847
+ }
722
848
  try {
723
- // 可选:当文件中存在指定清理/反证模式时,跳过该规则以降低误报
724
- if (Array.isArray(rule.requiresAbsent) && rule.requiresAbsent.length > 0 && typeof rule.pattern !== 'function') {
849
+ // 保留通用 requiresAbsent 跳过,但当 requiresAbsent 明显是“函数调用样式”时,改为逐匹配的配对校验
850
+ const requiresAbsent = Array.isArray(rule.requiresAbsent) ? rule.requiresAbsent : null;
851
+ const isPairable = !!(requiresAbsent && requiresAbsent.some(s => /\(/.test(String(s))));
852
+ if (!isPairable && requiresAbsent && requiresAbsent.length > 0 && typeof rule.pattern !== 'function') {
725
853
  const hasCleanup = rule.requiresAbsent.some(rxStr => {
726
854
  try {
727
855
  const rx = this.getCachedRegex(rxStr, rule.flags || 'gm');
@@ -730,7 +858,7 @@ export class CodeReviewer {
730
858
  return false;
731
859
  }
732
860
  });
733
- if (hasCleanup) continue; // 文件已存在对应清理逻辑,跳过此规则
861
+ if (hasCleanup) continue;
734
862
  }
735
863
 
736
864
  // 简化:pattern 支持函数。若返回片段字符串或字符串数组,则直接使用该片段作为结果;
@@ -772,6 +900,44 @@ export class CodeReviewer {
772
900
  // 记录该规则已在哪些代码片段上报过,避免重复片段
773
901
  const reportedSnippets = new Set();
774
902
 
903
+ const escapeRegExp = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
904
+ const findAssignedHandleBefore = (src, mIndex) => {
905
+ const lineStart = src.lastIndexOf('\n', Math.max(0, mIndex - 1)) + 1;
906
+ const left = src.slice(lineStart, mIndex);
907
+ // 通用赋值检测:匹配“标识符或属性” + “= 或 :=”结尾(覆盖多语言常见语法)
908
+ const assignMatch = /([A-Za-z_][\w$]*(?:\.[A-Za-z_][\w$]*)*)\s*(?:=|:=)\s*$/.exec(left);
909
+ if (assignMatch) return assignMatch[1];
910
+ return null;
911
+ };
912
+ const isEffectiveIndex = (idx) => {
913
+ if (this.isIndexInRanges(idx, commentRanges)) return false;
914
+ if (this.isIndexInRanges(idx, disable.suppressRanges || [])) return false;
915
+ return true;
916
+ };
917
+ const buildPairedCleanupRegexes = (rxStr, handle, flags) => {
918
+ const raw = String(rxStr);
919
+ // 仅当 requiresAbsent 为“函数调用样式”时构建带句柄的清理匹配
920
+ if (!/(\(|\\\()/.test(raw)) return [];
921
+ const escapedHandle = escapeRegExp(handle);
922
+ const fFlags = flags || 'gm';
923
+ const out = [];
924
+ // 1) 函数调用样式:fnName( handle )
925
+ try {
926
+ const withHandle = raw.replace(/\\\(/, `\\(\\s*${escapedHandle}\\s*`);
927
+ const patternStr = withHandle.endsWith(')') || /\\\)\s*$/.test(withHandle) ? withHandle : `${withHandle}\\)`;
928
+ out.push(new RegExp(patternStr, fFlags));
929
+ } catch (_) {}
930
+ // 2) 句柄方法样式:handle.fnName( ... )
931
+ try {
932
+ const fnMatch = raw.match(/([A-Za-z_][A-Za-z0-9_.$]*)\s*(?:\\\(|\()/);
933
+ if (fnMatch) {
934
+ const fn = escapeRegExp(fnMatch[1]);
935
+ out.push(new RegExp(`${escapedHandle}\\s*\\.\\s*${fn}\\s*\\(`, fFlags));
936
+ }
937
+ } catch (_) {}
938
+ return out;
939
+ };
940
+
775
941
  while ((match = regex.exec(content)) !== null) {
776
942
  // 若匹配位置在注释中,跳过
777
943
  if (this.isIndexInRanges(match.index, commentRanges)) {
@@ -783,6 +949,32 @@ export class CodeReviewer {
783
949
  skippedByDirectives++;
784
950
  continue;
785
951
  }
952
+ // 当规则的 requiresAbsent 为“函数调用样式”时:逐匹配校验是否存在针对同一句柄的清理
953
+ if (isPairable && requiresAbsent && requiresAbsent.length > 0) {
954
+ const handle = findAssignedHandleBefore(content, match.index);
955
+ let cleaned = false;
956
+ if (handle) {
957
+ for (const rxStr of requiresAbsent) {
958
+ const pairedRegexes = buildPairedCleanupRegexes(rxStr, handle, rule.flags);
959
+ if (!pairedRegexes || pairedRegexes.length === 0) continue;
960
+ for (const pairedRx of pairedRegexes) {
961
+ let cm;
962
+ while ((cm = pairedRx.exec(content)) !== null) {
963
+ if (isEffectiveIndex(cm.index)) { cleaned = true; break; }
964
+ }
965
+ if (cleaned) break;
966
+ }
967
+ if (cleaned) break;
968
+ }
969
+ } else {
970
+ // 未保存返回句柄则视为未清理风险(无法对应清理目标)
971
+ cleaned = false;
972
+ }
973
+ if (cleaned) {
974
+ continue; // 已对同一句柄执行清理,不报警
975
+ }
976
+ }
977
+
786
978
  const lineNumber = this.getLineNumber(content, match.index);
787
979
  const snippetText = (match[0] || '').substring(0, BATCH_CONSTANTS.MAX_SNIPPET_LENGTH);
788
980
 
@@ -804,15 +996,15 @@ export class CodeReviewer {
804
996
  });
805
997
  }
806
998
  } catch (error) {
807
- logger.warn(`规则 ${rule.id} 执行失败:`, error.message);
999
+ logger.warn(t(this.config, 'rule_exec_failed_warn', { id: rule.id, error: error?.message || String(error) }));
808
1000
  }
809
1001
  }
810
1002
 
811
1003
  if (skippedByComments > 0) {
812
- logger.debug(`注释代码已跳过审查(${skippedByComments}条匹配)`);
1004
+ logger.debug(t(this.config, 'skip_by_comments_dbg', { count: skippedByComments }));
813
1005
  }
814
1006
  if (skippedByDirectives > 0) {
815
- logger.debug(`指令禁用范围已跳过审查(${skippedByDirectives}条匹配)`);
1007
+ logger.debug(t(this.config, 'skip_by_directives_dbg', { count: skippedByDirectives }));
816
1008
  }
817
1009
 
818
1010
  return issues;
@@ -876,7 +1068,7 @@ export class CodeReviewer {
876
1068
  if (!shouldInclude) {
877
1069
  return {
878
1070
  reviewable: false,
879
- reason: `文件扩展名 ${ext} 不在支持列表中`,
1071
+ reason: t(this.config, 'file_ext_not_supported_reason', { ext }),
880
1072
  matchedPattern: null
881
1073
  };
882
1074
  }
@@ -897,7 +1089,7 @@ export class CodeReviewer {
897
1089
  if (originalPattern === normalized || originalPattern === relativePath || originalPattern === basename) {
898
1090
  return {
899
1091
  reviewable: false,
900
- reason: '匹配精确模式',
1092
+ reason: t(this.config, 'file_reason_exact_match'),
901
1093
  matchedPattern: originalPattern
902
1094
  };
903
1095
  }
@@ -911,7 +1103,7 @@ export class CodeReviewer {
911
1103
  if (normalizedMatch || relativeMatch) {
912
1104
  return {
913
1105
  reviewable: false,
914
- reason: '匹配正则表达式',
1106
+ reason: t(this.config, 'file_reason_regex_match'),
915
1107
  matchedPattern: originalPattern
916
1108
  };
917
1109
  }
@@ -925,7 +1117,7 @@ export class CodeReviewer {
925
1117
  if (this.matchPattern(normalized, patternStr) || this.matchPattern(relativePath, patternStr)) {
926
1118
  return {
927
1119
  reviewable: false,
928
- reason: '匹配glob模式',
1120
+ reason: t(this.config, 'file_reason_glob_match'),
929
1121
  matchedPattern: originalPattern
930
1122
  };
931
1123
  }
@@ -1014,7 +1206,7 @@ export class CodeReviewer {
1014
1206
 
1015
1207
  // 检查文件大小限制
1016
1208
  if (content.length > (this.config.ai?.maxFileSizeKB || DEFAULT_CONFIG.MAX_FILE_SIZE_KB) * 1024) {
1017
- logger.info(`跳过AI分析大文件: ${filePath}`);
1209
+ logger.info(t(this.config, 'skip_ai_large_file', { file: filePath }));
1018
1210
  return false;
1019
1211
  }
1020
1212