Skip to main content

solidity_language_server/
build.rs

1use crate::utils::byte_offset_to_position;
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    errors
25        .iter()
26        .filter_map(|err| parse_diagnostic(err, path, content))
27        .collect()
28}
29
30/// Check whether the source path from forge's error output refers to the same
31/// file the editor has open.
32///
33/// Forge reports error paths relative to its working directory (wherever the
34/// LSP process runs from), e.g. `example/Shop.sol` or just `Shop.sol`.  The
35/// editor provides the full absolute path.  We simply check whether the
36/// absolute path ends with the relative path forge reported.
37fn source_location_matches(source_path: &str, path: &Path) -> bool {
38    let source_path = Path::new(source_path);
39    if source_path.is_absolute() {
40        source_path == path
41    } else {
42        path.ends_with(source_path)
43    }
44}
45
46fn parse_diagnostic(err: &Value, path: &Path, content: &str) -> Option<Diagnostic> {
47    if ignored_error_code_warning(err) {
48        return None;
49    }
50    let source_file = err
51        .get("sourceLocation")
52        .and_then(|loc| loc.get("file"))
53        .and_then(|f| f.as_str())?;
54
55    if !source_location_matches(source_file, path) {
56        return None;
57    }
58
59    let start_offset = err
60        .get("sourceLocation")
61        .and_then(|loc| loc.get("start"))
62        .and_then(|s| s.as_u64())
63        .unwrap_or(0) as usize;
64
65    let end_offset = err
66        .get("sourceLocation")
67        .and_then(|loc| loc.get("end"))
68        .and_then(|s| s.as_u64())
69        .map(|v| v as usize)
70        .unwrap_or(start_offset);
71
72    let (start_line, start_col) = byte_offset_to_position(content, start_offset);
73    let (end_line, end_col) = byte_offset_to_position(content, end_offset);
74
75    let range = Range {
76        start: Position {
77            line: start_line,
78            character: start_col,
79        },
80        end: Position {
81            line: end_line,
82            character: end_col,
83        },
84    };
85
86    let message = err
87        .get("message")
88        .and_then(|m| m.as_str())
89        .unwrap_or("Unknown error");
90
91    let severity = match err.get("severity").and_then(|s| s.as_str()) {
92        Some("error") => Some(DiagnosticSeverity::ERROR),
93        Some("warning") => Some(DiagnosticSeverity::WARNING),
94        Some("note") => Some(DiagnosticSeverity::INFORMATION),
95        Some("help") => Some(DiagnosticSeverity::HINT),
96        _ => Some(DiagnosticSeverity::INFORMATION),
97    };
98
99    let code = err
100        .get("errorCode")
101        .and_then(|c| c.as_str())
102        .map(|s| NumberOrString::String(s.to_string()));
103
104    Some(Diagnostic {
105        range,
106        severity,
107        code,
108        code_description: None,
109        source: Some("forge-build".to_string()),
110        message: format!("[forge build] {message}"),
111        related_information: None,
112        tags: None,
113        data: None,
114    })
115}