Skip to main content

solidity_language_server/
build.rs

1use crate::utils::byte_offset_to_position;
2use serde_json::Value;
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Range};
6
7/// Default error codes that are always suppressed (contract-size and
8/// code-size warnings that are noisy for LSP users).
9const DEFAULT_IGNORED_CODES: &[&str] = &["5574", "3860"];
10
11/// Check whether a solc error should be suppressed based on its error code.
12///
13/// Suppresses the hardcoded defaults plus any codes provided in `extra_codes`
14/// (from `foundry.toml` `ignored_error_codes`).
15pub fn ignored_error_code_warning(value: &serde_json::Value, extra_codes: &[u64]) -> bool {
16    let error_code = value
17        .get("errorCode")
18        .and_then(|v| v.as_str())
19        .unwrap_or_default();
20
21    if DEFAULT_IGNORED_CODES.contains(&error_code) {
22        return true;
23    }
24
25    // Check user-configured ignored codes from foundry.toml
26    if let Ok(code_num) = error_code.parse::<u64>()
27        && extra_codes.contains(&code_num)
28    {
29        return true;
30    }
31
32    false
33}
34
35pub fn build_output_to_diagnostics(
36    solc_output: &serde_json::Value,
37    path: impl AsRef<Path>,
38    content: &str,
39    ignored_error_codes: &[u64],
40) -> Vec<Diagnostic> {
41    let Some(errors) = solc_output.get("errors").and_then(|v| v.as_array()) else {
42        return Vec::new();
43    };
44    let path = path.as_ref();
45    errors
46        .iter()
47        .filter_map(|err| parse_diagnostic(err, path, content, ignored_error_codes))
48        .collect()
49}
50
51/// Check whether the source path from solc's error output refers to the same
52/// file the editor has open.
53///
54/// Solc reports error paths relative to its working directory (wherever the
55/// LSP process runs from), e.g. `example/Shop.sol` or just `Shop.sol`.  The
56/// editor provides the full absolute path.  We simply check whether the
57/// absolute path ends with the relative path solc reported.
58fn source_location_matches(source_path: &str, path: &Path) -> bool {
59    let source_path = Path::new(source_path);
60    if source_path.is_absolute() {
61        source_path == path
62    } else {
63        path.ends_with(source_path)
64    }
65}
66
67fn parse_diagnostic(
68    err: &Value,
69    path: &Path,
70    content: &str,
71    ignored_error_codes: &[u64],
72) -> Option<Diagnostic> {
73    if ignored_error_code_warning(err, ignored_error_codes) {
74        return None;
75    }
76    let source_file = err
77        .get("sourceLocation")
78        .and_then(|loc| loc.get("file"))
79        .and_then(|f| f.as_str())?;
80
81    if !source_location_matches(source_file, path) {
82        return None;
83    }
84
85    let start_offset = err
86        .get("sourceLocation")
87        .and_then(|loc| loc.get("start"))
88        .and_then(|s| s.as_u64())
89        .unwrap_or(0) as usize;
90
91    let end_offset = err
92        .get("sourceLocation")
93        .and_then(|loc| loc.get("end"))
94        .and_then(|s| s.as_u64())
95        .map(|v| v as usize)
96        .unwrap_or(start_offset);
97
98    let start = byte_offset_to_position(content, start_offset);
99    let end = byte_offset_to_position(content, end_offset);
100
101    let range = Range { start, end };
102
103    let message = err
104        .get("message")
105        .and_then(|m| m.as_str())
106        .unwrap_or("Unknown error");
107
108    let severity = match err.get("severity").and_then(|s| s.as_str()) {
109        Some("error") => Some(DiagnosticSeverity::ERROR),
110        Some("warning") => Some(DiagnosticSeverity::WARNING),
111        Some("note") => Some(DiagnosticSeverity::INFORMATION),
112        Some("help") => Some(DiagnosticSeverity::HINT),
113        _ => Some(DiagnosticSeverity::INFORMATION),
114    };
115
116    let code = err
117        .get("errorCode")
118        .and_then(|c| c.as_str())
119        .map(|s| NumberOrString::String(s.to_string()));
120
121    Some(Diagnostic {
122        range,
123        severity,
124        code,
125        code_description: None,
126        source: Some("solc".to_string()),
127        message: message.to_string(),
128        related_information: None,
129        tags: None,
130        data: None,
131    })
132}
133
134/// Extract error-level diagnostics for files OTHER than the one being compiled.
135///
136/// When compiling `A.sol`, solc may report errors in imported files (e.g.
137/// `B.sol` has `import {Test} from "./A.sol"` but `Test` was removed).
138/// `build_output_to_diagnostics` filters those out.  This function collects
139/// them so the LSP can publish diagnostics to the affected files.
140///
141/// Returns a map of `absolute_path → Vec<Diagnostic>`.  Only error-severity
142/// diagnostics are included.  The `project_root` resolves relative paths.
143pub fn cross_file_error_diagnostics(
144    solc_output: &Value,
145    current_file: &Path,
146    project_root: &Path,
147    ignored_error_codes: &[u64],
148) -> HashMap<PathBuf, Vec<Diagnostic>> {
149    let Some(errors) = solc_output.get("errors").and_then(|v| v.as_array()) else {
150        return HashMap::new();
151    };
152
153    let mut result: HashMap<PathBuf, Vec<Diagnostic>> = HashMap::new();
154
155    for err in errors {
156        if ignored_error_code_warning(err, ignored_error_codes) {
157            continue;
158        }
159        if err.get("severity").and_then(|s| s.as_str()) != Some("error") {
160            continue;
161        }
162        let Some(source_file) = err
163            .get("sourceLocation")
164            .and_then(|loc| loc.get("file"))
165            .and_then(|f| f.as_str())
166        else {
167            continue;
168        };
169        if source_location_matches(source_file, current_file) {
170            continue;
171        }
172
173        let source_path = Path::new(source_file);
174        let abs_path = if source_path.is_absolute() {
175            source_path.to_path_buf()
176        } else {
177            project_root.join(source_path)
178        };
179        let Ok(content) = std::fs::read_to_string(&abs_path) else {
180            continue;
181        };
182
183        let start_offset = err
184            .get("sourceLocation")
185            .and_then(|loc| loc.get("start"))
186            .and_then(|s| s.as_u64())
187            .unwrap_or(0) as usize;
188        let end_offset = err
189            .get("sourceLocation")
190            .and_then(|loc| loc.get("end"))
191            .and_then(|s| s.as_u64())
192            .map(|v| v as usize)
193            .unwrap_or(start_offset);
194
195        let start = byte_offset_to_position(&content, start_offset);
196        let end = byte_offset_to_position(&content, end_offset);
197
198        let code = err
199            .get("errorCode")
200            .and_then(|c| c.as_str())
201            .map(|s| NumberOrString::String(s.to_string()));
202
203        let message = err
204            .get("message")
205            .and_then(|m| m.as_str())
206            .unwrap_or("Unknown error");
207
208        result.entry(abs_path).or_default().push(Diagnostic {
209            range: Range { start, end },
210            severity: Some(DiagnosticSeverity::ERROR),
211            code,
212            code_description: None,
213            source: Some("solc".to_string()),
214            message: message.to_string(),
215            related_information: None,
216            tags: None,
217            data: None,
218        });
219    }
220
221    result
222}