rush_analyzer/
diagnostic.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Display, Formatter},
4};
5
6use rush_parser::{Error, Span};
7
8#[derive(PartialEq, Eq, Debug, Clone)]
9pub struct Diagnostic<'src> {
10    pub level: DiagnosticLevel,
11    pub message: Cow<'static, str>,
12    pub notes: Vec<Cow<'static, str>>,
13    pub span: Span<'src>,
14    pub source: &'src str,
15}
16
17impl<'src> From<Error<'src>> for Diagnostic<'src> {
18    fn from(err: Error<'src>) -> Self {
19        Self::new(
20            DiagnosticLevel::Error(ErrorKind::Syntax),
21            err.message,
22            vec![],
23            err.span,
24            err.source,
25        )
26    }
27}
28
29impl<'src> From<Box<Error<'src>>> for Diagnostic<'src> {
30    fn from(err: Box<Error<'src>>) -> Self {
31        Self::from(*err)
32    }
33}
34
35impl<'src> Diagnostic<'src> {
36    pub fn new(
37        level: DiagnosticLevel,
38        message: impl Into<Cow<'static, str>>,
39        notes: Vec<Cow<'static, str>>,
40        span: Span<'src>,
41        source: &'src str,
42    ) -> Self {
43        Self {
44            level,
45            message: message.into(),
46            notes,
47            span,
48            source,
49        }
50    }
51}
52
53impl Display for Diagnostic<'_> {
54    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
55        let ansi_col = |col: u8, bold: bool| -> Cow<'static, str> {
56            match (f.alternate(), bold) {
57                (true, true) => format!("\x1b[1;{col}m").into(),
58                (true, false) => format!("\x1b[{col}m").into(),
59                (false, _) => "".into(),
60            }
61        };
62        let ansi_reset = match f.alternate() {
63            true => "\x1b[0m",
64            false => "",
65        };
66
67        let lines: Vec<_> = self.source.split('\n').collect();
68
69        let (raw_marker, raw_marker_single, color) = match self.level {
70            DiagnosticLevel::Hint => ("~", "^", 5),     // magenta
71            DiagnosticLevel::Info => ("~", "^", 4),     // blue
72            DiagnosticLevel::Warning => ("~", "^", 3),  // yellow
73            DiagnosticLevel::Error(_) => ("^", "^", 1), // red
74        };
75
76        let notes: String = self
77            .notes
78            .iter()
79            .map(|note| {
80                format!(
81                    "\n {color}note:{ansi_reset} {note}",
82                    color = ansi_col(36, true),
83                )
84            })
85            .collect();
86
87        // take special action if the source code is empty or there is no useful span
88        if self.source.is_empty() || self.span.is_empty() {
89            return writeln!(
90                f,
91                " {color}{lvl}{reset_col} in {path}{ansi_reset} \n {msg}{notes}",
92                color = ansi_col(color + 30, true),
93                lvl = self.level,
94                reset_col = ansi_col(39, false),
95                path = self.span.start.path,
96                msg = self.message,
97            );
98        }
99
100        let line1 = match self.span.start.line > 1 {
101            true => format!(
102                "\n {}{: >3} | {ansi_reset}{}",
103                ansi_col(90, false),
104                self.span.start.line - 1,
105                lines[self.span.start.line - 2],
106            ),
107            false => String::new(),
108        };
109
110        let line2 = format!(
111            " {}{: >3} | {ansi_reset}{}",
112            ansi_col(90, false),
113            self.span.start.line,
114            lines[self.span.start.line - 1]
115        );
116
117        let line3 = match self.span.start.line < lines.len() {
118            true => format!(
119                "\n {}{: >3} | {ansi_reset}{}",
120                ansi_col(90, false),
121                self.span.start.line + 1,
122                lines[self.span.start.line]
123            ),
124            false => String::new(),
125        };
126
127        let markers = match (
128            self.span.start.line == self.span.end.line,
129            self.span.start.column + 1 == self.span.end.column,
130        ) {
131            // same line, wide column difference
132            (true, false) => raw_marker.repeat(self.span.end.column - self.span.start.column),
133            // same line, just one column difference
134            (true, true) => raw_marker_single.to_string(),
135            // multiline span
136            (_, _) => {
137                format!(
138                    "{marker} ...\n{space}{color}+ {line_count} more line{s}{ansi_reset}",
139                    marker = raw_marker
140                        .repeat(lines[self.span.start.line - 1].len() - self.span.start.column + 1),
141                    space = " ".repeat(self.span.start.column + 6),
142                    color = ansi_col(32, true),
143                    line_count = self.span.end.line - self.span.start.line,
144                    s = match self.span.end.line - self.span.start.line == 1 {
145                        true => "",
146                        false => "s",
147                    },
148                )
149            }
150        };
151
152        let marker = format!(
153            "{space}{color}{markers}{ansi_reset}",
154            color = ansi_col(color + 30, true),
155            space = " ".repeat(self.span.start.column + 6),
156        );
157
158        writeln!(
159            f,
160            " {color}{lvl}{reset_col} at {path}:{line}:{col}{ansi_reset}\n{line1}\n{line2}\n{marker}{line3}\n\n {color}{msg}{ansi_reset}{notes}",
161            color = ansi_col(color + 30, true),
162            lvl = self.level,
163            reset_col = ansi_col(39, false),
164            path = self.span.start.path,
165            line = self.span.start.line,
166            col = self.span.start.column,
167            msg = self.message,
168        )
169    }
170}
171
172#[derive(PartialEq, Eq, Debug, Clone)]
173pub enum DiagnosticLevel {
174    Hint,
175    Info,
176    Warning,
177    Error(ErrorKind),
178}
179
180impl Display for DiagnosticLevel {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        match self {
183            Self::Hint | Self::Info | Self::Warning => write!(f, "{self:?}"),
184            Self::Error(kind) => write!(f, "{kind}"),
185        }
186    }
187}
188
189#[derive(PartialEq, Eq, Debug, Clone)]
190pub enum ErrorKind {
191    Syntax,
192    Type,
193    Semantic,
194    Reference,
195}
196
197impl Display for ErrorKind {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        write!(f, "{self:?}Error")
200    }
201}