use std::collections::{HashMap, HashSet};
use crate::resolution::types::ResolutionContext;
const SKIP_DIRS: &[&str] = &["target", "node_modules", ".git", "dist", "build"];
const MAX_GLOB_WALK_DEPTH: usize = 5;
pub fn get_cargo_workspace_crate_map(
context: &mut dyn ResolutionContext,
) -> HashMap<String, String> {
let mut result = HashMap::new();
let Some(root_cargo_toml) = context.read_file("Cargo.toml") else {
return result;
};
let members = parse_workspace_members(&root_cargo_toml);
for member_path in expand_members(&members, context) {
let member_cargo_path = format!("{member_path}/Cargo.toml");
let Some(member_cargo_toml) = context.read_file(&member_cargo_path) else {
continue;
};
if let Some(package_name) = parse_package_name(&member_cargo_toml) {
add_crate_alias(&mut result, &package_name, &member_path);
}
}
result
}
fn get_section(content: &str, section_name: &str) -> Option<String> {
let mut in_section = false;
let mut section_lines = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if !in_section {
if trimmed == format!("[{section_name}]") {
in_section = true;
}
continue;
}
if trimmed.starts_with('[') && trimmed.ends_with(']') {
break;
}
section_lines.push(line);
}
in_section.then(|| section_lines.join("\n"))
}
fn extract_quoted_values(value_list: &str) -> Vec<String> {
let mut values = Vec::new();
let mut quote = None;
let mut escaped = false;
let mut current = String::new();
for ch in value_list.chars() {
if quote.is_none() {
if ch == '"' || ch == '\'' {
quote = Some(ch);
current.clear();
}
continue;
}
if escaped {
current.push(ch);
escaped = false;
continue;
}
if ch == '\\' {
escaped = true;
continue;
}
if Some(ch) == quote {
let value = current.trim();
if !value.is_empty() {
values.push(value.to_string());
}
quote = None;
current.clear();
continue;
}
current.push(ch);
}
values
}
fn get_array_value(section: &str, key: &str) -> Option<String> {
let assignment = section.find(&key.to_string())?;
let after_key = §ion[assignment + key.len()..];
let eq = after_key.find('=')?;
let mut i = assignment + key.len() + eq + 1;
while section
.as_bytes()
.get(i)
.is_some_and(|b| b.is_ascii_whitespace())
{
i += 1;
}
if section.as_bytes().get(i) != Some(&b'[') {
return None;
}
i += 1;
let start = i;
let mut quote = None;
let mut escaped = false;
let mut depth = 1usize;
let chars = section.char_indices().collect::<Vec<_>>();
let mut pos = chars
.iter()
.position(|(idx, _)| *idx >= i)
.unwrap_or(chars.len());
while pos < chars.len() {
let (idx, ch) = chars[pos];
if quote.is_some() {
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if Some(ch) == quote {
quote = None;
}
pos += 1;
continue;
}
if ch == '"' || ch == '\'' {
quote = Some(ch);
} else if ch == '[' {
depth += 1;
} else if ch == ']' {
depth -= 1;
if depth == 0 {
return Some(section[start..idx].to_string());
}
}
pos += 1;
}
None
}
fn parse_workspace_members(cargo_toml: &str) -> Vec<String> {
get_section(cargo_toml, "workspace")
.and_then(|section| get_array_value(§ion, "members"))
.map(|value| extract_quoted_values(&value))
.unwrap_or_default()
}
fn parse_package_name(cargo_toml: &str) -> Option<String> {
let section = get_section(cargo_toml, "package")?;
for line in section.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("name") {
continue;
}
let rhs = trimmed.split_once('=')?.1.trim();
let value = rhs.trim_matches(|ch| ch == '"' || ch == '\'');
if !value.is_empty() {
return Some(value.to_string());
}
}
None
}
fn add_crate_alias(map: &mut HashMap<String, String>, crate_name: &str, member_path: &str) {
let normalized = crate_name.replace('-', "_");
map.insert(crate_name.to_string(), member_path.to_string());
if normalized != crate_name {
map.insert(normalized, member_path.to_string());
}
}
fn clean_path(member_path: &str) -> String {
member_path
.replace('\\', "/")
.trim_end_matches('/')
.to_string()
}
fn has_glob(value: &str) -> bool {
value
.chars()
.any(|ch| matches!(ch, '*' | '?' | '[' | ']' | '{' | '}' | '!'))
}
fn expand_members(members: &[String], context: &mut dyn ResolutionContext) -> Vec<String> {
let mut expanded = Vec::new();
let mut seen = HashSet::new();
for member in members {
let candidates = if has_glob(member) {
expand_glob_member(member, context)
} else {
vec![member.clone()]
};
for candidate in candidates {
let cleaned = clean_path(&candidate);
if seen.insert(cleaned.clone()) {
expanded.push(cleaned);
}
}
}
expanded
}
fn expand_glob_member(member: &str, context: &mut dyn ResolutionContext) -> Vec<String> {
let first_glob = member
.find(['*', '?', '[', ']', '{', '}', '!'])
.unwrap_or(member.len());
let static_prefix = member[..first_glob]
.rsplit_once('/')
.map(|(prefix, _)| prefix)
.unwrap_or(".")
.trim_end_matches('/');
let mut matches = Vec::new();
let mut seen = HashSet::new();
walk_glob(
if static_prefix.is_empty() {
"."
} else {
static_prefix
},
0,
member,
context,
&mut seen,
&mut matches,
);
matches
}
fn walk_glob(
dir: &str,
depth: usize,
glob: &str,
context: &mut dyn ResolutionContext,
seen: &mut HashSet<String>,
matches: &mut Vec<String>,
) {
if depth > MAX_GLOB_WALK_DEPTH {
return;
}
for child in context.list_directories(dir) {
if SKIP_DIRS.contains(&child.as_str()) || child.starts_with('.') {
continue;
}
let rel = if dir == "." {
child
} else {
format!("{dir}/{child}")
};
if glob_match(glob, &rel) && seen.insert(rel.clone()) {
matches.push(rel.clone());
}
walk_glob(&rel, depth + 1, glob, context, seen, matches);
}
}
fn glob_match(glob: &str, value: &str) -> bool {
if glob == value {
return true;
}
if let Some((prefix, suffix)) = glob.split_once('*') {
return value.starts_with(prefix) && value.ends_with(suffix);
}
false
}