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 .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 tokio::fs::read_to_string(file_path)
105 .await
106 .map_err(|_| RunnerError::ReadError)
107 }
108 Some(1) => {
109 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, end: i32, }
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}