All files / src/ai engine.ts

10.63% Statements 75/705
80% Branches 8/10
11.11% Functions 2/18
10.63% Lines 75/705

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 7062x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x           2x           2x       2x                                                         2x 20x 20x 20x 2x 2x 18x 18x 20x     18x 18x 18x 20x               20x 2x 2x 20x 20x 20x 20x 20x 20x 20x 20x 3x 2x 2x 1x 1x 20x 2x                                                               2x                                                                                                                                                                     2x             2x             2x 2x                                                                                                                                                                                                                 2x 2x                                                                                                                                                                         2x                     2x                                                                                                                                   2x                                                                                                                                   2x                                                                                                                                   2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x                                                                         2x                                                      
import * as vscode from 'vscode';
import { EmptyContentError } from './providers/base';
import type { AIProvider, ChatOptions } from './providers/base';
import { createProvider } from './factory';
import { getAIProvider, getAIModel, getAIBaseUrl, getAITemperature, getAITimeout, getAIMaxTokens, getAIOutputLanguage, getApiKey } from '../config';
import type { LinterDiagnostic, CustomRule } from '../types';
import type {
  AIEngineResult,
  CustomRuleResult,
  TranslatedDiagnostic,
  AIFinding,
  MethodFinding,
  MethodReviewResult,
} from './schema';
import type { MethodScope } from '../scope/method-extractor';
import { t, getLanguage } from '../i18n/messages';
 
function buildCustomRulePrompt(rules: CustomRule[]): string {
  return rules.map(r =>
    `- [${r.id}] (${r.severity}) ${r.description}`
  ).join('\n');
}
 
function buildLinterDiagnosticsPrompt(diagnostics: LinterDiagnostic[]): string {
  return diagnostics.map(d =>
    `- [${d.ruleId}] L${d.range.start.line + 1}: ${d.message}`
  ).join('\n');
}
 
function addLineNumbers(code: string): string {
  return code.split('\n').map((line, i) => `${String(i + 1).padStart(4, ' ')}| ${line}`).join('\n');
}
 
function repairJsonEscapes(str: string): string {
  let inString = false;
  let out = '';
  for (let i = 0; i < str.length; i++) {
    const ch = str[i];
    if (ch === '\\') {
      out += ch;
      if (i + 1 < str.length) { out += str[++i]; }
    } else if (ch === '"') {
      if (!inString) {
        inString = true;
        out += ch;
      } else {
        let j = i + 1;
        while (j < str.length && str[j] === ' ') { j++; }
        if (j < str.length && ':,\]}'.includes(str[j])) {
          inString = false;
          out += ch;
        } else {
          out += '\\"';
        }
      }
    } else {
      out += ch;
    }
  }
  return out;
}
 
export function parseJsonResponse(raw: string): object {
  const trimmed = raw.trim();
  if (trimmed === '') {
    throw new Error(t('engine.emptyResponse'));
  }
  const start = trimmed.indexOf('{');
  const end = trimmed.lastIndexOf('}');
  if (start === -1 || end === -1) {
    throw new Error(t('engine.jsonNotFound', { 0: trimmed.slice(0, 200) }));
  }
  let jsonStr = trimmed.substring(start, end + 1);
  try {
    return JSON.parse(jsonStr);
  } catch {
    jsonStr = repairJsonEscapes(jsonStr);
    try {
      return JSON.parse(jsonStr);
    } catch {
      throw new Error(t('engine.jsonParseFail', { 0: jsonStr.slice(0, 200) }));
    }
  }
}
 
export async function chatWithRetry(
  provider: AIProvider,
  systemPrompt: string,
  userPrompt: string,
  options: ChatOptions
): Promise<string> {
  try {
    return await provider.chat(systemPrompt, userPrompt, options);
  } catch (err) {
    if (err instanceof EmptyContentError) {
      return provider.chat(systemPrompt, userPrompt, options);
    }
    throw err;
  }
}
 
