1use serde::{Deserialize, Serialize};
5use std::fs;
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CompilationResult {
13 pub success: bool,
14 pub errors: Vec<CompilationError>,
15 pub warnings: Vec<CompilationError>,
16 pub stdout: String,
17 pub stderr: String,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CompilationError {
23 pub level: ErrorLevel,
24 pub message: String,
25 pub code: Option<String>,
26 pub line: Option<usize>,
27 pub column: Option<usize>,
28 pub file: Option<String>,
29 pub suggestion: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum ErrorLevel {
35 Error,
36 Warning,
37 Note,
38 Help,
39}
40
41pub struct RustCompiler {
43 temp_dir: PathBuf,
44}
45
46impl RustCompiler {
47 pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
49 let temp_dir = std::env::temp_dir().join("blockly_rust_check");
50 fs::create_dir_all(&temp_dir)?;
51
52 Ok(Self { temp_dir })
53 }
54
55 pub fn check_code(&self, code: &str) -> Result<CompilationResult, Box<dyn std::error::Error>> {
59 let project_dir = self.temp_dir.join(format!("check_{}", uuid::Uuid::new_v4()));
61 fs::create_dir_all(&project_dir)?;
62
63 let cargo_toml = r#"[package]
65name = "blockly_check"
66version = "0.1.0"
67edition = "2021"
68
69[dependencies]
70"#;
71 fs::write(project_dir.join("Cargo.toml"), cargo_toml)?;
72
73 let src_dir = project_dir.join("src");
75 fs::create_dir_all(&src_dir)?;
76
77 let wrapped_code = if !code.contains("fn main") {
79 format!("fn main() {{\n{}\n}}", code)
80 } else {
81 code.to_string()
82 };
83
84 fs::write(src_dir.join("main.rs"), wrapped_code)?;
85
86 let output = Command::new("cargo")
88 .arg("check")
89 .arg("--message-format=json")
90 .current_dir(&project_dir)
91 .output()?;
92
93 let result = self.parse_cargo_output(&output.stdout, &output.stderr)?;
95
96 let _ = fs::remove_dir_all(&project_dir);
98
99 Ok(result)
100 }
101
102 pub fn check_code_with_deps(
104 &self,
105 code: &str,
106 dependencies: &[(&str, &str)],
107 ) -> Result<CompilationResult, Box<dyn std::error::Error>> {
108 let project_dir = self.temp_dir.join(format!("check_{}", uuid::Uuid::new_v4()));
110 fs::create_dir_all(&project_dir)?;
111
112 let mut cargo_toml = String::from(
114 r#"[package]
115name = "blockly_check"
116version = "0.1.0"
117edition = "2021"
118
119[dependencies]
120"#,
121 );
122
123 for (name, version) in dependencies {
124 cargo_toml.push_str(&format!("{} = \"{}\"\n", name, version));
125 }
126
127 fs::write(project_dir.join("Cargo.toml"), cargo_toml)?;
128
129 let src_dir = project_dir.join("src");
131 fs::create_dir_all(&src_dir)?;
132 fs::write(src_dir.join("main.rs"), code)?;
133
134 let output = Command::new("cargo")
136 .arg("check")
137 .arg("--message-format=json")
138 .current_dir(&project_dir)
139 .output()?;
140
141 let result = self.parse_cargo_output(&output.stdout, &output.stderr)?;
143
144 let _ = fs::remove_dir_all(&project_dir);
146
147 Ok(result)
148 }
149
150 pub fn quick_check(&self, code: &str) -> Result<CompilationResult, Box<dyn std::error::Error>> {
153 let temp_file = self.temp_dir.join(format!("check_{}.rs", uuid::Uuid::new_v4()));
155 fs::write(&temp_file, code)?;
156
157 let output = Command::new("rustc")
159 .arg("--crate-type=lib")
160 .arg("--error-format=json")
161 .arg(&temp_file)
162 .arg("-o")
163 .arg("/dev/null") .output()?;
165
166 let result = self.parse_rustc_output(&output.stdout, &output.stderr)?;
168
169 let _ = fs::remove_file(&temp_file);
171
172 Ok(result)
173 }
174
175 fn parse_cargo_output(
177 &self,
178 stdout: &[u8],
179 stderr: &[u8],
180 ) -> Result<CompilationResult, Box<dyn std::error::Error>> {
181 let stdout_str = String::from_utf8_lossy(stdout);
182 let stderr_str = String::from_utf8_lossy(stderr);
183
184 let mut errors = Vec::new();
185 let mut warnings = Vec::new();
186
187 for line in stdout_str.lines() {
189 if let Ok(msg) = serde_json::from_str::<serde_json::Value>(line) {
190 if let Some(message) = msg.get("message") {
191 if let Some(rendered) = message.get("rendered").and_then(|v| v.as_str()) {
192 let level = message
193 .get("level")
194 .and_then(|v| v.as_str())
195 .unwrap_or("error");
196
197 let error = CompilationError {
198 level: match level {
199 "error" => ErrorLevel::Error,
200 "warning" => ErrorLevel::Warning,
201 "note" => ErrorLevel::Note,
202 "help" => ErrorLevel::Help,
203 _ => ErrorLevel::Error,
204 },
205 message: rendered.to_string(),
206 code: message
207 .get("code")
208 .and_then(|c| c.get("code"))
209 .and_then(|v| v.as_str())
210 .map(String::from),
211 line: message
212 .get("spans")
213 .and_then(|s| s.as_array())
214 .and_then(|arr| arr.first())
215 .and_then(|span| span.get("line_start"))
216 .and_then(|v| v.as_u64())
217 .map(|n| n as usize),
218 column: message
219 .get("spans")
220 .and_then(|s| s.as_array())
221 .and_then(|arr| arr.first())
222 .and_then(|span| span.get("column_start"))
223 .and_then(|v| v.as_u64())
224 .map(|n| n as usize),
225 file: message
226 .get("spans")
227 .and_then(|s| s.as_array())
228 .and_then(|arr| arr.first())
229 .and_then(|span| span.get("file_name"))
230 .and_then(|v| v.as_str())
231 .map(String::from),
232 suggestion: None,
233 };
234
235 match error.level {
236 ErrorLevel::Error => errors.push(error),
237 ErrorLevel::Warning => warnings.push(error),
238 _ => {}
239 }
240 }
241 }
242 }
243 }
244
245 Ok(CompilationResult {
246 success: errors.is_empty(),
247 errors,
248 warnings,
249 stdout: stdout_str.to_string(),
250 stderr: stderr_str.to_string(),
251 })
252 }
253
254 fn parse_rustc_output(
256 &self,
257 stdout: &[u8],
258 stderr: &[u8],
259 ) -> Result<CompilationResult, Box<dyn std::error::Error>> {
260 self.parse_cargo_output(stdout, stderr)
262 }
263}
264
265impl Default for RustCompiler {
266 fn default() -> Self {
267 Self::new().expect("Failed to create RustCompiler")
268 }
269}
270
271pub fn is_rust_available() -> bool {
273 Command::new("rustc")
274 .arg("--version")
275 .output()
276 .map(|output| output.status.success())
277 .unwrap_or(false)
278}
279
280pub fn is_cargo_available() -> bool {
282 Command::new("cargo")
283 .arg("--version")
284 .output()
285 .map(|output| output.status.success())
286 .unwrap_or(false)
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_rust_available() {
295 let available = is_rust_available();
297 println!("Rust available: {}", available);
298 }
299
300 #[test]
301 fn test_valid_code() {
302 if !is_cargo_available() {
303 println!("Skipping test: cargo not available");
304 return;
305 }
306
307 let compiler = RustCompiler::new().unwrap();
308 let code = r#"
309 fn add(a: i32, b: i32) -> i32 {
310 a + b
311 }
312 "#;
313
314 let result = compiler.check_code(code).unwrap();
315 assert!(result.success, "Valid code should compile");
316 }
317
318 #[test]
319 fn test_invalid_code() {
320 if !is_cargo_available() {
321 println!("Skipping test: cargo not available");
322 return;
323 }
324
325 let compiler = RustCompiler::new().unwrap();
326 let code = r#"
327 fn add(a: i32, b: i32) -> i32 {
328 a + // Missing operand
329 }
330 "#;
331
332 let result = compiler.check_code(code).unwrap();
333 assert!(!result.success, "Invalid code should not compile");
334 assert!(!result.errors.is_empty(), "Should have errors");
335 }
336}