Files

6.2 KiB
Raw Permalink Blame History

Step 07 — Phase 2.6: JSP 适配器

依赖: Step 03, 04, 06(需要 PMD / ESLint / Stylelint 适配器)
参考设计: §3.5

目标

实现 JSP 组合适配器。将 JSP 文件拆分后分发检查,合并结果。

新建文件

# 文件 说明
1 src/jsp/jsp-extractor.ts JSP 内嵌代码块提取器
2 src/adapters/jsp.ts JspAdapter(组合适配器)

1. src/jsp/jsp-extractor.ts

export interface JspSection {
  language: 'javascript' | 'css' | 'java';
  code: string;
  lineOffset: number;
  sourceStart: number;
  sourceEnd: number;
}

export function extractJspSections(content: string): JspSection[] {
  const sections: JspSection[] = [];

  const scriptRegex = /<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi;
  let match: RegExpExecArray | null;
  while ((match = scriptRegex.exec(content)) !== null) {
    const code = match[1];
    const beforeMatch = content.substring(0, match.index);
    const lineOffset = beforeMatch.split('\n').length - 1;
    sections.push({
      language: 'javascript',
      code,
      lineOffset,
      sourceStart: match.index,
      sourceEnd: match.index + match[0].length,
    });
  }

  const styleRegex = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi;
  while ((match = styleRegex.exec(content)) !== null) {
    const code = match[1];
    const beforeMatch = content.substring(0, match.index);
    const lineOffset = beforeMatch.split('\n').length - 1;
    sections.push({
      language: 'css',
      code,
      lineOffset,
      sourceStart: match.index,
      sourceEnd: match.index + match[0].length,
    });
  }

  const scriptletRegex = /<%=?([\s\S]*?)%>/g;
  while ((match = scriptletRegex.exec(content)) !== null) {
    const code = match[1];
    const beforeMatch = content.substring(0, match.index);
    const lineOffset = beforeMatch.split('\n').length - 1;
    sections.push({
      language: 'java',
      code,
      lineOffset,
      sourceStart: match.index,
      sourceEnd: match.index + match[0].length,
    });
  }

  return sections;
}

2. src/adapters/jsp.ts

import * as vscode from 'vscode';
import { LinterAdapter, LinterDiagnostic, AdapterResult } from '../types';
import { PmdAdapter } from './pmd';
import { ESLintAdapter } from './eslint';
import { StylelintAdapter } from './stylelint';
import { extractJspSections, JspSection } from '../jsp/jsp-extractor';
import { getLinterConfig } from '../config';

export class JspAdapter implements LinterAdapter {
  id = 'jsp';
  supportedLanguages = ['jsp'];

  private pmdAdapter = new PmdAdapter();
  private eslintAdapter = new ESLintAdapter();
  private stylelintAdapter = new StylelintAdapter();

  async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
    const allDiagnostics: LinterDiagnostic[] = [];
    const errors: string[] = [];

    const config = getLinterConfig();
    const jsEnabled = config.languageMap.javascript !== '';
    const cssEnabled = config.languageMap.css !== '';
    const javaEnabled = config.languageMap.java !== '';

    const pmdResult = await this.pmdAdapter.check(document, workingDir);
    allDiagnostics.push(...pmdResult.diagnostics);
    if (pmdResult.status !== 'ok') {
      errors.push(`PMD: ${pmdResult.errorMessage ?? pmdResult.status}`);
    }

    const sections = extractJspSections(document.getText());

    for (const section of sections) {
      const isEnabled = (section.language === 'javascript' && jsEnabled)
        || (section.language === 'css' && cssEnabled)
        || (section.language === 'java' && javaEnabled);
      if (!isEnabled) { continue; }

      const adapter = this.getAdapter(section.language);
      if (!adapter) { continue; }

      try {
        const virtualDoc = await vscode.workspace.openTextDocument({
          content: section.code,
          language: section.language,
        });
        const result = await adapter.check(virtualDoc, workingDir);

        for (const diag of result.diagnostics) {
          const adjustedRange = new vscode.Range(
            diag.range.start.line + section.lineOffset,
            diag.range.start.character,
            diag.range.end.line + section.lineOffset,
            diag.range.end.character,
          );
          allDiagnostics.push({ ...diag, range: adjustedRange });
        }

        if (result.status !== 'ok') {
          errors.push(`${section.language}: ${result.errorMessage ?? result.status}`);
        }
      } catch (err) {
        errors.push(`${section.language}: ${err instanceof Error ? err.message : String(err)}`);
      }
    }

    const hasErrors = errors.length > 0;
    const hasUnavailable = errors.some(e => e.includes('未安装') || e.includes('tool-unavailable'));

    return {
      diagnostics: allDiagnostics,
      status: hasErrors ? (hasUnavailable ? 'tool-unavailable' : 'execution-failed') : 'ok',
      errorMessage: errors.join('; '),
    };
  }

  private getAdapter(language: string): LinterAdapter | null {
    switch (language) {
      case 'javascript': return this.eslintAdapter;
      case 'css': return this.stylelintAdapter;
      case 'java': return this.pmdAdapter;
      default: return null;
    }
  }

  isAvailable(): boolean {
    return true;
  }
}

关键逻辑

JspAdapter.check() 三步流程:

1. 调用 PmdAdapter.check(document) → JSP 规范检查
2. extractJspSections(document.getText()) → 提取内嵌代码块
3. 对每个 section:
   a. 按 language 选择对应适配器(ESLint/Stylelint/PMD
   b. 创建虚拟文档 (vscode.workspace.openTextDocument)
   c. 调用 adapter.check(virtualDoc)
   d. 修正行号偏移 (section.lineOffset)
4. 合并所有结果

提取器正则规则:

代码块类型 正则 目标适配器
<script> /<script\b[^>]*>([\s\S]*?)<\/script\s*>/gi ESLint
<style> /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi Stylelint
<%=? %> /<%=?([\s\S]*?)%>/g PMD
  • 行号偏移修正:lineOffset 从提取位置之前的换行符数计算
  • 根据 linters.<language> 配置决定是否启用对应子适配器
  • 组合状态:任一子适配器不可用则标记 tool-unavailable

验收

  • 2 个文件创建完成
  • npm run compile 通过
  • npm run lint 通过