function buildCustomRuleSystemPrompt(): string {
  const lang = getLanguage();
  if (lang === 'ja') {
    return `あなたはコードルールレビュアーです。以下のカスタムルールに違反しているかどうかのみを評価してください。
意味を理解し、テキストの一致ではなく判断してください。
JSONのみを出力、形式:
{ "customRuleResults": [{ "ruleId": "ルールID", "line": 行番号, "severity": "error|warning|info", "message": "違反の説明", "suggestion": "具体的な修正提案", "fix": { "originalText": "置換対象のコード原文(コードコンテキスト内に完全一致すること、行番号プレフィックスなし)", "newText": "修正後のコード片" } }] }
各違反に対して必ず実行可能な "suggestion" を含め、可能な場合は適用可能な "fix" も提供してください。
ルールに違反していない場合は空の配列を返してください。

出力言語:ja`;
  }
  if (lang === 'en') {
    return `You are a code rule reviewer. Only evaluate whether the following custom rules are violated.
Understand semantics, not text matching.
Output JSON only, format:
{ "customRuleResults": [{ "ruleId": "rule id", "line": line number, "severity": "error|warning|info", "message": "violation description", "suggestion": "concrete fix suggestion", "fix": { "originalText": "the exact code snippet to replace (must exist verbatim in the code context, without the line-number prefix)", "newText": "the fixed code snippet" } }] }
Always include a concrete actionable "suggestion" for each violation, and provide an applicable "fix" snippet when possible.
If no rules are violated, return an empty array.

Output language: en`;
  }
  return `你是代码规则审查员,只评估以下自定义规则是否被违反。
理解语义而非文本匹配。
仅输出 JSON,格式:
{ "customRuleResults": [{ "ruleId": "规则ID", "line": 行号, "severity": "error|warning|info", "message": "触发描述", "suggestion": "具体的修复建议", "fix": { "originalText": "待替换的代码原文(必须在代码上下文中逐字存在,不含行号前缀)", "newText": "修复后的代码片段" } }] }
每条违规都必须给出可执行的 "suggestion" 修复建议,并尽量提供可应用的 "fix" 修复片段。
如果没有违反任何规则,返回空数组。

输出语言:zh-CN`;
}
 
