use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Mutex};
use oxc_span::Span;
use rustc_hash::FxHashMap;
use fallow_types::discover::{DiscoveredFile, FileId};
use fallow_types::extract::{DynamicImportInfo, DynamicImportPattern, ImportInfo, ImportedName};
use super::ResolveResult;
use super::ResolvedImport;
use super::fallbacks::make_glob_from_pattern;
use super::specifier::resolve_import_specifier;
use super::types::ResolveContext;
use fallow_config::extglob::ExtglobMatcher;
pub(super) fn resolve_dynamic_imports(
ctx: &ResolveContext,
file_path: &Path,
dynamic_imports: &[DynamicImportInfo],
) -> Vec<ResolvedImport> {
dynamic_imports
.iter()
.flat_map(|imp| resolve_single_dynamic_import(ctx, file_path, imp))
.collect()
}
pub(super) fn resolve_single_dynamic_import(
ctx: &ResolveContext,
file_path: &Path,
imp: &DynamicImportInfo,
) -> Vec<ResolvedImport> {
let target = resolve_import_specifier(ctx, file_path, &imp.source, false);
if imp.is_speculative
&& (target.is_bare_package() || matches!(target, ResolveResult::Unresolvable(_)))
{
if let Some(file_id) = resolve_root_manual_mock(ctx, file_path, &imp.source) {
return vec![dynamic_import_with(
imp,
ImportedName::Namespace,
imp.local_name.clone().unwrap_or_default(),
ResolveResult::SyntheticAutoImport(file_id),
)];
}
return Vec::new();
}
if !imp.destructured_names.is_empty() {
return resolve_destructured_dynamic_import(imp, &target);
}
if imp.local_name.is_some() {
return vec![dynamic_import_with(
imp,
ImportedName::Namespace,
imp.local_name.clone().unwrap_or_default(),
target,
)];
}
if imp.value_escapes {
return vec![dynamic_import_with(
imp,
ImportedName::Namespace,
String::new(),
target,
)];
}
vec![dynamic_import_with(
imp,
ImportedName::SideEffect,
String::new(),
target,
)]
}
fn resolve_root_manual_mock(
ctx: &ResolveContext<'_>,
file_path: &Path,
source: &str,
) -> Option<FileId> {
let candidate = source.strip_prefix("__mocks__/")?;
if candidate.is_empty() {
return None;
}
let mut dir = file_path.parent();
while let Some(current) = dir {
let base = current.join("__mocks__").join(candidate);
for ext in ctx.extensions {
let mut with_ext = base.clone().into_os_string();
with_ext.push(ext);
if let Some(file_id) = project_file_id(ctx, Path::new(&with_ext)) {
return Some(file_id);
}
let index = base.join(format!("index{ext}"));
if let Some(file_id) = project_file_id(ctx, &index) {
return Some(file_id);
}
}
if current == ctx.root {
break;
}
dir = current.parent();
}
None
}
fn project_file_id(ctx: &ResolveContext<'_>, path: &Path) -> Option<FileId> {
ctx.raw_path_to_id
.get(path)
.or_else(|| ctx.path_to_id.get(path))
.copied()
}
fn resolve_destructured_dynamic_import(
imp: &DynamicImportInfo,
target: &ResolveResult,
) -> Vec<ResolvedImport> {
imp.destructured_names
.iter()
.map(|name| {
let imported_name = if name == "default" {
ImportedName::Default
} else {
ImportedName::Named(name.clone())
};
dynamic_import_with(imp, imported_name, name.clone(), target.clone())
})
.collect()
}
fn dynamic_import_with(
imp: &DynamicImportInfo,
imported_name: ImportedName,
local_name: String,
target: ResolveResult,
) -> ResolvedImport {
ResolvedImport {
info: ImportInfo {
source: imp.source.clone(),
imported_name,
local_name,
is_type_only: false,
is_type_only_star: false,
from_style: false,
span: imp.span,
source_span: Span::default(),
},
target,
}
}
#[derive(Default)]
pub(super) struct GlobMatcherCache {
map: Mutex<FxHashMap<String, Option<Arc<ExtglobMatcher>>>>,
}
impl GlobMatcherCache {
fn get(&self, glob_str: &str, excludes: &[&str]) -> Option<Arc<ExtglobMatcher>> {
let mut key = glob_str.to_string();
for exclude in excludes {
key.push('\0');
key.push_str(exclude);
}
if let Ok(cache) = self.map.lock()
&& let Some(value) = cache.get(&key)
{
return value.clone();
}
let value = ExtglobMatcher::new(glob_str, excludes).map(Arc::new);
let first_insert = self
.map
.lock()
.map_or(true, |mut cache| cache.insert(key, value.clone()).is_none());
if value.is_none() && first_insert {
tracing::warn!(
"Dynamic import pattern `{glob_str}` is not a valid glob and matches no files"
);
}
value
}
}
pub(super) fn resolve_dynamic_patterns(
glob_cache: &GlobMatcherCache,
root: &Path,
from_dir: &Path,
patterns: &[DynamicImportPattern],
canonical_paths: &[PathBuf],
files: &[DiscoveredFile],
) -> Vec<(DynamicImportPattern, Vec<FileId>)> {
patterns
.iter()
.map(|pattern| {
let glob_str = make_glob_from_pattern(pattern);
let Some(based) = BasedPattern::new(&glob_str, &pattern.excludes, from_dir, root)
else {
return (pattern.clone(), Vec::new());
};
let excludes: Vec<&str> = based.excludes.iter().map(String::as_str).collect();
let Some(matcher) = glob_cache.get(based.include, &excludes) else {
return (pattern.clone(), Vec::new());
};
let matches = |path: &Path| {
path.strip_prefix(based.base)
.is_ok_and(|relative| matcher.is_match(relative))
};
let matched: Vec<FileId> = if canonical_paths.is_empty() {
files
.iter()
.filter(|f| matches(&f.path))
.map(|f| f.id)
.collect()
} else {
canonical_paths
.iter()
.enumerate()
.filter(|(_idx, canonical)| matches(canonical))
.map(|(idx, _)| files[idx].id)
.collect()
};
(pattern.clone(), matched)
})
.collect()
}
struct BasedPattern<'a> {
base: &'a Path,
include: &'a str,
excludes: Vec<String>,
}
impl<'a> BasedPattern<'a> {
fn new(glob: &'a str, excludes: &[String], from_dir: &'a Path, root: &Path) -> Option<Self> {
let (ups, include) = split_parent_steps(glob);
let base = from_dir.ancestors().nth(ups)?;
if ups > 0 && !base.starts_with(root) {
return None;
}
let excludes = excludes
.iter()
.filter_map(|exclude| rebase_exclude(exclude, ups, from_dir))
.collect();
Some(Self {
base,
include,
excludes,
})
}
}
fn rebase_exclude(exclude: &str, ups: usize, from_dir: &Path) -> Option<String> {
let (exclude_ups, rest) = split_parent_steps(exclude);
if exclude_ups > ups {
return None;
}
let skipped: Vec<_> = from_dir
.components()
.rev()
.skip(exclude_ups)
.take(ups - exclude_ups)
.collect();
if skipped.len() != ups - exclude_ups {
return None;
}
let mut rebased = String::new();
for component in skipped.iter().rev() {
let Component::Normal(name) = component else {
return None;
};
escape_glob_literal(&name.to_string_lossy(), &mut rebased);
rebased.push('/');
}
rebased.push_str(rest);
Some(rebased)
}
fn split_parent_steps(glob: &str) -> (usize, &str) {
let mut ups = 0;
let mut rest = glob;
loop {
if let Some(after) = rest.strip_prefix("./") {
rest = after;
} else if let Some(after) = rest.strip_prefix("../") {
rest = after;
ups += 1;
} else {
return (ups, rest);
}
}
}
fn escape_glob_literal(text: &str, out: &mut String) {
for character in text.chars() {
if matches!(character, '*' | '?' | '[' | ']' | '{' | '}' | '(') {
out.push('[');
out.push(character);
out.push(']');
} else {
out.push(character);
}
}
}