impl RustCompiler {
#[must_use]
pub fn new() -> Self {
Self {
rustc_path: which_rustc(),
cargo_path: which_cargo(),
edition: RustEdition::default(),
target: None,
timeout: Duration::from_secs(60),
mode: CompilationMode::default(),
extra_flags: Vec::new(),
}
}
#[must_use]
pub fn edition(mut self, edition: RustEdition) -> Self {
self.edition = edition;
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn mode(mut self, mode: CompilationMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub fn target(mut self, target: &str) -> Self {
self.target = Some(target.to_string());
self
}
#[must_use]
pub fn extra_flag(mut self, flag: &str) -> Self {
self.extra_flags.push(flag.to_string());
self
}
fn parse_json_diagnostics(
&self,
output: &str,
) -> (Vec<CompilerDiagnostic>, Vec<CompilerDiagnostic>) {
let mut errors = Vec::new();
let mut warnings = Vec::new();
for line in output.lines() {
if line.starts_with('{') && line.ends_with('}') {
if line.contains("\"$message_type\":\"diagnostic\"")
|| (line.contains("\"level\":") && !line.contains("\"rendered\""))
{
if let Some(diag) = self.parse_single_json_diagnostic(line) {
if diag.message.contains("aborting due to") {
continue;
}
if diag.severity == DiagnosticSeverity::Error {
errors.push(diag);
} else {
warnings.push(diag);
}
}
}
}
}
(errors, warnings)
}
#[allow(clippy::unused_self)]
fn parse_single_json_diagnostic(&self, json: &str) -> Option<CompilerDiagnostic> {
let level = extract_json_string(json, "level")?;
let message = extract_json_string(json, "message")?;
let severity = match level.as_str() {
"error" => DiagnosticSeverity::Error,
"warning" => DiagnosticSeverity::Warning,
"note" => DiagnosticSeverity::Note,
"help" => DiagnosticSeverity::Help,
_ => return None,
};
let code_str = extract_nested_json_string(json, "code", "code")
.unwrap_or_else(|| "unknown".to_string());
let code = lookup_error_code(&code_str);
let span = extract_span_from_json(json);
let mut diag = CompilerDiagnostic::new(code, severity, &message, span);
if let Some(expected) = extract_json_string(json, "expected") {
diag = diag.with_expected(TypeInfo::new(&expected));
}
if let Some(found) = extract_json_string(json, "found") {
diag = diag.with_found(TypeInfo::new(&found));
}
if let Some(suggestions) = extract_suggestions_from_json(json) {
for suggestion in suggestions {
diag = diag.with_suggestion(suggestion);
}
}
Some(diag)
}
fn compile_standalone(
&self,
source: &str,
options: &CompileOptions,
) -> CITLResult<CompilationResult> {
let start = Instant::now();
let temp_dir = std::env::temp_dir();
let unique_id = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let source_file = temp_dir.join(format!(
"citl_compile_{}_{}.rs",
std::process::id(),
unique_id
));
std::fs::write(&source_file, source)?;
let mut cmd = Command::new(&self.rustc_path);
cmd.arg("--edition").arg(self.edition.as_str())
.arg("--crate-type").arg("lib") .arg("--error-format=json")
.arg("--emit=metadata") .arg("-o").arg(temp_dir.join("citl_output"))
.arg(&source_file)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(target) = &self.target {
cmd.arg("--target").arg(target);
}
for flag in &self.extra_flags {
cmd.arg(flag);
}
for flag in &options.extra_flags {
cmd.arg(flag);
}
let output = cmd.output()?;
let duration = start.elapsed();
std::fs::remove_file(&source_file).ok();
std::fs::remove_file(temp_dir.join("citl_output")).ok();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let (errors, warnings) = self.parse_json_diagnostics(&stderr);
if output.status.success() || errors.is_empty() {
Ok(CompilationResult::Success {
artifact: None,
warnings,
metrics: CompilationMetrics {
duration,
memory_bytes: None,
units: 1,
},
})
} else {
Ok(CompilationResult::Failure {
errors,
warnings,
raw_output: stderr,
})
}
}
fn compile_cargo_check(
&self,
source: &str,
manifest_path: &PathBuf,
_options: &CompileOptions,
) -> CITLResult<CompilationResult> {
let start = Instant::now();
let project_dir = manifest_path
.parent()
.ok_or_else(|| CITLError::ConfigurationError {
message: "Invalid manifest path".to_string(),
})?;
let src_dir = project_dir.join("src");
std::fs::create_dir_all(&src_dir)?;
let lib_file = src_dir.join("lib.rs");
std::fs::write(&lib_file, source)?;
let mut cmd = Command::new(&self.cargo_path);
cmd.arg("check")
.arg("--manifest-path")
.arg(manifest_path)
.arg("--message-format=json")
.current_dir(project_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = cmd.output()?;
let duration = start.elapsed();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let (errors, warnings) = self.parse_cargo_json_diagnostics(&stdout);
if output.status.success() || errors.is_empty() {
Ok(CompilationResult::Success {
artifact: None,
warnings,
metrics: CompilationMetrics {
duration,
memory_bytes: None,
units: 1,
},
})
} else {
Ok(CompilationResult::Failure {
errors,
warnings,
raw_output: stdout,
})
}
}
fn parse_cargo_json_diagnostics(
&self,
output: &str,
) -> (Vec<CompilerDiagnostic>, Vec<CompilerDiagnostic>) {
let mut errors = Vec::new();
let mut warnings = Vec::new();
for line in output.lines() {
if line.starts_with('{') && line.contains("\"reason\":\"compiler-message\"") {
if let Some(diag) = self.parse_cargo_message(line) {
if diag.message.contains("aborting due to") {
continue;
}
if diag.severity == DiagnosticSeverity::Error {
errors.push(diag);
} else {
warnings.push(diag);
}
}
}
}
(errors, warnings)
}
fn parse_cargo_message(&self, json: &str) -> Option<CompilerDiagnostic> {
let msg_start = json.find("\"message\":{")?;
let msg_rest = &json[msg_start + 11..];
let mut depth = 1;
let mut end = 0;
for (i, c) in msg_rest.char_indices() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = i;
break;
}
}
_ => {}
}
}
if end > 0 {
let inner_msg = format!("{{{}}}", &msg_rest[..end]);
self.parse_single_json_diagnostic(&inner_msg)
} else {
None
}
}
}
#[derive(Debug)]
pub struct CargoProject {
name: String,
edition: RustEdition,
dependencies: Vec<(String, String)>,
temp_dir: Option<PathBuf>,
manifest_path: Option<PathBuf>,
}
impl CargoProject {
#[must_use]
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
edition: RustEdition::default(),
dependencies: Vec::new(),
temp_dir: None,
manifest_path: None,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn edition(mut self, edition: RustEdition) -> Self {
self.edition = edition;
self
}
#[must_use]
pub fn dependency(mut self, name: &str, version: &str) -> Self {
self.dependencies
.push((name.to_string(), version.to_string()));
self
}
#[must_use]
pub fn dependencies(&self) -> &[(String, String)] {
&self.dependencies
}
#[must_use]
pub fn manifest_path(&self) -> Option<&PathBuf> {
self.manifest_path.as_ref()
}
#[must_use]
pub fn project_dir(&self) -> Option<&PathBuf> {
self.temp_dir.as_ref()
}
pub fn write_to_temp(mut self) -> CITLResult<Self> {
let temp_dir = std::env::temp_dir().join(format!("citl_{}", self.name));
std::fs::create_dir_all(&temp_dir)?;
let src_dir = temp_dir.join("src");
std::fs::create_dir_all(&src_dir)?;
let manifest_path = temp_dir.join("Cargo.toml");
let cargo_toml = self.generate_cargo_toml();
std::fs::write(&manifest_path, cargo_toml)?;
let lib_file = src_dir.join("lib.rs");
std::fs::write(&lib_file, "")?;
self.temp_dir = Some(temp_dir);
self.manifest_path = Some(manifest_path);
Ok(self)
}
fn generate_cargo_toml(&self) -> String {
use std::fmt::Write;
let mut toml = format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "{}"
[dependencies]
"#,
self.name,
self.edition.as_str()
);
for (name, version) in &self.dependencies {
let _ = writeln!(toml, "{name} = \"{version}\"");
}
toml
}
}