solidity_language_server/
solar_runner.rs

1use crate::runner::{Runner, RunnerError};
2use solar::{
3    interface::{
4        Session, SourceMap,
5        diagnostics::{Diag, DiagCtxt, InMemoryEmitter},
6    },
7    sema::Compiler,
8};
9use std::{io::Error, path::Path};
10use tokio::task;
11use tower_lsp::async_trait;
12use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range, Url};
13
14pub struct SolarRunner;
15
16fn solar_diag_to_lsp(
17    diag: &Diag,
18    target_file: &str,
19    source_map: &SourceMap,
20) -> Option<tower_lsp::lsp_types::Diagnostic> {
21    use tower_lsp::lsp_types::NumberOrString;
22
23    let primary_span = diag.span.primary_span()?;
24    let _uri = Url::from_file_path(target_file).ok()?;
25    let range = span_to_range(source_map, primary_span);
26
27    Some(tower_lsp::lsp_types::Diagnostic {
28        range,
29        severity: Some(severity(diag.level())),
30        code: diag
31            .code
32            .as_ref()
33            .map(|id| NumberOrString::String(id.as_string())),
34        code_description: None,
35        source: Some("solar".into()),
36        message: diag.label().into_owned(),
37        related_information: None, // TODO: Implement
38        tags: None,
39        data: None,
40    })
41}
42
43fn span_to_range(
44    source_map: &SourceMap,
45    span: solar::interface::Span,
46) -> tower_lsp::lsp_types::Range {
47    let start_loc = source_map.lookup_char_pos(span.lo());
48    let end_loc = source_map.lookup_char_pos(span.hi());
49    tower_lsp::lsp_types::Range {
50        start: Position {
51            line: start_loc.data.line as u32 - 1,
52            character: start_loc.data.col.0 as u32 - 1,
53        },
54        end: Position {
55            line: end_loc.data.line as u32 - 1,
56            character: end_loc.data.col.0 as u32 - 1,
57        },
58    }
59}
60
61fn severity(
62    level: solar::interface::diagnostics::Level,
63) -> tower_lsp::lsp_types::DiagnosticSeverity {
64    use solar::interface::diagnostics::Level::*;
65    match level {
66        Error | Fatal | Bug => tower_lsp::lsp_types::DiagnosticSeverity::ERROR,
67        Warning => tower_lsp::lsp_types::DiagnosticSeverity::WARNING,
68        Note | OnceNote => tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION,
69        Help | OnceHelp => tower_lsp::lsp_types::DiagnosticSeverity::HINT,
70        _ => tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION,
71    }
72}
73
74#[async_trait]
75impl Runner for SolarRunner {
76    async fn build(&self, _file: &str) -> Result<serde_json::Value, RunnerError> {
77        // For solar, build diagnostics are handled in get_build_diagnostics
78        // Return empty JSON for compatibility
79        Ok(serde_json::Value::Object(serde_json::Map::new()))
80    }
81
82    async fn lint(&self, _file: &str) -> Result<serde_json::Value, RunnerError> {
83        // For solar, lint diagnostics are handled in get_lint_diagnostics
84        // Return empty array for compatibility
85        Ok(serde_json::Value::Array(Vec::new()))
86    }
87
88    async fn ast(&self, file: &str) -> Result<serde_json::Value, RunnerError> {
89        // For solar, we can return the AST as JSON
90        // This is a simplified version; in practice, you might need to serialize the AST
91        let file = file.to_string();
92
93        task::spawn_blocking(move || {
94            let paths = [Path::new(&file)];
95            let sess = Session::builder()
96                .with_buffer_emitter(solar::interface::ColorChoice::Auto)
97                .build();
98            let mut compiler = Compiler::new(sess);
99
100            let _ = compiler.enter_mut(|compiler| -> solar::interface::Result<_> {
101                let mut parsing_context = compiler.parse();
102                parsing_context.load_files(paths)?;
103                parsing_context.parse();
104                Ok(())
105            });
106
107            // Get the AST - this is simplified, solar might have different API
108            // For now, return empty object
109            Ok(serde_json::Value::Object(serde_json::Map::new()))
110        })
111        .await
112        .map_err(|_| RunnerError::CommandError(Error::other("Task panicked")))?
113    }
114
115    async fn get_build_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError> {
116        let path = file.to_file_path().map_err(|_| RunnerError::InvalidUrl)?;
117        let path_str = path.to_str().ok_or(RunnerError::InvalidUrl)?.to_string();
118
119        // Read the file content
120        let content = tokio::fs::read_to_string(&path)
121            .await
122            .map_err(|_| RunnerError::ReadError)?;
123
124        task::spawn_blocking(move || {
125            let (emitter, diag_buffer) = InMemoryEmitter::new();
126            let sess = Session::builder()
127                .dcx(DiagCtxt::new(Box::new(emitter)))
128                .build();
129            let mut compiler = Compiler::new(sess);
130
131            let _ = compiler.enter_mut(|compiler| -> solar::interface::Result<_> {
132                let mut parsing_context = compiler.parse();
133                // Add the file with content to source_map
134                parsing_context.add_files(vec![
135                    compiler
136                        .sess()
137                        .source_map()
138                        .new_source_file(path_str.clone(), content)
139                        .unwrap(),
140                ]);
141                parsing_context.parse();
142                Ok(())
143            });
144
145            let _ = compiler.enter_mut(|compiler| -> solar::interface::Result<_> {
146                let _ = compiler.lower_asts();
147                let _ = compiler.analysis();
148                Ok(())
149            });
150
151            let mut diagnostics = Vec::new();
152            println!("Diag buffer has {} items", diag_buffer.read().len());
153            for diag in diag_buffer.read().iter() {
154                // Convert solar diagnostic to LSP diagnostic
155                if let Some(lsp_diag) =
156                    solar_diag_to_lsp(diag, &path_str, compiler.sess().source_map())
157                {
158                    diagnostics.push(lsp_diag);
159                }
160            }
161
162            Ok(diagnostics)
163        })
164        .await
165        .map_err(|_| RunnerError::CommandError(Error::other("Task panicked")))?
166    }
167
168    async fn get_lint_diagnostics(&self, file: &Url) -> Result<Vec<Diagnostic>, RunnerError> {
169        let mut diagnostics = self.get_build_diagnostics(file).await?;
170
171        // Add custom lint checks
172        let path = file.to_file_path().map_err(|_| RunnerError::InvalidUrl)?;
173        let content = tokio::fs::read_to_string(&path)
174            .await
175            .map_err(|_| RunnerError::ReadError)?;
176
177        // Check for SPDX license
178        if !content.contains("SPDX-License-Identifier") {
179            diagnostics.push(Diagnostic {
180                range: Range {
181                    start: Position {
182                        line: 0,
183                        character: 0,
184                    },
185                    end: Position {
186                        line: 0,
187                        character: 0,
188                    },
189                },
190                severity: Some(DiagnosticSeverity::WARNING),
191                code: Some(NumberOrString::String("missing-license".to_string())),
192                code_description: None,
193                source: Some("solar-lint".to_string()),
194                message: "Missing SPDX license identifier".to_string(),
195                related_information: None,
196                tags: None,
197                data: None,
198            });
199        }
200
201        // Check for pragma solidity
202        if !content.contains("pragma solidity") {
203            diagnostics.push(Diagnostic {
204                range: Range {
205                    start: Position {
206                        line: 0,
207                        character: 0,
208                    },
209                    end: Position {
210                        line: 0,
211                        character: 0,
212                    },
213                },
214                severity: Some(DiagnosticSeverity::WARNING),
215                code: Some(NumberOrString::String("missing-pragma".to_string())),
216                code_description: None,
217                source: Some("solar-lint".to_string()),
218                message: "Missing pragma solidity version".to_string(),
219                related_information: None,
220                tags: None,
221                data: None,
222            });
223        }
224
225        Ok(diagnostics)
226    }
227}