Skip to main content

solidity_language_server/
build.rs

1use crate::utils::{byte_offset_to_position, find_project_root};
2use serde_json::Value;
3use std::path::Path;
4use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range};
5
6pub fn ignored_error_code_warning(value: &serde_json::Value) -> bool {
7    let error_code = value
8        .get("errorCode")
9        .and_then(|v| v.as_str())
10        .unwrap_or_default();
11
12    error_code == "5574" || error_code == "3860"
13}
14
15pub fn build_output_to_diagnostics(
16    forge_output: &serde_json::Value,
17    path: impl AsRef<Path>,
18    content: &str,
19) -> Vec<Diagnostic> {
20    let Some(errors) = forge_output.get("errors").and_then(|v| v.as_array()) else {
21        return Vec::new();
22    };
23    let path = path.as_ref();
24    let project_root = find_project_root(path);
25    errors
26        .iter()
27        .filter_map(|err| parse_diagnostic(err, path, project_root.as_deref(), content))
28        .collect()
29}
30
31fn source_location_matches(source_path: &str, path: &Path, project_root: Option<&Path>) -> bool {
32    let source_path = Path::new(source_path);
33    // source_file can be absolute or relative to the project root.
34    // path is the absolute path from the LSP client.
35    if source_path.is_absolute() {
36        source_path == path
37    } else if let Some(root) = project_root {
38        // Make path relative to the project root and compare with forge's relative path
39        path.strip_prefix(root)
40            .map(|rel| rel == source_path)
41            .unwrap_or(false)
42    } else {
43        // Fallback: compare filenames only
44        source_path.file_name() == path.file_name()
45    }
46}
47
48fn parse_diagnostic(
49    err: &Value,
50    path: &Path,
51    project_root: Option<&Path>,
52    content: &str,
53) -> Option<Diagnostic> {
54    if ignored_error_code_warning(err) {
55        return None;
56    }
57    let source_file = err
58        .get("sourceLocation")
59        .and_then(|loc| loc.get("file"))
60        .and_then(|f| f.as_str())?;
61
62    if !source_location_matches(source_file, path, project_root) {
63        return None;
64    }
65
66    let start_offset = err
67        .get("sourceLocation")
68        .and_then(|loc| loc.get("start"))
69        .and_then(|s| s.as_u64())
70        .unwrap_or(0) as usize;
71
72    let end_offset = err
73        .get("sourceLocation")
74        .and_then(|loc| loc.get("end"))
75        .and_then(|s| s.as_u64())
76        .map(|v| v as usize)
77        .unwrap_or(start_offset);
78
79    let (start_line, start_col) = byte_offset_to_position(content, start_offset);
80    let (end_line, end_col) = byte_offset_to_position(content, end_offset);
81
82    let range = Range {
83        start: Position {
84            line: start_line,
85            character: start_col,
86        },
87        end: Position {
88            line: end_line,
89            character: end_col,
90        },
91    };
92
93    let message = err
94        .get("message")
95        .and_then(|m| m.as_str())
96        .unwrap_or("Unknown error");
97
98    let severity = match err.get("severity").and_then(|s| s.as_str()) {
99        Some("error") => Some(DiagnosticSeverity::ERROR),
100        Some("warning") => Some(DiagnosticSeverity::WARNING),
101        Some("note") => Some(DiagnosticSeverity::INFORMATION),
102        Some("help") => Some(DiagnosticSeverity::HINT),
103        _ => Some(DiagnosticSeverity::INFORMATION),
104    };
105
106    let code = err
107        .get("errorCode")
108        .and_then(|c| c.as_str())
109        .map(|s| NumberOrString::String(s.to_string()));
110
111    Some(Diagnostic {
112        range,
113        severity,
114        code,
115        code_description: None,
116        source: Some("forge-build".to_string()),
117        message: format!("[forge build] {message}"),
118        related_information: None,
119        tags: None,
120        data: None,
121    })
122}