use std::collections::HashMap;
use std::sync::Arc;
use salsa::Database;
use tower_lsp::lsp_types::Url;
use crate::db::index::file_index;
use crate::db::input::Workspace;
use crate::index::file_index::FileIndex;
#[derive(Debug, Clone, Copy)]
pub struct ClassRef {
pub file: u32,
pub class: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeclKind {
Function,
Class,
Method,
Property,
Constant,
EnumCase,
}
#[derive(Debug, Clone, Copy)]
pub struct DeclRef {
pub file: u32,
pub line: u32,
pub kind: DeclKind,
}
pub struct WorkspaceIndexData {
pub files: Vec<(Url, Arc<FileIndex>)>,
pub classes_by_name: HashMap<String, Vec<ClassRef>>,
pub subtypes_of: HashMap<Arc<str>, Vec<ClassRef>>,
pub decls_by_name: HashMap<String, Vec<DeclRef>>,
}
type BuildMapsResult = (
HashMap<String, Vec<ClassRef>>,
HashMap<Arc<str>, Vec<ClassRef>>,
HashMap<String, Vec<DeclRef>>,
);
fn build_maps(files: &[(Url, Arc<FileIndex>)]) -> BuildMapsResult {
let mut classes_by_name: HashMap<String, Vec<ClassRef>> = HashMap::new();
let mut subtypes_of: HashMap<Arc<str>, Vec<ClassRef>> = HashMap::new();
let mut decls_by_name: HashMap<String, Vec<DeclRef>> = HashMap::new();
let push_decl = |map: &mut HashMap<String, Vec<DeclRef>>,
name: &str,
file: u32,
line: u32,
kind: DeclKind| {
map.entry(name.to_string())
.or_default()
.push(DeclRef { file, line, kind });
};
for (file_idx, (_, idx)) in files.iter().enumerate() {
let file_idx = file_idx as u32;
for f in &idx.functions {
push_decl(
&mut decls_by_name,
&f.name,
file_idx,
f.start_line,
DeclKind::Function,
);
}
for (cls_idx, cls) in idx.classes.iter().enumerate() {
let cr = ClassRef {
file: file_idx,
class: cls_idx as u32,
};
classes_by_name
.entry(cls.name.as_ref().to_string())
.or_default()
.push(cr);
if let Some(parent) = &cls.parent {
subtypes_of.entry(Arc::clone(parent)).or_default().push(cr);
}
for iface in &cls.implements {
subtypes_of.entry(Arc::clone(iface)).or_default().push(cr);
if let Some((_, fqn)) = idx
.use_imports
.iter()
.find(|(alias, _)| alias.as_ref() == iface.as_ref())
{
let short = crate::text::fqn_short_name(fqn);
if short != iface.as_ref() {
subtypes_of.entry(Arc::from(short)).or_default().push(cr);
}
}
}
for trt in &cls.traits {
subtypes_of.entry(Arc::clone(trt)).or_default().push(cr);
}
push_decl(
&mut decls_by_name,
&cls.name,
file_idx,
cls.start_line,
DeclKind::Class,
);
for m in &cls.methods {
push_decl(
&mut decls_by_name,
&m.name,
file_idx,
m.start_line,
DeclKind::Method,
);
}
for p in &cls.properties {
push_decl(
&mut decls_by_name,
&p.name,
file_idx,
p.start_line,
DeclKind::Property,
);
}
for cc in &cls.constants {
push_decl(
&mut decls_by_name,
cc,
file_idx,
cls.start_line,
DeclKind::Constant,
);
}
for case in &cls.cases {
push_decl(
&mut decls_by_name,
case,
file_idx,
cls.start_line,
DeclKind::EnumCase,
);
}
}
}
(classes_by_name, subtypes_of, decls_by_name)
}
impl WorkspaceIndexData {
pub fn at(&self, r: ClassRef) -> Option<(&Url, &crate::index::file_index::ClassDef)> {
let (uri, idx) = self.files.get(r.file as usize)?;
let cls = idx.classes.get(r.class as usize)?;
Some((uri, cls))
}
pub fn find_declaration(
&self,
name: &str,
exclude: Option<&Url>,
) -> Option<tower_lsp::lsp_types::Location> {
let bare = crate::text::strip_variable_sigil(name);
let sigil = bare != name;
let refs = self.decls_by_name.get(bare)?;
for r in refs {
if sigil
&& !matches!(
r.kind,
DeclKind::Function | DeclKind::Class | DeclKind::Property
)
{
continue;
}
let (uri, _) = self.files.get(r.file as usize)?;
if exclude.is_some_and(|e| e == uri) {
continue;
}
return Some(tower_lsp::lsp_types::Location {
uri: uri.clone(),
range: crate::text::zero_width_range(r.line),
});
}
None
}
pub fn from_files(files: Vec<(Url, Arc<FileIndex>)>) -> Self {
let (classes_by_name, subtypes_of, decls_by_name) = build_maps(&files);
Self {
files,
classes_by_name,
subtypes_of,
decls_by_name,
}
}
}
#[derive(Clone)]
pub struct WorkspaceIndexArc(pub Arc<WorkspaceIndexData>);
impl WorkspaceIndexArc {
#[cfg(test)]
pub fn get(&self) -> &WorkspaceIndexData {
&self.0
}
}
crate::impl_arc_update!(WorkspaceIndexArc);
#[salsa::tracked(no_eq)]
pub fn workspace_index(db: &dyn Database, ws: Workspace) -> WorkspaceIndexArc {
let files_input = crate::db::input::workspace_files(db, ws);
let mut files: Vec<(Url, Arc<FileIndex>)> = Vec::with_capacity(files_input.len());
for sf in files_input.iter() {
let uri_arc = sf.uri(db);
let Ok(url) = Url::parse(&uri_arc) else {
continue;
};
let idx = file_index(db, *sf).0.clone();
files.push((url, idx));
}
let (classes_by_name, subtypes_of, decls_by_name) = build_maps(&files);
WorkspaceIndexArc(Arc::new(WorkspaceIndexData {
files,
classes_by_name,
subtypes_of,
decls_by_name,
}))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::db::analysis::AnalysisHost;
use crate::db::input::FileText;
use salsa::Setter;
fn new_file(host: &AnalysisHost, uri: &str, src: &str) -> (Arc<str>, FileText) {
let ft = FileText::new(host.db(), Arc::<str>::from(src), None);
(Arc::<str>::from(uri), ft)
}
#[test]
fn workspace_index_builds_name_and_subtype_maps() {
let host = AnalysisHost::new();
let f1 = new_file(&host, "file:///a.php", "<?php\nclass Animal {}");
let f2 = new_file(
&host,
"file:///b.php",
"<?php\nclass Dog extends Animal {}\nclass Cat extends Animal {}",
);
let ws = Workspace::new(
host.db(),
Arc::from([f1, f2]),
mir_analyzer::PhpVersion::LATEST,
);
let wi = workspace_index(host.db(), ws);
let data = wi.get();
assert!(data.classes_by_name.contains_key("Animal"));
assert!(data.classes_by_name.contains_key("Dog"));
let subs = data
.subtypes_of
.get("Animal")
.expect("Animal must have subtype entries");
assert_eq!(subs.len(), 2, "Dog + Cat extend Animal");
let names: Vec<_> = subs
.iter()
.filter_map(|r| data.at(*r).map(|(_, c)| c.name.clone()))
.collect();
assert!(names.iter().any(|n| n.as_ref() == "Dog"));
assert!(names.iter().any(|n| n.as_ref() == "Cat"));
}
#[test]
fn workspace_index_memoizes_and_invalidates() {
let mut host = AnalysisHost::new();
let (uri_arc, ft1) = new_file(&host, "file:///a.php", "<?php\nclass A {}");
let ws = Workspace::new(
host.db(),
Arc::from([(uri_arc, ft1)]),
mir_analyzer::PhpVersion::LATEST,
);
let a = workspace_index(host.db(), ws);
let b = workspace_index(host.db(), ws);
assert!(
Arc::ptr_eq(&a.0, &b.0),
"unchanged inputs must return the memoized Arc"
);
ft1.set_text(host.db_mut())
.to(Arc::<str>::from("<?php\nclass B {}"));
let c = workspace_index(host.db(), ws);
assert!(!Arc::ptr_eq(&a.0, &c.0), "an edit must produce a fresh Arc");
assert!(c.get().classes_by_name.contains_key("B"));
assert!(!c.get().classes_by_name.contains_key("A"));
}
#[test]
fn workspace_index_collects_interface_and_trait_subtypes() {
let host = AnalysisHost::new();
let src = concat!(
"<?php\n",
"interface Greeter {}\n",
"trait Shouting {}\n",
"class Hi implements Greeter { use Shouting; }\n",
);
let f = new_file(&host, "file:///m.php", src);
let ws = Workspace::new(host.db(), Arc::from([f]), mir_analyzer::PhpVersion::LATEST);
let wi = workspace_index(host.db(), ws);
let data = wi.get();
let greeter_subs = data.subtypes_of.get("Greeter").expect("Greeter subs");
assert_eq!(greeter_subs.len(), 1);
let shouting_subs = data.subtypes_of.get("Shouting").expect("Shouting subs");
assert_eq!(shouting_subs.len(), 1);
}
}