function buildDeepReviewSystemPrompt(): string {
  const lang = getLanguage();
  if (lang === 'ja') {
    return `あなたはシニアコードレビュー専門家です。2つのタスクを実行してください:
1. 英語の静的解析結果を出力言語に翻訳し、修正提案を追加する
2. コードを詳細にレビューし、静的解析でカバーされていない問題を発見する
重点分野:セキュリティ脆弱性、ロジックエラー、パフォーマンス問題、設計欠陥
静的解析ですでに報告された問題を重複しないでください。

translatedDiagnosticsの要件:
- 下記の「静的解析結果」に列挙された各診断に対して1件ずつ翻訳を返してください。件数と順序を一致させ、欠落させないでください
- "originalRuleId" はリスト内のルールID(eslint: 等のプレフィックスを含む)をそのままコピーし、書き換えないでください
- "translatedMessage" と "translatedSuggestion" は両方必須で、空にしないでください
- "translatedSuggestion" は具体的で実行可能な修正提案(例:この書き方に置き換える)を示してください
- 可能な場合は各診断に適用可能な "fix" を提供してください

findingsの要件:
- 可能な場合は各発見に適用可能な "fix" を提供してください
- "fix.originalText" は提供されたコード内に逐語的に存在すること(行番号プレフィックスなし)

JSONのみを出力。文字列内の二重引用符は \\" でエスケープしてください。
形式:
{
  "translatedDiagnostics": [{ "originalRuleId": "元のID", "translatedMessage": "翻訳メッセージ", "translatedSuggestion": "提案", "fix": { "originalText": "置換対象のコード原文(行番号プレフィックスなし)", "newText": "修正後のコード片" } }],
  "findings": [{ "ruleId": "kebab-case", "severity": "error|warning|info", "category": "bug|performance|security|style|design", "title": "タイトル", "description": "説明", "suggestion": "提案", "line": 行番号, "fix": { "originalText": "置換対象のコード原文(行番号プレフィックスなし)", "newText": "修正後のコード片" } }]
}

出力言語:ja`;
  }
  if (lang === 'en') {
    return `You are a senior code review expert. Complete two tasks:
1. Translate English static analysis results into the output language and add fix suggestions
2. Deeply review the code to find issues not covered by static analysis
Focus on: security vulnerabilities, logic errors, performance issues, design flaws
Do not duplicate issues already reported by static analysis.

translatedDiagnostics requirements:
- Return exactly one translation for every diagnostic listed in "Static Analysis Results", same count and order, do not omit any
- "originalRuleId" must be copied verbatim from the listed rule IDs (keep prefixes like eslint:), do not rewrite
- "translatedMessage" and "translatedSuggestion" are both required and must not be empty
- "translatedSuggestion" should be a concrete actionable fix suggestion (e.g. what to replace it with), not just a replacement snippet
- Provide an applicable "fix" snippet for each diagnostic when possible

findings requirements:
- Provide an applicable "fix" snippet for each finding when possible
- "fix.originalText" must exist verbatim in the provided code (without the line-number prefix)

Output JSON only. Double quotes in strings must be escaped with \\".
Format:
{
  "translatedDiagnostics": [{ "originalRuleId": "original id", "translatedMessage": "translated message", "translatedSuggestion": "suggestion", "fix": { "originalText": "exact snippet to replace (without line-number prefix)", "newText": "fixed snippet" } }],
  "findings": [{ "ruleId": "kebab-case", "severity": "error|warning|info", "category": "bug|performance|security|style|design", "title": "title", "description": "description", "suggestion": "suggestion", "line": line number, "fix": { "originalText": "exact snippet to replace (without line-number prefix)", "newText": "fixed snippet" } }]
}

Output language: en`;
  }
  return `你是资深代码审查专家,完成两个任务:
1. 将英文静态分析结果翻译为输出语言,并补充修复建议
2. 深度审查代码,发现静态分析未覆盖的问题
重点:安全漏洞、逻辑错误、性能问题、设计缺陷
不要重复静态分析已报告的问题。

translatedDiagnostics 要求:
- 必须为"静态分析结果"中列出的每一条诊断都返回一条翻译,条数与顺序一致,不得遗漏
- "originalRuleId" 必须原样复制列表中的规则 ID(保留 eslint: 等前缀),不得改写
- "translatedMessage" 与 "translatedSuggestion" 均为必填字段,不得为空
- "translatedSuggestion" 给出具体可执行的修复建议(如应替换成什么写法),不要只给替换片段
- 尽量为每条诊断提供 "fix" 可应用修复片段

findings 要求:
- 尽量为每条发现提供 "fix" 可应用修复片段
- "fix.originalText" 必须在提供的代码中逐字存在(不含行号前缀)

仅输出 JSON,字符串中的双引号必须用 \\" 转义。
格式:
{
  "translatedDiagnostics": [{ "originalRuleId": "原始ID", "translatedMessage": "翻译", "translatedSuggestion": "建议", "fix": { "originalText": "待替换的代码原文(不含行号前缀)", "newText": "修复后的代码片段" } }],
  "findings": [{ "ruleId": "kebab-case", "severity": "error|warning|info", "category": "bug|performance|security|style|design", "title": "标题", "description": "描述", "suggestion": "建议", "line": 行号, "fix": { "originalText": "待替换的代码原文(不含行号前缀)", "newText": "修复后的代码片段" } }]
}

输出语言:zh-CN`;
}
 
function buildUserPromptCustomRules(customRules: CustomRule[], numberedCode: string): string {
  const lang = getLanguage();
  const label = lang === 'ja' ? 'カスタムルール' : lang === 'en' ? 'Custom Rules' : '自定义规则';
  const codeLabel = lang === 'ja' ? 'コード(行番号付き)' : lang === 'en' ? 'Code (with line numbers)' : '代码(带行号)';
  return `## ${label}\n${buildCustomRulePrompt(customRules)}\n\n## ${codeLabel}\n${numberedCode}`;
}
 
function buildUserPromptDeepReview(numberedCode: string, staticDiagnostics: LinterDiagnostic[]): string {
  const lang = getLanguage();
  const codeLabel = lang === 'ja' ? 'コード(行番号付き)' : lang === 'en' ? 'Code (with line numbers)' : '代码(带行号)';
  const resultLabel = lang === 'ja' ? '静的解析結果' : lang === 'en' ? 'Static Analysis Results' : '静态分析结果(英文)';
  return `## ${codeLabel}\n${numberedCode}\n\n## ${resultLabel}\n${buildLinterDiagnosticsPrompt(staticDiagnostics)}`;
}
 
