use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::graph::external_tools::{tool_detector, tool_invoker};
#[derive(Debug, thiserror::Error)]
pub enum JavacCompilationError {
#[error("Javac not found: {0}")]
JavacNotFound(String),
#[error("Compilation failed: {0}")]
CompilationFailed(String),
#[error("Unsupported file type: {extension}")]
UnsupportedFileType { extension: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub fn compile_to_class(
source_path: &Path,
output_dir: &Path,
) -> Result<(), JavacCompilationError> {
let javac_path = tool_detector::find_javac().map_err(|e| match e {
tool_detector::ToolDetectionError::ToolNotFound { .. } => {
JavacCompilationError::JavacNotFound("javac not found in PATH".to_string())
}
_ => JavacCompilationError::JavacNotFound(format!("javac detection failed: {}", e)),
})?;
let extension = source_path
.extension()
.and_then(|e| e.to_str())
.ok_or_else(|| JavacCompilationError::UnsupportedFileType {
extension: "<none>".to_string(),
})?;
if extension.to_lowercase() != "java" {
return Err(JavacCompilationError::UnsupportedFileType {
extension: extension.to_string(),
});
}
if !output_dir.exists() {
std::fs::create_dir_all(output_dir)?;
}
let output = Command::new(&javac_path)
.arg("-d") .arg(output_dir)
.arg(source_path)
.output()
.map_err(|e| JavacCompilationError::Io(e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(JavacCompilationError::CompilationFailed(stderr.to_string()));
}
Ok(())
}
pub fn compile_to_class_temp(source_path: &Path) -> Result<PathBuf, JavacCompilationError> {
let temp_dir = tempfile::tempdir().map_err(|e| {
JavacCompilationError::Io(std::io::Error::new(std::io::ErrorKind::Other, e))
})?;
compile_to_class(source_path, temp_dir.path())?;
let class_files = find_class_files(temp_dir.path());
if class_files.is_empty() {
return Err(JavacCompilationError::CompilationFailed(
"No .class files were generated".to_string(),
));
}
Ok(class_files[0].clone())
}
fn find_class_files(dir: &Path) -> Vec<PathBuf> {
let mut class_files = Vec::new();
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
class_files.extend(find_class_files(&path));
} else if path.extension().and_then(|e| e.to_str()) == Some("class") {
class_files.push(path);
}
}
}
class_files
}
pub fn extract_class_name(source_path: &Path) -> Result<String> {
let content = std::fs::read_to_string(source_path)?;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("//") || line.starts_with("/*") {
continue;
}
if line.contains("class ") {
if let Some(class_pos) = line.find("class ") {
let rest = &line[class_pos + 6..];
let class_name: String = rest
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '$')
.collect();
if !class_name.is_empty() {
return Ok(class_name);
}
}
}
}
let file_stem = source_path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| {
JavacCompilationError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Invalid file name",
))
})?;
Ok(file_stem.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_extract_class_name_simple() {
let source = r#"
public class Foo {
public static void main(String[] args) {
System.out.println("Hello");
}
}
"#;
let mut temp_file = NamedTempFile::with_suffix(".java").unwrap();
temp_file.write_all(source.as_bytes()).unwrap();
let source_path = temp_file.path();
let class_name = extract_class_name(source_path);
assert!(class_name.is_ok());
assert_eq!(class_name.unwrap(), "Foo");
}
#[test]
fn test_extract_class_name_without_public() {
let source = r#"
class Bar {
public int value;
}
"#;
let mut temp_file = NamedTempFile::with_suffix(".java").unwrap();
temp_file.write_all(source.as_bytes()).unwrap();
let source_path = temp_file.path();
let class_name = extract_class_name(source_path);
assert!(class_name.is_ok());
assert_eq!(class_name.unwrap(), "Bar");
}
#[test]
fn test_extract_class_name_fallback() {
let source = "// Just a comment\n";
let mut temp_file = NamedTempFile::with_suffix(".java").unwrap();
temp_file.write_all(source.as_bytes()).unwrap();
let test_path = temp_file.path().with_file_name("TestClass.java");
std::fs::rename(temp_file.path(), &test_path).ok();
let class_name = extract_class_name(&test_path);
assert!(class_name.is_ok());
assert_eq!(class_name.unwrap(), "TestClass");
}
#[test]
fn test_compile_simple_java_class() {
if tool_detector::find_javac().is_err() {
return;
}
let source = r#"
public class Test {
public static int foo(int x) {
if (x > 0) {
return x * 2;
} else {
return x + 1;
}
}
}
"#;
let mut temp_file = NamedTempFile::with_suffix(".java").unwrap();
temp_file.write_all(source.as_bytes()).unwrap();
let source_path = temp_file.path();
let output_path = compile_to_class_temp(source_path);
assert!(output_path.is_ok());
let class_path = output_path.unwrap();
assert!(class_path.exists());
assert_eq!(class_path.extension().unwrap(), "class");
}
#[test]
fn test_compile_java_with_syntax_error() {
if tool_detector::find_javac().is_err() {
return;
}
let source = r#"
public class Broken {
public static void main( {
// Missing closing parenthesis
}
}
"#;
let mut temp_file = NamedTempFile::with_suffix(".java").unwrap();
temp_file.write_all(source.as_bytes()).unwrap();
let source_path = temp_file.path();
let result = compile_to_class_temp(source_path);
assert!(result.is_err());
}
}