use crate::span::{SourceMap, Span};
use std::fmt::Write as _;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Severity {
Error,
Warning,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DiagnosticCode {
Syntax,
Unsupported,
UnknownTable,
UnknownColumn,
UnknownType,
Duplicate,
TypeMismatch,
UntypedBindMarker,
Annotation,
Arity,
PrimaryKey,
UnsupportedType,
IncompleteKey,
InvalidRestriction,
BroadScan,
AllowFiltering,
NonKeyRestriction,
ClusteringGap,
UnknownFunction,
MissingAlias,
OrderBy,
ReservedWord,
}
impl DiagnosticCode {
pub const WARNINGS: [DiagnosticCode; 5] = [
DiagnosticCode::BroadScan,
DiagnosticCode::AllowFiltering,
DiagnosticCode::NonKeyRestriction,
DiagnosticCode::ClusteringGap,
DiagnosticCode::OrderBy,
];
pub fn parse_warning(s: &str) -> Option<DiagnosticCode> {
Self::WARNINGS
.into_iter()
.find(|c| c.as_str().eq_ignore_ascii_case(s))
}
pub fn as_str(self) -> &'static str {
use DiagnosticCode::*;
match self {
Syntax => "CQL001",
Unsupported => "CQL002",
UnknownTable => "CQL101",
UnknownColumn => "CQL102",
UnknownType => "CQL103",
Duplicate => "CQL104",
TypeMismatch => "CQL105",
UntypedBindMarker => "CQL106",
Annotation => "CQL107",
Arity => "CQL108",
PrimaryKey => "CQL109",
UnsupportedType => "CQL110",
IncompleteKey => "CQL111",
InvalidRestriction => "CQL112",
BroadScan => "CQL201",
AllowFiltering => "CQL202",
NonKeyRestriction => "CQL203",
ClusteringGap => "CQL204",
UnknownFunction => "CQL113",
MissingAlias => "CQL114",
OrderBy => "CQL205",
ReservedWord => "CQL115",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Diagnostic {
pub code: DiagnosticCode,
pub severity: Severity,
pub message: String,
pub span: Span,
pub help: Option<String>,
}
impl Diagnostic {
pub fn error(code: DiagnosticCode, span: Span, message: impl Into<String>) -> Self {
Diagnostic {
code,
severity: Severity::Error,
message: message.into(),
span,
help: None,
}
}
pub fn warning(code: DiagnosticCode, span: Span, message: impl Into<String>) -> Self {
Diagnostic {
severity: Severity::Warning,
..Diagnostic::error(code, span, message)
}
}
pub fn with_help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
pub fn is_error(&self) -> bool {
self.severity == Severity::Error
}
pub fn render(&self, map: &SourceMap) -> String {
let file = map.get(self.span.file);
let (line, col) = file.line_col(self.span.start as usize);
let line_text = file.line_text(line);
let line_start = file.line_start(line);
let line_end = line_start + line_text.len();
let span_end = (self.span.end as usize)
.min(line_end)
.max(self.span.start as usize);
let width = file
.text
.get(self.span.start as usize..span_end)
.map_or(1, |s| s.chars().count())
.max(1);
let sev = match self.severity {
Severity::Error => "error",
Severity::Warning => "warning",
};
const MAX_SHOWN: usize = 160;
const LEAD: usize = 40;
let chars: Vec<char> = line_text.chars().collect();
let (shown, caret_col, width) = if chars.len() > MAX_SHOWN {
let start = (col - 1).saturating_sub(LEAD);
let end = (start + MAX_SHOWN).min(chars.len());
let lead = if start > 0 { "…" } else { "" };
let trail = if end < chars.len() { "…" } else { "" };
let window: String = chars[start..end].iter().collect();
let caret = (col - 1 - start) + lead.chars().count();
let room = (end - (col - 1).min(end)).max(1);
(format!("{lead}{window}{trail}"), caret, width.min(room))
} else {
(line_text.to_string(), col - 1, width)
};
let gutter = " ".repeat(line.to_string().len());
let mut out = String::new();
let _ = writeln!(out, "{sev}[{}]: {}", self.code.as_str(), self.message);
let _ = writeln!(out, "{gutter}--> {}:{line}:{col}", file.name);
let _ = writeln!(out, "{gutter} |");
let _ = writeln!(out, "{line} | {shown}");
let _ = writeln!(
out,
"{gutter} | {}{}",
" ".repeat(caret_col),
"^".repeat(width)
);
if let Some(help) = &self.help {
let _ = writeln!(out, "{gutter} |");
let _ = writeln!(out, "{gutter} = help: {help}");
}
out
}
}