export async function runAIReview(
  context: vscode.ExtensionContext,
  code: string,
  staticDiagnostics: LinterDiagnostic[],
  customRules: CustomRule[]
): Promise<AIEngineResult> {
  const apiKey = await getApiKey(context);

  if (!apiKey) {
    return {
      customRuleResults: [],
      translatedDiagnostics: [],
      findings: [],
      degraded: true,
      error: t('adapter.noApiKey'),
    };
  }

  const providerId = getAIProvider();
  const baseUrl = getAIBaseUrl();

  let provider: AIProvider;
  try {
    provider = createProvider(providerId, apiKey, baseUrl, context.extensionUri);
  } catch (err) {
    return {
      customRuleResults: [],
      translatedDiagnostics: [],
      findings: [],
      degraded: true,
      error: t('adapter.createProviderFail', { 0: err instanceof Error ? err.message : String(err) }),
    };
  }

  const options = {
    model: getAIModel(),
    temperature: getAITemperature(),
    maxTokens: getAIMaxTokens(),
    timeoutMs: getAITimeout() * 1000,
  };

  const numberedCode = addLineNumbers(code);

  const requestA =
    customRules.length > 0
      ? chatWithRetry(
          provider,
          buildCustomRuleSystemPrompt(),
          buildUserPromptCustomRules(customRules, numberedCode),
          options
        )
      : Promise.resolve('{}');

  const requestB = chatWithRetry(
    provider,
    buildDeepReviewSystemPrompt(),
    buildUserPromptDeepReview(numberedCode, staticDiagnostics),
    options
  );

  const [resultA, resultB] = await Promise.allSettled([requestA, requestB]);

  const errors: string[] = [];

  let customRuleResults: CustomRuleResult[] = [];
  if (resultA.status === 'fulfilled') {
    try {
      const parsed = parseJsonResponse(resultA.value) as { customRuleResults?: CustomRuleResult[] };
      customRuleResults = (parsed.customRuleResults ?? []).map(r => ({
        ...r,
        ruleId: `custom:${r.ruleId}`,
      }));
    } catch (e) {
      errors.push(t('adapter.customRuleParseFail', { 0: e instanceof Error ? e.message : String(e) }));
    }
  } else {
    errors.push(t('adapter.customRuleRequestFail', { 0: resultA.reason }));
  }

  let translatedDiagnostics: TranslatedDiagnostic[] = [];
  let findings: AIFinding[] = [];
  if (resultB.status === 'fulfilled') {
    try {
      const parsed = parseJsonResponse(resultB.value) as {
        translatedDiagnostics?: TranslatedDiagnostic[];
        findings?: AIFinding[];
      };
      translatedDiagnostics = parsed.translatedDiagnostics ?? [];
      findings = parsed.findings ?? [];
    } catch (e) {
      errors.push(t('adapter.aiReviewParseFail', { 0: e instanceof Error ? e.message : String(e) }));
    }
  } else {
    errors.push(t('adapter.aiReviewRequestFail', { 0: resultB.reason }));
  }

  const degraded = errors.length > 0;
  return {
    customRuleResults,
    translatedDiagnostics,
    findings,
    degraded,
    error: errors.join('; '),
  };
}
 
