use std::collections::{HashMap, HashSet};
use std::fs::read_to_string;
use std::path::{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
}
fn add_dep_table_keys(parent: &Table, 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()) {
names.extend(table.keys().map(|k| normalize_crate(k)));
}
}
}
fn collect_cargo_deps(table: &Table) -> HashSet<String> {
let mut names = HashSet::new();
add_dep_table_keys(table, &mut names);
if let Some(workspace) = table.get("workspace").and_then(|v| v.as_table()) {
if let Some(deps) = workspace.get("dependencies").and_then(|v| v.as_table()) {
names.extend(deps.keys().map(|k| normalize_crate(k)));
}
}
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_table_keys(target, &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
}
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<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 deps = find_up(dir, "Cargo.toml")
.and_then(|path| read_to_string(&path).ok())
.and_then(|text| text.parse::<Table>().ok())
.map(|table| collect_cargo_deps(&table));
self.cargo.insert(dir.to_path_buf(), deps);
}
self.cargo.get(dir).and_then(|entry| entry.as_ref())
}
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
}
})
}
}