Files

8.0 KiB
Raw Permalink Blame History

Step 06 — Phase 2.5: PMD 适配器

依赖: Step 02
参考设计: §3.3, §3.4

目标

实现 Java 的 PMD 适配器。通过 Java 子进程调用 PmdRunner 包装器,支持 stdin 传入代码(虚拟文档)。

新建文件

# 文件 说明
1 src/adapters/pmd.ts PmdAdapter
2 jars/pmd/PmdRunner.java PMD Java 包装器(stdin + JSON 输出)
3 jars/pmd/pmd-java-ruleset.xml Java 规则集
4 jars/pmd/pmd-jsp-ruleset.xml JSP 规则集

目录结构

jars/pmd/
├── lib/                         # PMD 依赖 JAR(需下载)
├── PmdRunner.java               # 包装器
├── pmd-java-ruleset.xml
└── pmd-jsp-ruleset.xml

1. jars/pmd/PmdRunner.java

import java.io.*;
import java.nio.file.*;
import net.sourceforge.pmd.*;
import net.sourceforge.pmd.renderers.*;

public class PmdRunner {
    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.err.println("Usage: PmdRunner <filePath|- for stdin> <rulesetPath>");
            System.exit(1);
            return;
        }

        String filePath = args[0];
        String rulesetPath = args[1];

        Path tempFile = null;
        if ("-".equals(filePath)) {
            String code = new String(System.in.readAllBytes());
            tempFile = Files.createTempFile("pmd-stdin-", ".java");
            Files.writeString(tempFile, code);
            filePath = tempFile.toString();
        }

        try {
            PMDConfiguration config = new PMDConfiguration();
            config.setInputFilePath(Path.of(filePath));
            config.addRuleSet(Path.of(rulesetPath));
            config.setReportFormat("json");

            StringWriter writer = new StringWriter();
            config.setReportWriter(writer);

            PmdAnalysis pmd = PmdAnalysis.create(config);
            pmd.performAnalysis();

            System.out.print(writer.toString());
        } finally {
            if (tempFile != null) {
                Files.deleteIfExists(tempFile);
            }
        }
    }
}

编译命令classpath 需指向 jars/pmd/lib/*:

javac -cp "jars/pmd/lib/*" -d jars/pmd/ jars/pmd/PmdRunner.java

2. jars/pmd/pmd-java-ruleset.xml

<?xml version="1.0"?>
<ruleset name="Java Rules"
    xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
    <description>Java Code Review Rules</description>
    <rule ref="category/java/bestpractices.xml"/>
    <rule ref="category/java/codestyle.xml"/>
    <rule ref="category/java/design.xml"/>
    <rule ref="category/java/errorprone.xml"/>
    <rule ref="category/java/performance.xml"/>
    <rule ref="category/java/security.xml"/>
</ruleset>

3. jars/pmd/pmd-jsp-ruleset.xml

<?xml version="1.0"?>
<ruleset name="JSP Rules"
    xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
    <description>JSP Code Review Rules</description>
    <rule ref="category/jsp/bestpractices.xml"/>
    <rule ref="category/jsp/codestyle.xml"/>
    <rule ref="category/jsp/design.xml"/>
    <rule ref="category/jsp/errorprone.xml"/>
</ruleset>

4. src/adapters/pmd.ts

import * as vscode from 'vscode';
import * as path from 'path';
import * as child_process from 'child_process';
import { LinterAdapter, LinterDiagnostic, AdapterResult } from '../types';
import { getLinterConfig } from '../config';

export class PmdAdapter implements LinterAdapter {
  id = 'pmd';
  supportedLanguages = ['java'];

  private getPmdLibClasspath(): string {
    const extRoot = vscode.extensions.getExtension?.('vscode-code-reviewer')?.extensionPath
      ?? path.join(__dirname, '..', '..');
    const pmdLib = path.join(extRoot, 'jars', 'pmd', 'lib');
    return path.join(pmdLib, '*');
  }

  private getPmdRunnerClasspath(): string {
    const extRoot = vscode.extensions.getExtension?.('vscode-code-reviewer')?.extensionPath
      ?? path.join(__dirname, '..', '..');
    return path.join(extRoot, 'jars', 'pmd');
  }

  async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
    try {
      const config = getLinterConfig();
      const ruleset = config.pmdRulesetPath
        || 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')) {
        return { diagnostics: [], status: 'tool-unavailable', errorMessage: 'Java 11+ 未安装或不在 PATH 中' };
      }
      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 = child_process.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 {
      child_process.execSync('java -version 2>&1', { stdio: 'ignore' });
      return true;
    } catch {
      return false;
    }
  }
}

关键逻辑

  • 虚拟文档通过 stdin 传入代码(PmdRunner "-" ruleset
  • 真实文件传文件路径(PmdRunner filePath ruleset
  • classpath 使用 ; 分隔(Windows 目标平台)
  • isVirtual 判断依据:document.uri.scheme === 'untitled'
  • PMD exit code 0 和 4 都视为成功(4 表示有 violations 但执行成功)
  • isAvailable 检测 Java 是否可用

验收

  • 4 个文件创建完成
  • PMD JAR 依赖已下载到 jars/pmd/lib/
  • PmdRunner.java 编译成功
  • npm run compile 通过
  • npm run lint 通过