nu_lint/output/
vscode.rs

1use std::collections::HashMap;
2
3use serde::Serialize;
4
5use super::{Summary, calculate_line_column, read_source_code};
6use crate::{config::LintLevel, violation::Violation};
7
8fn lint_level_to_severity(lint_level: LintLevel) -> u8 {
9    match lint_level {
10        LintLevel::Deny => 1,
11        LintLevel::Warn => 2,
12        LintLevel::Allow => unreachable!("Allow level violations should never be created"),
13    }
14}
15
16#[must_use]
17pub fn format_vscode_json(violations: &[Violation]) -> String {
18    let mut diagnostics_by_file: HashMap<String, Vec<VsCodeDiagnostic>> = HashMap::new();
19
20    for violation in violations {
21        let file_path = violation
22            .file
23            .as_ref()
24            .map_or_else(|| "unknown".to_string(), ToString::to_string);
25        diagnostics_by_file
26            .entry(file_path)
27            .or_default()
28            .push(violation_to_vscode_diagnostic(violation));
29    }
30
31    let summary = Summary::from_violations(violations);
32    let output = VsCodeJsonOutput {
33        diagnostics: diagnostics_by_file,
34        summary,
35    };
36
37    serde_json::to_string_pretty(&output).unwrap_or_default()
38}
39
40fn violation_to_vscode_diagnostic(violation: &Violation) -> VsCodeDiagnostic {
41    let source_code = read_source_code(violation.file.as_ref());
42
43    let (line_start, column_start) = calculate_line_column(&source_code, violation.span.start);
44    let (line_end, column_end) = calculate_line_column(&source_code, violation.span.end);
45
46    let line_start_zero = line_start.saturating_sub(1);
47    let column_start_zero = column_start.saturating_sub(1);
48    let line_end_zero = line_end.saturating_sub(1);
49    let column_end_zero = column_end.saturating_sub(1);
50
51    VsCodeDiagnostic {
52        range: VsCodeRange {
53            start: VsCodePosition {
54                line: line_start_zero,
55                character: column_start_zero,
56            },
57            end: VsCodePosition {
58                line: line_end_zero,
59                character: column_end_zero,
60            },
61        },
62        severity: lint_level_to_severity(violation.lint_level),
63        code: violation.rule_id.to_string(),
64        source: "nu-lint".to_string(),
65        message: violation.message.to_string(),
66        related_information: violation.help.as_ref().map(|suggestion| {
67            vec![VsCodeRelatedInformation {
68                location: VsCodeLocation {
69                    uri: violation
70                        .file
71                        .as_ref()
72                        .map_or_else(|| "unknown".to_string(), ToString::to_string),
73                    range: VsCodeRange {
74                        start: VsCodePosition {
75                            line: line_start_zero,
76                            character: column_start_zero,
77                        },
78                        end: VsCodePosition {
79                            line: line_end_zero,
80                            character: column_end_zero,
81                        },
82                    },
83                },
84                message: suggestion.to_string(),
85            }]
86        }),
87        code_action: violation.fix.as_ref().map(|fix| VsCodeCodeAction {
88            title: fix.explanation.to_string(),
89            edits: fix
90                .replacements
91                .iter()
92                .map(|r| {
93                    let (r_line_start, r_col_start) =
94                        calculate_line_column(&source_code, r.span.start);
95                    let (r_line_end, r_col_end) = calculate_line_column(&source_code, r.span.end);
96                    VsCodeTextEdit {
97                        range: VsCodeRange {
98                            start: VsCodePosition {
99                                line: r_line_start.saturating_sub(1),
100                                character: r_col_start.saturating_sub(1),
101                            },
102                            end: VsCodePosition {
103                                line: r_line_end.saturating_sub(1),
104                                character: r_col_end.saturating_sub(1),
105                            },
106                        },
107                        replacement_text: r.replacement_text.to_string(),
108                    }
109                })
110                .collect(),
111        }),
112    }
113}
114
115#[derive(Serialize)]
116pub struct VsCodeJsonOutput {
117    pub diagnostics: HashMap<String, Vec<VsCodeDiagnostic>>,
118    pub summary: Summary,
119}
120
121#[derive(Serialize)]
122pub struct VsCodeDiagnostic {
123    pub range: VsCodeRange,
124    pub severity: u8,
125    pub code: String,
126    pub source: String,
127    pub message: String,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub related_information: Option<Vec<VsCodeRelatedInformation>>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub code_action: Option<VsCodeCodeAction>,
132}
133
134#[derive(Serialize)]
135pub struct VsCodeRange {
136    pub start: VsCodePosition,
137    pub end: VsCodePosition,
138}
139
140#[derive(Serialize)]
141pub struct VsCodePosition {
142    pub line: usize,
143    pub character: usize,
144}
145
146#[derive(Serialize)]
147pub struct VsCodeRelatedInformation {
148    pub location: VsCodeLocation,
149    pub message: String,
150}
151
152#[derive(Serialize)]
153pub struct VsCodeLocation {
154    pub uri: String,
155    pub range: VsCodeRange,
156}
157
158#[derive(Serialize)]
159pub struct VsCodeCodeAction {
160    pub title: String,
161    pub edits: Vec<VsCodeTextEdit>,
162}
163
164#[derive(Serialize)]
165pub struct VsCodeTextEdit {
166    pub range: VsCodeRange,
167    pub replacement_text: String,
168}