export async function runMethodReview(
  context: vscode.ExtensionContext,
  scope: MethodScope,
  customRules: CustomRule[]
): Promise<MethodReviewResult> {
  const apiKey = await getApiKey(context);
  if (!apiKey) {
    return {
      customRuleResults: [],
      findings: [],
      degraded: true,
      error: t('adapter.noApiKey'),
    };
  }

  const providerId = getAIProvider();
  const baseUrl = getAIBaseUrl();

  let provider: AIProvider;
  try {
    provider = createProvider(providerId, apiKey, baseUrl, context.extensionUri);
  } catch (err) {
    return {
      customRuleResults: [],
      findings: [],
      degraded: true,
      error: t('adapter.createProviderFail', { 0: err instanceof Error ? err.message : String(err) }),
    };
  }

  const options = {
    model: getAIModel(),
    temperature: getAITemperature(),
    maxTokens: getAIMaxTokens(),
    timeoutMs: getAITimeout() * 1000,
  };

  const numberedCode = addLineNumbers(scope.code);
  const hasRules = customRules.length > 0;

  let response: string;
  try {
    response = await chatWithRetry(
      provider,
      buildMethodReviewSystemPrompt(hasRules),
      buildMethodUserPrompt(scope, numberedCode, customRules),
      options
    );
  } catch (err) {
    return {
      customRuleResults: [],
      findings: [],
      degraded: true,
      error: t('adapter.aiReviewRequestFail', { 0: err instanceof Error ? err.message : String(err) }),
    };
  }

  const errors: string[] = [];
  let customRuleResults: CustomRuleResult[] = [];
  let findings: MethodFinding[] = [];

  try {
    const parsed = parseJsonResponse(response) as {
      customRuleResults?: CustomRuleResult[];
      findings?: MethodFinding[];
    };
    customRuleResults = (parsed.customRuleResults ?? []).map(r => {
      const id = String(r.ruleId ?? '');
      return { ...r, ruleId: id.startsWith('custom:') ? id : `custom:${id}` };
    });
    findings = (parsed.findings ?? []).map(f => {
      const id = String(f.ruleId ?? '');
      return { ...f, ruleId: id.startsWith('method:') ? id : `method:${id}` };
    });
  } catch (e) {
    errors.push(t('adapter.aiReviewParseFail', { 0: e instanceof Error ? e.message : String(e) }));
  }

  return {
    customRuleResults,
    findings,
    degraded: errors.length > 0,
    error: errors.join('; ') || undefined,
  };
}
 
function buildMethodReviewSystemPrompt(hasRules: boolean): string {
  const lang = getLanguage();
  if (lang === 'ja') {
    return buildMethodSystemPromptJa(hasRules);
  }
  if (lang === 'en') {
    return buildMethodSystemPromptEn(hasRules);
  }
  return buildMethodSystemPromptZh(hasRules);
}
 
function buildMethodSystemPromptEn(hasRules: boolean): string {
  const ruleSection = hasRules
    ? `## Task 1: Custom Rule Matching
Evaluate whether the method violates any of the provided custom rules.
Understand semantics, not text matching.
Report violations in "customRuleResults".\n\n`
    : '';
  const ruleOutput = hasRules
    ? `  "customRuleResults": [
    {
      "ruleId": "original rule id",
      "line": line_number,
      "severity": "error|warning|info",
      "message": "violation description",
      "suggestion": "concrete fix suggestion",
      "fix": { "originalText": "exact snippet to replace (without line-number prefix)", "newText": "fixed snippet" }
    }
  ],\n`
    : '';
  return `You are a senior code review expert reviewing a single method.
There is no static analysis before you — you handle rule matching AND deep review.

${ruleSection}## Review Strategy: Path Enumeration
- Walk through every if/else/switch branch, note coverage and gaps
- Enumerate boundary values for every parameter (null, empty collection, extreme values, wrong types)
- Check every throw/catch path for proper fallback strategy
- Trace the method's role in its call chain

## Required Dimensions (do not skip any)
A. Correctness: branch coverage, boundary conditions, exception path completeness
B. Security: input validation, injection risk, permission check, sensitive data leakage
C. Design: single responsibility, parameter design, return value contract, call chain adaptation
D. Convention: naming, cyclomatic complexity, magic numbers, missing comments
E. Performance: time/space complexity, resource leaks, unnecessary computation
F. Testability: side effect isolation, dependency mockability, deterministic output

## Call Chain Analysis
- Check whether callers' arguments match this method's expectations
- Check whether this method's return value is correctly handled by callers
- Check whether exceptions are caught or declared by callers

Provide an applicable "fix" snippet for each finding when possible.
"fix.originalText" must exist verbatim in the provided method code (without the line-number prefix).

Output JSON only. Double quotes in strings must be escaped with \\".
Format:
{
${ruleOutput}  "findings": [
    {
      "ruleId": "method-boundary-null",
      "severity": "error|warning|info",
      "category": "correctness|security|design|convention|performance|testability",
      "title": "issue title",
      "description": "detailed description",
      "suggestion": "fix suggestion",
      "line": line_number,
      "path": "trigger path description, e.g. if(order==null) -> NPE on .getId()",
      "fix": { "originalText": "exact snippet to replace (without line-number prefix)", "newText": "fixed snippet" }
    }
  ]
}
If no issues found, return empty arrays.

Output language: en`;
}
 
