Skip to main content

cqlc_core/
diagnostic.rs

1//! Compiler diagnostics with stable codes.
2
3use crate::span::{SourceMap, Span};
4use std::fmt::Write as _;
5
6/// How serious a diagnostic is.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum Severity {
10    /// The query is rejected.
11    Error,
12    /// Advice; the query is still generated.
13    Warning,
14}
15
16/// Stable diagnostic codes. `CQL0xx` are syntax problems, `CQL1xx` are
17/// errors from schema resolution / type checking, `CQL2xx` are warnings.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum DiagnosticCode {
21    /// Syntax error.
22    Syntax,
23    /// Valid CQL that cqlc does not support (yet).
24    Unsupported,
25    /// Unknown table.
26    UnknownTable,
27    /// Unknown column.
28    UnknownColumn,
29    /// Unknown type.
30    UnknownType,
31    /// Duplicate definition.
32    Duplicate,
33    /// Literal or value does not fit the column type.
34    TypeMismatch,
35    /// Bind marker in a position whose type cannot be inferred.
36    UntypedBindMarker,
37    /// Missing or invalid `-- name:` annotation.
38    Annotation,
39    /// Wrong number of values / too many bind markers.
40    Arity,
41    /// Invalid primary key definition.
42    PrimaryKey,
43    /// Type has no supported Rust mapping.
44    UnsupportedType,
45    /// Statement misses required primary key restrictions.
46    IncompleteKey,
47    /// Restriction or assignment that Cassandra rejects.
48    InvalidRestriction,
49    /// Query may scan more than one partition.
50    BroadScan,
51    /// `ALLOW FILTERING` in use.
52    AllowFiltering,
53    /// Restriction on a non-key column.
54    NonKeyRestriction,
55    /// Clustering restriction skips a column.
56    ClusteringGap,
57    /// Unknown function.
58    UnknownFunction,
59    /// Expression in the selection list needs an `AS` alias.
60    MissingAlias,
61    /// `ORDER BY` may not match the clustering order.
62    OrderBy,
63    /// Reserved CQL keyword used as an unquoted identifier.
64    ReservedWord,
65}
66
67impl DiagnosticCode {
68    /// Every warning code, i.e. the ones `-- allow:` may silence.
69    pub const WARNINGS: [DiagnosticCode; 5] = [
70        DiagnosticCode::BroadScan,
71        DiagnosticCode::AllowFiltering,
72        DiagnosticCode::NonKeyRestriction,
73        DiagnosticCode::ClusteringGap,
74        DiagnosticCode::OrderBy,
75    ];
76
77    /// Parses a code such as `CQL201` (case-insensitive) if it is a warning.
78    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    /// The code as shown to users, e.g. `CQL201`.
85    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/// One error or warning with its location.
115#[derive(Clone, Debug, PartialEq, Eq)]
116#[non_exhaustive]
117pub struct Diagnostic {
118    /// Stable code, e.g. `CQL102`.
119    pub code: DiagnosticCode,
120    /// Error or warning.
121    pub severity: Severity,
122    /// What is wrong.
123    pub message: String,
124    /// Where it is wrong.
125    pub span: Span,
126    /// How to fix it, if there is a suggestion.
127    pub help: Option<String>,
128}
129
130impl Diagnostic {
131    /// Creates an error.
132    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    /// Creates a warning.
143    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    /// Adds a help message.
151    pub fn with_help(mut self, help: impl Into<String>) -> Self {
152        self.help = Some(help.into());
153        self
154    }
155
156    /// Whether this is an error.
157    pub fn is_error(&self) -> bool {
158        self.severity == Severity::Error
159    }
160
161    /// Render in a rustc-like format.
162    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        // Very long lines (minified or generated input) show a window around the span.
181        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}