Lints and suggestions for the Nix programming language
1use std::{
2 io::{self, Write},
3 str,
4};
5
6use crate::{config::OutFormat, lint::LintResult};
7
8use ariadne::{
9 CharSet, Color, Config as CliConfig, Fmt, Label, LabelAttach, Report as CliReport,
10 ReportKind as CliReportKind, Source,
11};
12use lib::Severity;
13use rnix::{TextRange, TextSize};
14use vfs::ReadOnlyVfs;
15
16pub trait WriteDiagnostic {
17 fn write(
18 &mut self,
19 report: &LintResult,
20 vfs: &ReadOnlyVfs,
21 format: OutFormat,
22 ) -> io::Result<()>;
23}
24
25impl<T> WriteDiagnostic for T
26where
27 T: Write,
28{
29 fn write(
30 &mut self,
31 lint_result: &LintResult,
32 vfs: &ReadOnlyVfs,
33 format: OutFormat,
34 ) -> io::Result<()> {
35 match format {
36 #[cfg(feature = "json")]
37 OutFormat::Json => json::write_json(self, lint_result, vfs),
38 OutFormat::StdErr => write_stderr(self, lint_result, vfs),
39 OutFormat::Errfmt => write_errfmt(self, lint_result, vfs),
40 }
41 }
42}
43
44fn write_stderr<T: Write>(
45 writer: &mut T,
46 lint_result: &LintResult,
47 vfs: &ReadOnlyVfs,
48) -> io::Result<()> {
49 let file_id = lint_result.file_id;
50 let src = str::from_utf8(vfs.get(file_id)).unwrap();
51 let path = vfs.file_path(file_id);
52 let range = |at: TextRange| at.start().into()..at.end().into();
53 let src_id = path.to_str().unwrap_or("<unknown>");
54 for report in &lint_result.reports {
55 let offset = report
56 .diagnostics
57 .iter()
58 .map(|d| d.at.start().into())
59 .min()
60 .unwrap_or(0usize);
61 let report_kind = match report.severity {
62 Severity::Warn => CliReportKind::Warning,
63 Severity::Error => CliReportKind::Error,
64 Severity::Hint => CliReportKind::Advice,
65 };
66 report
67 .diagnostics
68 .iter()
69 .fold(
70 CliReport::build(report_kind, src_id, offset)
71 .with_config(
72 CliConfig::default()
73 .with_cross_gap(true)
74 .with_multiline_arrows(false)
75 .with_label_attach(LabelAttach::Middle)
76 .with_char_set(CharSet::Unicode),
77 )
78 .with_message(report.note)
79 .with_code(report.code),
80 |cli_report, diagnostic| {
81 cli_report.with_label(
82 Label::new((src_id, range(diagnostic.at)))
83 .with_message(colorize(&diagnostic.message))
84 .with_color(Color::Magenta),
85 )
86 },
87 )
88 .finish()
89 .write((src_id, Source::from(src)), &mut *writer)?;
90 }
91 Ok(())
92}
93
94fn write_errfmt<T: Write>(
95 writer: &mut T,
96 lint_result: &LintResult,
97 vfs: &ReadOnlyVfs,
98) -> io::Result<()> {
99 let file_id = lint_result.file_id;
100 let src = str::from_utf8(vfs.get(file_id)).unwrap();
101 let path = vfs.file_path(file_id);
102 for report in &lint_result.reports {
103 for diagnostic in &report.diagnostics {
104 let line = line(diagnostic.at.start(), src);
105 let col = column(diagnostic.at.start(), src);
106 writeln!(
107 writer,
108 "{filename}>{linenumber}:{columnnumber}:{errortype}:{errornumber}:{errormessage}",
109 filename = path.to_str().unwrap_or("<unknown>"),
110 linenumber = line,
111 columnnumber = col,
112 errortype = match report.severity {
113 Severity::Warn => "W",
114 Severity::Error => "E",
115 Severity::Hint => "I", /* "info" message */
116 },
117 errornumber = report.code,
118 errormessage = diagnostic.message
119 )?;
120 }
121 }
122 Ok(())
123}
124
125#[cfg(feature = "json")]
126mod json {
127 use crate::lint::LintResult;
128
129 use std::io::{self, Write};
130
131 use lib::Severity;
132 use rnix::TextRange;
133 use serde::Serialize;
134 use vfs::ReadOnlyVfs;
135
136 #[derive(Serialize)]
137 struct Out<'μ> {
138 #[serde(rename = "file")]
139 path: &'μ std::path::Path,
140 report: Vec<JsonReport<'μ>>,
141 }
142
143 #[derive(Serialize)]
144 struct JsonReport<'μ> {
145 note: &'static str,
146 code: u32,
147 severity: &'μ Severity,
148 diagnostics: Vec<JsonDiagnostic<'μ>>,
149 }
150
151 #[derive(Serialize)]
152 struct JsonDiagnostic<'μ> {
153 at: JsonSpan,
154 message: &'μ String,
155 suggestion: Option<JsonSuggestion>,
156 }
157
158 #[derive(Serialize)]
159 struct JsonSuggestion {
160 at: JsonSpan,
161 fix: String,
162 }
163
164 #[derive(Serialize)]
165 struct JsonSpan {
166 from: Position,
167 to: Position,
168 }
169
170 #[derive(Serialize)]
171 struct Position {
172 line: usize,
173 column: usize,
174 }
175
176 impl JsonSpan {
177 fn from_textrange(at: TextRange, src: &str) -> Self {
178 let start = at.start();
179 let end = at.end();
180 let from = Position {
181 line: super::line(start, src),
182 column: super::column(start, src),
183 };
184 let to = Position {
185 line: super::line(end, src),
186 column: super::column(end, src),
187 };
188 Self { from, to }
189 }
190 }
191
192 pub fn write_json<T: Write>(
193 writer: &mut T,
194 lint_result: &LintResult,
195 vfs: &ReadOnlyVfs,
196 ) -> io::Result<()> {
197 let file_id = lint_result.file_id;
198 let path = vfs.file_path(file_id);
199 let src = vfs.get_str(file_id);
200 let report = lint_result
201 .reports
202 .iter()
203 .map(|r| {
204 let note = r.note;
205 let code = r.code;
206 let severity = &r.severity;
207 let diagnostics = r
208 .diagnostics
209 .iter()
210 .map(|d| JsonDiagnostic {
211 at: JsonSpan::from_textrange(d.at, src),
212 message: &d.message,
213 suggestion: d.suggestion.as_ref().map(|s| JsonSuggestion {
214 at: JsonSpan::from_textrange(s.at, src),
215 fix: s.fix.to_string(),
216 }),
217 })
218 .collect::<Vec<_>>();
219 JsonReport {
220 note,
221 code,
222 severity,
223 diagnostics,
224 }
225 })
226 .collect();
227 writeln!(
228 writer,
229 "{}",
230 serde_json::to_string_pretty(&Out { path, report }).unwrap()
231 )?;
232 Ok(())
233 }
234}
235
236fn line(at: TextSize, src: &str) -> usize {
237 let at = at.into();
238 src[..at].chars().filter(|&c| c == '\n').count() + 1
239}
240
241fn column(at: TextSize, src: &str) -> usize {
242 let at = at.into();
243 src[..at].rfind('\n').map_or_else(|| at + 1, |c| at - c)
244}
245
246// everything within backticks is colorized, backticks are removed
247fn colorize(message: &str) -> String {
248 message
249 .split('`')
250 .enumerate()
251 .map(|(idx, part)| {
252 if idx % 2 == 1 {
253 part.fg(Color::Cyan).to_string()
254 } else {
255 part.to_string()
256 }
257 })
258 .collect::<String>()
259}