use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use regex::Regex;
use crate::cairo0_compiler::{
cairo0_script_correct_version,
enter_venv_instructions,
Cairo0Script,
Cairo0ScriptVersionError,
};
use crate::path::resolve_project_relative_path;
pub fn verify_cairo0_compiler_deps(script_type: &Cairo0Script) {
if let Err(verification_error) = cairo0_script_correct_version(script_type) {
let error = match verification_error {
Cairo0ScriptVersionError::CompilerNotFound { error, .. } => {
format!("No installed cairo-lang found. Original error: {error}.")
}
Cairo0ScriptVersionError::IncorrectVersion { existing, script, .. } => {
format!("Installed {script:?} version: {existing}")
}
};
panic!(
"{script_type:?} script not found or of incorrect version ({error}). Please enter a \
venv and rerun the test:\n{}",
enter_venv_instructions(script_type)
)
}
}
fn is_import_line(line: &str) -> bool {
(line.starts_with("from ") && line.contains(" import ")) || line.starts_with("import ")
}
fn find_imports_end(lines: &[&str]) -> usize {
let mut in_multi_line_import = false;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if in_multi_line_import {
if trimmed.starts_with(')') {
in_multi_line_import = false;
}
} else if is_import_line(trimmed) {
in_multi_line_import = trimmed.ends_with('(');
} else if !trimmed.is_empty() && !trimmed.starts_with("//") {
return i;
}
}
lines.len()
}
pub fn remove_unused_cairo0_imports(content: &str) -> String {
let lines: Vec<&str> = content.lines().collect();
let mut result_lines: Vec<String> = Vec::new();
let mut i = 0;
let imports_end = find_imports_end(&lines);
let code_section = lines[imports_end..].join("\n");
let single_import_re = Regex::new(r"^from\s+\S+\s+import\s+(.+)$").unwrap();
let multi_import_start_re = Regex::new(r"^from\s+\S+\s+import\s+\($").unwrap();
let direct_import_re = Regex::new(r"^import\s+(.+)$").unwrap();
while i < lines.len() {
let line = lines[i];
if multi_import_start_re.is_match(line) {
let mut import_lines: Vec<(usize, &str)> = Vec::new(); i += 1;
while i < lines.len() && !lines[i].trim().starts_with(')') {
let import_name = lines[i].trim().trim_end_matches(',').trim();
if !import_name.is_empty() {
import_lines.push((i, import_name));
}
i += 1;
}
let end_line = i;
let used_lines: Vec<_> = import_lines
.iter()
.filter(|(_, name)| is_import_used(name, &code_section))
.collect();
if !used_lines.is_empty() {
result_lines.push(line.to_string()); for (idx, _) in &used_lines {
result_lines.push(lines[*idx].to_string());
}
result_lines.push(lines[end_line].to_string()); }
i = end_line + 1;
continue;
}
if let Some(caps) = single_import_re.captures(line) {
let imports_part = caps.get(1).unwrap().as_str();
let import_names: Vec<&str> =
imports_part.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
let used_imports: Vec<_> = import_names
.into_iter()
.filter(|name| is_import_used(name, &code_section))
.collect();
if !used_imports.is_empty() {
let from_part = line.split(" import ").next().unwrap();
result_lines.push(format!("{} import {}", from_part, used_imports.join(", ")));
}
i += 1;
continue;
}
if let Some(caps) = direct_import_re.captures(line) {
let import_part = caps.get(1).unwrap().as_str().trim();
if is_import_used(import_part, &code_section) {
result_lines.push(line.to_string());
}
i += 1;
continue;
}
result_lines.push(line.to_string());
i += 1;
}
result_lines.join("\n") + if content.ends_with('\n') { "\n" } else { "" }
}
fn get_imported_name(import_item: &str) -> &str {
let trimmed = import_item.trim();
if let Some(as_pos) = trimmed.find(" as ") {
trimmed[as_pos + 4..].trim()
} else if let Some(dot_pos) = trimmed.rfind('.') {
&trimmed[dot_pos + 1..]
} else {
trimmed
}
}
fn is_import_used(import_item: &str, code: &str) -> bool {
let name_to_search = get_imported_name(import_item);
let pattern = format!(r"\b{}\b", regex::escape(name_to_search));
let re = Regex::new(&pattern).unwrap();
re.is_match(code)
}
pub fn cairo0_format(unformatted: &String) -> String {
let files: HashMap<String, &String> =
[("single_file.cairo".to_string(), unformatted)].into_iter().collect();
let results = cairo0_format_batch(files);
results.into_values().next().unwrap()
}
pub fn cairo0_format_batch<S: AsRef<str>>(files: HashMap<String, S>) -> HashMap<String, String> {
if files.is_empty() {
return HashMap::new();
}
let script_type = Cairo0Script::Format;
verify_cairo0_compiler_deps(&script_type);
let temp_dir = tempfile::TempDir::new().unwrap();
let mut file_paths: Vec<PathBuf> = Vec::with_capacity(files.len());
let mut filenames: Vec<String> = Vec::with_capacity(files.len());
for (filename, content) in files {
let without_unused = remove_unused_cairo0_imports(content.as_ref());
let file_path = temp_dir.path().join(&filename);
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&file_path, without_unused).unwrap();
file_paths.push(file_path);
filenames.push(filename);
}
let mut format_command = Command::new(script_type.script_name());
format_command.arg("-i");
for path in &file_paths {
format_command.arg(path);
}
run_command(format_command);
let mut isort_command = Command::new("isort");
let isort_config_path = resolve_project_relative_path(".isort.cfg").unwrap();
isort_command.args([
"--settings-file",
isort_config_path.to_str().unwrap(),
"--lai",
"1",
"-m",
"3",
"--tc",
]);
for path in &file_paths {
isort_command.arg(path);
}
run_command(isort_command);
filenames
.into_iter()
.zip(file_paths.iter().map(|path| std::fs::read_to_string(path).unwrap()))
.collect()
}
fn run_command(mut cmd: Command) {
let output = cmd.output().unwrap();
let stderr_output = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "Command '{cmd:?}' failed: {stderr_output}");
}