language_reporting/
emitter.rs1use 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}