1use std::collections::HashMap;
25use std::sync::Arc;
26
27use salsa::{Database, Update};
28use tower_lsp::lsp_types::Url;
29
30use crate::db::index::file_index;
31use crate::db::input::Workspace;
32use crate::file_index::FileIndex;
33
34#[derive(Debug, Clone, Copy)]
37pub struct ClassRef {
38 pub file: u32,
39 pub class: u32,
40}
41
42pub struct WorkspaceIndexData {
45 pub files: Vec<(Url, Arc<FileIndex>)>,
46 pub classes_by_name: HashMap<String, Vec<ClassRef>>,
47 pub subtypes_of: HashMap<Arc<str>, Vec<ClassRef>>,
52}
53
54impl WorkspaceIndexData {
55 pub fn at(&self, r: ClassRef) -> Option<(&Url, &crate::file_index::ClassDef)> {
57 let (uri, idx) = self.files.get(r.file as usize)?;
58 let cls = idx.classes.get(r.class as usize)?;
59 Some((uri, cls))
60 }
61
62 #[cfg(test)]
68 pub fn from_files(files: Vec<(Url, Arc<FileIndex>)>) -> Self {
69 let mut classes_by_name: HashMap<String, Vec<ClassRef>> = HashMap::new();
70 let mut subtypes_of: HashMap<Arc<str>, Vec<ClassRef>> = HashMap::new();
71 for (file_idx, (_, idx)) in files.iter().enumerate() {
72 let file_idx = file_idx as u32;
73 for (cls_idx, cls) in idx.classes.iter().enumerate() {
74 let cr = ClassRef {
75 file: file_idx,
76 class: cls_idx as u32,
77 };
78 classes_by_name
79 .entry(cls.name.clone())
80 .or_default()
81 .push(cr);
82 if let Some(parent) = &cls.parent {
83 subtypes_of.entry(Arc::clone(parent)).or_default().push(cr);
84 }
85 for iface in &cls.implements {
86 subtypes_of.entry(Arc::clone(iface)).or_default().push(cr);
87 }
88 for trt in &cls.traits {
89 subtypes_of.entry(Arc::clone(trt)).or_default().push(cr);
90 }
91 }
92 }
93 Self {
94 files,
95 classes_by_name,
96 subtypes_of,
97 }
98 }
99}
100
101#[derive(Clone)]
104pub struct WorkspaceIndexArc(pub Arc<WorkspaceIndexData>);
105
106impl WorkspaceIndexArc {
107 #[cfg(test)]
108 pub fn get(&self) -> &WorkspaceIndexData {
109 &self.0
110 }
111}
112
113unsafe impl Update for WorkspaceIndexArc {
116 unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
117 let old_ref = unsafe { &mut *old_pointer };
118 if Arc::ptr_eq(&old_ref.0, &new_value.0) {
119 false
120 } else {
121 *old_ref = new_value;
122 true
123 }
124 }
125}
126
127#[salsa::tracked(no_eq)]
134pub fn workspace_index(db: &dyn Database, ws: Workspace) -> WorkspaceIndexArc {
135 let files_input = ws.files(db);
136
137 let mut files: Vec<(Url, Arc<FileIndex>)> = Vec::with_capacity(files_input.len());
138 for sf in files_input.iter() {
139 let uri_arc = sf.uri(db);
140 let Ok(url) = Url::parse(&uri_arc) else {
145 continue;
146 };
147 let idx = file_index(db, *sf).0.clone();
148 files.push((url, idx));
149 }
150
151 let mut classes_by_name: HashMap<String, Vec<ClassRef>> = HashMap::new();
152 let mut subtypes_of: HashMap<Arc<str>, Vec<ClassRef>> = HashMap::new();
153
154 for (file_idx, (_, idx)) in files.iter().enumerate() {
155 let file_idx = file_idx as u32;
156 for (cls_idx, cls) in idx.classes.iter().enumerate() {
157 let cr = ClassRef {
158 file: file_idx,
159 class: cls_idx as u32,
160 };
161 classes_by_name
162 .entry(cls.name.clone())
163 .or_default()
164 .push(cr);
165 if let Some(parent) = &cls.parent {
166 subtypes_of.entry(Arc::clone(parent)).or_default().push(cr);
167 }
168 for iface in &cls.implements {
169 subtypes_of.entry(Arc::clone(iface)).or_default().push(cr);
170 }
171 for trt in &cls.traits {
172 subtypes_of.entry(Arc::clone(trt)).or_default().push(cr);
173 }
174 }
175 }
176
177 WorkspaceIndexArc(Arc::new(WorkspaceIndexData {
178 files,
179 classes_by_name,
180 subtypes_of,
181 }))
182}
183
184#[cfg(test)]
185mod tests {
186 use std::sync::Arc;
187
188 use super::*;
189 use crate::db::analysis::AnalysisHost;
190 use crate::db::input::{FileId, SourceFile};
191 use salsa::Setter;
192
193 fn new_file(host: &AnalysisHost, id: u32, uri: &str, src: &str) -> SourceFile {
194 SourceFile::new(
195 host.db(),
196 FileId(id),
197 Arc::<str>::from(uri),
198 Arc::<str>::from(src),
199 None,
200 )
201 }
202
203 #[test]
204 fn workspace_index_builds_name_and_subtype_maps() {
205 let host = AnalysisHost::new();
206 let f1 = new_file(&host, 0, "file:///a.php", "<?php\nclass Animal {}");
207 let f2 = new_file(
208 &host,
209 1,
210 "file:///b.php",
211 "<?php\nclass Dog extends Animal {}\nclass Cat extends Animal {}",
212 );
213 let ws = Workspace::new(
214 host.db(),
215 Arc::from([f1, f2]),
216 mir_analyzer::PhpVersion::LATEST,
217 );
218
219 let wi = workspace_index(host.db(), ws);
220 let data = wi.get();
221
222 assert!(data.classes_by_name.contains_key("Animal"));
223 assert!(data.classes_by_name.contains_key("Dog"));
224
225 let subs = data
226 .subtypes_of
227 .get("Animal")
228 .expect("Animal must have subtype entries");
229 assert_eq!(subs.len(), 2, "Dog + Cat extend Animal");
230
231 let names: Vec<_> = subs
232 .iter()
233 .filter_map(|r| data.at(*r).map(|(_, c)| c.name.clone()))
234 .collect();
235 assert!(names.contains(&"Dog".to_string()));
236 assert!(names.contains(&"Cat".to_string()));
237 }
238
239 #[test]
240 fn workspace_index_memoizes_and_invalidates() {
241 let mut host = AnalysisHost::new();
242 let f1 = new_file(&host, 0, "file:///a.php", "<?php\nclass A {}");
243 let ws = Workspace::new(host.db(), Arc::from([f1]), mir_analyzer::PhpVersion::LATEST);
244
245 let a = workspace_index(host.db(), ws);
246 let b = workspace_index(host.db(), ws);
247 assert!(
248 Arc::ptr_eq(&a.0, &b.0),
249 "unchanged inputs must return the memoized Arc"
250 );
251
252 f1.set_text(host.db_mut())
253 .to(Arc::<str>::from("<?php\nclass B {}"));
254 let c = workspace_index(host.db(), ws);
255 assert!(!Arc::ptr_eq(&a.0, &c.0), "an edit must produce a fresh Arc");
256 assert!(c.get().classes_by_name.contains_key("B"));
257 assert!(!c.get().classes_by_name.contains_key("A"));
258 }
259
260 #[test]
261 fn workspace_index_collects_interface_and_trait_subtypes() {
262 let host = AnalysisHost::new();
263 let src = concat!(
264 "<?php\n",
265 "interface Greeter {}\n",
266 "trait Shouting {}\n",
267 "class Hi implements Greeter { use Shouting; }\n",
268 );
269 let f = new_file(&host, 0, "file:///m.php", src);
270 let ws = Workspace::new(host.db(), Arc::from([f]), mir_analyzer::PhpVersion::LATEST);
271 let wi = workspace_index(host.db(), ws);
272 let data = wi.get();
273
274 let greeter_subs = data.subtypes_of.get("Greeter").expect("Greeter subs");
275 assert_eq!(greeter_subs.len(), 1);
276 let shouting_subs = data.subtypes_of.get("Shouting").expect("Shouting subs");
277 assert_eq!(shouting_subs.len(), 1);
278 }
279}