Skip to main content

solidity_language_server/
lint.rs

1use serde::{Deserialize, Serialize};
2use std::path::Path;
3use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
4
5pub fn lint_output_to_diagnostics(
6    forge_output: &serde_json::Value,
7    target_file: &str,
8) -> Vec<Diagnostic> {
9    let mut diagnostics = Vec::new();
10
11    if let serde_json::Value::Array(items) = forge_output {
12        for item in items {
13            if let Ok(forge_diag) = serde_json::from_value::<ForgeDiagnostic>(item.clone()) {
14                // Only include diagnostics for the target file
15                for span in &forge_diag.spans {
16                    let target_path = Path::new(target_file)
17                        .canonicalize()
18                        .unwrap_or_else(|_| Path::new(target_file).to_path_buf());
19                    let span_path = Path::new(&span.file_name)
20                        .canonicalize()
21                        .unwrap_or_else(|_| Path::new(&span.file_name).to_path_buf());
22                    if target_path == span_path && span.is_primary {
23                        let diagnostic = Diagnostic {
24                            range: Range {
25                                start: Position {
26                                    line: (span.line_start - 1),        // LSP is 0-based
27                                    character: (span.column_start - 1), // LSP is 0-based
28                                },
29                                end: Position {
30                                    line: (span.line_end - 1),
31                                    character: (span.column_end - 1),
32                                },
33                            },
34                            severity: Some(match forge_diag.level.as_str() {
35                                "error" => DiagnosticSeverity::ERROR,
36                                "warning" => DiagnosticSeverity::WARNING,
37                                "note" => DiagnosticSeverity::INFORMATION,
38                                "help" => DiagnosticSeverity::HINT,
39                                _ => DiagnosticSeverity::INFORMATION,
40                            }),
41                            code: forge_diag.code.as_ref().map(|c| {
42                                tower_lsp::lsp_types::NumberOrString::String(c.code.clone())
43                            }),
44                            code_description: None,
45                            source: Some("forge-lint".to_string()),
46                            // forge occasionally emits diagnostics with an empty message field;
47                            // fall back to rendered output or the span label so LSP clients
48                            // that require a non-empty message (e.g. trunk.io) don't crash.
49                            message: if !forge_diag.message.is_empty() {
50                                forge_diag.message.clone()
51                            } else if let Some(rendered) = &forge_diag.rendered {
52                                rendered.clone()
53                            } else if let Some(label) = &span.label {
54                                label.clone()
55                            } else {
56                                "Lint warning".to_string()
57                            },
58                            related_information: None,
59                            tags: None,
60                            data: None,
61                        };
62                        diagnostics.push(diagnostic);
63                        break; // Only take the first primary span per diagnostic
64                    }
65                }
66            }
67        }
68    }
69
70    diagnostics
71}
72
73#[derive(Debug, Deserialize, Serialize)]
74pub struct ForgeDiagnostic {
75    #[serde(rename = "$message_type")]
76    pub message_type: String,
77    pub message: String,
78    pub code: Option<ForgeLintCode>,
79    pub level: String,
80    pub spans: Vec<ForgeLintSpan>,
81    pub children: Vec<ForgeLintChild>,
82    pub rendered: Option<String>,
83}
84
85#[derive(Debug, Deserialize, Serialize)]
86pub struct ForgeLintCode {
87    pub code: String,
88    pub explanation: Option<String>,
89}
90
91#[derive(Debug, Deserialize, Serialize)]
92pub struct ForgeLintSpan {
93    pub file_name: String,
94    pub byte_start: u32,
95    pub byte_end: u32,
96    pub line_start: u32,
97    pub line_end: u32,
98    pub column_start: u32,
99    pub column_end: u32,
100    pub is_primary: bool,
101    pub text: Vec<ForgeLintText>,
102    pub label: Option<String>,
103}
104
105#[derive(Debug, Deserialize, Serialize)]
106pub struct ForgeLintText {
107    pub text: String,
108    pub highlight_start: u32,
109    pub highlight_end: u32,
110}
111
112#[derive(Debug, Deserialize, Serialize)]
113pub struct ForgeLintChild {
114    pub message: String,
115    pub code: Option<String>,
116    pub level: String,
117    pub spans: Vec<ForgeLintSpan>,
118    pub children: Vec<ForgeLintChild>,
119    pub rendered: Option<String>,
120}