use std::io;
use serde::Serialize;
use syntaqlite::Diagnostic;
use syntaqlite::analysis::{Help, Severity};
use syntaqlite::any::AnyDialect;
use syntaqlite::util::DiagnosticRenderer;
use syntaqlite::{AnalysisConfig, Catalog};
use crate::cli::{AnalysisOutput, AnalyzeArgs, HostLanguage};
use crate::config::{self, ConfigMode};
use crate::util::{self, Source};
pub(crate) fn run(
dialect: &AnyDialect,
config_mode: &ConfigMode<'_>,
args: &AnalyzeArgs,
) -> Result<(), String> {
let validator = Validator::new(dialect, config_mode, args)?;
let sources = util::load_sources(&args.files, args.expression.as_deref())?;
let renderer = select_renderer(args.output);
if validator.run(&sources, renderer.as_ref()) {
std::process::exit(1);
}
Ok(())
}
struct Validator<'a> {
dialect: &'a AnyDialect,
config: AnalysisConfig,
lang: Option<HostLanguage>,
schema_catalog: Catalog,
has_schema: bool,
}
impl<'a> Validator<'a> {
fn new(
dialect: &'a AnyDialect,
config_mode: &ConfigMode<'_>,
args: &AnalyzeArgs,
) -> Result<Self, String> {
let file_config = config::resolve(config_mode);
let schema_files = if args.schema.is_empty() {
util::schemas_from_project_config(&args.files, file_config.as_ref())
} else {
args.schema.clone()
};
let has_schema = !schema_files.is_empty();
let checks = util::build_check_config(
has_schema,
file_config.as_ref().map(|(c, _)| &c.checks),
&args.allow,
&args.warn,
&args.deny,
)?;
let schema_catalog = util::build_schema_catalog(dialect, &schema_files)?;
Ok(Self {
dialect,
config: AnalysisConfig::default().with_checks(checks),
lang: args.lang,
schema_catalog,
has_schema,
})
}
fn run(&self, sources: &[Source], renderer: &dyn Renderer) -> bool {
let multi = sources.len() > 1;
let mut any_errors = false;
let mut any_diagnostics = false;
for src in sources {
if multi && src.is_file() {
renderer.on_source_start(src);
}
let (errors, diags) = self.validate_one(src, renderer);
if !src.is_file() {
if errors {
std::process::exit(1);
}
continue;
}
any_errors |= errors;
any_diagnostics |= diags;
}
if any_diagnostics && !self.has_schema {
renderer.on_no_schema_hint();
}
any_errors
}
fn validate_one(&self, src: &Source, renderer: &dyn Renderer) -> (bool, bool) {
match self.analyze(src) {
Analysis::Diagnostics(diags) => {
let any_diags = !diags.is_empty();
let has_errors = renderer.render(src, &diags);
(has_errors, any_diags)
}
Analysis::NoEmbeddedFragments => {
renderer.on_no_fragments(src);
(false, false)
}
}
}
fn analyze(&self, src: &Source) -> Analysis {
let lang = util::resolve_language(self.lang, src, self.dialect);
let fragments = syntaqlite::embedded::fragments(self.dialect.clone(), &src.text, lang);
if lang.is_some() && fragments.is_empty() {
return Analysis::NoEmbeddedFragments;
}
let mut analyzer = syntaqlite::embedded::EmbeddedAnalyzer::new(self.dialect.clone())
.with_catalog(self.schema_catalog.clone())
.with_config(self.config);
Analysis::Diagnostics(analyzer.analyze(&fragments))
}
}
enum Analysis {
Diagnostics(Vec<Diagnostic>),
NoEmbeddedFragments,
}
trait Renderer {
fn render(&self, src: &Source, diagnostics: &[Diagnostic]) -> bool;
fn on_source_start(&self, _src: &Source) {}
fn on_no_fragments(&self, _src: &Source) {}
fn on_no_schema_hint(&self) {}
}
fn select_renderer(output: AnalysisOutput) -> Box<dyn Renderer> {
match output {
AnalysisOutput::Text => Box::new(TextRenderer),
AnalysisOutput::Json => Box::new(JsonRenderer),
}
}
struct TextRenderer;
impl Renderer for TextRenderer {
fn render(&self, src: &Source, diagnostics: &[Diagnostic]) -> bool {
DiagnosticRenderer::new(&src.text, &src.label)
.render_diagnostics(diagnostics, &mut io::stderr())
.unwrap_or(false)
}
fn on_source_start(&self, src: &Source) {
println!("==> {} <==", src.label);
}
fn on_no_fragments(&self, src: &Source) {
eprintln!("no SQL fragments found in {}", src.label);
}
fn on_no_schema_hint(&self) {
eprintln!(
"note: no schema provided; unresolved names are reported as warnings. \
Add a `syntaqlite.toml` with `schema = [\"schema.sql\"]` or pass `--schema` \
to treat them as errors."
);
}
}
struct JsonRenderer;
impl Renderer for JsonRenderer {
fn render(&self, src: &Source, diagnostics: &[Diagnostic]) -> bool {
let mut has_errors = false;
for diag in diagnostics {
if let Severity::Error = diag.severity() {
has_errors = true;
}
emit_diagnostic_json(&src.label, diag);
}
has_errors
}
}
fn emit_diagnostic_json(file: &str, diag: &Diagnostic) {
let record = DiagnosticRecord::from_diag(file, diag);
match serde_json::to_string(&record) {
Ok(s) => println!("{s}"),
Err(e) => eprintln!("error serializing diagnostic: {e}"),
}
}
const SCHEMA_VERSION: u32 = 0;
#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
enum JsonSeverity {
Error,
Warning,
Info,
Hint,
}
impl From<Severity> for JsonSeverity {
fn from(s: Severity) -> Self {
match s {
Severity::Error => JsonSeverity::Error,
Severity::Warning => JsonSeverity::Warning,
Severity::Info => JsonSeverity::Info,
Severity::Hint => JsonSeverity::Hint,
}
}
}
#[derive(Serialize)]
struct DiagnosticRecord<'a> {
kind: &'static str,
schema_version: u32,
file: &'a str,
severity: JsonSeverity,
message: String,
start_offset: usize,
end_offset: usize,
#[serde(skip_serializing_if = "Option::is_none")]
help: Option<String>,
}
impl<'a> DiagnosticRecord<'a> {
fn from_diag(file: &'a str, diag: &Diagnostic) -> Self {
let help = diag.help().map(|h| match h {
Help::Suggestion(s) => s.clone(),
});
Self {
kind: "diagnostic",
schema_version: SCHEMA_VERSION,
file,
severity: diag.severity().into(),
message: diag.message().to_string(),
start_offset: diag.start().as_usize(),
end_offset: diag.end().as_usize(),
help,
}
}
}