213 lines
6.2 KiB
Markdown
213 lines
6.2 KiB
Markdown
# 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` 通过
|