- providers/base.ts 引入 ApiRequestError 与 isRetryableError,按 429/5xx/超时/网络错误/401 分类 - chatWithRetry 升级为最多 3 次指数退避(1s/2s/4s),非重试错误立即上抛;新增 setRetryBaseDelayForTest 钩子 - aiFixEngine/customFixEngine 不再吞错,修复失败原因透传为 ai-error: <原因> - 扩展 ai-empty-response/customFixEngine 测试覆盖重试与分类
722 lines
28 KiB
TypeScript
722 lines
28 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import { isRetryableError } 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) }));
|
|
}
|
|
}
|
|
}
|
|
|
|
const RETRY_MAX_ATTEMPTS = 3;
|
|
const RETRY_BASE_DELAY_MS = 1000;
|
|
|
|
let retryBaseDelayMs = RETRY_BASE_DELAY_MS;
|
|
|
|
export function setRetryBaseDelayForTest(ms: number): void {
|
|
retryBaseDelayMs = ms;
|
|
}
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
export async function chatWithRetry(
|
|
provider: AIProvider,
|
|
systemPrompt: string,
|
|
userPrompt: string,
|
|
options: ChatOptions
|
|
): Promise<string> {
|
|
for (let attempt = 0; attempt <= RETRY_MAX_ATTEMPTS; attempt++) {
|
|
try {
|
|
return await provider.chat(systemPrompt, userPrompt, options);
|
|
} catch (err) {
|
|
if (attempt === RETRY_MAX_ATTEMPTS || !isRetryableError(err)) {
|
|
throw err;
|
|
}
|
|
await sleep(retryBaseDelayMs * 2 ** attempt);
|
|
}
|
|
}
|
|
throw new Error('unreachable');
|
|
}
|
|
|
|
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}`;
|
|
}
|