Skip to main content

solidity_language_server/
runner.rs

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