Lints and suggestions for the Nix programming language
at main 259 lines 7.4 kB view raw
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}