Files
2026Technology-Competition/src/adapters/sqlfluff.ts
T
范智鹏 247f19fd44 feat: 方法级代码审查 + 模板导入/预览增强 + SQLFluff 方言 + AI 空响应报错修复
- 方法级审查:CodeLens 触发 + 单次 AI 调用(规则匹配 + 6 维度深度审查),新增 method-extractor / status-cache / codeLensProvider
- 模板导入:severity 保留原始值 + 占位 id、去重对照统一 known-rules、重复提示条双语翻译、箭头展开/折叠 UI、520 条静态规则补 zh/ja 翻译
- SQL:sql-lint 重命名 sqlfluff + sqlfluff.dialect 方言可配置 + 默认方言调整
- ESLint:v9 flat config 接线修复(overrideConfigFile)+ legacy 迁移提示
- AI:空响应 EmptyContentError + 重试一次 + max_tokens 截断专用报错
- JSP:整文件检查走 PMD JSP 规则集 + scriptlet 包装解析 + 行号映射
- 诊断按 severity + 行号排序
2026-08-03 22:53:20 +08:00

195 lines
5.6 KiB
TypeScript

import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { spawn } from 'child_process';
import type { LinterAdapter, AdapterResult, LinterDiagnostic, Severity } from './adapter';
import { getSqlFluffConfigFile, getSqlFluffDialect } from '../config';
import { t } from '../i18n/messages';
import staticRules from '../rules/static-rules.json';
const DIALECT_MAP: Record<string, string> = {
sql: 'mysql',
plsql: 'oracle',
};
const SUPPORTED_DIALECTS = [
'ansi', 'athena', 'bigquery', 'clickhouse', 'databricks', 'db2', 'doris',
'duckdb', 'exasol', 'flink', 'greenplum', 'hive', 'impala', 'mariadb',
'materialize', 'mysql', 'oracle', 'postgres', 'redshift', 'snowflake',
'soql', 'sparksql', 'sqlite', 'starrocks', 'teradata', 'trino', 'tsql', 'vertica',
];
const BUILTIN_SQLFLUFF_RULES =
'core,AM03,AM05,AM08,CV01,CV02,CV06,CV08,CV12,LT13,LT14,LT15,ST01,ST02,ST04,ST05,ST06,ST07,ST09,ST10,ST11,ST12,RF02,RF04,RF05,RF06';
function buildBuiltinConfig(dialect: string): string {
return `[sqlfluff]
rules = ${BUILTIN_SQLFLUFF_RULES}
dialect = ${dialect}
max_line_length = 80
indent_unit = space
tab_space_size = 4
`;
}
interface RuleEntry { id: string; description: string; tier?: string; }
const tierMap = new Map<string, string>();
try {
const sqlfluffRules = (staticRules as any).rules?.['sqlfluff'] as RuleEntry[] | undefined;
if (sqlfluffRules) {
for (const rule of sqlfluffRules) {
if (rule.id && rule.tier) {
tierMap.set(rule.id.replace('sqlfluff/', ''), rule.tier);
}
}
}
} catch {}
function tierToSeverity(tier: string | undefined): Severity {
if (tier === 'P0' || tier === 'P1') { return 'error'; }
if (tier === 'P2') { return 'warning'; }
return 'warning';
}
function hasProjectSqlfluffConfig(workspaceRoot: string): boolean {
const candidates = ['.sqlfluff', '.sqlfluff.ini'];
for (const candidate of candidates) {
if (fs.existsSync(path.join(workspaceRoot, candidate))) {
return true;
}
}
return false;
}
interface SqlFluffViolation {
start_line_no: number;
start_line_pos: number;
end_line_no: number;
end_line_pos: number;
code: string;
description: string;
}
interface SqlFluffResult {
filepath: string;
violations: SqlFluffViolation[];
}
function runSqlfluff(code: string, cwd: string, configPath?: string, dialect?: string): Promise<string> {
return new Promise((resolve, reject) => {
const args = ['lint', '--format', 'json'];
if (dialect) {
args.push('--dialect', dialect);
}
if (configPath) {
args.push('--config', configPath);
}
args.push('-');
const child = spawn('sqlfluff', args, {
cwd,
timeout: 30000,
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data: Buffer) => { stdout += data.toString(); });
child.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
child.on('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'ENOENT') {
reject(new Error('tool-unavailable'));
} else {
reject(err);
}
});
child.on('close', (code: number | null) => {
if (stdout) {
resolve(stdout);
} else {
reject(new Error(stderr || `sqlfluff exited with code ${code}`));
}
});
child.stdin.write(code);
child.stdin.end();
});
}
export class SqlFluffAdapter implements LinterAdapter {
id = 'sqlfluff';
supportedLanguages = ['sql', 'plsql'];
isAvailable(): boolean {
return true;
}
async check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult> {
const languageId = document.languageId;
const fallbackDialect = DIALECT_MAP[languageId] ?? 'ansi';
const explicitDialect = getSqlFluffDialect();
const cliDialect = explicitDialect && SUPPORTED_DIALECTS.includes(explicitDialect)
? explicitDialect
: undefined;
let configPath: string | undefined;
let tempConfigPath: string | undefined;
const globalConfig = getSqlFluffConfigFile();
if (globalConfig && globalConfig.trim() !== '') {
configPath = globalConfig;
} else if (hasProjectSqlfluffConfig(workingDir)) {
} else {
tempConfigPath = path.join(os.tmpdir(), `vscode-code-reviewer-sqlfluff-${Date.now()}.cfg`);
fs.writeFileSync(tempConfigPath, buildBuiltinConfig(cliDialect ?? fallbackDialect), 'utf-8');
configPath = tempConfigPath;
}
try {
const stdout = await runSqlfluff(document.getText(), workingDir, configPath, cliDialect);
const results: SqlFluffResult[] = JSON.parse(stdout);
const diagnostics: LinterDiagnostic[] = [];
for (const result of results) {
for (const v of result.violations) {
diagnostics.push({
severity: tierToSeverity(tierMap.get(v.code)),
ruleId: `sqlfluff:${v.code}`,
message: v.description,
range: new vscode.Range(
v.start_line_no - 1,
v.start_line_pos - 1,
v.end_line_no - 1,
v.end_line_pos - 1
),
});
}
}
return { diagnostics, status: 'ok' };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message === 'tool-unavailable') {
return {
diagnostics: [],
status: 'tool-unavailable',
errorMessage: t('adapter.sqlfluffNotInstalled'),
};
}
return {
diagnostics: [],
status: 'execution-failed',
errorMessage: message,
};
} finally {
if (tempConfigPath) {
try { fs.unlinkSync(tempConfigPath); } catch {}
}
}
}
}