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