function buildMethodSystemPromptZh(hasRules: boolean): string {
  const ruleSection = hasRules
    ? `## 任务一:自定义规则匹配
评估方法是否违反了提供的自定义规则。
理解语义,而非文本匹配。
在 "customRuleResults" 中报告违规。\n\n`
    : '';
  const ruleOutput = hasRules
    ? `  "customRuleResults": [
    {
      "ruleId": "原始规则 ID",
      "line": 行号,
      "severity": "error|warning|info",
      "message": "违规描述",
      "suggestion": "具体的修复建议",
      "fix": { "originalText": "待替换的代码原文(不含行号前缀)", "newText": "修复后的代码片段" }
    }
  ],\n`
    : '';
  return `你是资深代码审查专家,正在审查单个方法。
没有静态分析的前置过滤——你同时负责规则匹配和深度审查。

${ruleSection}## 审查策略:逐路径枚举
- 遍历每个 if/else/switch 分支,标注覆盖与遗漏
- 枚举每个入参的边界值(null、空集合、极值、错误类型)
- 检查每个 throw/catch 路径的降级策略
- 追踪方法在调用链中的角色

## 必须覆盖的维度(不可跳过)
A. 正确性:分支覆盖、边界条件、异常路径完整性
B. 安全性:输入校验、注入风险、权限检查、敏感信息泄露
C. 设计:职责单一性、参数设计合理性、返回值契约、调用链适配
D. 规范:命名、圈复杂度、魔法数字、注释缺失
E. 性能:时间/空间复杂度、资源泄漏、不必要的计算
F. 可测试性:副作用隔离、依赖可 Mock 性、确定性输出

## 调用链分析
- 检查调用者传入的参数是否符合本方法预期
- 检查本方法的返回值是否被调用者正确处理
- 检查异常是否被调用者捕获或声明

尽可能为每条发现提供可应用的 "fix" 修复片段。
"fix.originalText" 必须在提供的方法代码中逐字存在(不含行号前缀)。

输出 JSON,字符串中的双引号必须用 \\" 转义。
格式:
{
${ruleOutput}  "findings": [
    {
      "ruleId": "method-boundary-null",
      "severity": "error|warning|info",
      "category": "correctness|security|design|convention|performance|testability",
      "title": "问题标题",
      "description": "详细描述",
      "suggestion": "修复建议",
      "line": 行号,
      "path": "触发路径描述,如 if(order==null) -> NPE on .getId()",
      "fix": { "originalText": "待替换的代码原文(不含行号前缀)", "newText": "修复后的代码片段" }
    }
  ]
}
如果未发现问题,返回空数组。

输出语言:zh-CN`;
}
 
