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.as_ref()).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.as_ref()).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.to_string(),
81                    severity: violation.severity.to_string(),
82                    message: violation.message.to_string(),
83                    file: violation
84                        .file
85                        .as_ref()
86                        .map(std::string::ToString::to_string),
87                    line,
88                    column,
89                    suggestion: violation
90                        .suggestion
91                        .as_ref()
92                        .map(std::string::ToString::to_string),
93                }
94            })
95            .collect();
96
97        let summary = Summary::from_violations(violations);
98        let output = JsonOutput {
99            violations: json_violations,
100            summary,
101        };
102
103        serde_json::to_string_pretty(&output).unwrap_or_default()
104    }
105}
106
107/// Calculate line and column number from byte offset in source
108/// Returns (line, column) as 1-indexed values
109fn calculate_line_column(source: &str, offset: usize) -> (usize, usize) {
110    let mut line = 1;
111    let mut column = 1;
112
113    for (pos, ch) in source.char_indices() {
114        if pos >= offset {
115            break;
116        }
117        if ch == '\n' {
118            line += 1;
119            column = 1;
120        } else {
121            column += 1;
122        }
123    }
124
125    (line, column)
126}
127
128#[derive(Debug, Clone)]
129struct ViolationDiagnostic {
130    violation: Violation,
131    source_code: String,
132}
133
134impl fmt::Display for ViolationDiagnostic {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(f, "{}", self.violation.message)
137    }
138}
139
140impl std::error::Error for ViolationDiagnostic {}
141
142impl Diagnostic for ViolationDiagnostic {
143    fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
144        Some(Box::new(format!(
145            "{}({})",
146            self.violation.severity, self.violation.rule_id
147        )))
148    }
149
150    fn severity(&self) -> Option<miette::Severity> {
151        Some(match self.violation.severity {
152            Severity::Error => miette::Severity::Error,
153            Severity::Warning => miette::Severity::Warning,
154            Severity::Info => miette::Severity::Advice,
155        })
156    }
157
158    fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
159        self.violation
160            .suggestion
161            .as_deref()
162            .map(|s| Box::new(s) as Box<dyn fmt::Display>)
163    }
164
165    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
166        let span = self.violation.to_source_span();
167        Some(Box::new(std::iter::once(LabeledSpan::new(
168            Some(self.violation.message.to_string()),
169            span.offset(),
170            span.len(),
171        ))))
172    }
173
174    fn source_code(&self) -> Option<&dyn SourceCode> {
175        Some(&self.source_code as &dyn SourceCode)
176    }
177}
178
179#[derive(Serialize)]
180pub struct JsonOutput {
181    pub violations: Vec<JsonViolation>,
182    pub summary: Summary,
183}
184
185#[derive(Serialize)]
186pub struct JsonViolation {
187    pub rule_id: String,
188    pub severity: String,
189    pub message: String,
190    pub file: Option<String>,
191    pub line: usize,
192    pub column: usize,
193    pub suggestion: Option<String>,
194}
195
196#[derive(Serialize)]
197pub struct Summary {
198    pub errors: usize,
199    pub warnings: usize,
200    pub info: usize,
201    pub files_checked: usize,
202}
203
204impl Summary {
205    #[must_use]
206    pub fn from_violations(violations: &[Violation]) -> Self {
207        let (errors, warnings, info) =
208            violations
209                .iter()
210                .fold((0, 0, 0), |(e, w, i), v| match v.severity {
211                    Severity::Error => (e + 1, w, i),
212                    Severity::Warning => (e, w + 1, i),
213                    Severity::Info => (e, w, i + 1),
214                });
215
216        Self {
217            errors,
218            warnings,
219            info,
220            files_checked: 1,
221        }
222    }
223}