use std::collections::HashMap;
use slotmap::{SecondaryMap, SlotMap};
use uuid::Uuid;
use crate::error::{InvalidSymbolId, RegistrationError, RenameError, UnregisterReexportError};
use crate::file_path::WorkspaceFilePath;
use crate::id::SymbolId;
use crate::kind::SymbolKind;
use crate::path::SymbolPath;
use crate::span::{FileSpan, Visibility};
use crate::var_scope::VarScope;
#[derive(Debug, Clone)]
pub struct ReExportInfo {
pub alias_path: SymbolPath,
pub origin_file: WorkspaceFilePath,
}
#[derive(Clone)]
pub struct SymbolRegistry {
id_to_path: SlotMap<SymbolId, SymbolPath>,
path_to_id: HashMap<SymbolPath, SymbolId>,
kinds: SecondaryMap<SymbolId, SymbolKind>,
spans: SecondaryMap<SymbolId, FileSpan>,
visibility: SecondaryMap<SymbolId, Visibility>,
parents: SecondaryMap<SymbolId, SymbolId>,
re_exports: SecondaryMap<SymbolId, Vec<ReExportInfo>>,
alias_to_canonical: HashMap<SymbolPath, SymbolId>,
id_to_uuid: SecondaryMap<SymbolId, Uuid>,
uuid_to_id: HashMap<Uuid, SymbolId>,
preloaded_uuids: HashMap<SymbolPath, Uuid>,
}
impl SymbolRegistry {
pub fn new() -> Self {
Self {
id_to_path: SlotMap::with_key(),
path_to_id: HashMap::new(),
kinds: SecondaryMap::new(),
spans: SecondaryMap::new(),
visibility: SecondaryMap::new(),
parents: SecondaryMap::new(),
re_exports: SecondaryMap::new(),
alias_to_canonical: HashMap::new(),
id_to_uuid: SecondaryMap::new(),
uuid_to_id: HashMap::new(),
preloaded_uuids: HashMap::new(),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
id_to_path: SlotMap::with_capacity_and_key(capacity),
path_to_id: HashMap::with_capacity(capacity),
kinds: SecondaryMap::new(),
spans: SecondaryMap::new(),
visibility: SecondaryMap::new(),
parents: SecondaryMap::new(),
re_exports: SecondaryMap::new(),
alias_to_canonical: HashMap::new(),
id_to_uuid: SecondaryMap::new(),
uuid_to_id: HashMap::with_capacity(capacity),
preloaded_uuids: HashMap::new(),
}
}
pub fn register(
&mut self,
path: SymbolPath,
kind: SymbolKind,
) -> Result<SymbolId, RegistrationError> {
if let Some(&existing_id) = self.path_to_id.get(&path) {
if let Some(&existing_kind) = self.kinds.get(existing_id) {
if existing_kind != kind {
return Err(RegistrationError::ConflictingKind {
path: Box::new(path),
existing: existing_kind,
new: kind,
});
}
}
return Ok(existing_id);
}
let id = self.id_to_path.insert(path.clone());
self.path_to_id.insert(path.clone(), id);
self.kinds.insert(id, kind);
let uuid = self
.preloaded_uuids
.remove(&path)
.unwrap_or_else(Uuid::new_v4);
self.id_to_uuid.insert(id, uuid);
self.uuid_to_id.insert(uuid, id);
Ok(id)
}
pub fn register_with_metadata(
&mut self,
path: SymbolPath,
kind: SymbolKind,
span: Option<FileSpan>,
vis: Option<Visibility>,
) -> Result<SymbolId, RegistrationError> {
let id = self.register(path, kind)?;
if let Some(span) = span {
self.spans.insert(id, span);
}
if let Some(vis) = vis {
self.visibility.insert(id, vis);
}
Ok(id)
}
pub fn register_var(
&mut self,
containing_symbol: SymbolId,
scope: VarScope,
name: &str,
kind: SymbolKind,
) -> Result<SymbolId, RegistrationError> {
let parent_path = self
.path(containing_symbol)
.ok_or(RegistrationError::InvalidParent)?
.clone();
let var_path = parent_path
.with_var_scope(scope, name)
.map_err(RegistrationError::InvalidPath)?;
let id = self.register(var_path, kind)?;
self.parents.insert(id, containing_symbol);
Ok(id)
}
#[inline]
pub fn lookup(&self, path: &SymbolPath) -> Option<SymbolId> {
if let Some(&id) = self.path_to_id.get(path) {
return Some(id);
}
self.alias_to_canonical.get(path).copied()
}
#[inline]
pub fn resolve(&self, id: SymbolId) -> Option<&SymbolPath> {
self.id_to_path.get(id)
}
#[inline]
pub fn path(&self, id: SymbolId) -> Option<&SymbolPath> {
self.resolve(id)
}
#[inline]
pub fn get_ref(&self, id: SymbolId) -> Option<crate::SymbolRef> {
self.resolve(id)
.map(|path| crate::SymbolRef::new(id, path.clone()))
}
#[inline]
pub fn contains(&self, id: SymbolId) -> bool {
self.id_to_path.contains_key(id)
}
#[inline]
pub fn kind(&self, id: SymbolId) -> Option<SymbolKind> {
self.kinds.get(id).copied()
}
#[inline]
pub fn span(&self, id: SymbolId) -> Option<&FileSpan> {
self.spans.get(id)
}
#[inline]
pub fn visibility(&self, id: SymbolId) -> Option<&Visibility> {
self.visibility.get(id)
}
#[inline]
pub fn parent(&self, id: SymbolId) -> Option<SymbolId> {
self.parents.get(id).copied()
}
pub fn set_span(&mut self, id: SymbolId, span: FileSpan) -> Result<(), InvalidSymbolId> {
if !self.contains(id) {
return Err(InvalidSymbolId(id));
}
self.spans.insert(id, span);
Ok(())
}
pub fn set_visibility(&mut self, id: SymbolId, vis: Visibility) -> Result<(), InvalidSymbolId> {
if !self.contains(id) {
return Err(InvalidSymbolId(id));
}
self.visibility.insert(id, vis);
Ok(())
}
pub fn set_kind(&mut self, id: SymbolId, kind: SymbolKind) -> Result<(), InvalidSymbolId> {
if !self.contains(id) {
return Err(InvalidSymbolId(id));
}
self.kinds.insert(id, kind);
Ok(())
}
pub fn remove(&mut self, id: SymbolId) -> Option<SymbolPath> {
let path = self.id_to_path.remove(id)?;
self.path_to_id.remove(&path);
self.kinds.remove(id);
self.spans.remove(id);
self.visibility.remove(id);
self.parents.remove(id);
if let Some(aliases) = self.re_exports.remove(id) {
for info in aliases {
self.alias_to_canonical.remove(&info.alias_path);
}
}
if let Some(uuid) = self.id_to_uuid.remove(id) {
self.uuid_to_id.remove(&uuid);
}
Some(path)
}
pub fn rename(
&mut self,
id: SymbolId,
new_path: SymbolPath,
) -> Result<SymbolPath, RenameError> {
let old_path = self
.id_to_path
.get(id)
.ok_or(RenameError::InvalidId(id))?
.clone();
if self.path_to_id.contains_key(&new_path) {
return Err(RenameError::PathExists(Box::new(new_path)));
}
self.path_to_id.remove(&old_path);
self.path_to_id.insert(new_path.clone(), id);
self.id_to_path[id] = new_path;
Ok(old_path)
}
pub fn find_by_name(&self, name: &str) -> Vec<SymbolId> {
let mut results: Vec<SymbolId> = self
.id_to_path
.iter()
.filter(|(_, path)| path.name() == name)
.map(|(id, _)| id)
.collect();
for (alias_path, &canonical_id) in &self.alias_to_canonical {
if alias_path.name() == name && !results.contains(&canonical_id) {
results.push(canonical_id);
}
}
results
}
pub fn lookup_by_name(&self, name: &str) -> Option<SymbolId> {
self.id_to_path
.iter()
.find(|(_, path)| path.name() == name)
.map(|(id, _)| id)
}
pub fn register_reexport(
&mut self,
canonical_id: SymbolId,
alias_path: SymbolPath,
origin_file: WorkspaceFilePath,
) -> Result<(), InvalidSymbolId> {
if !self.contains(canonical_id) {
return Err(InvalidSymbolId(canonical_id));
}
self.alias_to_canonical
.insert(alias_path.clone(), canonical_id);
let info = ReExportInfo {
alias_path,
origin_file,
};
self.re_exports
.entry(canonical_id)
.expect("canonical_id was validated by self.contains() above")
.or_default()
.push(info);
Ok(())
}
pub fn unregister_reexport(
&mut self,
alias_path: &SymbolPath,
) -> Result<(), UnregisterReexportError> {
let canonical_id = self
.alias_to_canonical
.remove(alias_path)
.ok_or(UnregisterReexportError::NotFound)?;
if let Some(aliases) = self.re_exports.get_mut(canonical_id) {
aliases.retain(|info| &info.alias_path != alias_path);
if aliases.is_empty() {
self.re_exports.remove(canonical_id);
}
}
Ok(())
}
pub fn re_exports(&self, id: SymbolId) -> Option<&[ReExportInfo]> {
self.re_exports.get(id).map(|v| v.as_slice())
}
pub fn register_persistent(
&mut self,
path: SymbolPath,
kind: SymbolKind,
uuid: Option<Uuid>,
) -> Result<(SymbolId, Uuid), RegistrationError> {
let id = self.register(path, kind)?;
if let Some(&existing_uuid) = self.id_to_uuid.get(id) {
if let Some(provided) = uuid {
if provided != existing_uuid {
return Err(RegistrationError::UuidConflict {
id,
existing: existing_uuid,
provided,
});
}
}
return Ok((id, existing_uuid));
}
let final_uuid = uuid.unwrap_or_else(Uuid::new_v4);
self.id_to_uuid.insert(id, final_uuid);
self.uuid_to_id.insert(final_uuid, id);
Ok((id, final_uuid))
}
pub fn assign_uuid(
&mut self,
id: SymbolId,
uuid: Option<Uuid>,
) -> Result<Uuid, InvalidSymbolId> {
if !self.contains(id) {
return Err(InvalidSymbolId(id));
}
if let Some(&existing) = self.id_to_uuid.get(id) {
return Ok(existing);
}
let final_uuid = uuid.unwrap_or_else(Uuid::new_v4);
self.id_to_uuid.insert(id, final_uuid);
self.uuid_to_id.insert(final_uuid, id);
Ok(final_uuid)
}
#[inline]
pub fn uuid(&self, id: SymbolId) -> Option<Uuid> {
self.id_to_uuid.get(id).copied()
}
#[inline]
pub fn lookup_by_uuid(&self, uuid: Uuid) -> Option<SymbolId> {
self.uuid_to_id.get(&uuid).copied()
}
#[inline]
pub fn has_uuid(&self, id: SymbolId) -> bool {
self.id_to_uuid.contains_key(id)
}
pub fn iter_persistent(&self) -> impl Iterator<Item = (SymbolId, Uuid)> + '_ {
self.id_to_uuid.iter().map(|(id, &uuid)| (id, uuid))
}
pub fn persistent_count(&self) -> usize {
self.id_to_uuid.len()
}
pub fn preload_uuid_mapping(&mut self, mappings: HashMap<SymbolPath, Uuid>) {
self.preloaded_uuids = mappings;
}
pub fn export_uuid_mapping(&self) -> HashMap<SymbolPath, Uuid> {
self.id_to_path
.iter()
.filter_map(|(id, path)| self.id_to_uuid.get(id).map(|&uuid| (path.clone(), uuid)))
.collect()
}
pub fn export_uuid_mapping_strings(&self) -> HashMap<String, String> {
self.export_uuid_mapping()
.into_iter()
.map(|(path, uuid)| (path.to_string(), uuid.to_string()))
.collect()
}
pub fn preload_uuid_mapping_strings(&mut self, mappings: HashMap<String, String>) {
let parsed: HashMap<SymbolPath, Uuid> = mappings
.into_iter()
.filter_map(|(path_str, uuid_str)| {
let path = SymbolPath::parse(&path_str).ok()?;
let uuid = Uuid::parse_str(&uuid_str).ok()?;
Some((path, uuid))
})
.collect();
self.preloaded_uuids = parsed;
}
pub fn iter(&self) -> impl Iterator<Item = (SymbolId, &SymbolPath)> {
self.id_to_path.iter()
}
pub fn iter_by_kind(&self, kind: SymbolKind) -> impl Iterator<Item = SymbolId> + '_ {
self.kinds
.iter()
.filter(move |(_, &k)| k == kind)
.map(|(id, _)| id)
}
pub fn iter_in_crate<'a>(&'a self, crate_name: &'a str) -> impl Iterator<Item = SymbolId> + 'a {
self.id_to_path
.iter()
.filter(move |(_, path)| path.crate_name() == crate_name)
.map(|(id, _)| id)
}
pub fn len(&self) -> usize {
self.id_to_path.len()
}
pub fn is_empty(&self) -> bool {
self.id_to_path.is_empty()
}
pub fn memory_stats(&self) -> MemoryStats {
MemoryStats {
symbol_count: self.id_to_path.len(),
estimated_bytes: self.id_to_path.len() * 64 + self.path_to_id.len() * 80
+ self.kinds.len() * 8
+ self.spans.len() * 48
+ self.visibility.len() * 16,
}
}
}
impl Default for SymbolRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MemoryStats {
pub symbol_count: usize,
pub estimated_bytes: usize,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_path(s: &str) -> SymbolPath {
SymbolPath::parse(s).unwrap()
}
#[test]
fn test_register_and_lookup() {
let mut registry = SymbolRegistry::new();
let path = make_path("my_crate::MyStruct");
let id = registry.register(path.clone(), SymbolKind::Struct).unwrap();
assert!(registry.contains(id));
assert_eq!(registry.lookup(&path), Some(id));
assert_eq!(registry.resolve(id), Some(&path));
assert_eq!(registry.kind(id), Some(SymbolKind::Struct));
}
#[test]
fn test_register_duplicate() {
let mut registry = SymbolRegistry::new();
let path = make_path("my_crate::MyStruct");
let id1 = registry.register(path.clone(), SymbolKind::Struct).unwrap();
let id2 = registry.register(path.clone(), SymbolKind::Struct).unwrap();
assert_eq!(id1, id2);
}
#[test]
fn test_register_conflicting_kind() {
let mut registry = SymbolRegistry::new();
let path = make_path("my_crate::MyStruct");
registry.register(path.clone(), SymbolKind::Struct).unwrap();
let result = registry.register(path, SymbolKind::Enum);
assert!(matches!(
result,
Err(RegistrationError::ConflictingKind { .. })
));
}
#[test]
fn test_register_var() {
let mut registry = SymbolRegistry::new();
let fn_path = make_path("my_crate::my_fn");
let fn_id = registry.register(fn_path, SymbolKind::Function).unwrap();
let var_id = registry
.register_var(fn_id, VarScope::Local, "result", SymbolKind::Variable)
.unwrap();
let var_path = registry.resolve(var_id).unwrap();
assert_eq!(var_path.to_string(), "my_crate::my_fn::$var::result");
assert_eq!(registry.parent(var_id), Some(fn_id));
}
#[test]
fn test_reexport() {
let mut registry = SymbolRegistry::new();
let canonical_path = make_path("std::collections::hash_map::HashMap");
let canonical_id = registry
.register(canonical_path, SymbolKind::Struct)
.unwrap();
let alias_path = make_path("std::collections::HashMap");
let origin = WorkspaceFilePath::new_for_test("src/collections/mod.rs", "/std", "std");
registry
.register_reexport(canonical_id, alias_path.clone(), origin)
.unwrap();
assert_eq!(registry.lookup(&alias_path), Some(canonical_id));
}
#[test]
fn test_iter_by_kind() {
let mut registry = SymbolRegistry::new();
registry
.register(make_path("my_crate::Struct1"), SymbolKind::Struct)
.unwrap();
registry
.register(make_path("my_crate::Struct2"), SymbolKind::Struct)
.unwrap();
registry
.register(make_path("my_crate::func"), SymbolKind::Function)
.unwrap();
let structs: Vec<_> = registry.iter_by_kind(SymbolKind::Struct).collect();
assert_eq!(structs.len(), 2);
let funcs: Vec<_> = registry.iter_by_kind(SymbolKind::Function).collect();
assert_eq!(funcs.len(), 1);
}
#[test]
fn test_register_persistent_new() {
let mut registry = SymbolRegistry::new();
let path = make_path("my_crate::MyStruct");
let (id, uuid) = registry
.register_persistent(path.clone(), SymbolKind::Struct, None)
.unwrap();
assert_eq!(registry.uuid(id), Some(uuid));
assert_eq!(registry.lookup_by_uuid(uuid), Some(id));
assert!(registry.has_uuid(id));
}
#[test]
fn test_register_persistent_returns_auto_uuid() {
let mut registry = SymbolRegistry::new();
let path = make_path("my_crate::MyStruct");
let (id, uuid) = registry
.register_persistent(path, SymbolKind::Struct, None)
.unwrap();
assert_eq!(registry.uuid(id), Some(uuid));
assert_eq!(registry.lookup_by_uuid(uuid), Some(id));
}
#[test]
fn test_register_persistent_idempotent() {
let mut registry = SymbolRegistry::new();
let path = make_path("my_crate::MyStruct");
let (id1, uuid1) = registry
.register_persistent(path.clone(), SymbolKind::Struct, None)
.unwrap();
let (id2, uuid2) = registry
.register_persistent(path, SymbolKind::Struct, None)
.unwrap();
assert_eq!(id1, id2);
assert_eq!(uuid1, uuid2);
}
#[test]
fn test_uuid_survives_rename() {
let mut registry = SymbolRegistry::new();
let old_path = make_path("my_crate::OldName");
let (id, uuid) = registry
.register_persistent(old_path, SymbolKind::Struct, None)
.unwrap();
let new_path = make_path("my_crate::NewName");
registry.rename(id, new_path.clone()).unwrap();
assert_eq!(registry.uuid(id), Some(uuid));
assert_eq!(registry.lookup_by_uuid(uuid), Some(id));
assert_eq!(registry.resolve(id), Some(&new_path));
}
#[test]
fn test_uuid_removed_on_delete() {
let mut registry = SymbolRegistry::new();
let path = make_path("my_crate::MyStruct");
let (id, uuid) = registry
.register_persistent(path, SymbolKind::Struct, None)
.unwrap();
registry.remove(id);
assert!(registry.uuid(id).is_none());
assert!(registry.lookup_by_uuid(uuid).is_none());
}
#[test]
fn test_auto_uuid_on_register() {
let mut registry = SymbolRegistry::new();
let path = make_path("my_crate::MyStruct");
let id = registry.register(path, SymbolKind::Struct).unwrap();
assert!(registry.has_uuid(id));
let uuid = registry.uuid(id).unwrap();
assert_eq!(registry.lookup_by_uuid(uuid), Some(id));
let uuid2 = registry.assign_uuid(id, None).unwrap();
assert_eq!(uuid, uuid2);
}
#[test]
fn test_iter_persistent() {
let mut registry = SymbolRegistry::new();
let id0 = registry
.register(make_path("my_crate::Symbol0"), SymbolKind::Struct)
.unwrap();
let id1 = registry
.register(make_path("my_crate::Symbol1"), SymbolKind::Struct)
.unwrap();
let id2 = registry
.register(make_path("my_crate::Symbol2"), SymbolKind::Enum)
.unwrap();
let persistent: Vec<_> = registry.iter_persistent().collect();
assert_eq!(persistent.len(), 3);
assert_eq!(registry.persistent_count(), 3);
assert!(registry.has_uuid(id0));
assert!(registry.has_uuid(id1));
assert!(registry.has_uuid(id2));
}
#[test]
fn test_uuid_conflict_error() {
let mut registry = SymbolRegistry::new();
let path = make_path("my_crate::MyStruct");
let different_uuid = Uuid::new_v4();
let id = registry.register(path.clone(), SymbolKind::Struct).unwrap();
let auto_uuid = registry.uuid(id).unwrap();
let result = registry.register_persistent(path, SymbolKind::Struct, Some(different_uuid));
assert!(matches!(
result,
Err(RegistrationError::UuidConflict { .. })
));
assert_eq!(registry.uuid(id), Some(auto_uuid));
}
#[test]
fn test_find_by_name_includes_aliases() {
let mut registry = SymbolRegistry::new();
let canonical_path = make_path("parking_lot::Mutex");
let canonical_id = registry
.register(canonical_path, SymbolKind::Struct)
.unwrap();
let alias_path = make_path("tokio::sync::Mutex");
let file_path = WorkspaceFilePath::new_for_test("src/sync/mod.rs", "/tmp/tokio", "tokio");
registry
.register_reexport(canonical_id, alias_path, file_path)
.unwrap();
let results = registry.find_by_name("Mutex");
assert_eq!(results.len(), 1);
assert_eq!(results[0], canonical_id);
}
#[test]
fn test_find_by_name_no_duplicate_with_alias() {
let mut registry = SymbolRegistry::new();
let canonical_path = make_path("parking_lot::Mutex");
let canonical_id = registry
.register(canonical_path, SymbolKind::Struct)
.unwrap();
let alias_path = make_path("tokio::sync::Mutex");
let file_path = WorkspaceFilePath::new_for_test("src/sync/mod.rs", "/tmp/tokio", "tokio");
registry
.register_reexport(canonical_id, alias_path, file_path)
.unwrap();
let results = registry.find_by_name("Mutex");
assert_eq!(results.len(), 1);
}
#[test]
fn test_lookup_resolves_alias() {
let mut registry = SymbolRegistry::new();
let canonical_path = make_path("my_crate::inner::Config");
let canonical_id = registry
.register(canonical_path, SymbolKind::Struct)
.unwrap();
let alias_path = make_path("my_crate::Config");
let file_path = WorkspaceFilePath::new_for_test("src/lib.rs", "/tmp/my_crate", "my_crate");
registry
.register_reexport(canonical_id, alias_path.clone(), file_path)
.unwrap();
assert_eq!(registry.lookup(&alias_path), Some(canonical_id));
}
}