function buildMethodSystemPromptJa(hasRules: boolean): string {
  const ruleSection = hasRules
    ? `## タスク1:カスタムルールマッチング
提供されたカスタムルールの違反があるか評価してください。
意味を理解し、テキストの一致ではなく判断してください。
違反を "customRuleResults" で報告してください。\n\n`
    : '';
  const ruleOutput = hasRules
    ? `  "customRuleResults": [
    {
      "ruleId": "元のルールID",
      "line": 行番号,
      "severity": "error|warning|info",
      "message": "違反の説明",
      "suggestion": "具体的な修正提案",
      "fix": { "originalText": "置換対象のコード原文(行番号プレフィックスなし)", "newText": "修正後のコード片" }
    }
  ],\n`
    : '';
  return `あなたはシニアコードレビュー専門家です。単一のメソッドをレビューしています。
事前の静的解析はありません——あなたがルールマッチングと詳細レビューの両方を担当します。

${ruleSection}## レビュー戦略:パス列挙
- すべての if/else/switch 分岐を辿り、カバレッジと漏れを確認
- すべての引数の境界値(null、空コレクション、極値、誤った型)を列挙
- すべての throw/catch パスのフォールバック戦略を確認
- コールチェーンにおけるメソッドの役割を追跡

## 必須カバレッジ(スキップ不可)
A. 正しさ:分岐カバレッジ、境界条件、例外パスの完全性
B. セキュリティ:入力検証、インジェクションリスク、権限チェック、機密情報漏洩
C. 設計:単一責任、パラメータ設計、戻り値契約、コールチェーン適合
D. 規約:命名、循環的複雑度、マジックナンバー、コメント欠落
E. パフォーマンス:時間/空間複雑度、リソースリーク、不要な計算
F. テスタビリティ:副作用の分離、依存のモック化容易性、決定的出力

## コールチェーン分析
- 呼び出し元の引数がこのメソッドの期待と一致しているか確認
- このメソッドの戻り値が呼び出し元で正しく処理されているか確認
- 例外が呼び出し元でキャッチまたは宣言されているか確認

可能な場合は各発見に適用可能な "fix" を提供してください。
"fix.originalText" は提供されたメソッドコード内に逐語的に存在すること(行番号プレフィックスなし)。

JSONのみを出力。文字列内の二重引用符は \\" でエスケープしてください。
形式:
{
${ruleOutput}  "findings": [
    {
      "ruleId": "method-boundary-null",
      "severity": "error|warning|info",
      "category": "correctness|security|design|convention|performance|testability",
      "title": "問題のタイトル",
      "description": "詳細な説明",
      "suggestion": "修正提案",
      "line": 行番号,
      "path": "トリガーパス説明、例: if(order==null) -> .getId() で NPE",
      "fix": { "originalText": "置換対象のコード原文(行番号プレフィックスなし)", "newText": "修正後のコード片" }
    }
  ]
}
問題がない場合は空配列を返してください。

出力言語:ja`;
}
 
interface MethodPromptLabels {
  signature: string;
  code: string;
  rule: string;
  chain: string;
  role: string;
  callers: string;
  callees: string;
  none: string;
}
 
function getMethodPromptLabels(lang: string): MethodPromptLabels {
  if (lang === 'ja') {
    return {
      signature: 'メソッド署名',
      code: 'メソッドコード(行番号付き)',
      rule: 'マッチングするカスタムルール',
      chain: 'コールチェーンコンテキスト',
      role: '業務フローでの役割',
      callers: '呼び出し元',
      callees: '呼び出し先',
      none: '(なし)',
    };
  }
  if (lang === 'en') {
    return {
      signature: 'Method Signature',
      code: 'Method Code (with line numbers)',
      rule: 'Custom Rules to Match',
      chain: 'Call Chain Context',
      role: 'Role in Business Flow',
      callers: 'Callers',
      callees: 'Callees',
      none: '(none)',
    };
  }
  return {
    signature: '方法签名',
    code: '方法代码(带行号)',
    rule: '需匹配的自定义规则',
    chain: '调用链上下文',
    role: '业务流中的角色',
    callers: '调用者',
    callees: '被调用者',
    none: '(无)',
  };
}
 
function buildMethodUserPrompt(
  scope: MethodScope,
  numberedCode: string,
  customRules: CustomRule[]
): string {
  const labels = getMethodPromptLabels(getLanguage());

  let ruleBlock = '';
  if (customRules.length > 0) {
    const ruleLines = customRules
      .map((r, i) => `${i + 1}. [${r.id}] (${r.severity}) ${r.description}\n   ${r.message}`)
      .join('\n');
    ruleBlock = `\n## ${labels.rule}\n${ruleLines}\n`;
  }

  return `## ${labels.signature}
${scope.signature}

## ${labels.code}
${numberedCode}
${ruleBlock}
## ${labels.chain}
${labels.role}: ${scope.role}
${labels.callers}: ${scope.callers.length > 0 ? scope.callers.join(', ') : labels.none}
${labels.callees}: ${scope.callees.length > 0 ? scope.callees.join(', ') : labels.none}`;
}