solidity_language_server/
runner.rs1use 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 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 tokio::fs::read_to_string(file_path)
112 .await
113 .map_err(|_| RunnerError::ReadError)
114 }
115 Some(1) => {
116 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, end: i32, }
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}