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, Range};
5
6/// Default error codes that are always suppressed (contract-size and
7/// code-size warnings that are noisy for LSP users).
8const DEFAULT_IGNORED_CODES: &[&str] = &["5574", "3860"];
9
10/// Check whether a solc error should be suppressed based on its error code.
11///
12/// Suppresses the hardcoded defaults plus any codes provided in `extra_codes`
13/// (from `foundry.toml` `ignored_error_codes`).
14pub fn ignored_error_code_warning(value: &serde_json::Value, extra_codes: &[u64]) -> bool {
15    let error_code = value
16        .get("errorCode")
17        .and_then(|v| v.as_str())
18        .unwrap_or_default();
19
20    if DEFAULT_IGNORED_CODES.contains(&error_code) {
21        return true;
22    }
23
24    // Check user-configured ignored codes from foundry.toml
25    if let Ok(code_num) = error_code.parse::<u64>() {
26        if extra_codes.contains(&code_num) {
27            return true;
28        }
29    }
30
31    false
32}
33
34pub fn build_output_to_diagnostics(
35    forge_output: &serde_json::Value,
36    path: impl AsRef<Path>,
37    content: &str,
38    ignored_error_codes: &[u64],
39) -> Vec<Diagnostic> {
40    let Some(errors) = forge_output.get("errors").and_then(|v| v.as_array()) else {
41        return Vec::new();
42    };
43    let path = path.as_ref();
44    errors
45        .iter()
46        .filter_map(|err| parse_diagnostic(err, path, content, ignored_error_codes))
47        .collect()
48}
49
50/// Check whether the source path from forge's error output refers to the same
51/// file the editor has open.
52///
53/// Forge reports error paths relative to its working directory (wherever the
54/// LSP process runs from), e.g. `example/Shop.sol` or just `Shop.sol`.  The
55/// editor provides the full absolute path.  We simply check whether the
56/// absolute path ends with the relative path forge reported.
57fn source_location_matches(source_path: &str, path: &Path) -> bool {
58    let source_path = Path::new(source_path);
59    if source_path.is_absolute() {
60        source_path == path
61    } else {
62        path.ends_with(source_path)
63    }
64}
65
66fn parse_diagnostic(
67    err: &Value,
68    path: &Path,
69    content: &str,
70    ignored_error_codes: &[u64],
71) -> Option<Diagnostic> {
72    if ignored_error_code_warning(err, ignored_error_codes) {
73        return None;
74    }
75    let source_file = err
76        .get("sourceLocation")
77        .and_then(|loc| loc.get("file"))
78        .and_then(|f| f.as_str())?;
79
80    if !source_location_matches(source_file, path) {
81        return None;
82    }
83
84    let start_offset = err
85        .get("sourceLocation")
86        .and_then(|loc| loc.get("start"))
87        .and_then(|s| s.as_u64())
88        .unwrap_or(0) as usize;
89
90    let end_offset = err
91        .get("sourceLocation")
92        .and_then(|loc| loc.get("end"))
93        .and_then(|s| s.as_u64())
94        .map(|v| v as usize)
95        .unwrap_or(start_offset);
96
97    let start = byte_offset_to_position(content, start_offset);
98    let end = byte_offset_to_position(content, end_offset);
99
100    let range = Range { start, end };
101
102    let message = err
103        .get("message")
104        .and_then(|m| m.as_str())
105        .unwrap_or("Unknown error");
106
107    let severity = match err.get("severity").and_then(|s| s.as_str()) {
108        Some("error") => Some(DiagnosticSeverity::ERROR),
109        Some("warning") => Some(DiagnosticSeverity::WARNING),
110        Some("note") => Some(DiagnosticSeverity::INFORMATION),
111        Some("help") => Some(DiagnosticSeverity::HINT),
112        _ => Some(DiagnosticSeverity::INFORMATION),
113    };
114
115    let code = err
116        .get("errorCode")
117        .and_then(|c| c.as_str())
118        .map(|s| NumberOrString::String(s.to_string()));
119
120    Some(Diagnostic {
121        range,
122        severity,
123        code,
124        code_description: None,
125        source: Some("forge-build".to_string()),
126        message: message.to_string(),
127        related_information: None,
128        tags: None,
129        data: None,
130    })
131}