feat: implement core code review extension
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import * as vscode from 'vscode';
|
||||
import type { LinterDiagnostic } from '../types';
|
||||
import type { AIProvider } from '../ai/providers/base';
|
||||
|
||||
export type FixCategory = 'naming' | 'style' | 'bug' | 'security' | 'performance';
|
||||
|
||||
export interface FixableDiagnostic {
|
||||
ruleId: string;
|
||||
message: string;
|
||||
line: number;
|
||||
severity: string;
|
||||
codeContext: string;
|
||||
source: 'linter' | 'custom';
|
||||
category: FixCategory;
|
||||
}
|
||||
|
||||
export interface CodeFix {
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
originalText: string;
|
||||
newText: string;
|
||||
matched: boolean;
|
||||
actualRange?: vscode.Range;
|
||||
}
|
||||
|
||||
const FIX_SYSTEM_PROMPT = `你是代码修复专家。根据提供的问题和代码上下文,输出修复后的代码。
|
||||
仅输出 JSON:{ "originalText": "需要替换的原文", "newText": "修复后的新代码" }`;
|
||||
|
||||
function detectCategory(diagnostic: LinterDiagnostic): FixCategory {
|
||||
if (diagnostic.ruleId.includes('naming') || diagnostic.ruleId.includes('Name')) { return 'naming'; }
|
||||
if (diagnostic.ruleId.includes('security') || diagnostic.ruleId.includes('injection') || diagnostic.ruleId.includes('secret')) { return 'security'; }
|
||||
if (diagnostic.ruleId.includes('perf')) { return 'performance'; }
|
||||
return 'style';
|
||||
}
|
||||
|
||||
function getContextRange(document: vscode.TextDocument, line: number, category: FixCategory): { startLine: number; endLine: number } {
|
||||
switch (category) {
|
||||
case 'naming':
|
||||
return {
|
||||
startLine: Math.max(0, line - 2),
|
||||
endLine: Math.min(document.lineCount - 1, line + 2),
|
||||
};
|
||||
case 'style':
|
||||
return {
|
||||
startLine: Math.max(0, line - 5),
|
||||
endLine: Math.min(document.lineCount - 1, line + 5),
|
||||
};
|
||||
case 'bug':
|
||||
case 'security':
|
||||
case 'performance': {
|
||||
const funcRange = findEnclosingFunction(document, line);
|
||||
return {
|
||||
startLine: funcRange?.start.line ?? Math.max(0, line - 10),
|
||||
endLine: funcRange?.end.line ?? Math.min(document.lineCount - 1, line + 10),
|
||||
};
|
||||
}
|
||||
default:
|
||||
return {
|
||||
startLine: Math.max(0, line - 5),
|
||||
endLine: Math.min(document.lineCount - 1, line + 5),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function findEnclosingFunction(document: vscode.TextDocument, line: number): { start: vscode.Position; end: vscode.Position } | null {
|
||||
const text = document.getText();
|
||||
const lines = text.split('\n');
|
||||
let braceDepth = 0;
|
||||
let funcStart = line;
|
||||
let funcEnd = line;
|
||||
|
||||
for (let i = line; i >= 0; i--) {
|
||||
const l = lines[i];
|
||||
braceDepth += (l.match(/\}/g) || []).length;
|
||||
braceDepth -= (l.match(/\{/g) || []).length;
|
||||
const isFunctionLine = /\b(function|def|class|method|public|private|protected|void|int|String|boolean|var|let|const|async)\s/.test(l);
|
||||
if (braceDepth < 0 && isFunctionLine) {
|
||||
funcStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
braceDepth = 0;
|
||||
for (let i = funcStart; i < lines.length; i++) {
|
||||
const l = lines[i];
|
||||
braceDepth += (l.match(/\{/g) || []).length;
|
||||
braceDepth -= (l.match(/\}/g) || []).length;
|
||||
if (braceDepth === 0 && (l.match(/\{/g) || []).length > 0) {
|
||||
funcEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
start: new vscode.Position(funcStart, 0),
|
||||
end: new vscode.Position(funcEnd, lines[funcEnd]?.length ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
function extractLines(document: vscode.TextDocument, startLine: number, endLine: number): string {
|
||||
const lines: string[] = [];
|
||||
for (let i = startLine; i <= endLine; i++) {
|
||||
const lineText = document.lineAt(i).text;
|
||||
lines.push(`${String(i + 1).padStart(4, ' ')}| ${lineText}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function prepareContext(document: vscode.TextDocument, diagnostic: LinterDiagnostic, source: 'linter' | 'custom'): FixableDiagnostic | null {
|
||||
const line = diagnostic.range.start.line;
|
||||
const category = detectCategory(diagnostic);
|
||||
const { startLine, endLine } = getContextRange(document, line, category);
|
||||
const codeContext = extractLines(document, startLine, endLine);
|
||||
|
||||
return {
|
||||
ruleId: diagnostic.ruleId,
|
||||
message: diagnostic.message,
|
||||
line,
|
||||
severity: diagnostic.severity,
|
||||
codeContext,
|
||||
source,
|
||||
category,
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateFix(
|
||||
provider: AIProvider,
|
||||
model: string,
|
||||
temperature: number,
|
||||
timeoutMs: number,
|
||||
diagnostic: FixableDiagnostic
|
||||
): Promise<CodeFix | null> {
|
||||
const userPrompt = `问题: [${diagnostic.ruleId}] ${diagnostic.message}\n代码上下文:\n${diagnostic.codeContext}`;
|
||||
|
||||
try {
|
||||
const response = await provider.chat(FIX_SYSTEM_PROMPT, userPrompt, {
|
||||
model,
|
||||
temperature,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
const trimmed = response.trim();
|
||||
const start = trimmed.indexOf('{');
|
||||
const end = trimmed.lastIndexOf('}');
|
||||
if (start === -1 || end === -1) { return null; }
|
||||
|
||||
const parsed = JSON.parse(trimmed.substring(start, end + 1));
|
||||
return {
|
||||
startLine: diagnostic.line,
|
||||
endLine: diagnostic.line,
|
||||
originalText: parsed.originalText ?? '',
|
||||
newText: parsed.newText ?? '',
|
||||
matched: false,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function matchAndValidate(document: vscode.TextDocument, fix: CodeFix): { matched: boolean; actualRange?: vscode.Range } {
|
||||
const lineContent = document.lineAt(fix.startLine).text;
|
||||
if (lineContent === fix.originalText.split('\n')[0]) {
|
||||
const range = new vscode.Range(fix.startLine, 0, fix.endLine, document.lineAt(fix.endLine).text.length);
|
||||
if (document.getText(range) === fix.originalText) {
|
||||
return { matched: true, actualRange: range };
|
||||
}
|
||||
}
|
||||
|
||||
const index = document.getText().indexOf(fix.originalText);
|
||||
if (index !== -1) {
|
||||
return {
|
||||
matched: true,
|
||||
actualRange: new vscode.Range(
|
||||
document.positionAt(index),
|
||||
document.positionAt(index + fix.originalText.length)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return { matched: false };
|
||||
}
|
||||
|
||||
export async function applySingleFix(editor: vscode.TextEditor, fix: CodeFix): Promise<boolean> {
|
||||
if (!fix.actualRange || !fix.matched) { return false; }
|
||||
return editor.edit(editBuilder => {
|
||||
editBuilder.replace(fix.actualRange!, fix.newText);
|
||||
});
|
||||
}
|
||||
|
||||
const snapshotStack: Map<string, string[]> = new Map();
|
||||
|
||||
export function saveSnapshot(document: vscode.TextDocument): void {
|
||||
const filePath = document.uri.fsPath;
|
||||
if (!snapshotStack.has(filePath)) { snapshotStack.set(filePath, []); }
|
||||
snapshotStack.get(filePath)!.push(document.getText());
|
||||
}
|
||||
|
||||
export async function undoLastFix(document: vscode.TextDocument): Promise<boolean> {
|
||||
const stack = snapshotStack.get(document.uri.fsPath);
|
||||
if (!stack || stack.length === 0) { return false; }
|
||||
const previousContent = stack.pop()!;
|
||||
const edit = new vscode.WorkspaceEdit();
|
||||
edit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), previousContent);
|
||||
return vscode.workspace.applyEdit(edit);
|
||||
}
|
||||
|
||||
export function hasSnapshot(document: vscode.TextDocument): boolean {
|
||||
const stack = snapshotStack.get(document.uri.fsPath);
|
||||
return !!(stack && stack.length > 0);
|
||||
}
|
||||
|
||||
export async function applyBatchFixes(document: vscode.TextDocument, fixes: CodeFix[]): Promise<number> {
|
||||
saveSnapshot(document);
|
||||
const validFixes = fixes.filter(f => f.matched);
|
||||
const sorted = [...validFixes].sort((a, b) => b.startLine - a.startLine);
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || editor.document.uri.toString() !== document.uri.toString()) { return 0; }
|
||||
|
||||
let applied = 0;
|
||||
for (const fix of sorted) {
|
||||
if (await applySingleFix(editor, fix)) { applied++; }
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
Reference in New Issue
Block a user