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