Skip to main content

solidity_language_server/
runner.rs

1use crate::{
2    build::build_output_to_diagnostics, lint::lint_output_to_diagnostics,
3    solc::normalize_forge_output,
4};
5use serde::{Deserialize, Serialize};
6use std::{io, path::PathBuf};
7use thiserror::Error;
8use tokio::process::Command;
9use tower_lsp::{
10    async_trait,
11    lsp_types::{Diagnostic, Url},
12};
13
14pub struct ForgeRunner;
15
16#[async_trait]
17pub trait Runner: Send + Sync {
18    async fn build(&self, file: &str) -> Result<serde_json::Value, RunnerError>;
19    async fn lint(&self, file: &str) -> Result<serde_json::Value, RunnerError>;
20    async fn ast(&self, file: &str) -> Result<serde_json::Value, RunnerError>;
21    async fn format(&self, file: &str) -> Result<String, RunnerError>;
22    async fn get_build_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError>;
23    async fn get_lint_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError>;
24}
25
26#[async_trait]
27impl Runner for ForgeRunner {
28    async fn lint(&self, file_path: &str) -> Result<serde_json::Value, RunnerError> {
29        let output = Command::new("forge")
30            .arg("lint")
31            .arg(file_path)
32            .arg("--json")
33            .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
34            .output()
35            .await?;
36
37        let stderr_str = String::from_utf8_lossy(&output.stderr);
38
39        // Parse JSON output line by line
40        let mut diagnostics = Vec::new();
41        for line in stderr_str.lines() {
42            if line.trim().is_empty() {
43                continue;
44            }
45
46            match serde_json::from_str::<serde_json::Value>(line) {
47                Ok(value) => diagnostics.push(value),
48                Err(_e) => {
49                    continue;
50                }
51            }
52        }
53
54        Ok(serde_json::Value::Array(diagnostics))
55    }
56
57    async fn build(&self, file_path: &str) -> Result<serde_json::Value, RunnerError> {
58        let output = Command::new("forge")
59            .arg("build")
60            .arg(file_path)
61            .arg("--json")
62            .arg("--no-cache")
63            .arg("--ast")
64            .arg("--ignore-eip-3860")
65            .args(["--ignored-error-codes", "5574"])
66            .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
67            .env("FOUNDRY_LINT_LINT_ON_BUILD", "false")
68            .output()
69            .await?;
70
71        let stdout_str = String::from_utf8_lossy(&output.stdout);
72        let parsed: serde_json::Value = serde_json::from_str(&stdout_str)?;
73
74        Ok(parsed)
75    }
76
77    async fn ast(&self, file_path: &str) -> Result<serde_json::Value, RunnerError> {
78        let output = Command::new("forge")
79            .arg("build")
80            .arg(file_path)
81            .arg("--json")
82            .arg("--no-cache")
83            .arg("--ast")
84            .arg("--ignore-eip-3860")
85            .args(["--ignored-error-codes", "5574"])
86            .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
87            .env("FOUNDRY_LINT_LINT_ON_BUILD", "false")
88            .output()
89            .await?;
90
91        let stdout_str = String::from_utf8_lossy(&output.stdout);
92        let parsed: serde_json::Value = serde_json::from_str(&stdout_str)?;
93
94        Ok(normalize_forge_output(parsed))
95    }
96
97    async fn format(&self, file_path: &str) -> Result<String, RunnerError> {
98        let output = Command::new("forge")
99            .arg("fmt")
100            .arg(file_path)
101            .arg("--check")
102            .arg("--raw")
103            .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
104            .output()
105            .await?;
106        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
107        let stderr = String::from_utf8_lossy(&output.stderr);
108        match output.status.code() {
109            Some(0) => {
110                // Already formatted, read the current file content
111                tokio::fs::read_to_string(file_path)
112                    .await
113                    .map_err(|_| RunnerError::ReadError)
114            }
115            Some(1) => {
116                // Needs formatting, stdout has the formatted content
117                if stdout.is_empty() {
118                    Err(RunnerError::CommandError(io::Error::other(format!(
119                        "forge fmt unexpected empty output on {}: exit code {}, stderr: {}",
120                        file_path, output.status, stderr
121                    ))))
122                } else {
123                    Ok(stdout)
124                }
125            }
126            _ => Err(RunnerError::CommandError(io::Error::other(format!(
127                "forge fmt failed on {}: exit code {}, stderr: {}",
128                file_path, output.status, stderr
129            )))),
130        }
131    }
132
133    async fn get_lint_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError> {
134        let path: PathBuf = file.to_file_path().map_err(|_| RunnerError::InvalidUrl)?;
135        let path_str = path.to_str().ok_or(RunnerError::InvalidUrl)?;
136        let lint_output = self.lint(path_str).await?;
137        let diagnostics = lint_output_to_diagnostics(&lint_output, path_str);
138        Ok(diagnostics)
139    }
140
141    async fn get_build_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError> {
142        let path = file.to_file_path().map_err(|_| RunnerError::InvalidUrl)?;
143        let path_str = path.to_str().ok_or(RunnerError::InvalidUrl)?;
144        let content = tokio::fs::read_to_string(&path)
145            .await
146            .map_err(|_| RunnerError::ReadError)?;
147        let build_output = self.build(path_str).await?;
148        let diagnostics = build_output_to_diagnostics(&build_output, &path, &content, &[]);
149        Ok(diagnostics)
150    }
151}
152
153#[derive(Error, Debug)]
154pub enum RunnerError {
155    #[error("Invalid file URL")]
156    InvalidUrl,
157    #[error("Failed to run command: {0}")]
158    CommandError(#[from] io::Error),
159    #[error("JSON error: {0}")]
160    JsonError(#[from] serde_json::Error),
161    #[error("Empty output from compiler")]
162    EmptyOutput,
163    #[error("ReadError")]
164    ReadError,
165}
166
167#[derive(Debug, Deserialize, Serialize)]
168pub struct SourceLocation {
169    file: String,
170    start: i32, // Changed to i32 to handle -1 values
171    end: i32,   // Changed to i32 to handle -1 values
172}
173
174#[derive(Debug, Deserialize, Serialize)]
175pub struct ForgeDiagnosticMessage {
176    #[serde(rename = "sourceLocation")]
177    source_location: SourceLocation,
178    #[serde(rename = "type")]
179    error_type: String,
180    component: String,
181    severity: String,
182    #[serde(rename = "errorCode")]
183    error_code: String,
184    message: String,
185    #[serde(rename = "formattedMessage")]
186    formatted_message: String,
187}
188
189#[derive(Debug, Deserialize, Serialize)]
190pub struct CompileOutput {
191    errors: Option<Vec<ForgeDiagnosticMessage>>,
192    sources: serde_json::Value,
193    contracts: serde_json::Value,
194    build_infos: Vec<serde_json::Value>,
195}