- 引入 i18n 系统,所有用户可见字符串替换为 t() 调用,支持中/EN/日三语 - 插件激活时读取 ai.outputLanguage 配置初始化语言,onDidChangeConfiguration 监听变更自动切换 - ReviewPanel 与 SetupViewProvider 注册 onLanguageChange 回调,语言切换时全量重渲染 - package.json description:"AI 输出语言" -> "插件语言" - 修复 repairJsonEscapes 无差别解引号导致 JSON 解析失败 - 设置页规则名称空值时输入框红框 + 错误提示 - 规则导入流程添加 withProgress 加载提示
129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import * as path from 'path';
|
|
import { existsSync } from 'fs';
|
|
import { execSync, spawn } from 'child_process';
|
|
import type { LinterAdapter, LinterDiagnostic, AdapterResult } from '../types';
|
|
import { getPMDRulesetPath } from '../config';
|
|
import { t } from '../i18n/messages';
|
|
|
|
export class PmdAdapter implements LinterAdapter {
|
|
id = 'pmd';
|
|
supportedLanguages = ['java'];
|
|
private pmdDir: string | null = null;
|
|
|
|
private resolvePmdDir(): string {
|
|
if (this.pmdDir) { return this.pmdDir; }
|
|
const candidates: string[] = [];
|
|
try {
|
|
const ext = vscode.extensions.getExtension?.('vscode-code-reviewer');
|
|
if (ext?.extensionPath) {
|
|
candidates.push(path.join(ext.extensionPath, 'jars', 'pmd'));
|
|
candidates.push(path.join(ext.extensionPath, 'out', 'jars', 'pmd'));
|
|
}
|
|
} catch { /* ignore */ }
|
|
candidates.push(path.join(path.resolve(__dirname, '..'), 'jars', 'pmd'));
|
|
candidates.push(path.join(path.resolve(__dirname, '..', '..'), 'jars', 'pmd'));
|
|
for (const dir of candidates) {
|
|
if (existsSync(path.join(dir, 'PmdRunner.class'))) {
|
|
this.pmdDir = dir;
|
|
return dir;
|
|
}
|
|
}
|
|
this.pmdDir = candidates[0];
|
|
return this.pmdDir;
|
|
}
|
|
|
|
private getPmdLibClasspath(): string {
|
|
return path.join(this.resolvePmdDir(), 'lib', '*');
|
|
}
|
|
|
|
private getPmdRunnerClasspath(): string {
|
|
return this.resolvePmdDir();
|
|
}
|
|
|
|
async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
|
|
try {
|
|
const ruleset = getPMDRulesetPath()
|
|
|| path.join(this.getPmdRunnerClasspath(), 'pmd-java-ruleset.xml');
|
|
const classpath = `${this.getPmdLibClasspath()};${this.getPmdRunnerClasspath()}`;
|
|
|
|
const isVirtual = document.uri.scheme === 'untitled';
|
|
const fileArg = isVirtual ? '-' : document.uri.fsPath;
|
|
|
|
const javaArgs = ['-cp', classpath, 'PmdRunner', fileArg, ruleset];
|
|
const result = await this.execPmd(javaArgs, isVirtual ? document.getText() : null, workingDir);
|
|
|
|
const diagnostics = this.parsePmdOutput(result);
|
|
return { diagnostics, status: 'ok' };
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
if (message.includes('ENOENT') || message.includes('java not found') || message.includes('Cannot find')) {
|
|
return { diagnostics: [], status: 'tool-unavailable', errorMessage: t('adapter.javaNotInstalled') };
|
|
}
|
|
return { diagnostics: [], status: 'execution-failed', errorMessage: message };
|
|
}
|
|
}
|
|
|
|
private execPmd(args: string[], stdinInput: string | null, cwd: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const proc = spawn('java', args, { cwd });
|
|
let stdout = '';
|
|
let stderr = '';
|
|
proc.stdout.on('data', (data: Buffer) => { stdout += data.toString(); });
|
|
proc.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
|
|
proc.on('close', (code) => {
|
|
if (code === 0 || code === 4 || stdout.length > 0) {
|
|
resolve(stdout);
|
|
} else {
|
|
reject(new Error(stderr || `PMD exited with code ${code}`));
|
|
}
|
|
});
|
|
proc.on('error', reject);
|
|
if (stdinInput !== null) {
|
|
proc.stdin.write(stdinInput);
|
|
proc.stdin.end();
|
|
}
|
|
});
|
|
}
|
|
|
|
private parsePmdOutput(output: string): LinterDiagnostic[] {
|
|
if (!output.trim()) { return []; }
|
|
try {
|
|
const data = JSON.parse(output);
|
|
const diagnostics: LinterDiagnostic[] = [];
|
|
for (const file of data.files ?? []) {
|
|
for (const violation of file.violations ?? []) {
|
|
const line = Math.max(0, (violation.beginline ?? 1) - 1);
|
|
const col = Math.max(0, (violation.begincolumn ?? 1) - 1);
|
|
const endCol = Math.max(col, (violation.endcolumn ?? col + 1) - 1);
|
|
const range = new vscode.Range(line, col, line, endCol);
|
|
diagnostics.push({
|
|
severity: this.mapPriority(violation.priority),
|
|
ruleId: `pmd:${violation.rule}`,
|
|
message: violation.description ?? '',
|
|
range,
|
|
});
|
|
}
|
|
}
|
|
return diagnostics;
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private mapPriority(priority: number): 'error' | 'warning' | 'info' {
|
|
if (priority <= 2) { return 'error'; }
|
|
if (priority === 3) { return 'warning'; }
|
|
return 'info';
|
|
}
|
|
|
|
isAvailable(): boolean {
|
|
try {
|
|
execSync('java -version 2>&1', { stdio: 'ignore' });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|