use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use crate::stack::module_path::normalize_module_path;
use crate::stack::reexports::reexport_sites;
use crate::stack::requires::require_sites;
use crate::stack::ImportBinding;
use crate::syntax::{SyntaxGraph, SyntaxNode};
use crate::{Language, NodeId};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum BindingReach {
Scope,
Importers,
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct ImportSite {
pub node: NodeId,
pub scope: NodeId,
pub symbol: String,
pub binding: ImportBinding,
pub reach: BindingReach,
}
#[must_use]
pub fn import_sites(syntax: &SyntaxGraph, language: Language) -> Vec<ImportSite> {
let read_binding = match language {
Language::Python => python_binding,
Language::JavaScript | Language::TypeScript => script_binding,
_ => return Vec::new(),
};
let bound_names: BTreeMap<NodeId, (NodeId, &str)> = syntax
.symbol_index
.iter()
.flat_map(|(symbol, scopes)| {
scopes.iter().flat_map(move |(scope, nodes)| {
nodes
.iter()
.map(move |node| (*node, (*scope, symbol.as_str())))
})
})
.collect();
let mut sites: Vec<ImportSite> = bound_names
.iter()
.filter_map(|(node, (scope, symbol))| {
let binding = read_binding(syntax.nodes.get(node)?, symbol)?;
Some(ImportSite {
node: *node,
scope: *scope,
symbol: String::from(*symbol),
binding,
reach: BindingReach::Scope,
})
})
.collect();
if matches!(language, Language::JavaScript | Language::TypeScript) {
sites.extend(require_sites(syntax, &bound_names));
sites.extend(reexport_sites(syntax));
sites.sort_by_key(|site| site.node);
}
sites
}
fn python_binding(node: &SyntaxNode, symbol: &str) -> Option<ImportBinding> {
let SyntaxNode::Import {
expression: Some(expression),
alias,
module_name,
..
} = node
else {
return None;
};
let Some(module_name) = module_name else {
let module = if alias.is_some() {
expression.as_str()
} else {
symbol
};
return Some(module_binding(module, 0));
};
let module = module_name.trim_start_matches('.');
let imported = expression.rsplit('.').next().unwrap_or(expression);
Some(symbol_binding(
module,
level_of(module_name.len().saturating_sub(module.len())),
imported,
symbol,
))
}
fn script_binding(node: &SyntaxNode, symbol: &str) -> Option<ImportBinding> {
match node {
SyntaxNode::Import {
alias: Some(_),
module_name: Some(specifier),
import_type,
..
} => {
let (module, relative_level) = script_module(specifier);
match import_type.as_deref() {
None => Some(symbol_binding(&module, relative_level, "default", symbol)),
Some("namespace_import" | "import_require_clause") => {
Some(module_binding(&module, relative_level))
}
Some(_) => None,
}
}
SyntaxNode::ModuleImport {
expression,
module_name: Some(specifier),
..
} => {
let imported = expression
.strip_prefix(specifier.as_str())?
.strip_prefix('.')?;
let (module, relative_level) = script_module(specifier);
Some(symbol_binding(&module, relative_level, imported, symbol))
}
_ => None,
}
}
pub(super) fn script_module(specifier: &str) -> (String, u32) {
let mut segments = specifier.split('/').peekable();
let mut relative = false;
let mut parents = 0_usize;
while let Some(segment) = segments.next_if(|segment| matches!(*segment, "." | "..")) {
relative = true;
parents = parents.saturating_add(usize::from(segment == ".."));
}
let module = normalize_module_path(&segments.collect::<Vec<_>>().join("/"));
let relative_level = if relative {
level_of(parents.saturating_add(1))
} else {
0
};
(module, relative_level)
}
pub(super) fn module_binding(module: &str, relative_level: u32) -> ImportBinding {
ImportBinding {
module_stem: String::from(module),
source_symbol: None,
relative_level,
binds_module: true,
}
}
pub(super) fn symbol_binding(
module: &str,
relative_level: u32,
imported: &str,
symbol: &str,
) -> ImportBinding {
ImportBinding {
module_stem: String::from(module),
source_symbol: (imported != symbol).then(|| String::from(imported)),
relative_level,
binds_module: false,
}
}
fn level_of(depth: usize) -> u32 {
u32::try_from(depth).unwrap_or(u32::MAX)
}