solidity_language_server/
runner.rs

1use 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        // Parse JSON output line by line
36        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, // Changed to i32 to handle -1 values
131    end: i32,   // Changed to i32 to handle -1 values
132}
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}