language_reporting/
emitter.rs

1use crate::components;
2use crate::diagnostic::Diagnostic;
3use crate::span::ReportingFiles;
4
5use log;
6use render_tree::{Component, Render, Stylesheet};
7use std::path::Path;
8use std::{fmt, io};
9use termcolor::WriteColor;
10
11pub fn emit<'doc, W, Files: ReportingFiles>(
12    writer: W,
13    files: &'doc Files,
14    diagnostic: &'doc Diagnostic<Files::Span>,
15    config: &'doc dyn Config,
16) -> io::Result<()>
17where
18    W: WriteColor,
19{
20    DiagnosticWriter { writer }.emit(DiagnosticData {
21        files,
22        diagnostic,
23        config,
24    })
25}
26
27struct DiagnosticWriter<W> {
28    writer: W,
29}
30
31impl<W> DiagnosticWriter<W>
32where
33    W: WriteColor,
34{
35    fn emit<'doc>(mut self, data: DiagnosticData<'doc, impl ReportingFiles>) -> io::Result<()> {
36        let document = Component(components::Diagnostic, data).into_fragment();
37
38        let styles = Stylesheet::new()
39            .add("** header **", "weight: bold")
40            .add("bug ** primary", "fg: red")
41            .add("error ** primary", "fg: red")
42            .add("warning ** primary", "fg: yellow")
43            .add("note ** primary", "fg: green")
44            .add("help ** primary", "fg: cyan")
45            .add("** secondary", "fg: blue")
46            .add("** gutter", "fg: blue");
47
48        if log::log_enabled!(log::Level::Debug) {
49            document.debug_write(&mut self.writer, &styles)?;
50        }
51
52        document.write_with(&mut self.writer, &styles)?;
53
54        Ok(())
55    }
56}
57
58pub trait Config: std::fmt::Debug {
59    fn filename(&self, path: &Path) -> String;
60}
61
62#[derive(Debug)]
63pub struct DefaultConfig;
64
65impl Config for DefaultConfig {
66    fn filename(&self, path: &Path) -> String {
67        format!("{}", path.display())
68    }
69}
70
71#[derive(Debug)]
72pub(crate) struct DiagnosticData<'doc, Files: ReportingFiles> {
73    pub(crate) files: &'doc Files,
74    pub(crate) diagnostic: &'doc Diagnostic<Files::Span>,
75    pub(crate) config: &'doc dyn Config,
76}
77
78pub fn format(f: impl Fn(&mut fmt::Formatter) -> fmt::Result) -> impl fmt::Display {
79    struct Display<F>(F);
80
81    impl<F> fmt::Display for Display<F>
82    where
83        F: Fn(&mut fmt::Formatter) -> fmt::Result,
84    {
85        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
86            (self.0)(f)
87        }
88    }
89    Display(f)
90}
91
92#[cfg(test)]
93mod default_emit_smoke_tests {
94    use super::*;
95    use crate::diagnostic::{Diagnostic, Label};
96    use crate::simple::*;
97    use crate::termcolor::Buffer;
98    use crate::Severity;
99
100    use regex;
101    use render_tree::stylesheet::ColorAccumulator;
102    use unindent::unindent;
103
104    fn emit_with_writer<W: WriteColor>(mut writer: W) -> W {
105        let mut files = SimpleReportingFiles::default();
106
107        let source = unindent(
108            r##"
109                (define test 123)
110                (+ test "")
111                ()
112            "##,
113        );
114
115        let file = files.add("test", source);
116
117        let str_start = files.byte_index(file, 1, 8).unwrap();
118        let error = Diagnostic::new(Severity::Error, "Unexpected type in `+` application")
119            .with_label(
120                Label::new_primary(SimpleSpan::new(file, str_start, str_start + 2))
121                    .with_message("Expected integer but got string"),
122            )
123            .with_label(
124                Label::new_secondary(SimpleSpan::new(file, str_start, str_start + 2))
125                    .with_message("Expected integer but got string"),
126            )
127            .with_code("E0001");
128
129        let line_start = files.byte_index(file, 1, 0).unwrap();
130        let warning = Diagnostic::new(
131            Severity::Warning,
132            "`+` function has no effect unless its result is used",
133        )
134        .with_label(Label::new_primary(SimpleSpan::new(
135            file,
136            line_start,
137            line_start + 11,
138        )));
139
140        let diagnostics = [error, warning];
141
142        for diagnostic in &diagnostics {
143            emit(&mut writer, &files, &diagnostic, &super::DefaultConfig).unwrap();
144        }
145
146        writer
147    }
148
149    #[test]
150    fn test_no_color() {
151        assert_eq!(
152            String::from_utf8_lossy(&emit_with_writer(Buffer::no_color()).into_inner()),
153            unindent(&format!(
154                r##"
155                    error[E0001]: Unexpected type in `+` application
156                    - test:2:9
157                    2 | (+ test "")
158                      |         ^^ Expected integer but got string
159                    - test:2:9
160                    2 | (+ test "")
161                      |         -- Expected integer but got string
162                    warning: `+` function has no effect unless its result is used
163                    - test:2:1
164                    2 | (+ test "")
165                      | ^^^^^^^^^^^
166                "##,
167            )),
168        );
169    }
170
171    #[cfg(windows)]
172    #[test]
173    fn test_color() {
174        assert_eq!(
175            emit_with_writer(ColorAccumulator::new()).to_string(),
176
177            normalize(
178                r#"
179                   {fg:Red bold bright} $$error[E0001]{bold bright}: Unexpected type in `+` application{/}
180                                        $$- test:2:9
181                              {fg:Cyan} $$2 | {/}(+ test {fg:Red}""{/})
182                              {fg:Cyan} $$  | {/}        {fg:Red}^^ Expected integer but got string{/}
183                                        $$- test:2:9
184                              {fg:Cyan} $$2 | {/}(+ test {fg:Cyan}""{/})
185                              {fg:Cyan} $$  | {/}        {fg:Cyan}-- Expected integer but got string{/}
186                {fg:Yellow bold bright} $$warning{bold bright}: `+` function has no effect unless its result is used{/}
187                                        $$- test:2:1
188                              {fg:Cyan} $$2 | {fg:Yellow}(+ test ""){/}
189                              {fg:Cyan} $$  | {fg:Yellow}^^^^^^^^^^^{/}
190            "#
191            )
192        );
193    }
194
195    #[cfg(not(windows))]
196    #[test]
197    fn test_color() {
198        assert_eq!(
199            emit_with_writer(ColorAccumulator::new()).to_string(),
200
201            normalize(
202                r#"
203                   {fg:Red bold bright} $$error[E0001]{bold bright}: Unexpected type in `+` application{/}
204                                        $$- test:2:9
205                              {fg:Blue} $$2 | {/}(+ test {fg:Red}""{/})
206                              {fg:Blue} $$  | {/}        {fg:Red}^^ Expected integer but got string{/}
207                                        $$- test:2:9
208                              {fg:Blue} $$2 | {/}(+ test {fg:Blue}""{/})
209                              {fg:Blue} $$  | {/}        {fg:Blue}-- Expected integer but got string{/}
210                {fg:Yellow bold bright} $$warning{bold bright}: `+` function has no effect unless its result is used{/}
211                                        $$- test:2:1
212                              {fg:Blue} $$2 | {fg:Yellow}(+ test ""){/}
213                              {fg:Blue} $$  | {fg:Yellow}^^^^^^^^^^^{/}
214            "#
215            )
216        );
217    }
218
219    fn split_line<'a>(line: &'a str, by: &str) -> (&'a str, &'a str) {
220        let mut splitter = line.splitn(2, by);
221        let first = splitter.next().unwrap_or("");
222        let second = splitter.next().unwrap_or("");
223        (first, second)
224    }
225
226    fn normalize(s: impl AsRef<str>) -> String {
227        let s = s.as_ref();
228        let s = unindent(s);
229
230        let regex = regex::Regex::new(r"\{-*\}").unwrap();
231
232        s.lines()
233            .map(|line| {
234                let (style, line) = split_line(line, " $$");
235                let line = regex.replace_all(&line, "").to_string();
236                format!("{style}{line}\n", style = style.trim(), line = line)
237            })
238            .collect()
239    }
240}