use std::sync::Arc;
use dashmap::{DashMap, DashSet};
use crate::interner::Interner;
type ReferenceLocations = DashMap<u32, Vec<(u32, u32, u32)>>;
use crate::storage::{
ClassStorage, EnumStorage, FunctionStorage, InterfaceStorage, MethodStorage, TraitStorage,
};
use mir_types::Union;
#[inline]
fn lookup_method<'a>(
map: &'a indexmap::IndexMap<Arc<str>, Arc<MethodStorage>>,
name: &str,
) -> Option<&'a Arc<MethodStorage>> {
map.get(name).or_else(|| {
map.iter()
.find(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
.map(|(_, v)| v)
})
}
#[inline]
fn record_ref(
sym_locs: &ReferenceLocations,
file_refs: &DashMap<u32, Vec<u32>>,
sym_id: u32,
file_id: u32,
start: u32,
end: u32,
) {
{
let mut entries = sym_locs.entry(sym_id).or_default();
let span = (file_id, start, end);
if !entries.contains(&span) {
entries.push(span);
}
}
{
let mut refs = file_refs.entry(file_id).or_default();
if !refs.contains(&sym_id) {
refs.push(sym_id);
}
}
}
#[derive(Debug, Default)]
struct CompactRefIndex {
entries: Vec<(u32, u32, u32, u32)>,
sym_offsets: Vec<u32>,
by_file: Vec<u32>,
file_offsets: Vec<u32>,
}
struct ClassInheritance {
parent: Option<Arc<str>>,
interfaces: Vec<Arc<str>>, traits: Vec<Arc<str>>, all_parents: Vec<Arc<str>>,
}
struct InterfaceInheritance {
extends: Vec<Arc<str>>, all_parents: Vec<Arc<str>>,
}
pub struct StructuralSnapshot {
classes: std::collections::HashMap<Arc<str>, ClassInheritance>,
interfaces: std::collections::HashMap<Arc<str>, InterfaceInheritance>,
}
#[derive(Debug, Default)]
pub struct Codebase {
pub classes: DashMap<Arc<str>, ClassStorage>,
pub interfaces: DashMap<Arc<str>, InterfaceStorage>,
pub traits: DashMap<Arc<str>, TraitStorage>,
pub enums: DashMap<Arc<str>, EnumStorage>,
pub functions: DashMap<Arc<str>, FunctionStorage>,
pub constants: DashMap<Arc<str>, Union>,
pub global_vars: DashMap<Arc<str>, Union>,
file_global_vars: DashMap<Arc<str>, Vec<Arc<str>>>,
referenced_methods: DashSet<u32>,
referenced_properties: DashSet<u32>,
referenced_functions: DashSet<u32>,
pub symbol_interner: Interner,
pub file_interner: Interner,
symbol_reference_locations: ReferenceLocations,
file_symbol_references: DashMap<u32, Vec<u32>>,
compact_ref_index: std::sync::RwLock<Option<CompactRefIndex>>,
is_compacted: std::sync::atomic::AtomicBool,
pub symbol_to_file: DashMap<Arc<str>, Arc<str>>,
pub known_symbols: DashSet<Arc<str>>,
pub file_imports: DashMap<Arc<str>, std::collections::HashMap<String, String>>,
pub file_namespaces: DashMap<Arc<str>, String>,
finalized: std::sync::atomic::AtomicBool,
}
impl Codebase {
pub fn new() -> Self {
Self::default()
}
pub fn compact_reference_index(&self) {
let mut entries: Vec<(u32, u32, u32, u32)> = self
.symbol_reference_locations
.iter()
.flat_map(|entry| {
let sym_id = *entry.key();
entry
.value()
.iter()
.map(move |&(file_id, start, end)| (sym_id, file_id, start, end))
.collect::<Vec<_>>()
})
.collect();
if entries.is_empty() {
return;
}
entries.sort_unstable();
entries.dedup();
let n = entries.len();
let max_sym = entries.iter().map(|&(s, ..)| s).max().unwrap_or(0) as usize;
let mut sym_offsets = vec![0u32; max_sym + 2];
for &(sym_id, ..) in &entries {
sym_offsets[sym_id as usize + 1] += 1;
}
for i in 1..sym_offsets.len() {
sym_offsets[i] += sym_offsets[i - 1];
}
let max_file = entries.iter().map(|&(_, f, ..)| f).max().unwrap_or(0) as usize;
let mut by_file: Vec<u32> = (0..n as u32).collect();
by_file.sort_unstable_by_key(|&i| {
let (sym_id, file_id, start, end) = entries[i as usize];
(file_id, sym_id, start, end)
});
let mut file_offsets = vec![0u32; max_file + 2];
for &idx in &by_file {
let file_id = entries[idx as usize].1;
file_offsets[file_id as usize + 1] += 1;
}
for i in 1..file_offsets.len() {
file_offsets[i] += file_offsets[i - 1];
}
*self.compact_ref_index.write().unwrap() = Some(CompactRefIndex {
entries,
sym_offsets,
by_file,
file_offsets,
});
self.is_compacted
.store(true, std::sync::atomic::Ordering::Release);
self.symbol_reference_locations.clear();
self.file_symbol_references.clear();
}
fn ensure_expanded(&self) {
if !self.is_compacted.load(std::sync::atomic::Ordering::Acquire) {
return;
}
let mut guard = self.compact_ref_index.write().unwrap();
if let Some(ci) = guard.take() {
for &(sym_id, file_id, start, end) in &ci.entries {
record_ref(
&self.symbol_reference_locations,
&self.file_symbol_references,
sym_id,
file_id,
start,
end,
);
}
self.is_compacted
.store(false, std::sync::atomic::Ordering::Release);
}
}
pub fn invalidate_finalization(&self) {
self.finalized
.store(false, std::sync::atomic::Ordering::SeqCst);
}
pub fn remove_file_definitions(&self, file_path: &str) {
let symbols: Vec<Arc<str>> = self
.symbol_to_file
.iter()
.filter(|entry| entry.value().as_ref() == file_path)
.map(|entry| entry.key().clone())
.collect();
for sym in &symbols {
self.classes.remove(sym.as_ref());
self.interfaces.remove(sym.as_ref());
self.traits.remove(sym.as_ref());
self.enums.remove(sym.as_ref());
self.functions.remove(sym.as_ref());
self.constants.remove(sym.as_ref());
self.symbol_to_file.remove(sym.as_ref());
self.known_symbols.remove(sym.as_ref());
}
self.file_imports.remove(file_path);
self.file_namespaces.remove(file_path);
if let Some((_, var_names)) = self.file_global_vars.remove(file_path) {
for name in var_names {
self.global_vars.remove(name.as_ref());
}
}
self.ensure_expanded();
if let Some(file_id) = self.file_interner.get_id(file_path) {
if let Some((_, sym_ids)) = self.file_symbol_references.remove(&file_id) {
for sym_id in sym_ids {
if let Some(mut entries) = self.symbol_reference_locations.get_mut(&sym_id) {
entries.retain(|&(fid, _, _)| fid != file_id);
}
}
}
}
self.invalidate_finalization();
}
pub fn file_structural_snapshot(&self, file_path: &str) -> StructuralSnapshot {
let symbols: Vec<Arc<str>> = self
.symbol_to_file
.iter()
.filter(|e| e.value().as_ref() == file_path)
.map(|e| e.key().clone())
.collect();
let mut classes = std::collections::HashMap::new();
let mut interfaces = std::collections::HashMap::new();
for sym in symbols {
if let Some(cls) = self.classes.get(sym.as_ref()) {
let mut ifaces = cls.interfaces.clone();
ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
let mut traits = cls.traits.clone();
traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
classes.insert(
sym,
ClassInheritance {
parent: cls.parent.clone(),
interfaces: ifaces,
traits,
all_parents: cls.all_parents.clone(),
},
);
} else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
let mut extends = iface.extends.clone();
extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
interfaces.insert(
sym,
InterfaceInheritance {
extends,
all_parents: iface.all_parents.clone(),
},
);
}
}
StructuralSnapshot {
classes,
interfaces,
}
}
pub fn structural_unchanged_after_pass1(
&self,
file_path: &str,
old: &StructuralSnapshot,
) -> bool {
let symbols: Vec<Arc<str>> = self
.symbol_to_file
.iter()
.filter(|e| e.value().as_ref() == file_path)
.map(|e| e.key().clone())
.collect();
let mut seen_classes = 0usize;
let mut seen_interfaces = 0usize;
for sym in &symbols {
if let Some(cls) = self.classes.get(sym.as_ref()) {
seen_classes += 1;
let Some(old_cls) = old.classes.get(sym.as_ref()) else {
return false; };
if old_cls.parent != cls.parent {
return false;
}
let mut new_ifaces = cls.interfaces.clone();
new_ifaces.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
if old_cls.interfaces != new_ifaces {
return false;
}
let mut new_traits = cls.traits.clone();
new_traits.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
if old_cls.traits != new_traits {
return false;
}
} else if let Some(iface) = self.interfaces.get(sym.as_ref()) {
seen_interfaces += 1;
let Some(old_iface) = old.interfaces.get(sym.as_ref()) else {
return false; };
let mut new_extends = iface.extends.clone();
new_extends.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
if old_iface.extends != new_extends {
return false;
}
}
}
seen_classes == old.classes.len() && seen_interfaces == old.interfaces.len()
}
pub fn restore_all_parents(&self, file_path: &str, snapshot: &StructuralSnapshot) {
let symbols: Vec<Arc<str>> = self
.symbol_to_file
.iter()
.filter(|e| e.value().as_ref() == file_path)
.map(|e| e.key().clone())
.collect();
for sym in &symbols {
if let Some(old_cls) = snapshot.classes.get(sym.as_ref()) {
if let Some(mut cls) = self.classes.get_mut(sym.as_ref()) {
cls.all_parents = old_cls.all_parents.clone();
}
} else if let Some(old_iface) = snapshot.interfaces.get(sym.as_ref()) {
if let Some(mut iface) = self.interfaces.get_mut(sym.as_ref()) {
iface.all_parents = old_iface.all_parents.clone();
}
}
}
self.finalized
.store(true, std::sync::atomic::Ordering::SeqCst);
}
pub fn register_global_var(&self, file: &Arc<str>, name: Arc<str>, ty: Union) {
self.file_global_vars
.entry(file.clone())
.or_default()
.push(name.clone());
self.global_vars.insert(name, ty);
}
pub fn get_property(
&self,
fqcn: &str,
prop_name: &str,
) -> Option<crate::storage::PropertyStorage> {
if let Some(cls) = self.classes.get(fqcn) {
if let Some(p) = cls.own_properties.get(prop_name) {
return Some(p.clone());
}
}
let all_parents = {
if let Some(cls) = self.classes.get(fqcn) {
cls.all_parents.clone()
} else {
return None;
}
};
for ancestor_fqcn in &all_parents {
if let Some(ancestor_cls) = self.classes.get(ancestor_fqcn.as_ref()) {
if let Some(p) = ancestor_cls.own_properties.get(prop_name) {
return Some(p.clone());
}
}
}
let trait_list = {
if let Some(cls) = self.classes.get(fqcn) {
cls.traits.clone()
} else {
vec![]
}
};
for trait_fqcn in &trait_list {
if let Some(tr) = self.traits.get(trait_fqcn.as_ref()) {
if let Some(p) = tr.own_properties.get(prop_name) {
return Some(p.clone());
}
}
}
None
}
pub fn get_method(&self, fqcn: &str, method_name: &str) -> Option<Arc<MethodStorage>> {
let method_lower = method_name.to_lowercase();
let method_name = method_lower.as_str();
if let Some(cls) = self.classes.get(fqcn) {
if let Some(m) = lookup_method(&cls.own_methods, method_name) {
return Some(Arc::clone(m));
}
let own_traits = cls.traits.clone();
let ancestors = cls.all_parents.clone();
drop(cls);
for tr_fqcn in &own_traits {
if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
return Some(m);
}
}
for ancestor_fqcn in &ancestors {
if let Some(anc) = self.classes.get(ancestor_fqcn.as_ref()) {
if let Some(m) = lookup_method(&anc.own_methods, method_name) {
return Some(Arc::clone(m));
}
let anc_traits = anc.traits.clone();
drop(anc);
for tr_fqcn in &anc_traits {
if let Some(m) = self.get_method_in_trait(tr_fqcn, method_name) {
return Some(m);
}
}
} else if let Some(iface) = self.interfaces.get(ancestor_fqcn.as_ref()) {
if let Some(m) = lookup_method(&iface.own_methods, method_name) {
let mut ms = (**m).clone();
ms.is_abstract = true;
return Some(Arc::new(ms));
}
}
}
return None;
}
if let Some(iface) = self.interfaces.get(fqcn) {
if let Some(m) = lookup_method(&iface.own_methods, method_name) {
return Some(Arc::clone(m));
}
let parents = iface.all_parents.clone();
drop(iface);
for parent_fqcn in &parents {
if let Some(parent_iface) = self.interfaces.get(parent_fqcn.as_ref()) {
if let Some(m) = lookup_method(&parent_iface.own_methods, method_name) {
return Some(Arc::clone(m));
}
}
}
return None;
}
if let Some(tr) = self.traits.get(fqcn) {
if let Some(m) = lookup_method(&tr.own_methods, method_name) {
return Some(Arc::clone(m));
}
return None;
}
if let Some(e) = self.enums.get(fqcn) {
if let Some(m) = lookup_method(&e.own_methods, method_name) {
return Some(Arc::clone(m));
}
if matches!(method_name, "cases" | "from" | "tryfrom") {
return Some(Arc::new(crate::storage::MethodStorage {
fqcn: Arc::from(fqcn),
name: Arc::from(method_name),
params: vec![],
return_type: Some(mir_types::Union::mixed()),
inferred_return_type: None,
visibility: crate::storage::Visibility::Public,
is_static: true,
is_abstract: false,
is_constructor: false,
template_params: vec![],
assertions: vec![],
throws: vec![],
is_final: false,
is_internal: false,
is_pure: false,
is_deprecated: false,
location: None,
}));
}
}
None
}
pub fn extends_or_implements(&self, child: &str, ancestor: &str) -> bool {
if child == ancestor {
return true;
}
if let Some(cls) = self.classes.get(child) {
return cls.implements_or_extends(ancestor);
}
if let Some(iface) = self.interfaces.get(child) {
return iface.all_parents.iter().any(|p| p.as_ref() == ancestor);
}
if let Some(en) = self.enums.get(child) {
if en.interfaces.iter().any(|i| i.as_ref() == ancestor) {
return true;
}
if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
return true;
}
if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && en.scalar_type.is_some()
{
return true;
}
}
false
}
pub fn type_exists(&self, fqcn: &str) -> bool {
self.classes.contains_key(fqcn)
|| self.interfaces.contains_key(fqcn)
|| self.traits.contains_key(fqcn)
|| self.enums.contains_key(fqcn)
}
pub fn function_exists(&self, fqn: &str) -> bool {
self.functions.contains_key(fqn)
}
pub fn is_abstract_class(&self, fqcn: &str) -> bool {
self.classes.get(fqcn).is_some_and(|c| c.is_abstract)
}
pub fn get_class_template_params(&self, fqcn: &str) -> Vec<crate::storage::TemplateParam> {
if let Some(cls) = self.classes.get(fqcn) {
return cls.template_params.clone();
}
if let Some(iface) = self.interfaces.get(fqcn) {
return iface.template_params.clone();
}
if let Some(tr) = self.traits.get(fqcn) {
return tr.template_params.clone();
}
vec![]
}
pub fn has_magic_get(&self, fqcn: &str) -> bool {
self.get_method(fqcn, "__get").is_some()
}
pub fn has_unknown_ancestor(&self, fqcn: &str) -> bool {
if let Some(iface) = self.interfaces.get(fqcn) {
let parents = iface.all_parents.clone();
drop(iface);
for p in &parents {
if !self.type_exists(p.as_ref()) {
return true;
}
}
return false;
}
let (parent, interfaces, traits, all_parents) = {
let Some(cls) = self.classes.get(fqcn) else {
return false;
};
(
cls.parent.clone(),
cls.interfaces.clone(),
cls.traits.clone(),
cls.all_parents.clone(),
)
};
if let Some(ref p) = parent {
if !self.type_exists(p.as_ref()) {
return true;
}
}
for iface in &interfaces {
if !self.type_exists(iface.as_ref()) {
return true;
}
}
for tr in &traits {
if !self.type_exists(tr.as_ref()) {
return true;
}
}
for ancestor in &all_parents {
if !self.type_exists(ancestor.as_ref()) {
return true;
}
}
false
}
pub fn resolve_class_name(&self, file: &str, name: &str) -> String {
let name = name.trim_start_matches('\\');
if name.is_empty() {
return name.to_string();
}
if name.contains('\\') {
let first_segment = name.split('\\').next().unwrap_or(name);
if let Some(imports) = self.file_imports.get(file) {
if let Some(resolved_prefix) = imports.get(first_segment) {
let rest = &name[first_segment.len()..]; return format!("{}{}", resolved_prefix, rest);
}
}
if self.type_exists(name) {
return name.to_string();
}
if let Some(ns) = self.file_namespaces.get(file) {
let qualified = format!("{}\\{}", *ns, name);
if self.type_exists(&qualified) {
return qualified;
}
}
return name.to_string();
}
match name {
"self" | "parent" | "static" | "this" => return name.to_string(),
_ => {}
}
if let Some(imports) = self.file_imports.get(file) {
if let Some(resolved) = imports.get(name) {
return resolved.clone();
}
let name_lower = name.to_lowercase();
for (alias, resolved) in imports.iter() {
if alias.to_lowercase() == name_lower {
return resolved.clone();
}
}
}
if let Some(ns) = self.file_namespaces.get(file) {
let qualified = format!("{}\\{}", *ns, name);
if self.type_exists(&qualified) {
return qualified;
}
if self.type_exists(name) {
return name.to_string();
}
return qualified;
}
name.to_string()
}
pub fn get_symbol_location(&self, fqcn: &str) -> Option<crate::storage::Location> {
if let Some(cls) = self.classes.get(fqcn) {
return cls.location.clone();
}
if let Some(iface) = self.interfaces.get(fqcn) {
return iface.location.clone();
}
if let Some(tr) = self.traits.get(fqcn) {
return tr.location.clone();
}
if let Some(en) = self.enums.get(fqcn) {
return en.location.clone();
}
if let Some(func) = self.functions.get(fqcn) {
return func.location.clone();
}
None
}
pub fn get_member_location(
&self,
fqcn: &str,
member_name: &str,
) -> Option<crate::storage::Location> {
if let Some(method) = self.get_method(fqcn, member_name) {
return method.location.clone();
}
if let Some(prop) = self.get_property(fqcn, member_name) {
return prop.location.clone();
}
if let Some(cls) = self.classes.get(fqcn) {
if let Some(c) = cls.own_constants.get(member_name) {
return c.location.clone();
}
}
if let Some(iface) = self.interfaces.get(fqcn) {
if let Some(c) = iface.own_constants.get(member_name) {
return c.location.clone();
}
}
if let Some(tr) = self.traits.get(fqcn) {
if let Some(c) = tr.own_constants.get(member_name) {
return c.location.clone();
}
}
if let Some(en) = self.enums.get(fqcn) {
if let Some(c) = en.own_constants.get(member_name) {
return c.location.clone();
}
if let Some(case) = en.cases.get(member_name) {
return case.location.clone();
}
}
None
}
pub fn mark_method_referenced(&self, fqcn: &str, method_name: &str) {
let key = format!("{}::{}", fqcn, method_name.to_lowercase());
let id = self.symbol_interner.intern_str(&key);
self.referenced_methods.insert(id);
}
pub fn mark_property_referenced(&self, fqcn: &str, prop_name: &str) {
let key = format!("{}::{}", fqcn, prop_name);
let id = self.symbol_interner.intern_str(&key);
self.referenced_properties.insert(id);
}
pub fn mark_function_referenced(&self, fqn: &str) {
let id = self.symbol_interner.intern_str(fqn);
self.referenced_functions.insert(id);
}
pub fn is_method_referenced(&self, fqcn: &str, method_name: &str) -> bool {
let key = format!("{}::{}", fqcn, method_name.to_lowercase());
match self.symbol_interner.get_id(&key) {
Some(id) => self.referenced_methods.contains(&id),
None => false,
}
}
pub fn is_property_referenced(&self, fqcn: &str, prop_name: &str) -> bool {
let key = format!("{}::{}", fqcn, prop_name);
match self.symbol_interner.get_id(&key) {
Some(id) => self.referenced_properties.contains(&id),
None => false,
}
}
pub fn is_function_referenced(&self, fqn: &str) -> bool {
match self.symbol_interner.get_id(fqn) {
Some(id) => self.referenced_functions.contains(&id),
None => false,
}
}
pub fn mark_method_referenced_at(
&self,
fqcn: &str,
method_name: &str,
file: Arc<str>,
start: u32,
end: u32,
) {
let key = format!("{}::{}", fqcn, method_name.to_lowercase());
self.ensure_expanded();
let sym_id = self.symbol_interner.intern_str(&key);
let file_id = self.file_interner.intern(file);
self.referenced_methods.insert(sym_id);
record_ref(
&self.symbol_reference_locations,
&self.file_symbol_references,
sym_id,
file_id,
start,
end,
);
}
pub fn mark_property_referenced_at(
&self,
fqcn: &str,
prop_name: &str,
file: Arc<str>,
start: u32,
end: u32,
) {
let key = format!("{}::{}", fqcn, prop_name);
self.ensure_expanded();
let sym_id = self.symbol_interner.intern_str(&key);
let file_id = self.file_interner.intern(file);
self.referenced_properties.insert(sym_id);
record_ref(
&self.symbol_reference_locations,
&self.file_symbol_references,
sym_id,
file_id,
start,
end,
);
}
pub fn mark_function_referenced_at(&self, fqn: &str, file: Arc<str>, start: u32, end: u32) {
self.ensure_expanded();
let sym_id = self.symbol_interner.intern_str(fqn);
let file_id = self.file_interner.intern(file);
self.referenced_functions.insert(sym_id);
record_ref(
&self.symbol_reference_locations,
&self.file_symbol_references,
sym_id,
file_id,
start,
end,
);
}
pub fn mark_class_referenced_at(&self, fqcn: &str, file: Arc<str>, start: u32, end: u32) {
self.ensure_expanded();
let sym_id = self.symbol_interner.intern_str(fqcn);
let file_id = self.file_interner.intern(file);
record_ref(
&self.symbol_reference_locations,
&self.file_symbol_references,
sym_id,
file_id,
start,
end,
);
}
pub fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u32)]) {
if locs.is_empty() {
return;
}
self.ensure_expanded();
let file_id = self.file_interner.intern(file);
for (symbol_key, start, end) in locs {
let sym_id = self.symbol_interner.intern_str(symbol_key);
record_ref(
&self.symbol_reference_locations,
&self.file_symbol_references,
sym_id,
file_id,
*start,
*end,
);
}
}
pub fn get_reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u32)> {
let Some(sym_id) = self.symbol_interner.get_id(symbol) else {
return Vec::new();
};
if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
let id = sym_id as usize;
if id + 1 >= ci.sym_offsets.len() {
return Vec::new();
}
let start = ci.sym_offsets[id] as usize;
let end = ci.sym_offsets[id + 1] as usize;
return ci.entries[start..end]
.iter()
.map(|&(_, file_id, s, e)| (self.file_interner.get(file_id), s, e))
.collect();
}
let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
return Vec::new();
};
entries
.iter()
.map(|&(file_id, start, end)| (self.file_interner.get(file_id), start, end))
.collect()
}
pub fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u32)> {
let Some(file_id) = self.file_interner.get_id(file) else {
return Vec::new();
};
if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
let id = file_id as usize;
if id + 1 >= ci.file_offsets.len() {
return Vec::new();
}
let start = ci.file_offsets[id] as usize;
let end = ci.file_offsets[id + 1] as usize;
return ci.by_file[start..end]
.iter()
.map(|&entry_idx| {
let (sym_id, _, s, e) = ci.entries[entry_idx as usize];
(self.symbol_interner.get(sym_id), s, e)
})
.collect();
}
let Some(sym_ids) = self.file_symbol_references.get(&file_id) else {
return Vec::new();
};
let mut out = Vec::new();
for &sym_id in sym_ids.iter() {
let Some(entries) = self.symbol_reference_locations.get(&sym_id) else {
continue;
};
let sym_key = self.symbol_interner.get(sym_id);
for &(entry_file_id, start, end) in entries.iter() {
if entry_file_id == file_id {
out.push((sym_key.clone(), start, end));
}
}
}
out
}
pub fn file_has_symbol_references(&self, file: &str) -> bool {
let Some(file_id) = self.file_interner.get_id(file) else {
return false;
};
if let Some(ref ci) = *self.compact_ref_index.read().unwrap() {
let id = file_id as usize;
return id + 1 < ci.file_offsets.len() && ci.file_offsets[id] < ci.file_offsets[id + 1];
}
self.file_symbol_references.contains_key(&file_id)
}
pub fn finalize(&self) {
if self.finalized.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
let class_keys: Vec<Arc<str>> = self.classes.iter().map(|e| e.key().clone()).collect();
for fqcn in &class_keys {
let parents = self.collect_class_ancestors(fqcn);
if let Some(mut cls) = self.classes.get_mut(fqcn.as_ref()) {
cls.all_parents = parents;
}
}
let iface_keys: Vec<Arc<str>> = self.interfaces.iter().map(|e| e.key().clone()).collect();
for fqcn in &iface_keys {
let parents = self.collect_interface_ancestors(fqcn);
if let Some(mut iface) = self.interfaces.get_mut(fqcn.as_ref()) {
iface.all_parents = parents;
}
}
self.finalized
.store(true, std::sync::atomic::Ordering::SeqCst);
}
fn get_method_in_trait(
&self,
tr_fqcn: &Arc<str>,
method_name: &str,
) -> Option<Arc<MethodStorage>> {
let mut visited = std::collections::HashSet::new();
self.get_method_in_trait_inner(tr_fqcn, method_name, &mut visited)
}
fn get_method_in_trait_inner(
&self,
tr_fqcn: &Arc<str>,
method_name: &str,
visited: &mut std::collections::HashSet<String>,
) -> Option<Arc<MethodStorage>> {
if !visited.insert(tr_fqcn.to_string()) {
return None; }
let tr = self.traits.get(tr_fqcn.as_ref())?;
if let Some(m) = lookup_method(&tr.own_methods, method_name) {
return Some(Arc::clone(m));
}
let used_traits = tr.traits.clone();
drop(tr);
for used_fqcn in &used_traits {
if let Some(m) = self.get_method_in_trait_inner(used_fqcn, method_name, visited) {
return Some(m);
}
}
None
}
fn collect_class_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
let mut result = Vec::new();
let mut visited = std::collections::HashSet::new();
self.collect_class_ancestors_inner(fqcn, &mut result, &mut visited);
result
}
fn collect_class_ancestors_inner(
&self,
fqcn: &str,
out: &mut Vec<Arc<str>>,
visited: &mut std::collections::HashSet<String>,
) {
if !visited.insert(fqcn.to_string()) {
return; }
let (parent, interfaces, traits) = {
if let Some(cls) = self.classes.get(fqcn) {
(
cls.parent.clone(),
cls.interfaces.clone(),
cls.traits.clone(),
)
} else {
return;
}
};
if let Some(p) = parent {
out.push(p.clone());
self.collect_class_ancestors_inner(&p, out, visited);
}
for iface in interfaces {
out.push(iface.clone());
self.collect_interface_ancestors_inner(&iface, out, visited);
}
for t in traits {
out.push(t);
}
}
fn collect_interface_ancestors(&self, fqcn: &str) -> Vec<Arc<str>> {
let mut result = Vec::new();
let mut visited = std::collections::HashSet::new();
self.collect_interface_ancestors_inner(fqcn, &mut result, &mut visited);
result
}
fn collect_interface_ancestors_inner(
&self,
fqcn: &str,
out: &mut Vec<Arc<str>>,
visited: &mut std::collections::HashSet<String>,
) {
if !visited.insert(fqcn.to_string()) {
return;
}
let extends = {
if let Some(iface) = self.interfaces.get(fqcn) {
iface.extends.clone()
} else {
return;
}
};
for e in extends {
out.push(e.clone());
self.collect_interface_ancestors_inner(&e, out, visited);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn arc(s: &str) -> Arc<str> {
Arc::from(s)
}
#[test]
fn method_referenced_at_groups_spans_by_file() {
let cb = Codebase::new();
cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 10, 15);
cb.mark_method_referenced_at("Foo", "bar", arc("b.php"), 20, 25);
let locs = cb.get_reference_locations("Foo::bar");
let files: std::collections::HashSet<&str> =
locs.iter().map(|(f, _, _)| f.as_ref()).collect();
assert_eq!(files.len(), 2, "two files, not three spans");
assert!(locs.contains(&(arc("a.php"), 0, 5)));
assert!(locs.contains(&(arc("a.php"), 10, 15)));
assert_eq!(
locs.iter()
.filter(|(f, _, _)| f.as_ref() == "a.php")
.count(),
2
);
assert!(locs.contains(&(arc("b.php"), 20, 25)));
assert!(
cb.is_method_referenced("Foo", "bar"),
"DashSet also updated"
);
}
#[test]
fn duplicate_spans_are_deduplicated() {
let cb = Codebase::new();
cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
cb.mark_method_referenced_at("Foo", "bar", arc("a.php"), 0, 5);
let count = cb
.get_reference_locations("Foo::bar")
.iter()
.filter(|(f, _, _)| f.as_ref() == "a.php")
.count();
assert_eq!(count, 1, "duplicate span deduplicated");
}
#[test]
fn method_key_is_lowercased() {
let cb = Codebase::new();
cb.mark_method_referenced_at("Cls", "MyMethod", arc("f.php"), 0, 3);
assert!(!cb.get_reference_locations("Cls::mymethod").is_empty());
}
#[test]
fn property_referenced_at_records_location() {
let cb = Codebase::new();
cb.mark_property_referenced_at("Bar", "count", arc("x.php"), 5, 10);
assert!(cb
.get_reference_locations("Bar::count")
.contains(&(arc("x.php"), 5, 10)));
assert!(cb.is_property_referenced("Bar", "count"));
}
#[test]
fn function_referenced_at_records_location() {
let cb = Codebase::new();
cb.mark_function_referenced_at("my_fn", arc("a.php"), 10, 15);
assert!(cb
.get_reference_locations("my_fn")
.contains(&(arc("a.php"), 10, 15)));
assert!(cb.is_function_referenced("my_fn"));
}
#[test]
fn class_referenced_at_records_location() {
let cb = Codebase::new();
cb.mark_class_referenced_at("Foo", arc("a.php"), 5, 8);
assert!(cb
.get_reference_locations("Foo")
.contains(&(arc("a.php"), 5, 8)));
}
#[test]
fn get_reference_locations_flattens_all_files() {
let cb = Codebase::new();
cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
cb.mark_function_referenced_at("fn1", arc("b.php"), 10, 15);
let mut locs = cb.get_reference_locations("fn1");
locs.sort_by_key(|(_, s, _)| *s);
assert_eq!(locs.len(), 2);
assert_eq!(locs[0], (arc("a.php"), 0, 5));
assert_eq!(locs[1], (arc("b.php"), 10, 15));
}
#[test]
fn replay_reference_locations_restores_index() {
let cb = Codebase::new();
let locs = vec![
("Foo::bar".to_string(), 0u32, 5u32),
("Foo::bar".to_string(), 10, 15),
("greet".to_string(), 20, 25),
];
cb.replay_reference_locations(arc("a.php"), &locs);
let bar_locs = cb.get_reference_locations("Foo::bar");
assert!(bar_locs.contains(&(arc("a.php"), 0, 5)));
assert!(bar_locs.contains(&(arc("a.php"), 10, 15)));
assert!(cb
.get_reference_locations("greet")
.contains(&(arc("a.php"), 20, 25)));
assert!(cb.file_has_symbol_references("a.php"));
}
#[test]
fn remove_file_clears_its_spans_only() {
let cb = Codebase::new();
cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
cb.mark_function_referenced_at("fn1", arc("b.php"), 10, 15);
cb.remove_file_definitions("a.php");
let locs = cb.get_reference_locations("fn1");
assert!(
!locs.iter().any(|(f, _, _)| f.as_ref() == "a.php"),
"a.php spans removed"
);
assert!(
locs.contains(&(arc("b.php"), 10, 15)),
"b.php spans untouched"
);
assert!(!cb.file_has_symbol_references("a.php"));
}
#[test]
fn remove_file_does_not_affect_other_files() {
let cb = Codebase::new();
cb.mark_property_referenced_at("Cls", "prop", arc("x.php"), 1, 4);
cb.mark_property_referenced_at("Cls", "prop", arc("y.php"), 7, 10);
cb.remove_file_definitions("x.php");
let locs = cb.get_reference_locations("Cls::prop");
assert!(!locs.iter().any(|(f, _, _)| f.as_ref() == "x.php"));
assert!(locs.contains(&(arc("y.php"), 7, 10)));
}
#[test]
fn remove_file_definitions_on_never_analyzed_file_is_noop() {
let cb = Codebase::new();
cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
cb.remove_file_definitions("ghost.php");
assert!(cb
.get_reference_locations("fn1")
.contains(&(arc("a.php"), 0, 5)));
assert!(!cb.file_has_symbol_references("ghost.php"));
}
#[test]
fn replay_reference_locations_with_empty_list_is_noop() {
let cb = Codebase::new();
cb.mark_function_referenced_at("fn1", arc("a.php"), 0, 5);
cb.replay_reference_locations(arc("b.php"), &[]);
assert!(
!cb.file_has_symbol_references("b.php"),
"empty replay must not create a file entry"
);
assert!(
cb.get_reference_locations("fn1")
.contains(&(arc("a.php"), 0, 5)),
"existing spans untouched"
);
}
#[test]
fn replay_reference_locations_twice_does_not_duplicate_spans() {
let cb = Codebase::new();
let locs = vec![("fn1".to_string(), 0u32, 5u32)];
cb.replay_reference_locations(arc("a.php"), &locs);
cb.replay_reference_locations(arc("a.php"), &locs);
let count = cb
.get_reference_locations("fn1")
.iter()
.filter(|(f, _, _)| f.as_ref() == "a.php")
.count();
assert_eq!(
count, 1,
"replaying the same location twice must not create duplicate spans"
);
}
}