php_lsp/db/
definitions.rs1use std::sync::Arc;
9
10use mir_codebase::storage::StubSlice;
11use salsa::{Database, Update};
12
13use crate::db::input::SourceFile;
14use crate::db::parse::parsed_doc;
15
16#[derive(Clone)]
17pub struct SliceArc(pub Arc<StubSlice>);
18
19impl SliceArc {
20 pub fn get(&self) -> &StubSlice {
21 &self.0
22 }
23}
24
25unsafe impl Update for SliceArc {
27 unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
28 let old_ref = unsafe { &mut *old_pointer };
29 if Arc::ptr_eq(&old_ref.0, &new_value.0) {
30 false
31 } else {
32 *old_ref = new_value;
33 true
34 }
35 }
36}
37
38#[salsa::tracked(no_eq)]
53pub fn file_definitions(db: &dyn Database, file: SourceFile) -> SliceArc {
54 if let Some(cached) = file.cached_slice(db) {
59 let _ = file.text(db);
60 return SliceArc(cached);
61 }
62
63 let doc = parsed_doc(db, file);
64 let text = file.text(db);
65 let file_path: Arc<str> = file.uri(db);
66 let source_map = php_rs_parser::source_map::SourceMap::new(&text);
67 let collector =
68 mir_analyzer::collector::DefinitionCollector::new_for_slice(file_path, &text, &source_map);
69 let (slice, _issues) = collector.collect_slice(doc.get().program());
70 SliceArc(Arc::new(slice))
71}
72
73#[cfg(test)]
74mod tests {
75 use std::sync::Arc;
76
77 use super::*;
78 use crate::db::analysis::AnalysisHost;
79 use crate::db::input::{FileId, SourceFile};
80 use salsa::Setter;
81
82 #[test]
83 fn file_definitions_extracts_class() {
84 let host = AnalysisHost::new();
85 let file = SourceFile::new(
86 host.db(),
87 FileId(0),
88 Arc::<str>::from("file:///t.php"),
89 Arc::<str>::from("<?php\nnamespace App;\nclass Foo {}"),
90 None,
91 );
92 let slice = file_definitions(host.db(), file);
93 let classes: Vec<&str> = slice
94 .get()
95 .classes
96 .iter()
97 .map(|c| c.fqcn.as_ref())
98 .collect();
99 assert_eq!(classes, vec!["App\\Foo"]);
100 }
101
102 #[test]
103 fn file_definitions_reruns_after_edit() {
104 let mut host = AnalysisHost::new();
105 let file = SourceFile::new(
106 host.db(),
107 FileId(1),
108 Arc::<str>::from("file:///t.php"),
109 Arc::<str>::from("<?php\nclass A {}"),
110 None,
111 );
112 let a1 = file_definitions(host.db(), file);
113 let first_ptr = Arc::as_ptr(&a1.0);
114
115 file.set_text(host.db_mut())
116 .to(Arc::<str>::from("<?php\nclass B {}"));
117 let a2 = file_definitions(host.db(), file);
118 assert_ne!(first_ptr, Arc::as_ptr(&a2.0));
119 let classes: Vec<&str> = a2.get().classes.iter().map(|c| c.fqcn.as_ref()).collect();
120 assert_eq!(classes, vec!["B"]);
121 }
122
123 #[test]
129 fn file_definitions_returns_seeded_slice_without_parsing() {
130 let mut host = AnalysisHost::new();
131 let file = SourceFile::new(
135 host.db(),
136 FileId(2),
137 Arc::<str>::from("file:///t.php"),
138 Arc::<str>::from("<?php\nclass Text {}"),
139 None,
140 );
141
142 let seeded = {
143 let src = "<?php\nclass Cached {}";
148 let source_map = php_rs_parser::source_map::SourceMap::new(src);
149 let (doc, _) = crate::diagnostics::parse_document(src);
150 let collector = mir_analyzer::collector::DefinitionCollector::new_for_slice(
151 Arc::<str>::from("file:///t.php"),
152 src,
153 &source_map,
154 );
155 let (slice, _) = collector.collect_slice(doc.program());
156 Arc::new(slice)
157 };
158 file.set_cached_slice(host.db_mut()).to(Some(seeded));
159
160 let out = file_definitions(host.db(), file);
161 let classes: Vec<&str> = out.get().classes.iter().map(|c| c.fqcn.as_ref()).collect();
162 assert_eq!(
163 classes,
164 vec!["Cached"],
165 "seeded cached_slice must short-circuit parse + collect"
166 );
167 }
168
169 #[test]
173 fn edit_invalidates_seeded_slice() {
174 let mut host = AnalysisHost::new();
175 let file = SourceFile::new(
176 host.db(),
177 FileId(3),
178 Arc::<str>::from("file:///t.php"),
179 Arc::<str>::from("<?php\nclass Original {}"),
180 None,
181 );
182
183 let misleading = {
185 let src = "<?php\nclass Misleading {}";
186 let source_map = php_rs_parser::source_map::SourceMap::new(src);
187 let (doc, _) = crate::diagnostics::parse_document(src);
188 let collector = mir_analyzer::collector::DefinitionCollector::new_for_slice(
189 Arc::<str>::from("file:///t.php"),
190 src,
191 &source_map,
192 );
193 let (s, _) = collector.collect_slice(doc.program());
194 Arc::new(s)
195 };
196 file.set_cached_slice(host.db_mut()).to(Some(misleading));
197
198 let out1 = file_definitions(host.db(), file);
199 let names: Vec<&str> = out1.get().classes.iter().map(|c| c.fqcn.as_ref()).collect();
200 assert_eq!(names, vec!["Misleading"]);
201
202 file.set_text(host.db_mut())
206 .to(Arc::<str>::from("<?php\nclass Edited {}"));
207 file.set_cached_slice(host.db_mut()).to(None);
208
209 let out2 = file_definitions(host.db(), file);
210 let names: Vec<&str> = out2.get().classes.iter().map(|c| c.fqcn.as_ref()).collect();
211 assert_eq!(
212 names,
213 vec!["Edited"],
214 "edit must invalidate cached slice — fresh parse of new text"
215 );
216 }
217}