1use std::panic::AssertUnwindSafe;
2use std::sync::Arc;
3
4use rustc_hash::FxHashSet as HashSet;
5use salsa::Cancelled;
6
7use super::index_state::{
8 hash_files, AnalyzedFile, DefsFreshness, IndexState, QueryGeneration, RefFreshness,
9 RefQueryCacheKey, RetireEpoch, SubtypeQueryCacheKey, ViewStamp,
10};
11use super::queries::{identifier_char_col, span_range, ReferenceGate};
12use super::SubtypeClassSite;
13use crate::cache::AnalysisCache;
14use crate::db::{MirDatabase, MirDbStorage};
15use crate::php_version::PhpVersion;
16
17#[derive(Clone)]
33pub struct AnalysisSnapshot {
34 pub(super) db: MirDbStorage,
35 pub(super) index: Arc<IndexState>,
36 pub(super) cache: Option<Arc<AnalysisCache>>,
37 pub(super) php_version: PhpVersion,
38 pub(super) index_generation: u64,
41 pub(super) retire_epoch: RetireEpoch,
44}
45
46const _: () = {
47 const fn assert_send_clone<T: Send + Clone>() {}
48 assert_send_clone::<AnalysisSnapshot>();
49};
50
51pub(crate) struct DbView<'a> {
55 snapshot: AnalysisSnapshot,
56 _owner: std::marker::PhantomData<&'a super::AnalysisSession>,
57}
58
59impl<'a> DbView<'a> {
60 pub(super) fn new(_owner: &'a super::AnalysisSession, snapshot: AnalysisSnapshot) -> Self {
61 Self {
62 snapshot,
63 _owner: std::marker::PhantomData,
64 }
65 }
66}
67
68struct ConstantDeclSite {
71 file: Arc<str>,
72 line: u32,
73 stmt_col: u32,
74 stmt_end: u32,
75 name_col: u32,
76 name_len: u32,
77}
78
79impl Drop for DbView<'_> {
80 fn drop(&mut self) {}
81}
82
83impl std::ops::Deref for DbView<'_> {
84 type Target = AnalysisSnapshot;
85 fn deref(&self) -> &AnalysisSnapshot {
86 &self.snapshot
87 }
88}
89
90struct WarmPass {
92 mention_scanner: Option<Arc<crate::db::class_mention_index::MentionScanner>>,
93 analyzed: Vec<AnalyzedFile>,
94}
95
96type MentionScanRecord = (Arc<str>, Arc<str>, Box<[mir_types::Name]>);
99
100const MIN_PARALLEL_FILES: usize = 4;
102
103fn stage_in_parallel(
109 db: &MirDbStorage,
110 files: &[Arc<str>],
111 cancel: &crate::IndexCancel,
112 parallel: bool,
113 stage: impl Fn(&MirDbStorage, &Arc<str>) -> Option<AnalyzedFile> + Sync,
114) -> Option<Vec<AnalyzedFile>> {
115 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
116 use std::sync::Mutex;
117
118 let threads = std::thread::available_parallelism()
119 .map_or(1, |n| n.get())
120 .min(files.len());
121 if !parallel || threads < 2 || files.len() < MIN_PARALLEL_FILES {
122 let mut analyzed = Vec::with_capacity(files.len());
123 for path in files {
124 if cancel.is_cancelled() {
125 return None;
126 }
127 analyzed.extend(stage(db, path));
128 }
129 return Some(analyzed);
130 }
131
132 let next = AtomicUsize::new(0);
133 let stop = AtomicBool::new(false);
134 let panic = Mutex::new(None::<Box<dyn std::any::Any + Send>>);
135 let slots: Vec<Mutex<Option<AnalyzedFile>>> = files.iter().map(|_| Mutex::new(None)).collect();
136
137 let work = |db: MirDbStorage| loop {
138 if stop.load(Ordering::Relaxed) {
139 return;
140 }
141 if cancel.is_cancelled() {
142 stop.store(true, Ordering::Relaxed);
143 return;
144 }
145 let i = next.fetch_add(1, Ordering::Relaxed);
146 let Some(path) = files.get(i) else { return };
147 match std::panic::catch_unwind(AssertUnwindSafe(|| stage(&db, path))) {
148 Ok(staged) => *slots[i].lock().unwrap() = staged,
149 Err(payload) => {
150 stop.store(true, Ordering::Relaxed);
151 panic.lock().unwrap().get_or_insert(payload);
152 return;
153 }
154 }
155 };
156 std::thread::scope(|scope| {
157 let handles: Vec<_> = (0..threads)
158 .filter_map(|n| {
159 let db = db.clone();
160 std::thread::Builder::new()
161 .name(format!("mir-warm-{n}"))
162 .stack_size(crate::recursion::STACK_SIZE)
163 .spawn_scoped(scope, || work(db))
164 .ok()
165 })
166 .collect();
167 if handles.is_empty() {
168 work(db.clone());
169 }
170 for h in handles {
171 let _ = h.join();
172 }
173 });
174
175 if let Some(payload) = panic.into_inner().unwrap() {
176 std::panic::resume_unwind(payload);
177 }
178 if stop.load(Ordering::Relaxed) {
179 return None;
180 }
181 Some(
182 slots
183 .into_iter()
184 .filter_map(|s| s.into_inner().unwrap())
185 .collect(),
186 )
187}
188
189pub(super) fn catch<T>(f: impl FnOnce() -> T) -> Result<T, Cancelled> {
190 Cancelled::catch(AssertUnwindSafe(f))
191}
192
193pub(super) fn unwind(cancelled: Cancelled) -> ! {
195 std::panic::resume_unwind(Box::new(cancelled))
196}
197
198impl AnalysisSnapshot {
199 #[doc(hidden)]
205 pub fn db(&self) -> &MirDbStorage {
206 &self.db
207 }
208
209 #[doc(hidden)]
214 pub fn read<R>(&self, f: impl FnOnce(&dyn MirDatabase) -> R) -> Result<R, Cancelled> {
215 catch(|| f(&self.db))
216 }
217
218 pub(crate) fn index(&self) -> &IndexState {
219 &self.index
220 }
221
222 pub(crate) fn stamp(&self) -> ViewStamp {
223 ViewStamp {
224 generation: self.index_generation,
225 retire_epoch: self.retire_epoch,
226 }
227 }
228
229 pub fn php_version(&self) -> PhpVersion {
230 self.php_version
231 }
232
233 pub fn index_generation(&self) -> u64 {
235 self.index_generation
236 }
237
238 pub fn text_revision(&self) -> salsa::Revision {
240 self.db.current_revision()
241 }
242
243 pub(crate) fn query_cache_generation(&self) -> QueryGeneration {
252 (
253 self.db.current_revision(),
254 self.db.subtype_edges_epoch(),
255 self.index.retire_seq(),
256 )
257 }
258
259 pub fn find_class_like(&self, fqcn: &str) -> Result<Option<crate::db::ClassLike>, Cancelled> {
260 self.read(|db| crate::db::find_class_like(db, crate::db::Fqcn::from_str(db, fqcn)))
261 }
262
263 pub fn is_builtin_class(&self, fqcn: &str) -> Result<bool, Cancelled> {
268 self.read(|db| {
269 let fqcn = fqcn.trim_start_matches('\\');
270 crate::db::class_like_decl_file(db, crate::db::Fqcn::from_str(db, fqcn))
271 .is_some_and(|file| crate::stubs::is_stub_path(&file))
272 })
273 }
274
275 pub fn find_function(&self, fqn: &str) -> Result<Option<Arc<crate::FunctionDef>>, Cancelled> {
276 self.read(|db| crate::db::find_function(db, crate::db::Fqcn::from_str(db, fqn)))
277 }
278
279 #[allow(clippy::type_complexity)]
282 pub fn find_method_in_chain(
283 &self,
284 class: &str,
285 method: &str,
286 ) -> Result<Option<(Arc<str>, Arc<mir_codebase::definitions::MethodDef>)>, Cancelled> {
287 self.read(|db| {
288 crate::db::find_method_in_chain(db, crate::db::Fqcn::from_str(db, class), method)
289 })
290 }
291
292 pub fn implemented_method(
297 &self,
298 method: &crate::Name,
299 ) -> Result<Option<crate::Name>, Cancelled> {
300 let crate::Name::Method { class, name } = method else {
301 return Ok(None);
302 };
303 self.read(|db| {
304 let here = crate::db::Fqcn::from_str(db, class.as_ref());
305 crate::db::class_ancestors_by_fqcn(db, here)
306 .iter()
307 .skip(1)
308 .find_map(|ancestor| {
309 let fqcn = crate::db::Fqcn::from_str(db, ancestor.as_ref());
310 let is_interface = matches!(
311 crate::db::find_class_like(db, fqcn)?,
312 crate::db::ClassLike::Interface(_)
313 );
314 let m = crate::db::find_method_in_class(db, fqcn, name)?;
315 (is_interface || m.is_abstract)
316 .then(|| crate::Name::method(ancestor.clone(), name))
317 })
318 })
319 }
320
321 pub fn definition_of_cached(
324 &self,
325 symbol: &crate::Name,
326 ) -> Result<Result<mir_types::Location, crate::SymbolLookupError>, Cancelled> {
327 catch(|| self.definition_of_unguarded(symbol))
328 }
329
330 fn definition_of_unguarded(
331 &self,
332 symbol: &crate::Name,
333 ) -> Result<mir_types::Location, crate::SymbolLookupError> {
334 let db = &self.db;
335 match symbol {
336 crate::Name::Class(fqcn) => {
337 let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
338 let class = crate::db::find_class_like(db, here)
339 .ok_or(crate::SymbolLookupError::NotFound)?;
340 class
341 .location()
342 .cloned()
343 .ok_or(crate::SymbolLookupError::NoSourceLocation)
344 }
345 crate::Name::Function(fqn) => {
346 let here = crate::db::Fqcn::from_str(db, fqn.as_ref());
347 let f =
348 crate::db::find_function(db, here).ok_or(crate::SymbolLookupError::NotFound)?;
349 f.location
350 .clone()
351 .ok_or(crate::SymbolLookupError::NoSourceLocation)
352 }
353 crate::Name::Method { .. }
354 | crate::Name::Property { .. }
355 | crate::Name::ClassConstant { .. } => {
356 crate::db::member_location(db, symbol).ok_or(crate::SymbolLookupError::NotFound)
357 }
358 crate::Name::GlobalConstant(fqn) => self
359 .global_constant_location(fqn)
360 .ok_or(crate::SymbolLookupError::NoSourceLocation),
361 }
362 }
363
364 pub fn files_mentioning_class(
373 &self,
374 files: &[Arc<str>],
375 class_name: &str,
376 ) -> Result<Vec<Arc<str>>, Cancelled> {
377 self.files_mentioning_any(files, &[class_name])
378 }
379
380 pub fn files_mentioning_any(
385 &self,
386 files: &[Arc<str>],
387 needles: &[&str],
388 ) -> Result<Vec<Arc<str>>, Cancelled> {
389 catch(|| self.mentioning_any_unguarded(files, needles))
390 }
391
392 fn mentioning_any_unguarded(&self, files: &[Arc<str>], needles: &[&str]) -> Vec<Arc<str>> {
393 use rayon::prelude::*;
394
395 if needles.is_empty() {
396 return files.to_vec();
397 }
398 let db = self.db.clone();
399 db.add_literal_mention_names(needles.iter().copied());
400 let queries: Vec<_> = needles
402 .iter()
403 .filter_map(|n| db.prepare_class_mention_query(n))
404 .collect();
405 if queries.is_empty() {
406 return files.to_vec();
407 }
408 let Some(scanner) = db.class_mention_scanner() else {
409 return files.to_vec();
410 };
411
412 files
413 .par_iter()
414 .map_with(db, |db, f| -> Option<Arc<str>> {
415 let sf = db.lookup_source_file(f.as_ref())?;
416 let text = sf.text(&*db as &dyn MirDatabase).clone();
417 for q in &queries {
418 match db.class_mention_answer(f.as_ref(), q, &text) {
419 Some(true) => return Some(f.clone()),
420 Some(false) => continue,
421 None => {
422 let names = scanner.scan(text.as_ref());
423 let hit = queries
424 .iter()
425 .any(|q2| names.binary_search(&q2.name).is_ok());
426 db.set_file_class_mentions(f, &text, scanner.epoch(), names);
427 return hit.then(|| f.clone());
428 }
429 }
430 }
431 None
432 })
433 .flatten()
434 .collect()
435 }
436
437 pub fn indexed_references_to(
471 &self,
472 symbol: &crate::Name,
473 files: &[Arc<str>],
474 include_declaration: bool,
475 includes: crate::ReferenceIncludes,
476 ) -> Result<Vec<(Arc<str>, crate::Range)>, Cancelled> {
477 let key = self.reference_query_key(symbol, files, include_declaration, includes);
478 if let Some(hit) = self.index.ref_queries.get(&key) {
479 return Ok(hit);
480 }
481 let stale = catch(|| self.stale_reference_candidates_unguarded(symbol, files))?;
482 if !stale.is_empty() {
483 self.commit_reference_candidates(&stale)?;
484 }
485 let out = catch(|| self.read_references(symbol, files, include_declaration, includes))?;
486 self.ensure_no_retire_since(key.generation)?;
487 self.memoize_references(key, &out);
488 Ok(out)
489 }
490
491 pub(super) fn ensure_no_retire_since(
494 &self,
495 generation: QueryGeneration,
496 ) -> Result<(), Cancelled> {
497 if self.index.retire_seq() == generation.2 {
498 Ok(())
499 } else {
500 Err(Cancelled::PendingWrite)
501 }
502 }
503
504 pub fn indexed_use_import_locations(
508 &self,
509 symbol: &crate::Name,
510 files: &[Arc<str>],
511 ) -> Result<Vec<(Arc<str>, crate::Range)>, Cancelled> {
512 self.indexed_references_to(symbol, files, false, crate::ReferenceIncludes::UseImports)
513 }
514
515 pub(super) fn reference_query_key(
516 &self,
517 symbol: &crate::Name,
518 files: &[Arc<str>],
519 include_declaration: bool,
520 includes: crate::ReferenceIncludes,
521 ) -> RefQueryCacheKey {
522 RefQueryCacheKey {
523 symbol: symbol.codebase_key(),
524 include_declaration,
525 includes,
526 generation: self.query_cache_generation(),
527 files_hash: hash_files(files),
528 }
529 }
530
531 pub(super) fn memoize_references(
536 &self,
537 key: RefQueryCacheKey,
538 out: &[(Arc<str>, crate::Range)],
539 ) {
540 let generation = self.query_cache_generation();
541 if generation == key.generation {
542 self.index.ref_queries.insert(generation, key, out);
543 }
544 }
545
546 pub fn stale_reference_candidates(
554 &self,
555 symbol: &crate::Name,
556 files: &[Arc<str>],
557 ) -> Result<Vec<Arc<str>>, Cancelled> {
558 catch(|| self.stale_reference_candidates_unguarded(symbol, files))
559 }
560
561 pub(super) fn stale_reference_candidates_unguarded(
583 &self,
584 symbol: &crate::Name,
585 files: &[Arc<str>],
586 ) -> Vec<Arc<str>> {
587 let db = &self.db;
588 let gate = self.reference_gate(symbol);
589 let has_needles = !gate.idents.is_empty() || !gate.raw.is_empty();
590 let (mention_queries, mention_scanner) = if has_needles {
591 db.add_literal_mention_names(gate.idents.iter().map(|s| s.as_str()));
592 db.add_raw_mention_needles(gate.raw.iter().map(|s| s.as_str()));
593 let queries: Vec<_> = gate
594 .idents
595 .iter()
596 .chain(gate.raw.iter())
597 .filter_map(|s| db.prepare_class_mention_query(s))
598 .collect();
599 (queries, db.class_mention_scanner())
600 } else {
601 (Vec::new(), None)
602 };
603 let gate_complete = mention_queries.len() == gate.idents.len() + gate.raw.len()
607 && mention_scanner.is_some();
608 let current_gen = self.index_generation;
609 let mut stale = Vec::new();
610 let mut scanned: Vec<MentionScanRecord> = Vec::new();
611 for f in files {
612 let Some(sf) = db.lookup_source_file(f.as_ref()) else {
613 continue;
614 };
615 let text = sf.text(db as &dyn MirDatabase);
616 match self.index.ref_freshness(sf, text, current_gen) {
617 RefFreshness::Fresh => continue,
618 RefFreshness::Stale => {
619 stale.push(f.clone());
620 continue;
621 }
622 RefFreshness::StaleByGenerationOnly | RefFreshness::Uncommitted => {}
623 }
624 if has_needles && gate_complete {
625 let mut answer = Some(false);
629 for q in &mention_queries {
630 match db.class_mention_answer(f.as_ref(), q, text) {
631 Some(true) => {
632 answer = Some(true);
633 break;
634 }
635 Some(false) => {}
636 None => answer = None,
637 }
638 }
639 match answer {
640 Some(true) => {}
641 Some(false) => continue,
642 None => {
643 let scanner = mention_scanner
644 .as_ref()
645 .expect("gate_complete implies a scanner");
646 let names = scanner.scan(text);
647 let hit = mention_queries
648 .iter()
649 .any(|q| names.binary_search(&q.name).is_ok());
650 scanned.push((f.clone(), text.clone(), names));
651 if hit {
652 stale.push(f.clone());
653 }
654 continue;
655 }
656 }
657 }
658 stale.push(f.clone());
659 }
660 if let Some(scanner) = &mention_scanner {
663 for (file, text, names) in scanned {
664 db.set_file_class_mentions(&file, &text, scanner.epoch(), names);
665 }
666 }
667 stale
668 }
669
670 pub(super) fn commit_reference_candidates(&self, stale: &[Arc<str>]) -> Result<(), Cancelled> {
674 self.warm_pass(stale, &crate::IndexCancel::new(), false)
675 .map(drop)
676 }
677
678 pub fn warm_files(
685 &self,
686 files: &[Arc<str>],
687 cancel: &crate::IndexCancel,
688 ) -> Result<bool, Cancelled> {
689 Ok(self.warm_pass(files, cancel, true)?.is_some())
690 }
691
692 #[allow(clippy::type_complexity)]
702 pub fn reanalyze_files(
703 &self,
704 files: &[Arc<str>],
705 cancel: &crate::IndexCancel,
706 ) -> Result<Option<Vec<(Arc<str>, crate::FileAnalysis)>>, Cancelled> {
707 Ok(self.warm_pass(files, cancel, true)?.map(|analyzed| {
708 analyzed
709 .into_iter()
710 .map(|a| {
711 (
712 a.file,
713 crate::FileAnalysis {
714 issues: a.out.issues.to_vec(),
715 symbols: Vec::new(),
716 },
717 )
718 })
719 .collect()
720 }))
721 }
722
723 pub(super) fn warm_pass(
724 &self,
725 files: &[Arc<str>],
726 cancel: &crate::IndexCancel,
727 parallel: bool,
728 ) -> Result<Option<Vec<AnalyzedFile>>, Cancelled> {
729 let Some(mut pass) = self.stage_warm(files, cancel, parallel)? else {
730 return Ok(None);
731 };
732 self.commit_warm(&mut pass)?;
733 Ok(Some(pass.analyzed))
734 }
735
736 fn stage_warm(
741 &self,
742 files: &[Arc<str>],
743 cancel: &crate::IndexCancel,
744 parallel: bool,
745 ) -> Result<Option<WarmPass>, Cancelled> {
746 catch(|| {
747 let mut db = self.db.clone();
748 db.freeze_workspace_index();
749 let mention_scanner = db.class_mention_scanner_reusing_cached();
750 let cache = self.cache.as_deref();
751 let index = &self.index;
752 let scanner = mention_scanner.as_deref();
753 let stage =
754 |db: &MirDbStorage, path: &Arc<str>| index.stage_analyzed(db, cache, scanner, path);
755 let analyzed = stage_in_parallel(&db, files, cancel, parallel, stage)?;
756 Some(WarmPass {
757 mention_scanner,
758 analyzed,
759 })
760 })
761 }
762
763 fn commit_warm(&self, pass: &mut WarmPass) -> Result<(), Cancelled> {
766 self.index.commit_analyzed(
767 &self.db,
768 self.cache.as_deref(),
769 pass.mention_scanner.as_deref(),
770 &mut pass.analyzed,
771 self.stamp(),
772 )
773 }
774
775 pub(super) fn read_references(
788 &self,
789 symbol: &crate::Name,
790 files: &[Arc<str>],
791 include_declaration: bool,
792 includes: crate::ReferenceIncludes,
793 ) -> Vec<(Arc<str>, crate::Range)> {
794 let key = symbol.codebase_key();
795 let hierarchy: Vec<String> = match symbol {
796 crate::Name::Method { class, name } => {
797 if class.is_empty() {
798 Vec::new()
799 } else if name.as_ref() == "__construct" {
800 vec![class.trim_start_matches('\\').to_string()]
801 } else {
802 self.member_hierarchy_classes(class.as_ref())
803 }
804 }
805 crate::Name::Property { class, .. } | crate::Name::ClassConstant { class, .. } => {
806 if class.is_empty() {
807 Vec::new()
808 } else {
809 self.member_hierarchy_classes(class.as_ref())
810 }
811 }
812 _ => Vec::new(),
813 };
814 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
815 let read_symbol_key = |symbol_key: &str| -> Vec<(Arc<str>, crate::Range)> {
816 self.db
817 .reference_locations(symbol_key)
818 .into_iter()
819 .filter(|(file, ..)| scope.contains(file.as_ref()))
820 .map(|(file, line, col_start, col_end)| {
821 (file, span_range(line, col_start as u32, col_end as u32))
822 })
823 .collect()
824 };
825 let mut scratch_key = String::new();
826 let mut read_composed_key = |prefix: &str, middle: &str, separator: &str, suffix: &str| {
827 scratch_key.clear();
828 scratch_key.reserve(prefix.len() + middle.len() + separator.len() + suffix.len());
829 scratch_key.push_str(prefix);
830 scratch_key.push_str(middle);
831 scratch_key.push_str(separator);
832 scratch_key.push_str(suffix);
833 read_symbol_key(&scratch_key)
834 };
835 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
836 if !matches!(includes, crate::ReferenceIncludes::UseImports) {
837 let member_prefix = match symbol {
838 crate::Name::Method { name, .. } => Some(("meth:", name)),
839 crate::Name::Property { name, .. } => Some(("prop:", name)),
840 crate::Name::ClassConstant { name, .. } => Some(("cnst:", name)),
841 _ => None,
842 };
843 match member_prefix {
844 Some((prefix, name)) => {
845 for class in &hierarchy {
846 out.extend(read_composed_key(prefix, class, "::", name));
847 }
848 }
849 None => out.extend(read_symbol_key(key.as_str())),
850 }
851 }
852 if !matches!(includes, crate::ReferenceIncludes::Plain) {
853 out.extend(read_composed_key("use:", key.as_str(), "", ""));
854 }
855 if matches!(includes, crate::ReferenceIncludes::Plain) && out.is_empty() {
856 match symbol {
857 crate::Name::Method { name, .. } => {
858 out = read_composed_key("methname:", name.as_ref(), "", "");
859 }
860 crate::Name::Property { name, .. } => {
861 out = read_composed_key("propname:", name.as_ref(), "", "");
862 }
863 _ => {}
864 }
865 }
866 out.sort_by(|a, b| {
867 a.0.cmp(&b.0)
868 .then(a.1.start.line.cmp(&b.1.start.line))
869 .then(a.1.start.column.cmp(&b.1.start.column))
870 });
871 out.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
872
873 if include_declaration {
874 let decls: Vec<(Arc<str>, crate::Range)> = match symbol {
875 crate::Name::Method { class, name }
876 | crate::Name::Property { class, name }
877 | crate::Name::ClassConstant { class, name } => {
878 if class.is_empty() {
879 let prefix = match symbol {
883 crate::Name::Method { .. } => "methdecl:",
884 crate::Name::Property { .. } => "propdecl:",
885 _ => "cnstdecl:",
886 };
887 read_composed_key(prefix, name.as_ref(), "", "")
888 } else {
889 self.member_decl_sites(&hierarchy, symbol)
890 }
891 }
892 _ => self.declaration_name_range(symbol).into_iter().collect(),
893 };
894 for (file, range) in decls {
895 if scope.contains(file.as_ref())
896 && !out.iter().any(|(f, r)| *f == file && *r == range)
897 {
898 out.push((file, range));
899 }
900 }
901 }
902 out
903 }
904
905 fn member_hierarchy_classes(&self, class_fqn: &str) -> Vec<String> {
911 let target = class_fqn.trim_start_matches('\\');
912 let mut out = vec![target.to_string()];
913 let db = &self.db;
914 let here = crate::db::Fqcn::from_str(db, target);
915 let ancestors = crate::db::class_ancestors_by_fqcn(db, here)
917 .iter()
918 .skip(1)
919 .map(|a| a.trim_start_matches('\\').to_string())
920 .collect::<Vec<_>>();
921 out.extend(ancestors);
922 out.extend(
923 db.subtype_sites_of(target, true, false)
924 .into_iter()
925 .map(|s| s.fqcn.trim_start_matches('\\').to_string()),
926 );
927 let mut seen: HashSet<String> = HashSet::default();
928 out.retain(|c| seen.insert(c.to_ascii_lowercase()));
929 out
930 }
931
932 fn member_decl_sites(
938 &self,
939 classes: &[String],
940 symbol: &crate::Name,
941 ) -> Vec<(Arc<str>, crate::Range)> {
942 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
943 let db = &self.db;
944 for class in classes {
945 let here = crate::db::Fqcn::from_str(db, class);
946 let (loc, needle) = match symbol {
947 crate::Name::Method { name, .. } => {
948 let Some(m) = crate::db::find_method_in_class(db, here, name) else {
949 continue;
950 };
951 (m.location.clone(), name.to_string())
952 }
953 crate::Name::Property { name, .. } => {
954 let Some(p) = crate::db::find_property_in_class(db, here, name) else {
955 continue;
956 };
957 (p.location.clone(), name.to_string())
958 }
959 crate::Name::ClassConstant { name, .. } => {
960 let Some(c) = crate::db::find_class_constant_in_class(db, here, name) else {
961 continue;
962 };
963 (c.location.clone(), name.to_string())
964 }
965 _ => continue,
966 };
967 let Some(loc) = loc else { continue };
968 let range = self.refine_location_to_name(&loc, &needle);
969 out.push((loc.file.clone(), range));
970 }
971 out
972 }
973
974 pub fn declaration_name_range_cached(
979 &self,
980 symbol: &crate::Name,
981 ) -> Result<Option<(Arc<str>, crate::Range)>, Cancelled> {
982 catch(|| self.declaration_name_range(symbol))
983 }
984
985 pub(super) fn declaration_name_range(
989 &self,
990 symbol: &crate::Name,
991 ) -> Option<(Arc<str>, crate::Range)> {
992 if let crate::Name::GlobalConstant(fqn) = symbol {
993 return self.global_constant_decl_range(fqn);
994 }
995 let loc = self.definition_of_unguarded(symbol).ok()?;
996 let declared_method_name: Arc<str>;
997 let short = match symbol {
998 crate::Name::Class(f) | crate::Name::Function(f) | crate::Name::GlobalConstant(f) => {
999 crate::db::subtype_index::short_name_of(f)
1000 }
1001 crate::Name::Method { class, name } => {
1002 declared_method_name = crate::db::trait_alias_original_name(&self.db, class, name)
1003 .unwrap_or_else(|| name.clone());
1004 declared_method_name.as_ref()
1005 }
1006 crate::Name::Property { name, .. } | crate::Name::ClassConstant { name, .. } => {
1007 name.as_ref()
1008 }
1009 };
1010 let range = self.refine_location_to_name(&loc, short);
1014 Some((loc.file.clone(), range))
1015 }
1016
1017 fn refine_location_to_name(&self, loc: &mir_types::Location, needle: &str) -> crate::Range {
1022 let fallback = span_range(loc.line, loc.col_start as u32, loc.col_end as u32);
1023 let Some(text) = self
1024 .db
1025 .lookup_source_file(loc.file.as_ref())
1026 .map(|sf| sf.text(&self.db as &dyn MirDatabase).clone())
1027 else {
1028 return fallback;
1029 };
1030 let needle_chars = needle.chars().count() as u32;
1031 let first_line = loc.line.saturating_sub(1) as usize;
1032 for case_insensitive in [false, true] {
1037 for (idx, line_text) in text.lines().enumerate().skip(first_line) {
1038 let line_no = idx as u32 + 1;
1039 if line_no > loc.line_end {
1040 break;
1041 }
1042 let min_col = if line_no == loc.line {
1043 loc.col_start as usize
1044 } else {
1045 0
1046 };
1047 if let Some(col) = identifier_char_col(line_text, needle, min_col, case_insensitive)
1048 {
1049 return span_range(line_no, col, col + needle_chars);
1050 }
1051 }
1052 }
1053 fallback
1054 }
1055
1056 fn global_constant_decl_site(&self, fqn: &str) -> Option<ConstantDeclSite> {
1060 let short = crate::db::subtype_index::short_name_of(fqn);
1061 let db = &self.db;
1062 let index = crate::db::workspace_index(db);
1063 let loc = index.constant_loc(mir_types::Name::from(fqn.trim_start_matches('\\')))?;
1064 let file = loc.file().path(db).clone();
1065 let sf = db.lookup_source_file(file.as_ref())?;
1066 let text = sf.text(db as &dyn MirDatabase);
1067 for (idx, line) in text.lines().enumerate() {
1068 let trimmed = line.trim_start();
1069 let is_decl_line = trimmed.starts_with("const ")
1070 || trimmed.contains("define(")
1071 || trimmed.contains("define (");
1072 if !is_decl_line {
1073 continue;
1074 }
1075 if let Some(name_col) = identifier_char_col(line, short, 0, false) {
1076 let stmt_end = match line.find(';') {
1077 Some(byte) => line[..=byte].chars().count(),
1078 None => line.chars().count(),
1079 };
1080 return Some(ConstantDeclSite {
1081 file,
1082 line: idx as u32 + 1,
1083 stmt_col: (line.chars().count() - trimmed.chars().count()) as u32,
1084 stmt_end: stmt_end as u32,
1085 name_col,
1086 name_len: short.chars().count() as u32,
1087 });
1088 }
1089 }
1090 None
1091 }
1092
1093 fn global_constant_decl_range(&self, fqn: &str) -> Option<(Arc<str>, crate::Range)> {
1094 let site = self.global_constant_decl_site(fqn)?;
1095 let range = span_range(site.line, site.name_col, site.name_col + site.name_len);
1096 Some((site.file, range))
1097 }
1098
1099 fn global_constant_location(&self, fqn: &str) -> Option<mir_types::Location> {
1101 let site = self.global_constant_decl_site(fqn)?;
1102 Some(mir_types::Location {
1103 file: site.file,
1104 line: site.line,
1105 line_end: site.line,
1106 col_start: site.stmt_col as u16,
1107 col_end: site.stmt_end as u16,
1108 })
1109 }
1110
1111 pub fn subtype_files(
1117 &self,
1118 class_fqn: &str,
1119 include_trait_users: bool,
1120 ) -> Result<Vec<Arc<str>>, Cancelled> {
1121 let files = self.db.source_file_paths();
1122 let mut out: Vec<Arc<str>> = self
1123 .indexed_subtype_classes(class_fqn, &files, include_trait_users)?
1124 .into_iter()
1125 .map(|s| s.file)
1126 .collect();
1127 out.sort();
1128 out.dedup();
1129 Ok(out)
1130 }
1131
1132 pub fn indexed_subtype_classes(
1151 &self,
1152 class_fqn: &str,
1153 files: &[Arc<str>],
1154 include_trait_users: bool,
1155 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1156 self.subtype_classes_memoized(class_fqn, files, include_trait_users, false)
1157 }
1158
1159 pub fn indexed_direct_subtype_classes(
1163 &self,
1164 class_fqn: &str,
1165 files: &[Arc<str>],
1166 include_trait_users: bool,
1167 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1168 self.subtype_classes_memoized(class_fqn, files, include_trait_users, true)
1169 }
1170
1171 fn subtype_classes_memoized(
1172 &self,
1173 class_fqn: &str,
1174 files: &[Arc<str>],
1175 include_trait_users: bool,
1176 direct_only: bool,
1177 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1178 catch(|| {
1179 let key = SubtypeQueryCacheKey {
1180 class_fqn: class_fqn.trim_start_matches('\\').to_ascii_lowercase(),
1181 include_trait_users,
1182 direct_only,
1183 generation: self.query_cache_generation(),
1184 files_hash: hash_files(files),
1185 };
1186 if let Some(hit) = self.index.subtype_queries.get(&key) {
1187 return hit;
1188 }
1189 let out =
1190 self.subtype_classes_uncached(class_fqn, files, include_trait_users, direct_only);
1191 if let Err(cancelled) = self.ensure_no_retire_since(key.generation) {
1192 unwind(cancelled);
1193 }
1194 let generation = self.query_cache_generation();
1196 if generation == key.generation {
1197 self.index.subtype_queries.insert(generation, key, &out);
1198 }
1199 out
1200 })
1201 }
1202
1203 fn subtype_classes_uncached(
1204 &self,
1205 class_fqn: &str,
1206 files: &[Arc<str>],
1207 include_trait_users: bool,
1208 direct_only: bool,
1209 ) -> Vec<SubtypeClassSite> {
1210 let db = &self.db;
1211 let mut scanned: HashSet<String> = HashSet::default();
1212 let mut pending: Vec<String> = vec![class_fqn.trim_start_matches('\\').to_string()];
1213 let mut sites: Vec<crate::db::SubtypeSite> = Vec::new();
1214 while !pending.is_empty() {
1215 let needles: Vec<String> = pending
1218 .drain(..)
1219 .filter(|f| scanned.insert(f.clone()))
1220 .map(|f| crate::db::subtype_index::short_name_of(&f).to_string())
1221 .collect();
1222 if !needles.is_empty() {
1223 self.commit_defs_for_matching(files, &needles);
1224 }
1225 sites = db.subtype_sites_of(class_fqn, include_trait_users, direct_only);
1226 if direct_only {
1227 break;
1228 }
1229 pending = sites
1230 .iter()
1231 .map(|s| s.fqcn.trim_start_matches('\\').to_string())
1232 .filter(|f| !scanned.contains(f))
1233 .collect();
1234 }
1235 let mut out: Vec<SubtypeClassSite> = sites
1236 .into_iter()
1237 .filter_map(|s| {
1238 let loc = s.location.as_ref()?;
1239 let short = crate::db::subtype_index::short_name_of(&s.fqcn).to_string();
1240 let range = self.refine_location_to_name(loc, &short);
1241 Some(SubtypeClassSite {
1242 fqcn: s.fqcn,
1243 kind: s.kind,
1244 is_abstract: s.is_abstract,
1245 file: s.file,
1246 range,
1247 })
1248 })
1249 .collect();
1250 let root_lc = class_fqn.trim_start_matches('\\').to_ascii_lowercase();
1254 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
1255 for (file, line, cs, ce) in db.reference_locations(&format!("impl:{root_lc}")) {
1256 if !scope.contains(file.as_ref()) {
1257 continue;
1258 }
1259 let range = span_range(line, cs as u32, ce as u32);
1260 if out.iter().any(|s| s.file == file && s.range == range) {
1261 continue;
1262 }
1263 out.push(SubtypeClassSite {
1264 fqcn: Arc::from("class@anonymous"),
1265 kind: crate::db::ClassLikeKind::Class,
1266 is_abstract: false,
1267 file,
1268 range,
1269 });
1270 }
1271 out
1272 }
1273
1274 #[allow(clippy::type_complexity)]
1280 pub fn indexed_method_implementations(
1281 &self,
1282 class_fqn: &str,
1283 method: &str,
1284 files: &[Arc<str>],
1285 ) -> Result<Vec<(Arc<str>, Arc<str>, crate::Range)>, Cancelled> {
1286 let subs = self.indexed_subtype_classes(class_fqn, files, false)?;
1287 catch(|| {
1288 let db = &self.db;
1289 let mut out: Vec<(Arc<str>, Arc<str>, crate::Range)> = Vec::new();
1290 for sub in &subs {
1291 let here = crate::db::Fqcn::from_str(db, sub.fqcn.as_ref());
1292 let Some((_, m)) = crate::db::find_method_in_chain(db, here, method) else {
1293 continue;
1294 };
1295 if m.is_abstract {
1296 continue;
1297 }
1298 let Some(loc) = m.location.as_ref() else {
1299 continue;
1300 };
1301 let range = self.refine_location_to_name(loc, method);
1302 out.push((sub.fqcn.clone(), loc.file.clone(), range));
1303 }
1304 out.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.start.line.cmp(&b.2.start.line)));
1305 out.dedup_by(|a, b| a.1 == b.1 && a.2 == b.2);
1306 out
1307 })
1308 }
1309
1310 fn commit_defs_for_matching(&self, files: &[Arc<str>], shorts: &[String]) {
1323 use rayon::prelude::*;
1324
1325 self.db
1329 .add_literal_mention_names(shorts.iter().map(|s| s.as_str()));
1330 let queries: Vec<_> = shorts
1331 .iter()
1332 .filter_map(|s| self.db.prepare_class_mention_query(s))
1333 .collect();
1334 let mention_scanner = self.db.class_mention_scanner();
1335 let use_mentions = queries.len() == shorts.len() && mention_scanner.is_some();
1339 type Work = (Arc<str>, Arc<str>, Vec<crate::db::SubtypeEntry>);
1340 let index = &*self.index;
1341 let results: Vec<(Option<Work>, Option<MentionScanRecord>)> = files
1342 .par_iter()
1343 .map_with(self.db.clone(), |db, path| {
1344 let Some(sf) = db.lookup_source_file(path.as_ref()) else {
1345 return (None, None);
1346 };
1347 let text = sf.text(&*db as &dyn MirDatabase).clone();
1348 let freshness = index.defs_freshness(sf, &text);
1349 if freshness == DefsFreshness::Fresh {
1350 return (None, None);
1351 }
1352 let mut scan_rec: Option<MentionScanRecord> = None;
1356 if use_mentions && freshness == DefsFreshness::Uncommitted {
1357 let mut answer = Some(false);
1358 for q in &queries {
1359 match db.class_mention_answer(path.as_ref(), q, &text) {
1360 Some(true) => {
1361 answer = Some(true);
1362 break;
1363 }
1364 Some(false) => {}
1365 None => answer = None,
1366 }
1367 }
1368 let hit = match answer {
1369 Some(hit) => hit,
1370 None => {
1371 let scanner = mention_scanner.as_ref().unwrap();
1374 let names = scanner.scan(&text);
1375 let hit = queries.iter().any(|q| names.binary_search(&q.name).is_ok());
1376 scan_rec = Some((path.clone(), text.clone(), names));
1377 hit
1378 }
1379 };
1380 if !hit {
1381 return (None, scan_rec);
1382 }
1383 }
1384 let defs = crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
1385 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
1386 (Some((path.clone(), text, entries)), scan_rec)
1387 })
1388 .collect();
1389 let mut refused = Ok(());
1393 for (work, scan_rec) in results {
1394 if let (Some(scanner), Some((file, text, names))) = (&mention_scanner, scan_rec) {
1395 self.db
1396 .set_file_class_mentions(&file, &text, scanner.epoch(), names);
1397 }
1398 if let Some((file, text, entries)) = work {
1399 if let Err(cancelled) = self.index.commit_class_edges(
1400 &self.db,
1401 &file,
1402 &text,
1403 entries,
1404 self.retire_epoch,
1405 ) {
1406 refused = Err(cancelled);
1407 }
1408 }
1409 }
1410 if let Err(cancelled) = refused {
1411 unwind(cancelled);
1412 }
1413 }
1414
1415 pub fn class_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1423 catch(|| {
1424 let db = &self.db;
1425 let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
1426 let file_data: Vec<(Arc<str>, Arc<str>)> = files
1427 .iter()
1428 .filter_map(|f| {
1429 let sf = db.lookup_source_file(f)?;
1430 Some((f.clone(), sf.text(db as &dyn MirDatabase).clone()))
1431 })
1432 .collect();
1433 crate::class::ClassAnalyzer::with_files(db, file_set, &file_data).analyze_all()
1434 })
1435 }
1436
1437 pub fn collector_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1446 catch(|| {
1447 files
1448 .iter()
1449 .filter_map(|f| self.db.lookup_source_file(f))
1450 .flat_map(|sf| {
1451 crate::db::collect_file_definitions(&self.db, sf)
1452 .issues
1453 .as_ref()
1454 .clone()
1455 })
1456 .collect()
1457 })
1458 }
1459
1460 pub(super) fn reference_gate(&self, symbol: &crate::Name) -> ReferenceGate {
1471 ReferenceGate::for_symbol(&self.db, symbol)
1472 }
1473}
1474
1475#[cfg(test)]
1476mod tests {
1477 use super::*;
1478 use crate::{AnalysisSession, IndexCancel, Name, ReferenceIncludes};
1479 use salsa::Database as _;
1480
1481 const BASE: &str =
1482 "<?php\nclass Base { public function run(): void {} public function stop(): void {} }\n";
1483 const CALLS_RUN: &str = "<?php\nfunction go(Base $b): void { $b->run(); }\n";
1484 const CALLS_STOP: &str = "<?php\nfunction go(Base $b): void { $b->stop(); }\n";
1485
1486 #[test]
1490 fn warm_commit_racing_a_text_write_leaves_the_file_stale() {
1491 let (mut session, files) = prepared_workspace(CALLS_RUN);
1492 let snap = session.snapshot();
1493 let mut pass = snap
1494 .stage_warm(&files[1..], &IndexCancel::new(), false)
1495 .unwrap()
1496 .unwrap();
1497
1498 let caller = files[1].clone();
1499 let writer = std::thread::spawn(move || {
1500 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1501 session
1502 });
1503 await_pending_write(&snap);
1504 snap.commit_warm(&mut pass).unwrap();
1505 drop(snap);
1506 let mut session = writer.join().unwrap();
1507
1508 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1509 assert!(callers_of(&mut session, &files, "run").is_empty());
1510 }
1511
1512 fn many_callers_workspace(count: usize) -> (AnalysisSession, Vec<Arc<str>>) {
1513 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1514 session.ingest_file(Arc::from("base.php"), Arc::from(BASE));
1515 let callers: Vec<Arc<str>> = (0..count)
1516 .map(|i| Arc::from(format!("caller{i}.php")))
1517 .collect();
1518 for caller in &callers {
1519 session.ingest_file(caller.clone(), Arc::from(CALLS_RUN));
1520 }
1521 session.prepare_for_query(None);
1522 (session, callers)
1523 }
1524
1525 #[test]
1526 fn parallel_warm_keeps_file_order_and_commits_every_file() {
1527 let (mut session, callers) = many_callers_workspace(24);
1528 let snap = session.snapshot();
1529 let analyzed = snap
1530 .reanalyze_files(&callers, &IndexCancel::new())
1531 .unwrap()
1532 .unwrap();
1533 drop(snap);
1534
1535 let order: Vec<_> = analyzed.into_iter().map(|(file, _)| file).collect();
1536 assert_eq!(order, callers);
1537 let mut found = callers_of(&mut session, &callers, "run");
1538 found.sort();
1539 let mut expected = callers.clone();
1540 expected.sort();
1541 assert_eq!(found, expected);
1542 }
1543
1544 #[test]
1545 fn worker_panic_stops_the_pass_and_reaches_the_caller() {
1546 use std::sync::atomic::{AtomicUsize, Ordering};
1547
1548 let files: Vec<Arc<str>> = (0..4000).map(|i| Arc::from(format!("f{i}.php"))).collect();
1549 let started = AtomicUsize::new(0);
1550 let db = MirDbStorage::default();
1551 let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| {
1552 stage_in_parallel(&db, &files, &IndexCancel::new(), true, |_, _| {
1553 if started.fetch_add(1, Ordering::Relaxed) == 2 {
1554 std::panic::resume_unwind(Box::new("worker boom"));
1555 }
1556 std::thread::sleep(std::time::Duration::from_micros(200));
1557 None
1558 })
1559 }));
1560
1561 let Err(payload) = outcome else {
1562 panic!("panic must reach the caller");
1563 };
1564 assert_eq!(payload.downcast_ref::<&str>(), Some(&"worker boom"));
1565 assert!(started.load(Ordering::Relaxed) < files.len());
1566 }
1567
1568 #[test]
1569 fn serial_mode_never_spawns_workers() {
1570 let files: Vec<Arc<str>> = (0..16).map(|i| Arc::from(format!("f{i}.php"))).collect();
1571 let caller = std::thread::current().id();
1572 let db = MirDbStorage::default();
1573 let staged = stage_in_parallel(&db, &files, &IndexCancel::new(), false, |_, _| {
1574 assert_eq!(std::thread::current().id(), caller);
1575 None
1576 });
1577 assert!(staged.is_some());
1578 }
1579
1580 #[test]
1581 fn parallel_warm_stops_when_cancelled() {
1582 let (session, callers) = many_callers_workspace(24);
1583 let cancel = IndexCancel::new();
1584 cancel.cancel();
1585 let snap = session.snapshot();
1586 assert!(!snap.warm_files(&callers, &cancel).unwrap());
1587 }
1588
1589 #[test]
1590 fn warm_commit_after_an_ingest_retired_the_file_is_refused() {
1591 let (session, files) = prepared_workspace(CALLS_RUN);
1592 let snap = session.snapshot();
1593 let mut pass = snap
1594 .stage_warm(&files[1..], &IndexCancel::new(), false)
1595 .unwrap()
1596 .unwrap();
1597
1598 let caller = files[1].clone();
1599 let writer = std::thread::spawn(move || {
1600 let mut session = session;
1601 session.ingest_file(caller, Arc::from(CALLS_STOP));
1602 session
1603 });
1604 await_pending_write(&snap);
1605 assert!(snap.commit_warm(&mut pass).is_err());
1606 drop(snap);
1607 let mut session = writer.join().unwrap();
1608
1609 assert!(callers_of(&mut session, &files, "run").is_empty());
1610 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1611 }
1612
1613 #[test]
1614 fn commits_after_invalidate_file_retired_the_file_are_refused() {
1615 const CHILD_CALLS_RUN: &str =
1616 "<?php\nclass Child extends Base {}\nfunction go(Base $b): void { $b->run(); }\n";
1617 let (session, files) = prepared_workspace(CHILD_CALLS_RUN);
1618 let snap = session.snapshot();
1619 let mut pass = snap
1620 .stage_warm(&files[1..], &IndexCancel::new(), false)
1621 .unwrap()
1622 .unwrap();
1623 let parsed = php_rs_parser::parse(CHILD_CALLS_RUN);
1624
1625 let caller = files[1].clone();
1626 let writer = std::thread::spawn(move || {
1627 let mut session = session;
1628 session.invalidate_file(&caller);
1629 session
1630 });
1631 await_pending_write(&snap);
1632 assert!(snap.commit_warm(&mut pass).is_err());
1633 assert!(snap
1634 .analyze(
1635 files[1].clone(),
1636 CHILD_CALLS_RUN,
1637 &parsed.program,
1638 &parsed.source_map
1639 )
1640 .is_err());
1641 drop(snap);
1642 let mut session = writer.join().unwrap();
1643
1644 assert!(callers_of(&mut session, &files, "run").is_empty());
1645 assert!(session
1646 .indexed_subtype_classes("Base", &files, false)
1647 .is_empty());
1648 }
1649
1650 #[test]
1651 fn cancelled_reference_commit_reports_cancellation() {
1652 let (mut session, files) = prepared_workspace(CALLS_RUN);
1653 let snap = session.snapshot();
1654 let stale = snap.stale_reference_candidates_unguarded(&Name::method("Base", "run"), &files);
1655 assert!(stale.contains(&files[1]));
1656
1657 let caller = files[1].clone();
1658 let writer = std::thread::spawn(move || {
1659 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1660 session
1661 });
1662 await_pending_write(&snap);
1663 assert!(snap.commit_reference_candidates(&stale).is_err());
1664 drop(snap);
1665 writer.join().unwrap();
1666 }
1667
1668 const ROUTES_CALLS_RUN: &str = "<?php\n(new Base())->run();\n";
1671
1672 fn registered_routes_workspace() -> (AnalysisSession, Vec<Arc<str>>) {
1676 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1677 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("routes.php")];
1678 session.ingest_file(files[0].clone(), Arc::from(BASE));
1679 session.upsert_source_file(
1680 files[1].clone(),
1681 Arc::from(ROUTES_CALLS_RUN),
1682 salsa::Durability::LOW,
1683 );
1684 session.prepare_for_query(None);
1685 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1686 (session, files)
1687 }
1688
1689 #[test]
1693 fn ingest_of_unchanged_text_mid_query_keeps_references() {
1694 let (mut session, files) = registered_routes_workspace();
1695 let symbol = Name::method("Base", "run");
1696 let revision = session.text_revision();
1697
1698 let snap = session.snapshot();
1699 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1700 assert!(snap
1701 .stale_reference_candidates_unguarded(&symbol, &files)
1702 .is_empty());
1703 session.ingest_file(files[1].clone(), Arc::from(ROUTES_CALLS_RUN));
1704 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1705 assert!(snap.ensure_no_retire_since(key.generation).is_ok());
1706 snap.memoize_references(key, &out);
1707 drop(snap);
1708
1709 assert_eq!(session.text_revision(), revision, "ingest wrote salsa");
1710 assert_eq!(
1711 out.into_iter().map(|(file, _)| file).collect::<Vec<_>>(),
1712 [files[1].clone()]
1713 );
1714 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1715 }
1716
1717 #[test]
1721 fn query_spanning_a_retirement_is_cancelled_and_not_memoized() {
1722 let (mut session, files) = registered_routes_workspace();
1723 let symbol = Name::method("Base", "run");
1724
1725 let snap = session.snapshot();
1726 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1727 assert!(snap
1728 .stale_reference_candidates_unguarded(&symbol, &files)
1729 .is_empty());
1730 session
1731 .index
1732 .retire_references(&session.db.salsa, files[1].as_ref());
1733 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1734 assert!(out.is_empty());
1735 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1736 snap.memoize_references(key, &out);
1737 drop(snap);
1738
1739 let hits = session.ref_query_cache_hits();
1740 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1741 assert_eq!(session.ref_query_cache_hits(), hits);
1742 }
1743
1744 #[test]
1747 fn re_analyze_file_cache_hit_cancels_spanning_queries() {
1748 let dir = tempfile::tempdir().unwrap();
1749 let mut session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(dir.path());
1750 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1751 let opts = crate::BatchOptions::new();
1752 session.ingest_file(files[0].clone(), Arc::from(BASE));
1753 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1754 session.prepare_for_query(None);
1755 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1756
1757 let symbol = Name::method("Base", "run");
1758 let snap = session.snapshot();
1759 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1760 let revision = session.text_revision();
1761 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1762 assert_eq!(session.text_revision(), revision, "took the analysis path");
1763 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1764 drop(snap);
1765
1766 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1767 }
1768
1769 fn prepared_workspace(caller_text: &str) -> (AnalysisSession, Vec<Arc<str>>) {
1770 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1771 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1772 session.ingest_file(files[0].clone(), Arc::from(BASE));
1773 session.ingest_file(files[1].clone(), Arc::from(caller_text));
1774 session.prepare_for_query(Some(&files[1]));
1775 (session, files)
1776 }
1777
1778 fn await_pending_write(snap: &AnalysisSnapshot) {
1781 while catch(|| snap.db.unwind_if_revision_cancelled()).is_ok() {
1782 std::thread::yield_now();
1783 }
1784 }
1785
1786 fn callers_of(
1787 session: &mut AnalysisSession,
1788 files: &[Arc<str>],
1789 method: &str,
1790 ) -> Vec<Arc<str>> {
1791 session
1792 .indexed_references_to(
1793 &Name::method("Base", method),
1794 files,
1795 false,
1796 ReferenceIncludes::Plain,
1797 &|| false,
1798 )
1799 .unwrap()
1800 .into_iter()
1801 .map(|(file, _)| file)
1802 .collect()
1803 }
1804
1805 #[derive(Debug, PartialEq)]
1808 struct AnalysisOutcome {
1809 issues: Vec<String>,
1810 symbols: usize,
1811 vendor_class_loaded: bool,
1812 vendor_function_loaded: bool,
1813 eager_files_drained: bool,
1814 stubs_loaded: usize,
1815 prepared: bool,
1816 prepare_generation: u64,
1817 references: Vec<String>,
1818 }
1819
1820 const DEPENDENT: &str = "<?php\nnamespace App;\nfunction go(\\Vendor\\Base $b): void { vendor_helper('x'); new \\ArrayObject(); }\n";
1821
1822 fn composer_project(root: &std::path::Path) -> AnalysisSession {
1823 let vendor = root.join("vendor");
1824 std::fs::create_dir_all(vendor.join("composer")).unwrap();
1825 std::fs::create_dir_all(vendor.join("helpers")).unwrap();
1826 std::fs::create_dir_all(root.join("lib")).unwrap();
1827 std::fs::write(
1828 vendor.join("composer/autoload_files.php"),
1829 "<?php\n$vendorDir = dirname(__DIR__);\n$baseDir = dirname($vendorDir);\nreturn array(\n 'abc123' => $vendorDir . '/helpers/functions.php',\n);\n",
1830 )
1831 .unwrap();
1832 for empty in ["psr4", "classmap", "namespaces"] {
1833 std::fs::write(
1834 vendor.join(format!("composer/autoload_{empty}.php")),
1835 "<?php\nreturn [];\n",
1836 )
1837 .unwrap();
1838 }
1839 std::fs::write(
1840 vendor.join("helpers/functions.php"),
1841 "<?php\nfunction vendor_helper(string $s): string { return $s; }\n",
1842 )
1843 .unwrap();
1844 std::fs::write(
1845 root.join("lib/Base.php"),
1846 "<?php\nnamespace Vendor;\nclass Base {}\n",
1847 )
1848 .unwrap();
1849 std::fs::write(
1850 root.join("composer.json"),
1851 r#"{"autoload":{"psr-4":{"App\\":"src/","Vendor\\":"lib/"}}}"#,
1852 )
1853 .unwrap();
1854 let psr4 = crate::composer::Psr4Map::from_composer(root).unwrap();
1855 AnalysisSession::new(PhpVersion::LATEST).with_psr4(Arc::new(psr4))
1856 }
1857
1858 fn outcome(
1859 session: &AnalysisSession,
1860 file: &Arc<str>,
1861 analysis: crate::FileAnalysis,
1862 ) -> AnalysisOutcome {
1863 let text = session.source_of(file).unwrap();
1864 AnalysisOutcome {
1865 issues: analysis
1866 .issues
1867 .iter()
1868 .map(|i| format!("{:?}", i.kind))
1869 .collect(),
1870 symbols: analysis.symbols.len(),
1871 vendor_class_loaded: {
1872 let view = session.db_view();
1873 let db = view.db();
1874 crate::db::class_like_indexed(db, crate::db::Fqcn::from_str(db, "Vendor\\Base"))
1875 },
1876 vendor_function_loaded: session.contains_function("vendor_helper"),
1877 eager_files_drained: session.pending_eager_function_files.is_none(),
1878 stubs_loaded: session.loaded_stub_count(),
1879 prepared: session.is_prepared_for_analysis(
1880 file,
1881 &text,
1882 session.prepare_generation_snapshot(),
1883 ),
1884 prepare_generation: session.prepare_generation_snapshot(),
1885 references: ["cls:Vendor\\Base", "fn:vendor_helper", "cls:ArrayObject"]
1886 .into_iter()
1887 .map(|key| format!("{key}={:?}", session.reference_locations(key)))
1888 .collect(),
1889 }
1890 }
1891
1892 #[test]
1896 fn prepare_for_query_then_snapshot_analyze_matches_file_analyzer() {
1897 let file: Arc<str> = Arc::from("consumer.php");
1898 let parsed = php_rs_parser::parse(DEPENDENT);
1899
1900 let root = tempfile::tempdir().unwrap();
1901 let mut via_analyzer = composer_project(root.path());
1902 via_analyzer.ingest_file(file.clone(), Arc::from(DEPENDENT));
1903 let analysis = crate::FileAnalyzer::new(&mut via_analyzer).analyze(
1904 file.clone(),
1905 DEPENDENT,
1906 &parsed.program,
1907 &parsed.source_map,
1908 );
1909 let expected = outcome(&via_analyzer, &file, analysis);
1910
1911 let root = tempfile::tempdir().unwrap();
1912 let mut via_snapshot = composer_project(root.path());
1913 via_snapshot.ingest_file(file.clone(), Arc::from(DEPENDENT));
1914 via_snapshot.prepare_for_query(Some(&file));
1915 let analysis = via_snapshot
1916 .snapshot()
1917 .analyze(file.clone(), DEPENDENT, &parsed.program, &parsed.source_map)
1918 .unwrap();
1919 let actual = outcome(&via_snapshot, &file, analysis);
1920
1921 assert!(
1922 expected.prepared && expected.vendor_function_loaded,
1923 "{expected:#?}"
1924 );
1925 assert_eq!(actual, expected);
1926 }
1927
1928 #[test]
1932 fn prepare_for_query_warms_nothing_for_a_file_that_was_never_ingested() {
1933 let file: Arc<str> = Arc::from("consumer.php");
1934 let parsed = php_rs_parser::parse(DEPENDENT);
1935 let unresolved = |analysis: &crate::FileAnalysis| {
1936 analysis
1937 .issues
1938 .iter()
1939 .any(|i| matches!(i.kind, crate::IssueKind::UndefinedFunction { .. }))
1940 };
1941
1942 let root = tempfile::tempdir().unwrap();
1943 let mut via_analyzer = composer_project(root.path());
1944 let analysis = crate::FileAnalyzer::new(&mut via_analyzer).analyze(
1945 file.clone(),
1946 DEPENDENT,
1947 &parsed.program,
1948 &parsed.source_map,
1949 );
1950 assert!(!unresolved(&analysis), "{:?}", analysis.issues);
1951
1952 let root = tempfile::tempdir().unwrap();
1953 let mut via_snapshot = composer_project(root.path());
1954 via_snapshot.prepare_for_query(Some(&file));
1955 let analysis = via_snapshot
1956 .snapshot()
1957 .analyze(file, DEPENDENT, &parsed.program, &parsed.source_map)
1958 .unwrap();
1959 assert!(unresolved(&analysis), "{:?}", analysis.issues);
1960 }
1961}