1use std::collections::HashMap;
25use std::sync::Arc;
26
27use salsa::Database;
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.as_ref().to_string())
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
113crate::impl_arc_update!(WorkspaceIndexArc);
116
117#[salsa::tracked(no_eq)]
124pub fn workspace_index(db: &dyn Database, ws: Workspace) -> WorkspaceIndexArc {
125 let files_input = ws.files(db);
126
127 let mut files: Vec<(Url, Arc<FileIndex>)> = Vec::with_capacity(files_input.len());
128 for sf in files_input.iter() {
129 let uri_arc = sf.uri(db);
130 let Ok(url) = Url::parse(&uri_arc) else {
135 continue;
136 };
137 let idx = file_index(db, *sf).0.clone();
138 files.push((url, idx));
139 }
140
141 let mut classes_by_name: HashMap<String, Vec<ClassRef>> = HashMap::new();
142 let mut subtypes_of: HashMap<Arc<str>, Vec<ClassRef>> = HashMap::new();
143
144 for (file_idx, (_, idx)) in files.iter().enumerate() {
145 let file_idx = file_idx as u32;
146 for (cls_idx, cls) in idx.classes.iter().enumerate() {
147 let cr = ClassRef {
148 file: file_idx,
149 class: cls_idx as u32,
150 };
151 classes_by_name
152 .entry(cls.name.as_ref().to_string())
153 .or_default()
154 .push(cr);
155 if let Some(parent) = &cls.parent {
156 subtypes_of.entry(Arc::clone(parent)).or_default().push(cr);
157 }
158 for iface in &cls.implements {
159 subtypes_of.entry(Arc::clone(iface)).or_default().push(cr);
160 }
161 for trt in &cls.traits {
162 subtypes_of.entry(Arc::clone(trt)).or_default().push(cr);
163 }
164 }
165 }
166
167 WorkspaceIndexArc(Arc::new(WorkspaceIndexData {
168 files,
169 classes_by_name,
170 subtypes_of,
171 }))
172}
173
174#[cfg(test)]
175mod tests {
176 use std::sync::Arc;
177
178 use super::*;
179 use crate::db::analysis::AnalysisHost;
180 use crate::db::input::{FileId, SourceFile};
181 use salsa::Setter;
182
183 fn new_file(host: &AnalysisHost, id: u32, uri: &str, src: &str) -> SourceFile {
184 SourceFile::new(
185 host.db(),
186 FileId(id),
187 Arc::<str>::from(uri),
188 Arc::<str>::from(src),
189 None,
190 )
191 }
192
193 #[test]
194 fn workspace_index_builds_name_and_subtype_maps() {
195 let host = AnalysisHost::new();
196 let f1 = new_file(&host, 0, "file:///a.php", "<?php\nclass Animal {}");
197 let f2 = new_file(
198 &host,
199 1,
200 "file:///b.php",
201 "<?php\nclass Dog extends Animal {}\nclass Cat extends Animal {}",
202 );
203 let ws = Workspace::new(
204 host.db(),
205 Arc::from([f1, f2]),
206 mir_analyzer::PhpVersion::LATEST,
207 );
208
209 let wi = workspace_index(host.db(), ws);
210 let data = wi.get();
211
212 assert!(data.classes_by_name.contains_key("Animal"));
213 assert!(data.classes_by_name.contains_key("Dog"));
214
215 let subs = data
216 .subtypes_of
217 .get("Animal")
218 .expect("Animal must have subtype entries");
219 assert_eq!(subs.len(), 2, "Dog + Cat extend Animal");
220
221 let names: Vec<_> = subs
222 .iter()
223 .filter_map(|r| data.at(*r).map(|(_, c)| c.name.clone()))
224 .collect();
225 assert!(names.iter().any(|n| n.as_ref() == "Dog"));
226 assert!(names.iter().any(|n| n.as_ref() == "Cat"));
227 }
228
229 #[test]
230 fn workspace_index_memoizes_and_invalidates() {
231 let mut host = AnalysisHost::new();
232 let f1 = new_file(&host, 0, "file:///a.php", "<?php\nclass A {}");
233 let ws = Workspace::new(host.db(), Arc::from([f1]), mir_analyzer::PhpVersion::LATEST);
234
235 let a = workspace_index(host.db(), ws);
236 let b = workspace_index(host.db(), ws);
237 assert!(
238 Arc::ptr_eq(&a.0, &b.0),
239 "unchanged inputs must return the memoized Arc"
240 );
241
242 f1.set_text(host.db_mut())
243 .to(Arc::<str>::from("<?php\nclass B {}"));
244 let c = workspace_index(host.db(), ws);
245 assert!(!Arc::ptr_eq(&a.0, &c.0), "an edit must produce a fresh Arc");
246 assert!(c.get().classes_by_name.contains_key("B"));
247 assert!(!c.get().classes_by_name.contains_key("A"));
248 }
249
250 #[test]
251 fn workspace_index_collects_interface_and_trait_subtypes() {
252 let host = AnalysisHost::new();
253 let src = concat!(
254 "<?php\n",
255 "interface Greeter {}\n",
256 "trait Shouting {}\n",
257 "class Hi implements Greeter { use Shouting; }\n",
258 );
259 let f = new_file(&host, 0, "file:///m.php", src);
260 let ws = Workspace::new(host.db(), Arc::from([f]), mir_analyzer::PhpVersion::LATEST);
261 let wi = workspace_index(host.db(), ws);
262 let data = wi.get();
263
264 let greeter_subs = data.subtypes_of.get("Greeter").expect("Greeter subs");
265 assert_eq!(greeter_subs.len(), 1);
266 let shouting_subs = data.subtypes_of.get("Shouting").expect("Shouting subs");
267 assert_eq!(shouting_subs.len(), 1);
268 }
269}