use std::collections::{HashMap, HashSet};
use std::fs::read_to_string;
use std::path::{absolute, Path, PathBuf};
use serde_json::Value as JsonValue;
use toml::Table;
fn normalize_crate(name: &str) -> String {
name.replace('-', "_")
}
fn find_up(start: &Path, filename: &str) -> Option<PathBuf> {
let mut dir = Some(start);
while let Some(current) = dir {
let candidate = current.join(filename);
if candidate.is_file() {
return Some(candidate);
}
dir = current.parent();
}
None
}
type WorkspaceRoot = Option<(PathBuf, Table)>;
fn table_lib_name(table: &Table) -> Option<String> {
table
.get("lib")
.and_then(|v| v.as_table())
.and_then(|lib| lib.get("name"))
.and_then(|v| v.as_str())
.map(normalize_crate)
}
fn lib_name(dir: &Path) -> Option<String> {
let text = read_to_string(dir.join("Cargo.toml")).ok()?;
let table = text.parse::<Table>().ok()?;
table_lib_name(&table)
}
fn workspace_root(manifest_dir: &Path, table: &Table) -> WorkspaceRoot {
if table.get("workspace").is_some_and(|v| v.is_table()) {
return Some((manifest_dir.to_path_buf(), table.clone()));
}
manifest_dir.ancestors().skip(1).find_map(|dir| {
let text = read_to_string(dir.join("Cargo.toml")).ok()?;
let parsed = text.parse::<Table>().ok()?;
parsed
.get("workspace")
.is_some_and(|v| v.is_table())
.then(|| (dir.to_path_buf(), parsed))
})
}
fn path_dependency_lib_name(spec: &Table, base_dir: &Path) -> Option<String> {
if spec.contains_key("package") {
return None;
}
let path = spec.get("path").and_then(|v| v.as_str())?;
lib_name(&base_dir.join(path))
}
fn inherited_lib_name(root: &WorkspaceRoot, key: &str) -> Option<String> {
let (root_dir, root_table) = root.as_ref()?;
let entry = root_table
.get("workspace")
.and_then(|v| v.as_table())
.and_then(|w| w.get("dependencies"))
.and_then(|v| v.as_table())
.and_then(|d| d.get(key))
.and_then(|v| v.as_table())?;
path_dependency_lib_name(entry, root_dir)
}
struct DepSource<'a> {
manifest: &'a Table,
dir: &'a Path,
workspace: Option<WorkspaceRoot>,
}
fn add_dependencies(deps: &Table, source: &mut DepSource<'_>, names: &mut HashSet<String>) {
for (key, value) in deps {
names.insert(normalize_crate(key));
let Some(spec) = value.as_table() else {
continue;
};
if spec.contains_key("package") {
continue;
}
if spec.contains_key("path") {
names.extend(path_dependency_lib_name(spec, source.dir));
} else if spec.get("workspace").and_then(|v| v.as_bool()) == Some(true) {
let (manifest, dir) = (source.manifest, source.dir);
let root = source
.workspace
.get_or_insert_with(|| workspace_root(dir, manifest));
names.extend(inherited_lib_name(root, key));
}
}
}
fn add_dep_tables(parent: &Table, source: &mut DepSource<'_>, names: &mut HashSet<String>) {
for key in ["dependencies", "dev-dependencies", "build-dependencies"] {
if let Some(table) = parent.get(key).and_then(|v| v.as_table()) {
add_dependencies(table, source, names);
}
}
}
fn collect_cargo_deps(table: &Table, manifest_dir: &Path) -> HashSet<String> {
let mut names = HashSet::new();
let mut source = DepSource {
manifest: table,
dir: manifest_dir,
workspace: None,
};
add_dep_tables(table, &mut source, &mut names);
if let Some(deps) = table
.get("workspace")
.and_then(|v| v.as_table())
.and_then(|w| w.get("dependencies"))
.and_then(|v| v.as_table())
{
add_dependencies(deps, &mut source, &mut names);
}
if let Some(targets) = table.get("target").and_then(|v| v.as_table()) {
for target in targets.values().filter_map(|v| v.as_table()) {
add_dep_tables(target, &mut source, &mut names);
}
}
if let Some(name) = table
.get("package")
.and_then(|v| v.as_table())
.and_then(|p| p.get("name"))
.and_then(|v| v.as_str())
{
names.insert(normalize_crate(name));
}
names.extend(table_lib_name(table));
names
}
fn collect_package_json_deps(value: &JsonValue) -> HashSet<String> {
let mut names = HashSet::new();
for key in [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
] {
if let Some(obj) = value.get(key).and_then(|v| v.as_object()) {
names.extend(obj.keys().cloned());
}
}
names
}
fn strip_jsonc(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
let mut in_string = false;
let mut escaped = false;
while let Some(c) = chars.next() {
if in_string {
out.push(c);
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '"' {
in_string = false;
}
continue;
}
match c {
'"' => {
in_string = true;
out.push(c);
}
'/' if chars.peek() == Some(&'/') => {
for next in chars.by_ref() {
if next == '\n' {
out.push('\n');
break;
}
}
}
'/' if chars.peek() == Some(&'*') => {
chars.next();
let mut prev = '\0';
for next in chars.by_ref() {
if prev == '*' && next == '/' {
break;
}
prev = next;
}
}
_ => out.push(c),
}
}
let chars: Vec<char> = out.chars().collect();
let mut cleaned = String::with_capacity(out.len());
for (i, &c) in chars.iter().enumerate() {
if c == ',' {
let next = chars[i + 1..].iter().find(|c| !c.is_whitespace()).copied();
if matches!(next, Some('}') | Some(']')) {
continue;
}
}
cleaned.push(c);
}
cleaned
}
fn collect_ts_aliases(value: &JsonValue) -> Vec<(String, bool)> {
let Some(paths) = value
.get("compilerOptions")
.and_then(|o| o.get("paths"))
.and_then(|p| p.as_object())
else {
return Vec::new();
};
paths
.keys()
.map(|key| match key.strip_suffix('*') {
Some(prefix) => (prefix.to_string(), true),
None => (key.clone(), false),
})
.collect()
}
#[derive(Default)]
pub struct ManifestResolver {
cargo: HashMap<PathBuf, Option<PathBuf>>,
cargo_by_manifest: HashMap<PathBuf, HashSet<String>>,
package_json: HashMap<PathBuf, Option<HashSet<String>>>,
ts_aliases: HashMap<PathBuf, Vec<(String, bool)>>,
}
impl ManifestResolver {
pub fn new() -> Self {
Self::default()
}
pub(super) fn cargo_deps(&mut self, dir: &Path) -> Option<&HashSet<String>> {
if !self.cargo.contains_key(dir) {
let manifest = find_up(dir, "Cargo.toml").and_then(|path| {
if self.cargo_by_manifest.contains_key(&path) {
return Some(path);
}
let table = read_to_string(&path).ok()?.parse::<Table>().ok()?;
let manifest_dir = path.parent().unwrap_or_else(|| Path::new("."));
let manifest_dir =
absolute(manifest_dir).unwrap_or_else(|_| manifest_dir.to_path_buf());
let deps = collect_cargo_deps(&table, &manifest_dir);
self.cargo_by_manifest.insert(path.clone(), deps);
Some(path)
});
self.cargo.insert(dir.to_path_buf(), manifest);
}
let manifest = self.cargo.get(dir)?.as_ref()?;
self.cargo_by_manifest.get(manifest)
}
pub(super) fn package_json_deps(&mut self, dir: &Path) -> Option<&HashSet<String>> {
if !self.package_json.contains_key(dir) {
let deps = find_up(dir, "package.json")
.and_then(|path| read_to_string(&path).ok())
.and_then(|text| serde_json::from_str::<JsonValue>(&text).ok())
.map(|value| collect_package_json_deps(&value));
self.package_json.insert(dir.to_path_buf(), deps);
}
self.package_json.get(dir).and_then(|entry| entry.as_ref())
}
fn ts_aliases(&mut self, dir: &Path) -> &[(String, bool)] {
if !self.ts_aliases.contains_key(dir) {
let aliases = find_up(dir, "tsconfig.json")
.and_then(|path| read_to_string(&path).ok())
.and_then(|text| serde_json::from_str::<JsonValue>(&strip_jsonc(&text)).ok())
.map(|value| collect_ts_aliases(&value))
.unwrap_or_default();
self.ts_aliases.insert(dir.to_path_buf(), aliases);
}
self.ts_aliases.get(dir).map(Vec::as_slice).unwrap_or(&[])
}
pub(super) fn ts_alias_matches(&mut self, dir: &Path, spec: &str) -> bool {
self.ts_aliases(dir).iter().any(|(prefix, wildcard)| {
if *wildcard {
spec.starts_with(prefix.as_str())
} else {
spec == prefix
}
})
}
}