feat: 方法级代码审查 + 模板导入/预览增强 + SQLFluff 方言 + AI 空响应报错修复

- 方法级审查:CodeLens 触发 + 单次 AI 调用(规则匹配 + 6 维度深度审查),新增 method-extractor / status-cache / codeLensProvider
- 模板导入:severity 保留原始值 + 占位 id、去重对照统一 known-rules、重复提示条双语翻译、箭头展开/折叠 UI、520 条静态规则补 zh/ja 翻译
- SQL:sql-lint 重命名 sqlfluff + sqlfluff.dialect 方言可配置 + 默认方言调整
- ESLint:v9 flat config 接线修复(overrideConfigFile)+ legacy 迁移提示
- AI:空响应 EmptyContentError + 重试一次 + max_tokens 截断专用报错
- JSP:整文件检查走 PMD JSP 规则集 + scriptlet 包装解析 + 行号映射
- 诊断按 severity + 行号排序
This commit is contained in:
范智鹏
2026-08-03 22:53:20 +08:00
parent 5848eaa82a
commit 247f19fd44
46 changed files with 6143 additions and 937 deletions
+384 -4
View File
@@ -1,5 +1,6 @@
import * as vscode from 'vscode';
import type { AIProvider } from './providers/base';
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';
@@ -8,7 +9,10 @@ import type {
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 {
@@ -56,8 +60,11 @@ function repairJsonEscapes(str: string): string {
return out;
}
function parseJsonResponse(raw: string): object {
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) {
@@ -76,6 +83,22 @@ function parseJsonResponse(raw: string): object {
}
}
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') {
@@ -214,14 +237,16 @@ export async function runAIReview(
const requestA =
customRules.length > 0
? provider.chat(
? chatWithRetry(
provider,
buildCustomRuleSystemPrompt(),
buildUserPromptCustomRules(customRules, numberedCode),
options
)
: Promise.resolve('{}');
const requestB = provider.chat(
const requestB = chatWithRetry(
provider,
buildDeepReviewSystemPrompt(),
buildUserPromptDeepReview(numberedCode, staticDiagnostics),
options
@@ -272,3 +297,358 @@ export async function runAIReview(
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"
}
],\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
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",
"codeDiff": "optional fix diff",
"line": line_number,
"path": "trigger path description, e.g. if(order==null) -> NPE on .getId()"
}
]
}
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": "违规描述"
}
],\n`
: '';
return `你是资深代码审查专家,正在审查单个方法。
没有静态分析的前置过滤——你同时负责规则匹配和深度审查。
${ruleSection}## 审查策略:逐路径枚举
- 遍历每个 if/else/switch 分支,标注覆盖与遗漏
- 枚举每个入参的边界值(null、空集合、极值、错误类型)
- 检查每个 throw/catch 路径的降级策略
- 追踪方法在调用链中的角色
## 必须覆盖的维度(不可跳过)
A. 正确性:分支覆盖、边界条件、异常路径完整性
B. 安全性:输入校验、注入风险、权限检查、敏感信息泄露
C. 设计:职责单一性、参数设计合理性、返回值契约、调用链适配
D. 规范:命名、圈复杂度、魔法数字、注释缺失
E. 性能:时间/空间复杂度、资源泄漏、不必要的计算
F. 可测试性:副作用隔离、依赖可 Mock 性、确定性输出
## 调用链分析
- 检查调用者传入的参数是否符合本方法预期
- 检查本方法的返回值是否被调用者正确处理
- 检查异常是否被调用者捕获或声明
输出 JSON,字符串中的双引号必须用 \\" 转义。
格式:
{
${ruleOutput} "findings": [
{
"ruleId": "method-boundary-null",
"severity": "error|warning|info",
"category": "correctness|security|design|convention|performance|testability",
"title": "问题标题",
"description": "详细描述",
"suggestion": "修复建议",
"codeDiff": "可选的修复 diff",
"line": 行号,
"path": "触发路径描述,如 if(order==null) -> NPE on .getId()"
}
]
}
如果未发现问题,返回空数组。
输出语言: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": "違反の説明"
}
],\n`
: '';
return `あなたはシニアコードレビュー専門家です。単一のメソッドをレビューしています。
事前の静的解析はありません——あなたがルールマッチングと詳細レビューの両方を担当します。
${ruleSection}## レビュー戦略:パス列挙
- すべての if/else/switch 分岐を辿り、カバレッジと漏れを確認
- すべての引数の境界値(null、空コレクション、極値、誤った型)を列挙
- すべての throw/catch パスのフォールバック戦略を確認
- コールチェーンにおけるメソッドの役割を追跡
## 必須カバレッジ(スキップ不可)
A. 正しさ:分岐カバレッジ、境界条件、例外パスの完全性
B. セキュリティ:入力検証、インジェクションリスク、権限チェック、機密情報漏洩
C. 設計:単一責任、パラメータ設計、戻り値契約、コールチェーン適合
D. 規約:命名、循環的複雑度、マジックナンバー、コメント欠落
E. パフォーマンス:時間/空間複雑度、リソースリーク、不要な計算
F. テスタビリティ:副作用の分離、依存のモック化容易性、決定的出力
## コールチェーン分析
- 呼び出し元の引数がこのメソッドの期待と一致しているか確認
- このメソッドの戻り値が呼び出し元で正しく処理されているか確認
- 例外が呼び出し元でキャッチまたは宣言されているか確認
JSONのみを出力。文字列内の二重引用符は \\" でエスケープしてください。
形式:
{
${ruleOutput} "findings": [
{
"ruleId": "method-boundary-null",
"severity": "error|warning|info",
"category": "correctness|security|design|convention|performance|testability",
"title": "問題のタイトル",
"description": "詳細な説明",
"suggestion": "修正提案",
"codeDiff": "オプションの修正diff",
"line": 行番号,
"path": "トリガーパス説明、例: if(order==null) -> .getId() で NPE"
}
]
}
問題がない場合は空配列を返してください。
出力言語: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}`;
}