Files
2026Technology-Competition/docs/superpowers/specs/implementation-steps/07-phase2.6-jsp-adapter.md
T

213 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
```typescript
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`
```typescript
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` 通过