use std::path::PathBuf;
use std::sync::{Arc, Condvar, Mutex};
use dashmap::DashMap;
use tower_lsp::Client;
use crate::file_analysis::{CrossFileLookup, FileAnalysis, SymKind};
#[cfg(test)]
use crate::file_analysis::InferredType;
use crate::module_resolver;
pub use crate::file_analysis::{CachedModule, SubInfo};
type InferredTypeOwned = crate::file_analysis::InferredType;
pub(crate) struct ResolveQueue {
pub priority: Mutex<Vec<String>>,
pub pending: Mutex<Vec<String>>,
pub condvar: Condvar,
}
pub(crate) struct ResolveNotify {
pub mu: Mutex<()>,
pub cv: Condvar,
}
pub(crate) struct WorkspaceRootChannel {
pub root: Mutex<Option<Option<String>>>,
pub condvar: Condvar,
}
pub struct ModuleEdgeIndexes {
names: DashMap<String, Vec<String>>,
bridges: DashMap<String, Vec<String>>,
children: DashMap<String, Vec<String>>,
}
impl ModuleEdgeIndexes {
pub fn new() -> Self {
ModuleEdgeIndexes {
names: DashMap::new(),
bridges: DashMap::new(),
children: DashMap::new(),
}
}
pub fn feed(&self, module_name: &str, analysis: &FileAnalysis) {
for name in Self::indexable_names(analysis) {
self.names
.entry(name)
.or_default()
.push(module_name.to_string());
}
for class in Self::bridge_classes(analysis) {
self.bridges
.entry(class)
.or_default()
.push(module_name.to_string());
}
for parent in Self::parent_classes(analysis) {
self.children
.entry(parent)
.or_default()
.push(module_name.to_string());
}
}
pub fn purge_module(&self, module_name: &str) {
for map in [&self.names, &self.bridges, &self.children] {
map.retain(|_key, mods| {
mods.retain(|m| m != module_name);
!mods.is_empty()
});
}
}
pub fn clear(&self) {
self.names.clear();
self.bridges.clear();
self.children.clear();
}
fn indexable_names(analysis: &FileAnalysis) -> Vec<String> {
let mut names: std::collections::HashSet<String> = std::collections::HashSet::new();
for sym in &analysis.symbols {
if matches!(
sym.kind,
SymKind::Sub | SymKind::Method | SymKind::Package | SymKind::Class
| SymKind::Module | SymKind::HashKeyDef | SymKind::Handler,
) {
names.insert(sym.name.clone());
}
}
names.extend(analysis.export.iter().cloned());
names.extend(analysis.export_ok.iter().cloned());
names.into_iter().collect()
}
fn bridge_classes(analysis: &FileAnalysis) -> Vec<String> {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
for ns in &analysis.plugin_namespaces {
for crate::file_analysis::Bridge::Class(c) in &ns.bridges {
seen.insert(c.clone());
}
}
seen.into_iter().collect()
}
fn parent_classes(analysis: &FileAnalysis) -> Vec<String> {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
for parents in analysis.package_parents.values() {
for p in parents {
seen.insert(p.clone());
}
}
seen.into_iter().collect()
}
}
#[allow(dead_code)]
pub struct ModuleIndex {
cache: Arc<DashMap<String, Option<Arc<CachedModule>>>>,
edges: Arc<ModuleEdgeIndexes>,
loaded_modules: Arc<DashMap<String, ()>>,
workspace_modules: Arc<DashMap<String, ()>>,
loader_config_shapes: Arc<DashMap<String, Vec<(String, InferredTypeOwned)>>>,
stale_modules: Arc<DashMap<String, ()>>,
builtins: Arc<DashMap<String, String>>,
available_modules: Arc<DashMap<String, std::path::PathBuf>>,
queue: Arc<ResolveQueue>,
resolved: Arc<ResolveNotify>,
workspace_root: Arc<WorkspaceRootChannel>,
refresh_diagnostics: Arc<dyn Fn() + Send + Sync>,
}
impl ModuleIndex {
pub fn new(client: Client, on_diagnostics_refresh: impl Fn() + Send + Sync + 'static) -> Self {
let cache: Arc<DashMap<String, Option<Arc<CachedModule>>>> = Arc::new(DashMap::new());
let edges = Arc::new(ModuleEdgeIndexes::new());
let stale_modules: Arc<DashMap<String, ()>> = Arc::new(DashMap::new());
let available_modules: Arc<DashMap<String, std::path::PathBuf>> = Arc::new(DashMap::new());
let builtins: Arc<DashMap<String, String>> = Arc::new(DashMap::new());
let queue = Arc::new(ResolveQueue {
priority: Mutex::new(Vec::new()),
pending: Mutex::new(Vec::new()),
condvar: Condvar::new(),
});
let resolved = Arc::new(ResolveNotify {
mu: Mutex::new(()),
cv: Condvar::new(),
});
let workspace_root = Arc::new(WorkspaceRootChannel {
root: Mutex::new(None),
condvar: Condvar::new(),
});
let refresh = Arc::new(on_diagnostics_refresh);
let refresh_clone = Arc::clone(&refresh);
module_resolver::spawn_resolver(
Arc::clone(&cache),
Arc::clone(&edges),
Arc::clone(&stale_modules),
Arc::clone(&available_modules),
Arc::clone(&builtins),
Arc::clone(&queue),
Arc::clone(&resolved),
Arc::clone(&workspace_root),
client,
Box::new(move || refresh_clone()),
);
ModuleIndex {
cache,
edges,
loaded_modules: Arc::new(DashMap::new()),
workspace_modules: Arc::new(DashMap::new()),
loader_config_shapes: Arc::new(DashMap::new()),
stale_modules,
available_modules,
builtins,
queue,
resolved,
workspace_root,
refresh_diagnostics: refresh,
}
}
pub fn builtin_doc(&self, name: &str) -> Option<String> {
self.builtins.get(name).map(|e| e.clone())
}
pub fn set_workspace_root(&self, root: Option<&str>) {
let mut guard = self.workspace_root.root.lock().unwrap();
if root.is_none() {
log::warn!("No workspace root from client; using global module cache");
}
*guard = Some(root.map(String::from));
self.workspace_root.condvar.notify_one();
}
pub fn workspace_root(&self) -> Option<String> {
self.workspace_root.root.lock().ok()
.and_then(|guard| guard.as_ref().and_then(|opt| opt.clone()))
}
pub fn request_resolve(&self, module_name: &str) {
let is_stale = self.stale_modules.contains_key(module_name);
if self.cache.contains_key(module_name) && !is_stale {
return; }
if is_stale {
let mut priority = self.queue.priority.lock().unwrap();
if !priority.contains(&module_name.to_string()) {
priority.push(module_name.to_string());
}
} else {
let mut pending = self.queue.pending.lock().unwrap();
pending.push(module_name.to_string());
}
self.queue.condvar.notify_one();
}
pub fn get_cached(&self, module_name: &str) -> Option<Arc<CachedModule>> {
self.cache.get(module_name).and_then(|entry| entry.clone())
}
pub fn for_each_reexport_module<F>(&self, start: impl IntoIterator<Item = String>, mut visit: F)
where
F: FnMut(&Arc<CachedModule>) -> std::ops::ControlFlow<()>,
{
const MAX: usize = 256;
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut queue: std::collections::VecDeque<String> = start.into_iter().collect();
let mut visited = 0usize;
while let Some(module) = queue.pop_front() {
if !seen.insert(module.clone()) {
continue;
}
visited += 1;
if visited > MAX {
break;
}
let Some(cached) = self.get_cached(&module) else { continue };
if visit(&cached).is_break() {
return;
}
for next in &cached.analysis.reexport_modules {
if !seen.contains(next) {
queue.push_back(next.clone());
}
}
}
}
pub fn defining_module_cached(
&self,
entry: &str,
name: &str,
) -> Option<Arc<CachedModule>> {
use std::ops::ControlFlow;
let mut found = None;
self.for_each_reexport_module(std::iter::once(entry.to_string()), |cached| {
if cached.sub_info(name).is_some() {
found = Some(Arc::clone(cached));
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
});
found
}
pub fn module_path_cached(&self, module_name: &str) -> Option<PathBuf> {
self.cache
.get(module_name)
.and_then(|entry| entry.as_ref().map(|m| m.path.clone()))
}
pub fn parents_cached(&self, module_name: &str) -> Vec<String> {
let cached = match self.get_cached(module_name) {
Some(c) => c,
None => return Vec::new(),
};
primary_package_parents(&cached.analysis, module_name)
}
pub fn for_each_cached<F: FnMut(&str, &Arc<CachedModule>)>(&self, mut f: F) {
for entry in self.cache.iter() {
if let Some(ref cached) = *entry.value() {
f(entry.key(), cached);
}
}
}
pub fn complete_module_names(&self, prefix: &str) -> Vec<(String, bool)> {
let prefix_lower = prefix.to_lowercase();
let mut seen = std::collections::HashSet::new();
let mut results = Vec::new();
for entry in self.cache.iter() {
if entry.value().is_some() {
let name = entry.key();
if name.to_lowercase().starts_with(&prefix_lower) && seen.insert(name.clone()) {
results.push((name.clone(), true));
}
}
}
for entry in self.available_modules.iter() {
let name = entry.key();
if name.to_lowercase().starts_with(&prefix_lower) && seen.insert(name.clone()) {
results.push((name.clone(), false));
}
}
results
}
#[cfg(test)]
pub fn get_return_type_cached(&self, func_name: &str) -> Option<InferredType> {
let modules = self.edges.names.get(func_name)?;
for module_name in modules.value() {
if let Some(cached) = self.get_cached(module_name) {
if let Some(ty) = cached.analysis.sub_return_type_local(func_name) {
return Some(ty.clone());
}
}
}
None
}
pub fn find_exporters(&self, func_name: &str) -> Vec<String> {
let mut result: Vec<String> = self.modules_with_symbol(func_name)
.into_iter()
.filter(|m| {
self.get_cached(m)
.map(|c| c.analysis.export.iter().any(|e| e == func_name)
|| c.analysis.export_ok.iter().any(|e| e == func_name))
.unwrap_or(false)
})
.collect();
result.sort();
result.dedup();
result
}
pub fn modules_with_symbol(&self, name: &str) -> Vec<String> {
match self.edges.names.get(name) {
Some(modules) => {
let mut result = modules.clone();
result.sort();
result.dedup();
result
}
None => Vec::new(),
}
}
pub fn module_declaring_method_in_package(
&self,
name: &str,
class: &str,
) -> Option<String> {
self.modules_with_symbol(name)
.into_iter()
.find(|mod_name| {
self.get_cached(mod_name)
.map(|c| c.has_sub_in_package(name, class))
.unwrap_or(false)
})
}
pub fn new_for_cli() -> Self {
let cache: Arc<DashMap<String, Option<Arc<CachedModule>>>> = Arc::new(DashMap::new());
let edges = Arc::new(ModuleEdgeIndexes::new());
let stale_modules: Arc<DashMap<String, ()>> = Arc::new(DashMap::new());
let available_modules: Arc<DashMap<String, std::path::PathBuf>> = Arc::new(DashMap::new());
let builtins: Arc<DashMap<String, String>> = Arc::new(DashMap::new());
let queue = Arc::new(ResolveQueue {
priority: Mutex::new(Vec::new()),
pending: Mutex::new(Vec::new()),
condvar: Condvar::new(),
});
let resolved = Arc::new(ResolveNotify {
mu: Mutex::new(()),
cv: Condvar::new(),
});
let workspace_root = Arc::new(WorkspaceRootChannel {
root: Mutex::new(None),
condvar: Condvar::new(),
});
module_resolver::spawn_test_resolver(
Arc::clone(&cache),
Arc::clone(&edges),
Arc::clone(&stale_modules),
Arc::clone(&available_modules),
Arc::clone(&queue),
Arc::clone(&resolved),
Arc::clone(&workspace_root),
);
ModuleIndex {
cache,
edges,
loaded_modules: Arc::new(DashMap::new()),
workspace_modules: Arc::new(DashMap::new()),
loader_config_shapes: Arc::new(DashMap::new()),
stale_modules,
available_modules,
builtins,
queue,
resolved,
workspace_root,
refresh_diagnostics: Arc::new(|| {}),
}
}
#[cfg(test)]
pub fn new_for_test() -> Self {
let cache: Arc<DashMap<String, Option<Arc<CachedModule>>>> = Arc::new(DashMap::new());
let edges = Arc::new(ModuleEdgeIndexes::new());
let stale_modules: Arc<DashMap<String, ()>> = Arc::new(DashMap::new());
let available_modules: Arc<DashMap<String, std::path::PathBuf>> = Arc::new(DashMap::new());
let builtins: Arc<DashMap<String, String>> = Arc::new(DashMap::new());
let queue = Arc::new(ResolveQueue {
priority: Mutex::new(Vec::new()),
pending: Mutex::new(Vec::new()),
condvar: Condvar::new(),
});
let resolved = Arc::new(ResolveNotify {
mu: Mutex::new(()),
cv: Condvar::new(),
});
let workspace_root = Arc::new(WorkspaceRootChannel {
root: Mutex::new(None),
condvar: Condvar::new(),
});
module_resolver::spawn_test_resolver(
Arc::clone(&cache),
Arc::clone(&edges),
Arc::clone(&stale_modules),
Arc::clone(&available_modules),
Arc::clone(&queue),
Arc::clone(&resolved),
Arc::clone(&workspace_root),
);
ModuleIndex {
cache,
edges,
loaded_modules: Arc::new(DashMap::new()),
workspace_modules: Arc::new(DashMap::new()),
loader_config_shapes: Arc::new(DashMap::new()),
stale_modules,
available_modules,
builtins,
queue,
resolved,
workspace_root,
refresh_diagnostics: Arc::new(|| {}),
}
}
#[cfg(test)]
pub fn seed_builtin_for_test(&self, name: &str, doc: &str) {
self.builtins.insert(name.to_string(), doc.to_string());
}
pub fn cache_raw(&self) -> &DashMap<String, Option<Arc<CachedModule>>> {
&self.cache
}
pub fn insert_cache(&self, module_name: &str, cached: Option<Arc<CachedModule>>) {
if let Some(ref m) = cached {
self.edges.feed(module_name, &m.analysis);
self.record_loader_shapes(module_name, &m.analysis);
}
self.cache.insert(module_name.to_string(), cached);
}
fn record_loader_shapes(&self, contributor: &str, analysis: &FileAnalysis) {
self.loader_config_shapes.retain(|_n, v| {
v.retain(|(c, _)| c != contributor);
!v.is_empty()
});
for f in &analysis.plugin_loads {
let Some(span) = f.config_span else { continue };
if let Some(t) = analysis.expr_type_at_span(span, None) {
self.loader_config_shapes
.entry(f.name.clone())
.or_default()
.push((contributor.to_string(), t));
}
}
}
pub fn register_workspace_module(&self, path: std::path::PathBuf, analysis: Arc<FileAnalysis>) {
for imp in &analysis.imports {
self.loaded_modules.insert(imp.module_name.clone(), ());
}
for f in &analysis.plugin_loads {
self.loaded_modules.insert(f.name.clone(), ());
}
self.record_loader_shapes(&path.display().to_string(), &analysis);
let Some(module_name) = first_package_name(&analysis) else { return };
self.workspace_modules.insert(module_name.clone(), ());
let path = std::fs::canonicalize(&path).unwrap_or(path);
let cached = Arc::new(CachedModule::new(path, analysis.clone()));
self.edges.purge_module(&module_name);
self.edges.feed(&module_name, &analysis);
self.cache.insert(module_name, Some(cached));
}
pub fn rebuild_reverse_index_from_cache(&self) {
self.edges.clear();
for entry in self.cache.iter() {
if let Some(ref cached) = *entry.value() {
self.edges.feed(entry.key(), &cached.analysis);
}
}
}
pub fn is_module_loaded(&self, module: &str) -> bool {
if self.loaded_modules.contains_key(module) {
return true;
}
let tail = module.rsplit("::").next().unwrap_or(module);
self.loaded_modules
.iter()
.any(|e| e.key().rsplit("::").next() == Some(tail))
}
pub fn is_workspace_module(&self, module: &str) -> bool {
self.workspace_modules.contains_key(module)
}
pub fn modules_bridging_to(&self, class_name: &str) -> Vec<String> {
match self.edges.bridges.get(class_name) {
Some(mods) => {
let mut result = mods.clone();
result.sort();
result.dedup();
result
}
None => Vec::new(),
}
}
pub fn modules_with_parent(&self, class_name: &str) -> Vec<String> {
match self.edges.children.get(class_name) {
Some(mods) => {
let mut result = mods.clone();
result.sort();
result.dedup();
result
}
None => Vec::new(),
}
}
#[cfg(test)]
pub fn for_each_descendant_package<F>(&self, class: &str, mut visit: F)
where
F: FnMut(&str, &Arc<CachedModule>) -> std::ops::ControlFlow<()>,
{
const MAX: usize = 512;
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut queue: std::collections::VecDeque<String> =
std::collections::VecDeque::from([class.to_string()]);
let mut visited = 0usize;
while let Some(current) = queue.pop_front() {
if !seen.insert(current.clone()) {
continue;
}
visited += 1;
if visited > MAX {
break;
}
for module_name in self.modules_with_parent(¤t) {
let Some(cached) = self.get_cached(&module_name) else { continue };
for (pkg, parents) in &cached.analysis.package_parents {
if !parents.iter().any(|p| p == ¤t) {
continue;
}
if seen.contains(pkg) {
continue;
}
if visit(pkg, &cached).is_break() {
return;
}
queue.push_back(pkg.clone());
}
}
}
}
pub fn for_each_entity_bridged_to(
&self,
class_name: &str,
mut visit: impl FnMut(&str, &Arc<CachedModule>, &crate::file_analysis::Symbol),
) {
for mod_name in self.modules_bridging_to(class_name) {
let Some(cached) = self.get_cached(&mod_name) else { continue };
for ns in &cached.analysis.plugin_namespaces {
let bridges_class = ns.bridges.iter().any(|b|
matches!(b, crate::file_analysis::Bridge::Class(c) if c == class_name));
if !bridges_class { continue; }
for sym_id in &ns.entities {
let idx = sym_id.0 as usize;
let Some(sym) = cached.analysis.symbols.get(idx) else { continue };
visit(&mod_name, &cached, sym);
}
}
}
}
#[doc(hidden)]
pub fn wait_resolved(&self, module_name: &str, timeout: std::time::Duration) -> bool {
let deadline = std::time::Instant::now() + timeout;
let mut guard = self.resolved.mu.lock().unwrap();
loop {
if self.cache.contains_key(module_name) {
return true;
}
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
if remaining.is_zero() {
return false;
}
let (g, result) = self.resolved.cv.wait_timeout(guard, remaining).unwrap();
guard = g;
if result.timed_out() && !self.cache.contains_key(module_name) {
return false;
}
}
}
#[cfg(test)]
pub fn get_cached_blocking(&self, module_name: &str) -> Option<Arc<CachedModule>> {
if let Some(entry) = self.cache.get(module_name) {
return entry.clone();
}
let inc_paths = module_resolver::discover_inc_paths();
let mut parser = module_resolver::create_parser();
let result = module_resolver::resolve_and_parse(&inc_paths, module_name, &mut parser);
self.cache.insert(module_name.to_string(), result.clone());
result
}
#[cfg(test)]
fn inc_paths(&self) -> Vec<PathBuf> {
module_resolver::discover_inc_paths()
}
#[cfg(test)]
pub fn resolve_module(&self, module_name: &str) -> Option<PathBuf> {
let inc_paths = module_resolver::discover_inc_paths();
module_resolver::resolve_module_path(&inc_paths, module_name)
}
}
impl CrossFileLookup for ModuleIndex {
fn get_cached(&self, module_name: &str) -> Option<Arc<CachedModule>> {
self.get_cached(module_name)
}
fn parents_cached(&self, module_name: &str) -> Vec<String> {
self.parents_cached(module_name)
}
fn modules_with_symbol(&self, name: &str) -> Vec<String> {
self.modules_with_symbol(name)
}
fn find_exporters(&self, func_name: &str) -> Vec<String> {
self.find_exporters(func_name)
}
fn defining_module_cached(&self, entry: &str, name: &str) -> Option<Arc<CachedModule>> {
self.defining_module_cached(entry, name)
}
fn module_declaring_method_in_package(&self, name: &str, class: &str) -> Option<String> {
self.module_declaring_method_in_package(name, class)
}
fn for_each_cached(&self, f: &mut dyn FnMut(&str, &Arc<CachedModule>)) {
self.for_each_cached(f)
}
fn for_each_reexport_module(
&self,
start: Vec<String>,
visit: &mut dyn FnMut(&Arc<CachedModule>) -> std::ops::ControlFlow<()>,
) {
self.for_each_reexport_module(start, visit)
}
fn for_each_entity_bridged_to(
&self,
class_name: &str,
f: &mut dyn FnMut(&str, &Arc<CachedModule>, &crate::file_analysis::Symbol),
) {
self.for_each_entity_bridged_to(class_name, f)
}
fn direct_children_of(&self, class: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
for module in self.modules_with_parent(class) {
let Some(cached) = self.get_cached(&module) else { continue };
for (pkg, parents) in &cached.analysis.package_parents {
if parents.iter().any(|p| p == class) {
out.push((pkg.clone(), module.clone()));
}
}
}
out.sort();
out.dedup();
out
}
fn for_each_loader_shape(&self, f: &mut dyn FnMut(&str, &crate::file_analysis::InferredType)) {
for entry in self.loader_config_shapes.iter() {
for (_contributor, t) in entry.value() {
f(entry.key(), t);
}
}
}
}
pub fn first_package_name(analysis: &FileAnalysis) -> Option<String> {
for sym in &analysis.symbols {
if matches!(sym.kind, SymKind::Package | SymKind::Class) {
return Some(sym.name.clone());
}
}
None
}
pub fn primary_package_parents(analysis: &FileAnalysis, module_name: &str) -> Vec<String> {
if let Some(parents) = analysis.package_parents.get(module_name) {
return parents.clone();
}
if analysis.package_parents.len() == 1 {
if let Some((_pkg, parents)) = analysis.package_parents.iter().next() {
return parents.clone();
}
}
Vec::new()
}
#[cfg(test)]
#[path = "module_index_tests.rs"]
mod tests;