1use std::collections::HashMap;
30use std::sync::Arc;
31
32use salsa::Database;
33use tower_lsp::lsp_types::Url;
34
35use crate::db::index::file_index;
36use crate::db::input::Workspace;
37use crate::index::file_index::FileIndex;
38
39#[derive(Debug, Clone, Copy)]
42pub struct ClassRef {
43 pub file: u32,
44 pub class: u32,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum DeclKind {
53 Function,
54 Class,
55 Method,
56 Property,
57 Constant,
58 EnumCase,
59}
60
61#[derive(Debug, Clone, Copy)]
67pub struct DeclRef {
68 pub file: u32,
69 pub line: u32,
70 pub kind: DeclKind,
71}
72
73pub struct WorkspaceIndexData {
76 pub files: Vec<(Url, Arc<FileIndex>)>,
77 pub classes_by_name: HashMap<String, Vec<ClassRef>>,
78 pub subtypes_of: HashMap<Arc<str>, Vec<ClassRef>>,
83 pub decls_by_name: HashMap<String, Vec<DeclRef>>,
88}
89
90type BuildMapsResult = (
91 HashMap<String, Vec<ClassRef>>,
92 HashMap<Arc<str>, Vec<ClassRef>>,
93 HashMap<String, Vec<DeclRef>>,
94);
95
96fn build_maps(files: &[(Url, Arc<FileIndex>)]) -> BuildMapsResult {
97 let mut classes_by_name: HashMap<String, Vec<ClassRef>> = HashMap::new();
98 let mut subtypes_of: HashMap<Arc<str>, Vec<ClassRef>> = HashMap::new();
99 let mut decls_by_name: HashMap<String, Vec<DeclRef>> = HashMap::new();
100 let push_decl = |map: &mut HashMap<String, Vec<DeclRef>>,
101 name: &str,
102 file: u32,
103 line: u32,
104 kind: DeclKind| {
105 map.entry(name.to_string())
106 .or_default()
107 .push(DeclRef { file, line, kind });
108 };
109 for (file_idx, (_, idx)) in files.iter().enumerate() {
110 let file_idx = file_idx as u32;
111 for f in &idx.functions {
112 push_decl(
113 &mut decls_by_name,
114 &f.name,
115 file_idx,
116 f.start_line,
117 DeclKind::Function,
118 );
119 }
120 for (cls_idx, cls) in idx.classes.iter().enumerate() {
121 let cr = ClassRef {
122 file: file_idx,
123 class: cls_idx as u32,
124 };
125 classes_by_name
126 .entry(cls.name.as_ref().to_string())
127 .or_default()
128 .push(cr);
129 if let Some(parent) = &cls.parent {
130 subtypes_of.entry(Arc::clone(parent)).or_default().push(cr);
131 }
132 for iface in &cls.implements {
133 subtypes_of.entry(Arc::clone(iface)).or_default().push(cr);
134 if let Some((_, fqn)) = idx
138 .use_imports
139 .iter()
140 .find(|(alias, _)| alias.as_ref() == iface.as_ref())
141 {
142 let short = crate::text::fqn_short_name(fqn);
143 if short != iface.as_ref() {
144 subtypes_of.entry(Arc::from(short)).or_default().push(cr);
145 }
146 }
147 }
148 for trt in &cls.traits {
149 subtypes_of.entry(Arc::clone(trt)).or_default().push(cr);
150 }
151 push_decl(
152 &mut decls_by_name,
153 &cls.name,
154 file_idx,
155 cls.start_line,
156 DeclKind::Class,
157 );
158 for m in &cls.methods {
159 push_decl(
160 &mut decls_by_name,
161 &m.name,
162 file_idx,
163 m.start_line,
164 DeclKind::Method,
165 );
166 }
167 for p in &cls.properties {
168 push_decl(
169 &mut decls_by_name,
170 &p.name,
171 file_idx,
172 p.start_line,
173 DeclKind::Property,
174 );
175 }
176 for cc in &cls.constants {
177 push_decl(
178 &mut decls_by_name,
179 cc,
180 file_idx,
181 cls.start_line,
182 DeclKind::Constant,
183 );
184 }
185 for case in &cls.cases {
186 push_decl(
187 &mut decls_by_name,
188 case,
189 file_idx,
190 cls.start_line,
191 DeclKind::EnumCase,
192 );
193 }
194 }
195 }
196 (classes_by_name, subtypes_of, decls_by_name)
197}
198
199impl WorkspaceIndexData {
200 pub fn at(&self, r: ClassRef) -> Option<(&Url, &crate::index::file_index::ClassDef)> {
202 let (uri, idx) = self.files.get(r.file as usize)?;
203 let cls = idx.classes.get(r.class as usize)?;
204 Some((uri, cls))
205 }
206
207 pub fn find_declaration(
214 &self,
215 name: &str,
216 exclude: Option<&Url>,
217 ) -> Option<tower_lsp::lsp_types::Location> {
218 let bare = crate::text::strip_variable_sigil(name);
219 let sigil = bare != name;
220 let refs = self.decls_by_name.get(bare)?;
221 for r in refs {
222 if sigil
223 && !matches!(
224 r.kind,
225 DeclKind::Function | DeclKind::Class | DeclKind::Property
226 )
227 {
228 continue;
229 }
230 let (uri, _) = self.files.get(r.file as usize)?;
231 if exclude.is_some_and(|e| e == uri) {
232 continue;
233 }
234 return Some(tower_lsp::lsp_types::Location {
235 uri: uri.clone(),
236 range: crate::text::zero_width_range(r.line),
237 });
238 }
239 None
240 }
241
242 pub fn from_files(files: Vec<(Url, Arc<FileIndex>)>) -> Self {
249 let (classes_by_name, subtypes_of, decls_by_name) = build_maps(&files);
250 Self {
251 files,
252 classes_by_name,
253 subtypes_of,
254 decls_by_name,
255 }
256 }
257}
258
259#[derive(Clone)]
262pub struct WorkspaceIndexArc(pub Arc<WorkspaceIndexData>);
263
264impl WorkspaceIndexArc {
265 #[cfg(test)]
266 pub fn get(&self) -> &WorkspaceIndexData {
267 &self.0
268 }
269}
270
271crate::impl_arc_update!(WorkspaceIndexArc);
274
275#[salsa::tracked(no_eq)]
282pub fn workspace_index(db: &dyn Database, ws: Workspace) -> WorkspaceIndexArc {
283 let files_input = crate::db::input::workspace_files(db, ws);
284
285 let mut files: Vec<(Url, Arc<FileIndex>)> = Vec::with_capacity(files_input.len());
286 for sf in files_input.iter() {
287 let uri_arc = sf.uri(db);
288 let Ok(url) = Url::parse(&uri_arc) else {
293 continue;
294 };
295 let idx = file_index(db, *sf).0.clone();
296 files.push((url, idx));
297 }
298
299 let (classes_by_name, subtypes_of, decls_by_name) = build_maps(&files);
300
301 WorkspaceIndexArc(Arc::new(WorkspaceIndexData {
302 files,
303 classes_by_name,
304 subtypes_of,
305 decls_by_name,
306 }))
307}
308
309#[cfg(test)]
310mod tests {
311 use std::sync::Arc;
312
313 use super::*;
314 use crate::db::analysis::AnalysisHost;
315 use crate::db::input::FileText;
316 use salsa::Setter;
317
318 fn new_file(host: &AnalysisHost, uri: &str, src: &str) -> (Arc<str>, FileText) {
319 let ft = FileText::new(host.db(), Arc::<str>::from(src), None);
320 (Arc::<str>::from(uri), ft)
321 }
322
323 #[test]
324 fn workspace_index_builds_name_and_subtype_maps() {
325 let host = AnalysisHost::new();
326 let f1 = new_file(&host, "file:///a.php", "<?php\nclass Animal {}");
327 let f2 = new_file(
328 &host,
329 "file:///b.php",
330 "<?php\nclass Dog extends Animal {}\nclass Cat extends Animal {}",
331 );
332 let ws = Workspace::new(
333 host.db(),
334 Arc::from([f1, f2]),
335 mir_analyzer::PhpVersion::LATEST,
336 );
337
338 let wi = workspace_index(host.db(), ws);
339 let data = wi.get();
340
341 assert!(data.classes_by_name.contains_key("Animal"));
342 assert!(data.classes_by_name.contains_key("Dog"));
343
344 let subs = data
345 .subtypes_of
346 .get("Animal")
347 .expect("Animal must have subtype entries");
348 assert_eq!(subs.len(), 2, "Dog + Cat extend Animal");
349
350 let names: Vec<_> = subs
351 .iter()
352 .filter_map(|r| data.at(*r).map(|(_, c)| c.name.clone()))
353 .collect();
354 assert!(names.iter().any(|n| n.as_ref() == "Dog"));
355 assert!(names.iter().any(|n| n.as_ref() == "Cat"));
356 }
357
358 #[test]
359 fn workspace_index_memoizes_and_invalidates() {
360 let mut host = AnalysisHost::new();
361 let (uri_arc, ft1) = new_file(&host, "file:///a.php", "<?php\nclass A {}");
362 let ws = Workspace::new(
363 host.db(),
364 Arc::from([(uri_arc, ft1)]),
365 mir_analyzer::PhpVersion::LATEST,
366 );
367
368 let a = workspace_index(host.db(), ws);
369 let b = workspace_index(host.db(), ws);
370 assert!(
371 Arc::ptr_eq(&a.0, &b.0),
372 "unchanged inputs must return the memoized Arc"
373 );
374
375 ft1.set_text(host.db_mut())
376 .to(Arc::<str>::from("<?php\nclass B {}"));
377 let c = workspace_index(host.db(), ws);
378 assert!(!Arc::ptr_eq(&a.0, &c.0), "an edit must produce a fresh Arc");
379 assert!(c.get().classes_by_name.contains_key("B"));
380 assert!(!c.get().classes_by_name.contains_key("A"));
381 }
382
383 #[test]
384 fn workspace_index_collects_interface_and_trait_subtypes() {
385 let host = AnalysisHost::new();
386 let src = concat!(
387 "<?php\n",
388 "interface Greeter {}\n",
389 "trait Shouting {}\n",
390 "class Hi implements Greeter { use Shouting; }\n",
391 );
392 let f = new_file(&host, "file:///m.php", src);
393 let ws = Workspace::new(host.db(), Arc::from([f]), mir_analyzer::PhpVersion::LATEST);
394 let wi = workspace_index(host.db(), ws);
395 let data = wi.get();
396
397 let greeter_subs = data.subtypes_of.get("Greeter").expect("Greeter subs");
398 assert_eq!(greeter_subs.len(), 1);
399 let shouting_subs = data.subtypes_of.get("Shouting").expect("Shouting subs");
400 assert_eq!(shouting_subs.len(), 1);
401 }
402}