use std::collections::HashMap;
use std::sync::{Arc, OnceLock};
use tower_lsp_server::ls_types::Uri;
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<(Uri, 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>>,
pub classes_by_lowercase_name: Vec<(Box<str>, ClassRef)>,
func_signatures: OnceLock<Arc<HashMap<String, FuncSignature>>>,
}
#[derive(Clone)]
pub struct FuncSignature {
pub params: Vec<String>,
pub variadic_last: bool,
pub return_type: Option<String>,
}
pub fn build_func_signatures(files: &[(Uri, Arc<FileIndex>)]) -> HashMap<String, FuncSignature> {
let mut map = HashMap::new();
for (_, idx) in files {
for func in &idx.functions {
let func_name = func.name.to_string();
if map.contains_key(&func_name) {
continue;
}
let params: Vec<String> = func.params.iter().map(|p| p.name.to_string()).collect();
let variadic_last = func.params.last().map(|p| p.variadic).unwrap_or(false);
map.insert(
func_name,
FuncSignature {
params,
variadic_last,
return_type: func.return_type.as_ref().map(|r| r.to_string()),
},
);
}
for class in &idx.classes {
for method in &class.methods {
let method_name = method.name.to_string();
let params: Vec<String> =
method.params.iter().map(|p| p.name.to_string()).collect();
let variadic_last = method.params.last().map(|p| p.variadic).unwrap_or(false);
let sig = FuncSignature {
params: params.clone(),
variadic_last,
return_type: method.return_type.as_ref().map(|r| r.to_string()),
};
let cn = class.name.as_ref();
let qualified = format!("{}::{}", cn, method_name);
map.insert(qualified, sig.clone());
if method_name == "__construct" {
map.entry(cn.to_string()).or_insert_with(|| FuncSignature {
params: params.clone(),
variadic_last,
return_type: None,
});
}
map.entry(method_name).or_insert(sig);
}
}
}
map
}
pub(crate) type BuildMapsResult = (
HashMap<String, Vec<ClassRef>>,
HashMap<Arc<str>, Vec<ClassRef>>,
HashMap<String, Vec<DeclRef>>,
Vec<(Box<str>, ClassRef)>,
);
pub(crate) fn build_maps(files: &[(Uri, 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,
);
}
}
}
let mut classes_by_lowercase_name: Vec<(Box<str>, ClassRef)> = classes_by_name
.iter()
.filter_map(|(name, refs)| refs.first().map(|cr| (name.to_lowercase().into(), *cr)))
.collect();
classes_by_lowercase_name.sort_unstable_by(|a, b| a.0.cmp(&b.0));
(
classes_by_name,
subtypes_of,
decls_by_name,
classes_by_lowercase_name,
)
}
impl WorkspaceIndexData {
pub fn at(&self, r: ClassRef) -> Option<(&Uri, &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 resolve_class_ref(&self, name: &str) -> Option<ClassRef> {
let trimmed = name.trim_start_matches('\\');
let short = trimmed.rsplit('\\').next().unwrap_or(trimmed);
let candidates = self.classes_by_name.get(short)?;
if trimmed.contains('\\')
&& let Some(cr) = candidates.iter().find(|cr| {
self.at(**cr).is_some_and(|(_, cls)| {
cls.fqn
.trim_start_matches('\\')
.eq_ignore_ascii_case(trimmed)
})
})
{
return Some(*cr);
}
candidates.first().copied()
}
pub fn find_declaration(
&self,
name: &str,
exclude: Option<&Uri>,
) -> Option<tower_lsp_server::ls_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_server::ls_types::Location {
uri: uri.clone(),
range: crate::text::zero_width_range(r.line),
});
}
None
}
pub fn from_files(files: Vec<(Uri, Arc<FileIndex>)>) -> Self {
let (classes_by_name, subtypes_of, decls_by_name, classes_by_lowercase_name) =
build_maps(&files);
Self {
files,
classes_by_name,
subtypes_of,
decls_by_name,
classes_by_lowercase_name,
func_signatures: OnceLock::new(),
}
}
pub fn func_signatures(&self) -> Arc<HashMap<String, FuncSignature>> {
Arc::clone(
self.func_signatures
.get_or_init(|| Arc::new(build_func_signatures(&self.files))),
)
}
}
#[derive(Clone)]
pub struct WorkspaceIndexArc(pub Arc<WorkspaceIndexData>);
impl WorkspaceIndexArc {
#[cfg(test)]
pub fn get(&self) -> &WorkspaceIndexData {
&self.0
}
}