solidity_language_server/
runner.rs1use crate::{build::build_output_to_diagnostics, lint::lint_output_to_diagnostics};
2use serde::{Deserialize, Serialize};
3use std::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 get_build_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError>;
19 async fn get_lint_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError>;
20}
21
22#[async_trait]
23impl Runner for ForgeRunner {
24 async fn lint(&self, file_path: &str) -> Result<serde_json::Value, RunnerError> {
25 let output = Command::new("forge")
26 .arg("lint")
27 .arg(file_path)
28 .arg("--json")
29 .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
30 .output()
31 .await?;
32
33 let stderr_str = String::from_utf8_lossy(&output.stderr);
34
35 let mut diagnostics = Vec::new();
37 for line in stderr_str.lines() {
38 if line.trim().is_empty() {
39 continue;
40 }
41
42 match serde_json::from_str::<serde_json::Value>(line) {
43 Ok(value) => diagnostics.push(value),
44 Err(_e) => {
45 continue;
46 }
47 }
48 }
49
50 Ok(serde_json::Value::Array(diagnostics))
51 }
52
53 async fn build(&self, file_path: &str) -> Result<serde_json::Value, RunnerError> {
54 let output = Command::new("forge")
55 .arg("build")
56 .arg(file_path)
57 .arg("--json")
58 .arg("--no-cache")
59 .arg("--ast")
60 .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
61 .env("FOUNDRY_LINT_LINT_ON_BUILD", "false")
62 .output()
63 .await?;
64
65 let stdout_str = String::from_utf8_lossy(&output.stdout);
66 let parsed: serde_json::Value = serde_json::from_str(&stdout_str)?;
67
68 Ok(parsed)
69 }
70
71 async fn ast(&self, file_path: &str) -> Result<serde_json::Value, RunnerError> {
72 let output = Command::new("forge")
73 .arg("build")
74 .arg(file_path)
75 .arg("--json")
76 .arg("--no-cache")
77 .arg("--ast")
78 .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
79 .env("FOUNDRY_LINT_LINT_ON_BUILD", "false")
80 .output()
81 .await?;
82
83 let stdout_str = String::from_utf8_lossy(&output.stdout);
84 let parsed: serde_json::Value = serde_json::from_str(&stdout_str)?;
85
86 Ok(parsed)
87 }
88
89 async fn get_lint_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError> {
90 let path: PathBuf = file.to_file_path().map_err(|_| RunnerError::InvalidUrl)?;
91 let path_str = path.to_str().ok_or(RunnerError::InvalidUrl)?;
92 let lint_output = self.lint(path_str).await?;
93 let diagnostics = lint_output_to_diagnostics(&lint_output, path_str);
94 Ok(diagnostics)
95 }
96
97 async fn get_build_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError> {
98 let path = file.to_file_path().map_err(|_| RunnerError::InvalidUrl)?;
99 let path_str = path.to_str().ok_or(RunnerError::InvalidUrl)?;
100 let filename = path
101 .file_name()
102 .and_then(|os_str| os_str.to_str())
103 .ok_or(RunnerError::InvalidUrl)?;
104 let content = tokio::fs::read_to_string(&path)
105 .await
106 .map_err(|_| RunnerError::ReadError)?;
107 let build_output = self.build(path_str).await?;
108 let diagnostics = build_output_to_diagnostics(&build_output, filename, &content);
109 Ok(diagnostics)
110 }
111}
112
113#[derive(Error, Debug)]
114pub enum RunnerError {
115 #[error("Invalid file URL")]
116 InvalidUrl,
117 #[error("Failed to run command: {0}")]
118 CommandError(#[from] std::io::Error),
119 #[error("JSON error: {0}")]
120 JsonError(#[from] serde_json::Error),
121 #[error("Empty output from compiler")]
122 EmptyOutput,
123 #[error("ReadError")]
124 ReadError,
125}
126
127#[derive(Debug, Deserialize, Serialize)]
128pub struct SourceLocation {
129 file: String,
130 start: i32, end: i32, }
133
134#[derive(Debug, Deserialize, Serialize)]
135pub struct ForgeDiagnosticMessage {
136 #[serde(rename = "sourceLocation")]
137 source_location: SourceLocation,
138 #[serde(rename = "type")]
139 error_type: String,
140 component: String,
141 severity: String,
142 #[serde(rename = "errorCode")]
143 error_code: String,
144 message: String,
145 #[serde(rename = "formattedMessage")]
146 formatted_message: String,
147}
148
149#[derive(Debug, Deserialize, Serialize)]
150pub struct CompileOutput {
151 errors: Option<Vec<ForgeDiagnosticMessage>>,
152 sources: serde_json::Value,
153 contracts: serde_json::Value,
154 build_infos: Vec<serde_json::Value>,
155}