feat: 编辑器波浪线自动标记 + SQLFluff 默认方言改 oracle + AI 审查行号修正

- 波浪线标记:打开/编辑/保存自动静态分析,新增 markers.enabled 配置与 onStartupFinished 激活,带防抖与版本竞态保护
- SQLFluff:默认方言 sql->oracle;PRS 解析错误改用 i18n 三语友好提示(含当前方言与配置指引)并提升为 error
- AI 审查:mergeResults 统一 aiFindings 行号为 0 基,修复整文件审查行号偏移 1 行
This commit is contained in:
范智鹏
2026-08-04 22:10:41 +08:00
parent 247f19fd44
commit 50855dae67
9 changed files with 232 additions and 20 deletions
+7 -1
View File
@@ -12,6 +12,7 @@ import { exportTemplate } from '../rules/export-service';
import { extractMethodScope } from '../scope/method-extractor';
import { ReviewStatusCache } from '../scope/status-cache';
import { MethodCodeLensProvider } from '../views/codeLensProvider';
import { DiagnosticMarkers, isMarkersEnabled } from '../diagnostics/diagnosticMarkers';
import type { CustomRule } from '../types';
let currentReport: MergedReport | null = null;
@@ -21,6 +22,7 @@ export function registerCommands(
orchestrator: Orchestrator,
codeLensProvider: MethodCodeLensProvider,
statusCache: ReviewStatusCache,
markers: DiagnosticMarkers,
): void {
context.subscriptions.push(
@@ -73,6 +75,10 @@ export function registerCommands(
const panel = ReviewPanel.createOrShow(context.extensionUri);
panel.update(currentReport);
if (isMarkersEnabled()) {
markers.apply(document.uri, currentReport.linterDiagnostics);
}
});
})
);
@@ -151,7 +157,7 @@ export function registerCommands(
translatedDiagnostics: [],
aiFindings: result.findings.map(f => ({
...f,
line: f.line + methodLine - 1,
line: f.line + methodLine,
})),
errors: result.error ? [result.error] : [],
degraded: result.degraded,
+15 -3
View File
@@ -9,7 +9,7 @@ import { t } from '../i18n/messages';
import staticRules from '../rules/static-rules.json';
const DIALECT_MAP: Record<string, string> = {
sql: 'mysql',
sql: 'oracle',
plsql: 'oracle',
};
@@ -53,6 +53,16 @@ function tierToSeverity(tier: string | undefined): Severity {
return 'warning';
}
export function buildPRSMessage(description: string, dialect: string): string {
const match = /Found unparsable section: '([\s\S]*)'/.exec(description);
let fragment = match ? match[1] : description;
fragment = fragment.replace(/\n/g, '\\n');
if (fragment.length > 80) {
fragment = fragment.slice(0, 80) + '...';
}
return t('adapter.sqlfluffPRS', { 0: dialect, 1: fragment });
}
function hasProjectSqlfluffConfig(workspaceRoot: string): boolean {
const candidates = ['.sqlfluff', '.sqlfluff.ini'];
for (const candidate of candidates) {
@@ -135,6 +145,7 @@ export class SqlFluffAdapter implements LinterAdapter {
const cliDialect = explicitDialect && SUPPORTED_DIALECTS.includes(explicitDialect)
? explicitDialect
: undefined;
const effectiveDialect = cliDialect ?? fallbackDialect;
let configPath: string | undefined;
let tempConfigPath: string | undefined;
@@ -156,10 +167,11 @@ export class SqlFluffAdapter implements LinterAdapter {
for (const result of results) {
for (const v of result.violations) {
const isPRS = v.code === 'PRS';
diagnostics.push({
severity: tierToSeverity(tierMap.get(v.code)),
severity: isPRS ? 'error' : tierToSeverity(tierMap.get(v.code)),
ruleId: `sqlfluff:${v.code}`,
message: v.description,
message: isPRS ? buildPRSMessage(v.description, effectiveDialect) : v.description,
range: new vscode.Range(
v.start_line_no - 1,
v.start_line_pos - 1,
+38
View File
@@ -0,0 +1,38 @@
import * as vscode from 'vscode';
import type { LinterDiagnostic } from '../types';
export function isMarkersEnabled(): boolean {
return vscode.workspace.getConfiguration('vscode-code-reviewer').get<boolean>('markers.enabled', true);
}
export function toVscodeDiagnostics(diagnostics: LinterDiagnostic[]): vscode.Diagnostic[] {
return diagnostics.map(d => {
const severity =
d.severity === 'error'
? vscode.DiagnosticSeverity.Error
: d.severity === 'warning'
? vscode.DiagnosticSeverity.Warning
: vscode.DiagnosticSeverity.Information;
return new vscode.Diagnostic(d.range, `[${d.ruleId}] ${d.message}`, severity);
});
}
export class DiagnosticMarkers {
private collection: vscode.DiagnosticCollection;
constructor() {
this.collection = vscode.languages.createDiagnosticCollection('codeReviewer');
}
apply(uri: vscode.Uri, diagnostics: LinterDiagnostic[]): void {
this.collection.set(uri, toVscodeDiagnostics(diagnostics));
}
clear(uri: vscode.Uri): void {
this.collection.delete(uri);
}
dispose(): void {
this.collection.dispose();
}
}
+58 -14
View File
@@ -6,8 +6,46 @@ import { setLanguage, t, type Language } from './i18n/messages';
import { getAIOutputLanguage } from './config';
import { ReviewStatusCache } from './scope/status-cache';
import { MethodCodeLensProvider } from './views/codeLensProvider';
import { DiagnosticMarkers, isMarkersEnabled } from './diagnostics/diagnosticMarkers';
let orchestrator: Orchestrator;
let markers: DiagnosticMarkers;
async function runStaticAndApply(document: vscode.TextDocument): Promise<void> {
const version = document.version;
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '';
const workingDir = workspaceRoot || vscode.Uri.joinPath(document.uri, '..').fsPath;
const result = await orchestrator.runStaticAnalysis(document, workingDir);
const current = vscode.workspace.textDocuments.find(d => d.uri.toString() === document.uri.toString());
if (!current || current.version !== version) { return; }
markers.apply(document.uri, result.diagnostics);
}
const analysisTimers = new Map<string, NodeJS.Timeout>();
function scheduleAnalysis(document: vscode.TextDocument, delay: number): void {
if (document.uri.scheme !== 'file' || !isMarkersEnabled()) { return; }
const key = document.uri.toString();
const existing = analysisTimers.get(key);
if (existing) { clearTimeout(existing); }
const timer = setTimeout(async () => {
analysisTimers.delete(key);
await runStaticAndApply(document);
}, delay);
analysisTimers.set(key, timer);
}
async function analyzeOpenDocuments(): Promise<void> {
const docs = vscode.workspace.textDocuments;
const active = vscode.window.activeTextEditor?.document;
const ordered = [...docs].sort((a, b) => a === active ? -1 : b === active ? 1 : 0);
for (const doc of ordered) {
if (doc.uri.scheme !== 'file' || !isMarkersEnabled()) { continue; }
await runStaticAndApply(doc);
}
}
export function activate(context: vscode.ExtensionContext) {
const lang = getAIOutputLanguage() as Language;
@@ -15,6 +53,8 @@ export function activate(context: vscode.ExtensionContext) {
console.log(t('extension.activated'));
orchestrator = new Orchestrator();
markers = new DiagnosticMarkers();
context.subscriptions.push(markers);
const setupProvider = new SetupViewProvider(context);
context.subscriptions.push(
@@ -24,6 +64,8 @@ export function activate(context: vscode.ExtensionContext) {
const statusCache = new ReviewStatusCache();
const codeLensProvider = new MethodCodeLensProvider(statusCache);
void analyzeOpenDocuments();
context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
{ scheme: 'file' },
@@ -34,27 +76,29 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.workspace.onDidCloseTextDocument((document) => {
statusCache.clearDocument(document.uri);
markers.clear(document.uri);
})
);
registerCommands(context, orchestrator, codeLensProvider, statusCache);
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((document) => {
if (document.uri.scheme !== 'file' || !isMarkersEnabled()) { return; }
void runStaticAndApply(document);
})
);
const debounceTimers = new Map<string, NodeJS.Timeout>();
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((event) => {
markers.clear(event.document.uri);
scheduleAnalysis(event.document, 1000);
})
);
registerCommands(context, orchestrator, codeLensProvider, statusCache, markers);
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument((document) => {
const key = document.uri.toString();
const existing = debounceTimers.get(key);
if (existing) { clearTimeout(existing); }
const timer = setTimeout(() => {
debounceTimers.delete(key);
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '';
const workingDir = workspaceRoot || vscode.Uri.joinPath(document.uri, '..').fsPath;
orchestrator.runStaticAnalysis(document, workingDir);
}, 500);
debounceTimers.set(key, timer);
scheduleAnalysis(document, 500);
})
);
+5
View File
@@ -1068,6 +1068,11 @@ const messages: Record<string, Record<Language, string>> = {
en: 'sqlfluff not installed, run: pip install sqlfluff',
ja: 'sqlfluffがインストールされていません、pip install sqlfluff を実行してください',
},
'adapter.sqlfluffPRS': {
'zh-CN': 'SQL 解析失败(当前方言:{0}),可能是方言不匹配或语法错误。请在设置中配置 sqlfluff.dialect 或添加项目 .sqlfluff 指定正确方言。无法解析片段:{1}',
en: 'Failed to parse SQL (current dialect: {0}). Possible dialect mismatch or syntax error. Configure sqlfluff.dialect in settings or add a project .sqlfluff. Unparsable fragment: {1}',
ja: 'SQLの解析に失敗しました(現在の方言:{0})。方言の不一致または構文エラーの可能性があります。設定で sqlfluff.dialect を構成するか、プロジェクトに .sqlfluff を追加してください。解析不能な断片:{1}',
},
'adapter.invalidApiKey': {
'zh-CN': 'API Key 无效,请重新设置',
en: 'Invalid API Key, please reconfigure',
+4 -1
View File
@@ -77,7 +77,10 @@ export function mergeResults(input: MergeInput): MergedReport {
d => d.range.start.line
);
const aiFindings = sortBySeverityAndLine(input.aiFindings, f => f.line);
const aiFindings = sortBySeverityAndLine(
input.aiFindings.map(f => ({ ...f, line: Math.max(0, f.line - 1) })),
f => f.line
);
const linterCount = linterDiagnostics.length;
const customRuleCount = customRuleDiagnostics.length;