use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default)]
pub struct ProjectAliases {
#[allow(dead_code)]
pub rust_crate_name: Option<String>,
pub go_module: Option<String>,
pub ts_path_aliases: Vec<(String, String)>,
pub php_psr4: Vec<(String, String)>,
}
pub fn detect_aliases(root: &Path) -> ProjectAliases {
ProjectAliases {
rust_crate_name: parse_cargo_name(&root.join("Cargo.toml")),
go_module: parse_go_module(&root.join("go.mod")),
ts_path_aliases: parse_tsconfig_paths(&root.join("tsconfig.json")),
php_psr4: parse_composer_psr4(&root.join("composer.json")),
}
}
pub fn parse_go_module(path: &Path) -> Option<String> {
let s = std::fs::read_to_string(path).ok()?;
for line in s.lines() {
let t = line.trim();
if let Some(rest) = t.strip_prefix("module ") {
let n = rest.trim().trim_matches('"').trim();
if !n.is_empty() {
return Some(n.to_string());
}
}
}
None
}
fn parse_cargo_name(path: &Path) -> Option<String> {
let s = std::fs::read_to_string(path).ok()?;
let mut in_package = false;
for line in s.lines() {
let t = line.trim();
if t.starts_with('[') {
in_package = t == "[package]";
continue;
}
if in_package {
if let Some(rest) = t.strip_prefix("name") {
if let Some(eq) = rest.find('=') {
let val = rest[eq + 1..].trim().trim_matches('"').trim_matches('\'');
if !val.is_empty() {
return Some(val.replace('-', "_"));
}
}
}
}
}
None
}
pub fn parse_tsconfig_paths(path: &Path) -> Vec<(String, String)> {
let Ok(s) = std::fs::read_to_string(path) else {
return Vec::new();
};
let Ok(v) = serde_json::from_str::<serde_json::Value>(strip_jsonc(&s).as_str()) else {
return Vec::new();
};
let Some(co) = v.get("compilerOptions") else {
return Vec::new();
};
let base_url = co
.get("baseUrl")
.and_then(|x| x.as_str())
.map(|s| s.trim_start_matches("./").to_string())
.unwrap_or_default();
let mut out = Vec::new();
let Some(paths) = co.get("paths").and_then(|x| x.as_object()) else {
return out;
};
for (prefix, targets) in paths {
let target = targets
.as_array()
.and_then(|a| a.first())
.and_then(|x| x.as_str())
.unwrap_or("");
if target.is_empty() {
continue;
}
let prefix_norm = prefix.replace("/*", "/");
let mut target_norm = target.replace("/*", "/");
target_norm = target_norm.trim_start_matches("./").to_string();
let combined = if base_url.is_empty() {
target_norm
} else {
format!("{}/{}", base_url.trim_end_matches('/'), target_norm)
};
out.push((prefix_norm, combined));
}
out
}
fn strip_jsonc(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for line in s.lines() {
if let Some(idx) = line.find("//") {
let before = &line[..idx];
let quotes = before.chars().filter(|c| *c == '"').count();
if quotes % 2 == 0 {
out.push_str(before);
out.push('\n');
continue;
}
}
out.push_str(line);
out.push('\n');
}
let mut cleaned = String::with_capacity(out.len());
let bytes = out.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b',' {
let mut j = i + 1;
while j < bytes.len() && (bytes[j] as char).is_whitespace() {
j += 1;
}
if j < bytes.len() && (bytes[j] == b'}' || bytes[j] == b']') {
i += 1;
continue;
}
}
cleaned.push(bytes[i] as char);
i += 1;
}
cleaned
}
pub fn parse_composer_psr4(path: &Path) -> Vec<(String, String)> {
let Ok(s) = std::fs::read_to_string(path) else {
return Vec::new();
};
let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) else {
return Vec::new();
};
let mut out = Vec::new();
for section in ["autoload", "autoload-dev"] {
let Some(autoload) = v.get(section) else { continue };
let Some(psr4) = autoload.get("psr-4").and_then(|x| x.as_object()) else {
continue;
};
for (prefix, target) in psr4 {
let prefix_norm = prefix.replace('\\', "/");
let dirs: Vec<String> = if let Some(s) = target.as_str() {
vec![s.to_string()]
} else if let Some(arr) = target.as_array() {
arr.iter()
.filter_map(|x| x.as_str().map(str::to_string))
.collect()
} else {
continue;
};
for dir in dirs {
if dir.is_empty() {
continue;
}
let dir_norm = dir.trim_end_matches('/').to_string();
out.push((prefix_norm.clone(), dir_norm));
}
}
}
out.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
out
}
#[allow(dead_code)]
pub fn cargo_workspace_members(root: &Path) -> Vec<PathBuf> {
let s = match std::fs::read_to_string(root.join("Cargo.toml")) {
Ok(s) => s,
Err(_) => return Vec::new(),
};
let mut in_ws = false;
let mut members: Vec<String> = Vec::new();
for raw in s.lines() {
let line = raw.trim();
if line.starts_with('[') {
in_ws = line == "[workspace]";
continue;
}
if in_ws {
if let Some(rest) = line.strip_prefix("members") {
if let Some(eq) = rest.find('=') {
let val = rest[eq + 1..].trim();
if let Some(inner) = val.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
for tok in inner.split(',') {
let t = tok.trim().trim_matches('"').trim_matches('\'');
if !t.is_empty() {
members.push(t.to_string());
}
}
}
}
}
}
}
members.into_iter().map(|m| root.join(m)).collect()
}