feat: 编辑器波浪线自动标记 + SQLFluff 默认方言改 oracle + AI 审查行号修正
- 波浪线标记:打开/编辑/保存自动静态分析,新增 markers.enabled 配置与 onStartupFinished 激活,带防抖与版本竞态保护 - SQLFluff:默认方言 sql->oracle;PRS 解析错误改用 i18n 三语友好提示(含当前方言与配置指引)并提升为 error - AI 审查:mergeResults 统一 aiFindings 行号为 0 基,修复整文件审查行号偏移 1 行
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user