nu_lint/
output.rs

1use std::{fmt, fmt::Write};
2
3use miette::{Diagnostic, LabeledSpan, Report, SourceCode};
4use serde::Serialize;
5
6use crate::lint::{Severity, Violation};
7#[derive(Debug, Clone, Copy)]
8pub enum OutputFormat {
9    Text,
10    Json,
11    Github,
12}
13
14pub trait OutputFormatter {
15    fn format(&self, violations: &[Violation], source: &str) -> String;
16}
17
18#[derive(Debug, Default)]
19pub struct TextFormatter;
20
21impl OutputFormatter for TextFormatter {
22    fn format(&self, violations: &[Violation], _source: &str) -> String {
23        if violations.is_empty() {
24            return String::from("No violations found!");
25        }
26
27        let mut output = String::new();
28
29        for violation in violations {
30            let source_code = violation
31                .file
32                .as_ref()
33                .and_then(|path| std::fs::read_to_string(path).ok())
34                .unwrap_or_default();
35
36            let (line, column) = calculate_line_column(&source_code, violation.span.start);
37
38            if let Some(file_path) = &violation.file {
39                writeln!(output, "\x1b[1m{file_path}:{line}:{column}\x1b[0m").unwrap();
40            }
41
42            let diagnostic = ViolationDiagnostic {
43                violation: violation.clone(),
44                source_code,
45            };
46
47            let report = Report::new(diagnostic);
48            writeln!(output, "{report:?}").unwrap();
49        }
50
51        let summary = Summary::from_violations(violations);
52        writeln!(
53            output,
54            "\n{} error(s), {} warning(s), {} info",
55            summary.errors, summary.warnings, summary.info
56        )
57        .unwrap();
58
59        output
60    }
61}
62
63#[derive(Debug, Default)]
64pub struct JsonFormatter;
65
66impl OutputFormatter for JsonFormatter {
67    fn format(&self, violations: &[Violation], _source: &str) -> String {
68        let json_violations: Vec<JsonViolation> = violations
69            .iter()
70            .map(|violation| {
71                let source_code = violation
72                    .file
73                    .as_ref()
74                    .and_then(|path| std::fs::read_to_string(path).ok())
75                    .unwrap_or_default();
76
77                let (line, column) = calculate_line_column(&source_code, violation.span.start);
78
79                JsonViolation {
80                    rule_id: violation.rule_id.clone(),
81                    severity: violation.severity.to_string(),
82                    message: violation.message.clone(),
83                    file: violation.file.clone(),
84                    line,
85                    column,
86                    suggestion: violation.suggestion.clone(),
87                }
88            })
89            .collect();
90
91        let summary = Summary::from_violations(violations);
92        let output = JsonOutput {
93            violations: json_violations,
94            summary,
95        };
96
97        serde_json::to_string_pretty(&output).unwrap_or_default()
98    }
99}
100
101/// Calculate line and column number from byte offset in source
102/// Returns (line, column) as 1-indexed values
103fn calculate_line_column(source: &str, offset: usize) -> (usize, usize) {
104    let mut line = 1;
105    let mut column = 1;
106
107    for (pos, ch) in source.char_indices() {
108        if pos >= offset {
109            break;
110        }
111        if ch == '\n' {
112            line += 1;
113            column = 1;
114        } else {
115            column += 1;
116        }
117    }
118
119    (line, column)
120}
121
122#[derive(Debug, Clone)]
123struct ViolationDiagnostic {
124    violation: Violation,
125    source_code: String,
126}
127
128impl fmt::Display for ViolationDiagnostic {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "{}", self.violation.message)
131    }
132}
133
134impl std::error::Error for ViolationDiagnostic {}
135
136impl Diagnostic for ViolationDiagnostic {
137    fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
138        Some(Box::new(format!(
139            "{}({})",
140            self.violation.severity, self.violation.rule_id
141        )))
142    }
143
144    fn severity(&self) -> Option<miette::Severity> {
145        Some(match self.violation.severity {
146            Severity::Error => miette::Severity::Error,
147            Severity::Warning => miette::Severity::Warning,
148            Severity::Info => miette::Severity::Advice,
149        })
150    }
151
152    fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
153        self.violation
154            .suggestion
155            .as_deref()
156            .map(|s| Box::new(s) as Box<dyn fmt::Display>)
157    }
158
159    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
160        let span = self.violation.to_source_span();
161        Some(Box::new(std::iter::once(LabeledSpan::new(
162            Some(self.violation.message.clone()),
163            span.offset(),
164            span.len(),
165        ))))
166    }
167
168    fn source_code(&self) -> Option<&dyn SourceCode> {
169        Some(&self.source_code as &dyn SourceCode)
170    }
171}
172
173#[derive(Serialize)]
174pub struct JsonOutput {
175    pub violations: Vec<JsonViolation>,
176    pub summary: Summary,
177}
178
179#[derive(Serialize)]
180pub struct JsonViolation {
181    pub rule_id: String,
182    pub severity: String,
183    pub message: String,
184    pub file: Option<String>,
185    pub line: usize,
186    pub column: usize,
187    pub suggestion: Option<String>,
188}
189
190#[derive(Serialize)]
191pub struct Summary {
192    pub errors: usize,
193    pub warnings: usize,
194    pub info: usize,
195    pub files_checked: usize,
196}
197
198impl Summary {
199    #[must_use]
200    pub fn from_violations(violations: &[Violation]) -> Self {
201        let (errors, warnings, info) =
202            violations
203                .iter()
204                .fold((0, 0, 0), |(e, w, i), v| match v.severity {
205                    Severity::Error => (e + 1, w, i),
206                    Severity::Warning => (e, w + 1, i),
207                    Severity::Info => (e, w, i + 1),
208                });
209
210        Self {
211            errors,
212            warnings,
213            info,
214            files_checked: 1,
215        }
216    }
217}