use crate::model::ModuleId;
use rustc_hash::FxHashMap;
use std::path::{Component, Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathAlias {
pub scope: PathBuf,
pub prefix: String,
pub wildcard: bool,
pub targets: Vec<PathBuf>,
}
impl PathAlias {
pub fn apply(&self, specifier: &str) -> Vec<PathBuf> {
let suffix = match self.wildcard {
true => match specifier.strip_prefix(&self.prefix) {
Some(rest) if !rest.is_empty() => rest,
_ => return Vec::new(),
},
false if specifier == self.prefix => "",
false => return Vec::new(),
};
self.targets
.iter()
.map(|target| match suffix.is_empty() {
true => target.clone(),
false => normalize(&target.join(suffix)),
})
.collect()
}
fn specificity(&self) -> (usize, usize) {
(self.scope.components().count(), self.prefix.len())
}
}
pub struct ModuleIndex {
by_path: FxHashMap<PathBuf, ModuleId>,
roots: Vec<PathBuf>,
aliases: Vec<PathAlias>,
import_roots: Vec<PathBuf>,
}
impl ModuleIndex {
pub fn new(roots: Vec<PathBuf>) -> Self {
Self {
by_path: FxHashMap::default(),
roots,
aliases: Vec::new(),
import_roots: Vec::new(),
}
}
pub fn set_import_roots(&mut self, mut roots: Vec<PathBuf>) {
roots.sort();
roots.dedup();
self.import_roots = roots;
}
pub fn import_roots(&self) -> &[PathBuf] {
&self.import_roots
}
pub fn paths(&self) -> impl Iterator<Item = &Path> {
self.by_path.keys().map(PathBuf::as_path)
}
pub fn set_aliases(&mut self, mut aliases: Vec<PathAlias>) {
aliases.sort_by_key(|alias| std::cmp::Reverse(alias.specificity()));
self.aliases = aliases;
}
pub fn aliases(&self) -> &[PathAlias] {
&self.aliases
}
pub fn insert(&mut self, path: PathBuf, module: ModuleId) {
self.by_path.insert(path, module);
}
pub fn get(&self, path: &Path) -> Option<ModuleId> {
self.by_path.get(path).copied()
}
pub fn contains(&self, path: &Path) -> bool {
self.by_path.contains_key(path)
}
pub fn roots(&self) -> &[PathBuf] {
&self.roots
}
pub fn against_roots(
&self,
relative: &str,
try_at: impl Fn(&Path) -> Option<ModuleId>,
) -> Option<ModuleId> {
self.roots
.iter()
.find_map(|root| try_at(&normalize(&root.join(relative))))
}
pub fn against_aliases(
&self,
specifier: &str,
importer: &Path,
try_at: impl Fn(&Path) -> Option<ModuleId>,
) -> Option<ModuleId> {
self.aliases
.iter()
.filter(|alias| importer.starts_with(&alias.scope))
.flat_map(|alias| alias.apply(specifier))
.find_map(|candidate| try_at(&candidate))
}
pub fn len(&self) -> usize {
self.by_path.len()
}
pub fn is_empty(&self) -> bool {
self.by_path.is_empty()
}
}
pub fn append_extension(path: &Path, extension: &str) -> PathBuf {
let mut name = path
.file_name()
.map(|n| n.to_os_string())
.unwrap_or_default();
name.push(".");
name.push(extension);
path.with_file_name(name)
}
pub fn normalize(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if !out.pop() {
out.push("..");
}
}
other => out.push(other.as_os_str()),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_index_answers_exact_paths_only() {
let mut index = ModuleIndex::new(vec![PathBuf::from("/p")]);
index.insert(PathBuf::from("/p/src/a.ts"), ModuleId(3));
assert_eq!(index.get(Path::new("/p/src/a.ts")), Some(ModuleId(3)));
assert!(index.contains(Path::new("/p/src/a.ts")));
assert_eq!(index.get(Path::new("/p/src/a")), None);
assert_eq!(index.len(), 1);
assert!(!index.is_empty());
}
#[test]
fn against_roots_tries_each_root_in_order() {
let mut index = ModuleIndex::new(vec![PathBuf::from("/first"), PathBuf::from("/second")]);
index.insert(PathBuf::from("/second/lib/x.ts"), ModuleId(1));
let found = index.against_roots("lib/x.ts", |candidate| index.get(candidate));
assert_eq!(found, Some(ModuleId(1)));
assert_eq!(
index.against_roots("lib/missing.ts", |candidate| index.get(candidate)),
None
);
}
fn alias(scope: &str, prefix: &str, wildcard: bool, targets: &[&str]) -> PathAlias {
PathAlias {
scope: PathBuf::from(scope),
prefix: prefix.to_string(),
wildcard,
targets: targets.iter().map(PathBuf::from).collect(),
}
}
#[test]
fn a_wildcard_alias_joins_the_matched_suffix_to_each_target() {
let a = alias("/p", "@/", true, &["/p/src", "/p/lib"]);
assert_eq!(
a.apply("@/ui/button"),
vec![
PathBuf::from("/p/src/ui/button"),
PathBuf::from("/p/lib/ui/button")
]
);
assert!(
a.apply("@").is_empty(),
"a wildcard needs something to match"
);
assert!(a.apply("other/x").is_empty());
}
#[test]
fn an_exact_alias_matches_the_whole_specifier_only() {
let a = alias("/p", "@config", false, &["/p/src/config.ts"]);
assert_eq!(a.apply("@config"), vec![PathBuf::from("/p/src/config.ts")]);
assert!(a.apply("@config/deep").is_empty());
}
#[test]
fn aliases_are_tried_most_specific_first() {
let mut index = ModuleIndex::new(vec![PathBuf::from("/p")]);
index.insert(PathBuf::from("/p/narrow/button.ts"), ModuleId(1));
index.insert(PathBuf::from("/p/wide/ui/button.ts"), ModuleId(2));
index.set_aliases(vec![
alias("/p", "@/", true, &["/p/wide"]),
alias("/p", "@/ui/", true, &["/p/narrow"]),
]);
assert_eq!(index.aliases()[0].prefix, "@/ui/", "longer prefix first");
let found = index.against_aliases("@/ui/button", Path::new("/p/a.ts"), |candidate| {
index.get(&append_extension(candidate, "ts"))
});
assert_eq!(found, Some(ModuleId(1)));
}
#[test]
fn an_alias_outside_the_importers_directory_is_skipped() {
let mut index = ModuleIndex::new(vec![PathBuf::from("/p")]);
index.insert(PathBuf::from("/p/one/src/x.ts"), ModuleId(1));
index.set_aliases(vec![alias("/p/one", "@/", true, &["/p/one/src"])]);
let resolve = |importer: &str| {
index.against_aliases("@/x", Path::new(importer), |candidate| {
index.get(&append_extension(candidate, "ts"))
})
};
assert_eq!(resolve("/p/one/a.ts"), Some(ModuleId(1)));
assert_eq!(resolve("/p/two/b.ts"), None);
}
#[test]
fn normalize_collapses_dot_segments() {
assert_eq!(
normalize(Path::new("/p/src/./deep/../shared")),
PathBuf::from("/p/src/shared")
);
}
#[test]
fn climbing_above_the_root_does_not_panic() {
assert_eq!(
normalize(Path::new("/p/../../x")),
PathBuf::from("/../x"),
"the excess `..` survives as a path nothing in the index can match"
);
}
#[test]
fn append_extension_keeps_dots_already_in_the_name() {
assert_eq!(
append_extension(Path::new("/p/a.spec"), "ts"),
PathBuf::from("/p/a.spec.ts")
);
}
}