120 lines
3.6 KiB
Markdown
120 lines
3.6 KiB
Markdown
# Step 05 — Phase 2.4: sql-lint 适配器
|
||
|
||
**依赖**: Step 02
|
||
**参考设计**: §3.3
|
||
|
||
## 目标
|
||
|
||
实现 SQL / PL/SQL 的适配器,通过 CLI 子进程调用 sqlfluff。
|
||
|
||
## 新建文件
|
||
|
||
| # | 文件 | 说明 |
|
||
|---|------|------|
|
||
| 1 | `src/adapters/sql-lint.ts` | `SqlLintAdapter` |
|
||
|
||
---
|
||
|
||
## `src/adapters/sql-lint.ts`
|
||
|
||
```typescript
|
||
import * as vscode from 'vscode';
|
||
import * as child_process from 'child_process';
|
||
import { LinterAdapter, LinterDiagnostic, AdapterResult } from '../types';
|
||
import { getLinterConfig } from '../config';
|
||
|
||
const DIALECT_MAP: Record<string, string> = {
|
||
sql: 'ansi',
|
||
plsql: 'postgres',
|
||
};
|
||
|
||
export class SqlLintAdapter implements LinterAdapter {
|
||
id = 'sql-lint';
|
||
supportedLanguages = ['sql', 'plsql'];
|
||
|
||
async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
|
||
try {
|
||
const code = document.getText();
|
||
const languageId = document.languageId;
|
||
const dialect = DIALECT_MAP[languageId] ?? 'ansi';
|
||
const config = getLinterConfig();
|
||
|
||
const args = ['lint', '--format', 'json', '--dialect', dialect, '-'];
|
||
if (config.sqlLintConfigFile) {
|
||
args.push('--config', config.sqlLintConfigFile);
|
||
}
|
||
|
||
const result = await this.execSqlfluff(code, args, workingDir);
|
||
const output = JSON.parse(result);
|
||
|
||
const diagnostics: LinterDiagnostic[] = [];
|
||
for (const violation of output) {
|
||
const line = Math.max(0, (violation.line_no ?? violation.line_pos ?? 1) - 1);
|
||
const col = Math.max(0, (violation.line_pos ?? 1) - 1);
|
||
const range = new vscode.Range(line, col, line, col + 1);
|
||
diagnostics.push({
|
||
severity: 'warning',
|
||
ruleId: `sql-lint:${violation.code ?? violation.rule ?? 'unknown'}`,
|
||
message: violation.description ?? violation.message ?? '',
|
||
range,
|
||
});
|
||
}
|
||
return { diagnostics, status: 'ok' };
|
||
} catch (err) {
|
||
const message = err instanceof Error ? err.message : String(err);
|
||
if (message.includes('ENOENT') || message.includes('not found')) {
|
||
return { diagnostics: [], status: 'tool-unavailable', errorMessage: 'sqlfluff 未安装,请执行 pip install sqlfluff' };
|
||
}
|
||
return { diagnostics: [], status: 'execution-failed', errorMessage: message };
|
||
}
|
||
}
|
||
|
||
private execSqlfluff(code: string, args: string[], cwd: string): Promise<string> {
|
||
return new Promise((resolve, reject) => {
|
||
const proc = child_process.spawn('sqlfluff', 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 || stdout.length > 0) {
|
||
resolve(stdout);
|
||
} else {
|
||
reject(new Error(stderr || `sqlfluff exited with code ${code}`));
|
||
}
|
||
});
|
||
proc.on('error', reject);
|
||
proc.stdin.write(code);
|
||
proc.stdin.end();
|
||
});
|
||
}
|
||
|
||
isAvailable(): boolean {
|
||
try {
|
||
child_process.execSync('sqlfluff --version', { stdio: 'ignore' });
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 关键逻辑
|
||
|
||
- `check()`: spawn `sqlfluff lint --format json --dialect <dialect> -`,stdin 传入代码
|
||
- 方言映射:`sql → ansi`, `plsql → postgres`
|
||
- ruleId 加前缀 `sql-lint:`
|
||
- `isAvailable()`: `sqlfluff --version` 检测
|
||
- 通过 stdin 传代码,无需真实文件(支持虚拟文档)
|
||
|
||
---
|
||
|
||
## 验收
|
||
|
||
- [ ] 文件创建完成
|
||
- [ ] `npm run compile` 通过
|
||
- [ ] `npm run lint` 通过
|