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 = { 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(); 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 { 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 { 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 {} } } } }