pretty_lint/
lib.rs

1//! A library for pretty-printing lint errors with a given source text.
2//!
3//! The API is fairly minimal, and the output closely resembles rustc's.
4//!
5//! # Example usage:
6//!
7//! ```edition2018
8//! # use pretty_lint::{Span, PrettyLint};
9//! let src = "highlight me";
10//!
11//! let lint = PrettyLint::success(src)
12//!             .at(Span::range((1, 1), (1, src.len())))
13//!             .with_message("you have been highlighted")
14//!             .with_inline_message("look at this")
15//!             .with_file_path(file!());
16//!
17//! println!("{}", lint);
18//! ```
19//!
20
21use colored::Colorize;
22
23// Reexport for color control.
24pub use colored;
25
26/// Position in a source document 1-based.
27#[derive(Debug, Default)]
28pub struct Position {
29    pub line: usize,
30    pub col: usize,
31}
32
33impl Position {
34    pub fn new(line: usize, col: usize) -> Self {
35        Self { col, line }
36    }
37}
38
39/// The range the lint spans.
40#[derive(Debug, Default)]
41pub struct Span {
42    pub start: Position,
43    pub end: Position,
44}
45
46impl Span {
47    pub fn new(start: Position, end: Position) -> Self {
48        Self { start, end }
49    }
50
51    pub fn range(start: (usize, usize), end: (usize, usize)) -> Self {
52        Self {
53            start: Position::new(start.0, start.1),
54            end: Position::new(end.0, end.1),
55        }
56    }
57}
58
59/// Lint severity.
60#[derive(Debug)]
61pub enum Severity {
62    Error,
63    Warning,
64    Info,
65    Success,
66}
67
68/// Show a pretty lint message similar to rustc's messages.
69#[derive(Debug)]
70pub struct PrettyLint<'l> {
71    source: &'l str,
72    severity: Severity,
73    lint_code: Option<&'l str>,
74    file_path: Option<&'l str>,
75    span: Span,
76    message: Option<&'l str>,
77    inline_message: Option<&'l str>,
78    notes: &'l [&'l str],
79
80    is_additional: bool,
81
82    additional_lints: Vec<PrettyLint<'l>>,
83}
84
85impl<'l> PrettyLint<'l> {
86    pub fn new(source: &'l str) -> Self {
87        PrettyLint {
88            lint_code: None,
89            file_path: None,
90            inline_message: None,
91            message: None,
92            severity: Severity::Error,
93            span: Span::default(),
94            source,
95            notes: &[],
96            is_additional: false,
97            additional_lints: Vec::new(),
98        }
99    }
100
101    pub fn error(source: &'l str) -> Self {
102        PrettyLint::new(source).with_severity(Severity::Error)
103    }
104
105    pub fn warning(source: &'l str) -> Self {
106        PrettyLint::new(source).with_severity(Severity::Warning)
107    }
108
109    pub fn info(source: &'l str) -> Self {
110        PrettyLint::new(source).with_severity(Severity::Info)
111    }
112
113    pub fn success(source: &'l str) -> Self {
114        PrettyLint::new(source).with_severity(Severity::Success)
115    }
116
117    pub fn with_message(mut self, message: &'l str) -> Self {
118        self.message = Some(message);
119        self
120    }
121
122    pub fn with_code(mut self, code: &'l str) -> Self {
123        self.lint_code = Some(code);
124        self
125    }
126
127    pub fn with_severity(mut self, severity: Severity) -> Self {
128        self.severity = severity;
129        self
130    }
131
132    pub fn with_file_path(mut self, file_path: &'l str) -> Self {
133        self.file_path = Some(file_path);
134        self
135    }
136
137    pub fn with_inline_message(mut self, msg: &'l str) -> Self {
138        self.inline_message = Some(msg);
139        self
140    }
141
142    pub fn with_notes(mut self, notes: &'l [&'l str]) -> Self {
143        self.notes = notes;
144        self
145    }
146
147    pub fn at(mut self, span: Span) -> Self {
148        self.span = span;
149        self
150    }
151
152    pub fn and(mut self, mut lint: PrettyLint<'l>) -> Self {
153        lint.is_additional = true;
154        self.additional_lints.push(lint);
155        self
156    }
157}
158
159impl core::fmt::Display for PrettyLint<'_> {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        let multiline = self.span.start.line != self.span.end.line;
162
163        if let (Some(message), false) = (self.message, self.is_additional) {
164            match self.severity {
165                Severity::Error => "error".red().bold().fmt(f)?,
166                Severity::Warning => "warning".yellow().bold().fmt(f)?,
167                Severity::Info => "info".cyan().bold().fmt(f)?,
168                Severity::Success => "success".green().bold().fmt(f)?,
169            };
170
171            if let Some(code) = self.lint_code {
172                if !code.is_empty() {
173                    match self.severity {
174                        Severity::Error => {
175                            "(".red().bold().fmt(f)?;
176                            code.red().bold().fmt(f)?;
177                            ")".red().bold().fmt(f)?;
178                        }
179                        Severity::Warning => {
180                            "(".yellow().bold().fmt(f)?;
181                            code.yellow().bold().fmt(f)?;
182                            ")".yellow().bold().fmt(f)?;
183                        }
184                        Severity::Info => {
185                            "(".cyan().bold().fmt(f)?;
186                            code.cyan().bold().fmt(f)?;
187                            ")".cyan().bold().fmt(f)?;
188                        }
189                        Severity::Success => {
190                            "(".green().bold().fmt(f)?;
191                            code.green().bold().fmt(f)?;
192                            ")".green().bold().fmt(f)?;
193                        }
194                    };
195                }
196            }
197
198            ": ".bold().fmt(f)?;
199            message.bold().fmt(f)?;
200            f.write_str("\n")?;
201        }
202
203        let mut line_digits = digits(usize::max(self.span.start.line, self.span.end.line));
204
205        if multiline {
206            line_digits = usize::max(3, line_digits);
207        }
208
209        if !self.additional_lints.is_empty() {
210            for add in &self.additional_lints {
211                line_digits = usize::max(line_digits, digits(add.span.start.line));
212                line_digits = usize::max(line_digits, digits(add.span.end.line));
213            }
214        }
215
216        let num_padding = " ".repeat(line_digits);
217        let padding_sep = " | ".blue().bold();
218
219        let padding_sep_span = match self.severity {
220            Severity::Error => format!("{}{}", " |".blue().bold(), "|".red()),
221            Severity::Warning => format!("{}{}", " |".blue().bold(), "|".yellow()),
222            Severity::Info => format!("{}{}", " |".blue().bold(), "|".cyan()),
223            Severity::Success => format!("{}{}", " |".blue().bold(), "|".green()),
224        };
225
226        f.write_str(&num_padding)?;
227        if self.is_additional {
228            "... ".blue().bold().fmt(f)?;
229        } else {
230            "--> ".blue().bold().fmt(f)?;
231        }
232        if let Some(fpath) = self.file_path {
233            if !fpath.is_empty() {
234                f.write_str(fpath)?;
235                f.write_str(":")?;
236            }
237        }
238        f.write_str(&format!("{}:{}", self.span.start.line, self.span.start.col))?;
239        f.write_str("\n")?;
240
241        f.write_str(&num_padding)?;
242        padding_sep.fmt(f)?;
243        f.write_str("\n")?;
244
245        for (i, line) in self.source.split('\n').enumerate() {
246            let line_number = i + 1;
247
248            if line_number == self.span.start.line {
249                write!(
250                    f,
251                    "{: ^size$}",
252                    line_number.to_string().blue().bold(),
253                    size = line_digits
254                )?;
255
256                padding_sep.fmt(f)?;
257                f.write_str(line)?;
258                f.write_str("\n")?;
259            }
260
261            if line_number > self.span.end.line {
262                break;
263            }
264
265            if !multiline {
266                continue;
267            }
268
269            if line_number == self.span.start.line + 1 {
270                num_padding.fmt(f)?;
271                padding_sep.fmt(f)?;
272                for _ in 0..self.span.start.col.checked_sub(1).unwrap_or(0) {
273                    match self.severity {
274                        Severity::Error => {
275                            "_".red().fmt(f)?;
276                        }
277                        Severity::Warning => {
278                            "_".yellow().fmt(f)?;
279                        }
280                        Severity::Info => {
281                            "_".cyan().fmt(f)?;
282                        }
283                        Severity::Success => {
284                            "_".green().fmt(f)?;
285                        }
286                    }
287                }
288
289                match self.severity {
290                    Severity::Error => {
291                        "^".red().bold().fmt(f)?;
292                    }
293                    Severity::Warning => {
294                        "^".yellow().bold().fmt(f)?;
295                    }
296                    Severity::Info => {
297                        "^".cyan().bold().fmt(f)?;
298                    }
299                    Severity::Success => {
300                        "^".green().bold().fmt(f)?;
301                    }
302                }
303            }
304
305            if line_number == self.span.end.line {
306                if line_number - self.span.start.line > 1 {
307                    f.write_str("\n")?;
308                    ".".repeat(line_digits).bold().blue().fmt(f)?;
309                    padding_sep_span.fmt(f)?;
310                    f.write_str("\n")?;
311
312                    num_padding.fmt(f)?;
313                    padding_sep_span.fmt(f)?;
314                }
315                f.write_str("\n")?;
316
317                write!(
318                    f,
319                    "{: ^size$}",
320                    line_number.to_string().blue().bold(),
321                    size = line_digits
322                )?;
323                padding_sep_span.fmt(f)?;
324                f.write_str(line)?;
325                f.write_str("\n")?;
326
327                num_padding.fmt(f)?;
328                padding_sep_span.fmt(f)?;
329
330                for _ in 0..self.span.end.col.checked_sub(1).unwrap_or(0) {
331                    match self.severity {
332                        Severity::Error => {
333                            "_".red().fmt(f)?;
334                        }
335                        Severity::Warning => {
336                            "_".yellow().fmt(f)?;
337                        }
338                        Severity::Info => {
339                            "_".cyan().fmt(f)?;
340                        }
341                        Severity::Success => {
342                            "_".green().fmt(f)?;
343                        }
344                    }
345                }
346
347                match self.severity {
348                    Severity::Error => {
349                        "^".red().bold().fmt(f)?;
350                    }
351                    Severity::Warning => {
352                        "^".yellow().bold().fmt(f)?;
353                    }
354                    Severity::Info => {
355                        "^".cyan().bold().fmt(f)?;
356                    }
357                    Severity::Success => {
358                        "^".green().bold().fmt(f)?;
359                    }
360                }
361            }
362        }
363
364        if !multiline {
365            let error_len = self
366                .span
367                .end
368                .col
369                .checked_sub(self.span.start.col)
370                .unwrap_or(1);
371
372            f.write_str(&num_padding)?;
373            padding_sep.fmt(f)?;
374
375            for _ in 0..self.span.start.col.checked_sub(1).unwrap_or(0) {
376                f.write_str(" ")?;
377            }
378
379            for _ in 0..=error_len {
380                match self.severity {
381                    Severity::Error => {
382                        "^".red().bold().fmt(f)?;
383                    }
384                    Severity::Warning => {
385                        "^".yellow().bold().fmt(f)?;
386                    }
387                    Severity::Info => {
388                        "^".cyan().bold().fmt(f)?;
389                    }
390                    Severity::Success => {
391                        "^".green().bold().fmt(f)?;
392                    }
393                }
394            }
395        }
396
397        if let Some(msg) = self.inline_message {
398            f.write_str(" ")?;
399
400            match self.severity {
401                Severity::Error => {
402                    msg.bold().red().fmt(f)?;
403                }
404                Severity::Warning => {
405                    msg.bold().yellow().fmt(f)?;
406                }
407                Severity::Info => {
408                    msg.bold().cyan().fmt(f)?;
409                }
410                Severity::Success => {
411                    msg.bold().green().fmt(f)?;
412                }
413            }
414        }
415
416        if !self.additional_lints.is_empty() {
417            f.write_str("\n")?;
418            f.write_str(&num_padding)?;
419            padding_sep.fmt(f)?;
420            f.write_str("\n")?;
421            for add in &self.additional_lints {
422                add.fmt(f)?;
423            }
424        }
425
426        if !self.notes.is_empty() && !self.is_additional {
427            f.write_str("\n")?;
428            f.write_str(&num_padding)?;
429            padding_sep.fmt(f)?;
430            for note in self.notes {
431                f.write_str("\n")?;
432                num_padding.fmt(f)?;
433                " - ".bold().blue().fmt(f)?;
434                f.write_str(note)?;
435            }
436
437            for add in &self.additional_lints {
438                for note in add.notes {
439                    f.write_str("\n")?;
440                    num_padding.fmt(f)?;
441                    " - ".bold().blue().fmt(f)?;
442                    f.write_str(note)?;
443                }
444            }
445        }
446
447        Ok(())
448    }
449}
450
451/// Get the digit count of a number.
452/// Simplest way.
453fn digits(mut val: usize) -> usize {
454    if val == 0 {
455        return 1;
456    }
457
458    let mut count = 0;
459    while val != 0 {
460        val /= 10;
461        count += 1;
462    }
463
464    count
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn test_print() {
473        let src = r#"
474this is
475some text
476that is wrong
477"#;
478
479        let lint = PrettyLint::error(src)
480            .with_message("This is an error")
481            .with_file_path(file!())
482            .with_code("syntax")
483            .at(Span {
484                start: Position { line: 3, col: 1 },
485                end: Position { line: 3, col: 4 },
486            })
487            .with_inline_message("this is wrong")
488            .with_notes(&["This is a note", "This is another note"])
489            .and(
490                PrettyLint::info(src)
491                    .with_inline_message("stuff is here")
492                    .with_file_path(file!())
493                    .at(Span {
494                        start: Position { line: 2, col: 1 },
495                        end: Position { line: 2, col: 4 },
496                    }),
497            );
498
499        println!("{}", lint);
500    }
501}