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            .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
62            .env("FOUNDRY_LINT_LINT_ON_BUILD", "false")
63            .output()
64            .await?;
65
66        let stdout_str = String::from_utf8_lossy(&output.stdout);
67        let parsed: serde_json::Value = serde_json::from_str(&stdout_str)?;
68
69        Ok(parsed)
70    }
71
72    async fn ast(&self, file_path: &str) -> Result<serde_json::Value, RunnerError> {
73        let output = Command::new("forge")
74            .arg("build")
75            .arg(file_path)
76            .arg("--json")
77            .arg("--no-cache")
78            .arg("--ast")
79            .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
80            .env("FOUNDRY_LINT_LINT_ON_BUILD", "false")
81            .output()
82            .await?;
83
84        let stdout_str = String::from_utf8_lossy(&output.stdout);
85        let parsed: serde_json::Value = serde_json::from_str(&stdout_str)?;
86
87        Ok(parsed)
88    }
89
90    async fn format(&self, file_path: &str) -> Result<String, RunnerError> {
91        let output = Command::new("forge")
92            .arg("fmt")
93            .arg(file_path)
94            .arg("--check")
95            .arg("--raw")
96            .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
97            .output()
98            .await?;
99        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
100        let stderr = String::from_utf8_lossy(&output.stderr);
101        match output.status.code() {
102            Some(0) => {
103                // Already formatted, read the current file content
104                tokio::fs::read_to_string(file_path)
105                    .await
106                    .map_err(|_| RunnerError::ReadError)
107            }
108            Some(1) => {
109                // Needs formatting, stdout has the formatted content
110                if stdout.is_empty() {
111                    Err(RunnerError::CommandError(io::Error::other(format!(
112                        "forge fmt unexpected empty output on {}: exit code {}, stderr: {}",
113                        file_path, output.status, stderr
114                    ))))
115                } else {
116                    Ok(stdout)
117                }
118            }
119            _ => Err(RunnerError::CommandError(io::Error::other(format!(
120                "forge fmt failed on {}: exit code {}, stderr: {}",
121                file_path, output.status, stderr
122            )))),
123        }
124    }
125
126    async fn get_lint_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError> {
127        let path: PathBuf = file.to_file_path().map_err(|_| RunnerError::InvalidUrl)?;
128        let path_str = path.to_str().ok_or(RunnerError::InvalidUrl)?;
129        let lint_output = self.lint(path_str).await?;
130        let diagnostics = lint_output_to_diagnostics(&lint_output, path_str);
131        Ok(diagnostics)
132    }
133
134    async fn get_build_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError> {
135        let path = file.to_file_path().map_err(|_| RunnerError::InvalidUrl)?;
136        let path_str = path.to_str().ok_or(RunnerError::InvalidUrl)?;
137        let filename = path
138            .file_name()
139            .and_then(|os_str| os_str.to_str())
140            .ok_or(RunnerError::InvalidUrl)?;
141        let content = tokio::fs::read_to_string(&path)
142            .await
143            .map_err(|_| RunnerError::ReadError)?;
144        let build_output = self.build(path_str).await?;
145        let diagnostics = build_output_to_diagnostics(&build_output, filename, &content);
146        Ok(diagnostics)
147    }
148}
149
150#[derive(Error, Debug)]
151pub enum RunnerError {
152    #[error("Invalid file URL")]
153    InvalidUrl,
154    #[error("Failed to run command: {0}")]
155    CommandError(#[from] io::Error),
156    #[error("JSON error: {0}")]
157    JsonError(#[from] serde_json::Error),
158    #[error("Empty output from compiler")]
159    EmptyOutput,
160    #[error("ReadError")]
161    ReadError,
162}
163
164#[derive(Debug, Deserialize, Serialize)]
165pub struct SourceLocation {
166    file: String,
167    start: i32, // Changed to i32 to handle -1 values
168    end: i32,   // Changed to i32 to handle -1 values
169}
170
171#[derive(Debug, Deserialize, Serialize)]
172pub struct ForgeDiagnosticMessage {
173    #[serde(rename = "sourceLocation")]
174    source_location: SourceLocation,
175    #[serde(rename = "type")]
176    error_type: String,
177    component: String,
178    severity: String,
179    #[serde(rename = "errorCode")]
180    error_code: String,
181    message: String,
182    #[serde(rename = "formattedMessage")]
183    formatted_message: String,
184}
185
186#[derive(Debug, Deserialize, Serialize)]
187pub struct CompileOutput {
188    errors: Option<Vec<ForgeDiagnosticMessage>>,
189    sources: serde_json::Value,
190    contracts: serde_json::Value,
191    build_infos: Vec<serde_json::Value>,
192}