1use crate::span::{SourceMap, Span};
4use std::fmt::Write as _;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum Severity {
10 Error,
12 Warning,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum DiagnosticCode {
21 Syntax,
23 Unsupported,
25 UnknownTable,
27 UnknownColumn,
29 UnknownType,
31 Duplicate,
33 TypeMismatch,
35 UntypedBindMarker,
37 Annotation,
39 Arity,
41 PrimaryKey,
43 UnsupportedType,
45 IncompleteKey,
47 InvalidRestriction,
49 BroadScan,
51 AllowFiltering,
53 NonKeyRestriction,
55 ClusteringGap,
57 UnknownFunction,
59 MissingAlias,
61 OrderBy,
63 ReservedWord,
65}
66
67impl DiagnosticCode {
68 pub const WARNINGS: [DiagnosticCode; 5] = [
70 DiagnosticCode::BroadScan,
71 DiagnosticCode::AllowFiltering,
72 DiagnosticCode::NonKeyRestriction,
73 DiagnosticCode::ClusteringGap,
74 DiagnosticCode::OrderBy,
75 ];
76
77 pub fn parse_warning(s: &str) -> Option<DiagnosticCode> {
79 Self::WARNINGS
80 .into_iter()
81 .find(|c| c.as_str().eq_ignore_ascii_case(s))
82 }
83
84 pub fn as_str(self) -> &'static str {
86 use DiagnosticCode::*;
87 match self {
88 Syntax => "CQL001",
89 Unsupported => "CQL002",
90 UnknownTable => "CQL101",
91 UnknownColumn => "CQL102",
92 UnknownType => "CQL103",
93 Duplicate => "CQL104",
94 TypeMismatch => "CQL105",
95 UntypedBindMarker => "CQL106",
96 Annotation => "CQL107",
97 Arity => "CQL108",
98 PrimaryKey => "CQL109",
99 UnsupportedType => "CQL110",
100 IncompleteKey => "CQL111",
101 InvalidRestriction => "CQL112",
102 BroadScan => "CQL201",
103 AllowFiltering => "CQL202",
104 NonKeyRestriction => "CQL203",
105 ClusteringGap => "CQL204",
106 UnknownFunction => "CQL113",
107 MissingAlias => "CQL114",
108 OrderBy => "CQL205",
109 ReservedWord => "CQL115",
110 }
111 }
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
116#[non_exhaustive]
117pub struct Diagnostic {
118 pub code: DiagnosticCode,
120 pub severity: Severity,
122 pub message: String,
124 pub span: Span,
126 pub help: Option<String>,
128}
129
130impl Diagnostic {
131 pub fn error(code: DiagnosticCode, span: Span, message: impl Into<String>) -> Self {
133 Diagnostic {
134 code,
135 severity: Severity::Error,
136 message: message.into(),
137 span,
138 help: None,
139 }
140 }
141
142 pub fn warning(code: DiagnosticCode, span: Span, message: impl Into<String>) -> Self {
144 Diagnostic {
145 severity: Severity::Warning,
146 ..Diagnostic::error(code, span, message)
147 }
148 }
149
150 pub fn with_help(mut self, help: impl Into<String>) -> Self {
152 self.help = Some(help.into());
153 self
154 }
155
156 pub fn is_error(&self) -> bool {
158 self.severity == Severity::Error
159 }
160
161 pub fn render(&self, map: &SourceMap) -> String {
163 let file = map.get(self.span.file);
164 let (line, col) = file.line_col(self.span.start as usize);
165 let line_text = file.line_text(line);
166 let line_start = file.line_start(line);
167 let line_end = line_start + line_text.len();
168 let span_end = (self.span.end as usize)
169 .min(line_end)
170 .max(self.span.start as usize);
171 let width = file
172 .text
173 .get(self.span.start as usize..span_end)
174 .map_or(1, |s| s.chars().count())
175 .max(1);
176 let sev = match self.severity {
177 Severity::Error => "error",
178 Severity::Warning => "warning",
179 };
180 const MAX_SHOWN: usize = 160;
182 const LEAD: usize = 40;
183 let chars: Vec<char> = line_text.chars().collect();
184 let (shown, caret_col, width) = if chars.len() > MAX_SHOWN {
185 let start = (col - 1).saturating_sub(LEAD);
186 let end = (start + MAX_SHOWN).min(chars.len());
187 let lead = if start > 0 { "…" } else { "" };
188 let trail = if end < chars.len() { "…" } else { "" };
189 let window: String = chars[start..end].iter().collect();
190 let caret = (col - 1 - start) + lead.chars().count();
191 let room = (end - (col - 1).min(end)).max(1);
192 (format!("{lead}{window}{trail}"), caret, width.min(room))
193 } else {
194 (line_text.to_string(), col - 1, width)
195 };
196 let gutter = " ".repeat(line.to_string().len());
197 let mut out = String::new();
198 let _ = writeln!(out, "{sev}[{}]: {}", self.code.as_str(), self.message);
199 let _ = writeln!(out, "{gutter}--> {}:{line}:{col}", file.name);
200 let _ = writeln!(out, "{gutter} |");
201 let _ = writeln!(out, "{line} | {shown}");
202 let _ = writeln!(
203 out,
204 "{gutter} | {}{}",
205 " ".repeat(caret_col),
206 "^".repeat(width)
207 );
208 if let Some(help) = &self.help {
209 let _ = writeln!(out, "{gutter} |");
210 let _ = writeln!(out, "{gutter} = help: {help}");
211 }
212 out
213 }
214}