Skip to main content

rajac_diagnostics/
render_diagnostic.rs

1use crate::diagnostic::Diagnostic;
2use crate::severity::Severity;
3use rajac_base::shared_string::SharedString;
4
5#[allow(unused_imports)]
6use crate::source_chunk::SourceChunk;
7
8pub fn render_diagnostic(diagnostic: &Diagnostic) -> SharedString {
9    render_diagnostics(std::iter::once(diagnostic))
10}
11
12pub fn render_diagnostics<'a>(
13    diagnostics: impl IntoIterator<Item = &'a Diagnostic>,
14) -> SharedString {
15    use annotate_snippets::{
16        AnnotationKind, Group, Level, Renderer, Snippet, renderer::DecorStyle,
17    };
18
19    let mut groups = Vec::new();
20
21    for diagnostic in diagnostics {
22        let level = match diagnostic.severity {
23            Severity::Error => Level::ERROR,
24            Severity::Warning => Level::WARNING,
25            Severity::Note => Level::NOTE,
26            Severity::Help => Level::HELP,
27        };
28
29        let title = level.primary_title(&*diagnostic.message);
30
31        let mut group = Group::with_title(title);
32
33        for chunk in &diagnostic.chunks {
34            let path = chunk.path.as_str().to_string();
35            let mut snippet: Snippet<'static, annotate_snippets::Annotation<'static>> =
36                Snippet::source(chunk.fragment.as_str().to_string())
37                    .line_start(chunk.line)
38                    .path(path);
39
40            for annotation in &chunk.annotations {
41                snippet = snippet.annotation(
42                    AnnotationKind::Primary
43                        .span(annotation.span.0.clone())
44                        .label(annotation.message.as_str().to_string()),
45                );
46            }
47
48            group = group.element(snippet);
49        }
50
51        groups.push(group);
52    }
53
54    let renderer = Renderer::styled().decor_style(DecorStyle::Unicode);
55    SharedString::from(renderer.render(&groups).to_string())
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::annotation::Annotation;
62    use crate::span::Span;
63    use expect_test::expect;
64    use rajac_base::file_path::FilePath;
65    use strip_ansi::strip_ansi;
66
67    #[test]
68    /// Tests that an error diagnostic is rendered correctly.
69    fn test_render_error() {
70        let diagnostic = Diagnostic {
71            severity: Severity::Error,
72            message: "expected type, found `i32`".into(),
73            chunks: vec![SourceChunk {
74                path: FilePath::new("test.java"),
75                fragment: "let x: String = 42;".into(),
76                offset: 0,
77                line: 1,
78                annotations: vec![],
79            }],
80        };
81
82        let output = render_diagnostic(&diagnostic);
83        let stripped = strip_ansi(&output);
84
85        let expected = expect![[r#"
86            error: expected type, found `i32`
87              ╭▸ test.java
88              │"#]];
89        expected.assert_eq(&stripped);
90    }
91
92    #[test]
93    /// Tests that a warning diagnostic is rendered correctly.
94    fn test_render_warning() {
95        let diagnostic = Diagnostic {
96            severity: Severity::Warning,
97            message: "unused variable `x`".into(),
98            chunks: vec![SourceChunk {
99                path: FilePath::new("test.java"),
100                fragment: "let x = 42;".into(),
101                offset: 0,
102                line: 5,
103                annotations: vec![],
104            }],
105        };
106
107        let output = render_diagnostic(&diagnostic);
108        let stripped = strip_ansi(&output);
109
110        let expected = expect![[r#"
111            warning: unused variable `x`
112              ╭▸ test.java
113              │"#]];
114        expected.assert_eq(&stripped);
115    }
116
117    #[test]
118    /// Tests rendering a diagnostic with an annotation.
119    fn test_render_with_annotation() {
120        let diagnostic = Diagnostic {
121            severity: Severity::Error,
122            message: "mismatched types".into(),
123            chunks: vec![SourceChunk {
124                path: FilePath::new("test.java"),
125                fragment: "let x: String = 42;".into(),
126                offset: 0,
127                line: 1,
128                annotations: vec![Annotation {
129                    span: Span(9..15),
130                    message: "expected `String` but found `i32`".into(),
131                }],
132            }],
133        };
134
135        let output = render_diagnostic(&diagnostic);
136        let stripped = strip_ansi(&output);
137
138        let expected = expect![[r#"
139            error: mismatched types
140              ╭▸ test.java:1:10
141142            1 │ let x: String = 42;
143              ╰╴         ━━━━━━ expected `String` but found `i32`"#]];
144        expected.assert_eq(&stripped);
145    }
146
147    #[test]
148    /// Tests rendering a diagnostic with multiple chunks.
149    fn test_render_multiple_chunks() {
150        let diagnostic = Diagnostic {
151            severity: Severity::Error,
152            message: "undefined variable".into(),
153            chunks: vec![
154                SourceChunk {
155                    path: FilePath::new("main.java"),
156                    fragment: "fn main() {}".into(),
157                    offset: 0,
158                    line: 1,
159                    annotations: vec![],
160                },
161                SourceChunk {
162                    path: FilePath::new("main.java"),
163                    fragment: "    x;".into(),
164                    offset: 12,
165                    line: 2,
166                    annotations: vec![],
167                },
168            ],
169        };
170
171        let output = render_diagnostic(&diagnostic);
172        let stripped = strip_ansi(&output);
173
174        let expected = expect![[r#"
175            error: undefined variable
176              ╭▸ main.java
177178179              ⸬  main.java
180              │"#]];
181        expected.assert_eq(&stripped);
182    }
183}