- 方法级审查: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 + 行号排序
195 lines
5.6 KiB
TypeScript
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 {}
|
|
}
|
|
}
|
|
}
|
|
}
|