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, IndexState, QueryGeneration, RefQueryCacheKey, RetireEpoch,
9 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
68impl Drop for DbView<'_> {
71 fn drop(&mut self) {}
72}
73
74impl std::ops::Deref for DbView<'_> {
75 type Target = AnalysisSnapshot;
76 fn deref(&self) -> &AnalysisSnapshot {
77 &self.snapshot
78 }
79}
80
81struct WarmPass {
83 mention_scanner: Option<Arc<crate::db::class_mention_index::MentionScanner>>,
84 analyzed: Vec<AnalyzedFile>,
85}
86
87type MentionScanRecord = (Arc<str>, Arc<str>, Box<[mir_types::Name]>);
90
91pub(super) fn catch<T>(f: impl FnOnce() -> T) -> Result<T, Cancelled> {
92 Cancelled::catch(AssertUnwindSafe(f))
93}
94
95pub(super) fn unwind(cancelled: Cancelled) -> ! {
97 std::panic::resume_unwind(Box::new(cancelled))
98}
99
100impl AnalysisSnapshot {
101 #[doc(hidden)]
107 pub fn db(&self) -> &MirDbStorage {
108 &self.db
109 }
110
111 #[doc(hidden)]
116 pub fn read<R>(&self, f: impl FnOnce(&dyn MirDatabase) -> R) -> Result<R, Cancelled> {
117 catch(|| f(&self.db))
118 }
119
120 pub(crate) fn index(&self) -> &IndexState {
121 &self.index
122 }
123
124 pub(crate) fn stamp(&self) -> ViewStamp {
125 ViewStamp {
126 generation: self.index_generation,
127 retire_epoch: self.retire_epoch,
128 }
129 }
130
131 pub fn php_version(&self) -> PhpVersion {
132 self.php_version
133 }
134
135 pub fn index_generation(&self) -> u64 {
137 self.index_generation
138 }
139
140 pub fn text_revision(&self) -> salsa::Revision {
142 self.db.current_revision()
143 }
144
145 pub(crate) fn query_cache_generation(&self) -> QueryGeneration {
154 (
155 self.db.current_revision(),
156 self.db.subtype_edges_epoch(),
157 self.index.retire_seq(),
158 )
159 }
160
161 pub fn find_class_like(&self, fqcn: &str) -> Result<Option<crate::db::ClassLike>, Cancelled> {
162 self.read(|db| crate::db::find_class_like(db, crate::db::Fqcn::from_str(db, fqcn)))
163 }
164
165 pub fn find_function(&self, fqn: &str) -> Result<Option<Arc<crate::FunctionDef>>, Cancelled> {
166 self.read(|db| crate::db::find_function(db, crate::db::Fqcn::from_str(db, fqn)))
167 }
168
169 #[allow(clippy::type_complexity)]
172 pub fn find_method_in_chain(
173 &self,
174 class: &str,
175 method: &str,
176 ) -> Result<Option<(Arc<str>, Arc<mir_codebase::definitions::MethodDef>)>, Cancelled> {
177 self.read(|db| {
178 crate::db::find_method_in_chain(db, crate::db::Fqcn::from_str(db, class), method)
179 })
180 }
181
182 pub fn definition_of_cached(
185 &self,
186 symbol: &crate::Name,
187 ) -> Result<Result<mir_types::Location, crate::SymbolLookupError>, Cancelled> {
188 catch(|| self.definition_of_unguarded(symbol))
189 }
190
191 fn definition_of_unguarded(
192 &self,
193 symbol: &crate::Name,
194 ) -> Result<mir_types::Location, crate::SymbolLookupError> {
195 let db = &self.db;
196 match symbol {
197 crate::Name::Class(fqcn) => {
198 let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
199 let class = crate::db::find_class_like(db, here)
200 .ok_or(crate::SymbolLookupError::NotFound)?;
201 class
202 .location()
203 .cloned()
204 .ok_or(crate::SymbolLookupError::NoSourceLocation)
205 }
206 crate::Name::Function(fqn) => {
207 let here = crate::db::Fqcn::from_str(db, fqn.as_ref());
208 let f =
209 crate::db::find_function(db, here).ok_or(crate::SymbolLookupError::NotFound)?;
210 f.location
211 .clone()
212 .ok_or(crate::SymbolLookupError::NoSourceLocation)
213 }
214 crate::Name::Method { class, name }
215 | crate::Name::Property { class, name }
216 | crate::Name::ClassConstant { class, name } => {
217 crate::db::member_location(db, class, name)
218 .ok_or(crate::SymbolLookupError::NotFound)
219 }
220 crate::Name::GlobalConstant(_) => Err(crate::SymbolLookupError::NoSourceLocation),
221 }
222 }
223
224 pub fn files_mentioning_class(
233 &self,
234 files: &[Arc<str>],
235 class_name: &str,
236 ) -> Result<Vec<Arc<str>>, Cancelled> {
237 self.files_mentioning_any(files, &[class_name])
238 }
239
240 pub fn files_mentioning_any(
245 &self,
246 files: &[Arc<str>],
247 needles: &[&str],
248 ) -> Result<Vec<Arc<str>>, Cancelled> {
249 catch(|| self.mentioning_any_unguarded(files, needles))
250 }
251
252 fn mentioning_any_unguarded(&self, files: &[Arc<str>], needles: &[&str]) -> Vec<Arc<str>> {
253 use rayon::prelude::*;
254
255 if needles.is_empty() {
256 return files.to_vec();
257 }
258 let db = self.db.clone();
259 db.add_literal_mention_names(needles.iter().copied());
260 let queries: Vec<_> = needles
262 .iter()
263 .filter_map(|n| db.prepare_class_mention_query(n))
264 .collect();
265 if queries.is_empty() {
266 return files.to_vec();
267 }
268 let Some(scanner) = db.class_mention_scanner() else {
269 return files.to_vec();
270 };
271
272 files
273 .par_iter()
274 .map_with(db, |db, f| -> Option<Arc<str>> {
275 let sf = db.lookup_source_file(f.as_ref())?;
276 let text = sf.text(&*db as &dyn MirDatabase).clone();
277 for q in &queries {
278 match db.class_mention_answer(f.as_ref(), q, &text) {
279 Some(true) => return Some(f.clone()),
280 Some(false) => continue,
281 None => {
282 let names = scanner.scan(text.as_ref());
283 let hit = queries
284 .iter()
285 .any(|q2| names.binary_search(&q2.name).is_ok());
286 db.set_file_class_mentions(f, &text, scanner.epoch(), names);
287 return hit.then(|| f.clone());
288 }
289 }
290 }
291 None
292 })
293 .flatten()
294 .collect()
295 }
296
297 pub fn indexed_references_to(
329 &self,
330 symbol: &crate::Name,
331 files: &[Arc<str>],
332 include_declaration: bool,
333 includes: crate::ReferenceIncludes,
334 ) -> Result<Vec<(Arc<str>, crate::Range)>, Cancelled> {
335 let key = self.reference_query_key(symbol, files, include_declaration, includes);
336 if let Some(hit) = self.index.ref_queries.get(&key) {
337 return Ok(hit);
338 }
339 let stale = catch(|| self.stale_reference_candidates(symbol, files))?;
340 if !stale.is_empty() {
341 self.commit_reference_candidates(&stale)?;
342 }
343 let out = catch(|| self.read_references(symbol, files, include_declaration, includes))?;
344 self.ensure_no_retire_since(key.generation)?;
345 self.memoize_references(key, &out);
346 Ok(out)
347 }
348
349 pub(super) fn ensure_no_retire_since(
352 &self,
353 generation: QueryGeneration,
354 ) -> Result<(), Cancelled> {
355 if self.index.retire_seq() == generation.2 {
356 Ok(())
357 } else {
358 Err(Cancelled::PendingWrite)
359 }
360 }
361
362 pub fn indexed_use_import_locations(
366 &self,
367 symbol: &crate::Name,
368 files: &[Arc<str>],
369 ) -> Result<Vec<(Arc<str>, crate::Range)>, Cancelled> {
370 self.indexed_references_to(symbol, files, false, crate::ReferenceIncludes::UseImports)
371 }
372
373 pub(super) fn reference_query_key(
374 &self,
375 symbol: &crate::Name,
376 files: &[Arc<str>],
377 include_declaration: bool,
378 includes: crate::ReferenceIncludes,
379 ) -> RefQueryCacheKey {
380 RefQueryCacheKey {
381 symbol: symbol.codebase_key(),
382 include_declaration,
383 includes,
384 generation: self.query_cache_generation(),
385 files_hash: hash_files(files),
386 }
387 }
388
389 pub(super) fn memoize_references(
394 &self,
395 key: RefQueryCacheKey,
396 out: &[(Arc<str>, crate::Range)],
397 ) {
398 let generation = self.query_cache_generation();
399 if generation == key.generation {
400 self.index.ref_queries.insert(generation, key, out);
401 }
402 }
403
404 pub(super) fn stale_reference_candidates(
426 &self,
427 symbol: &crate::Name,
428 files: &[Arc<str>],
429 ) -> Vec<Arc<str>> {
430 let db = &self.db;
431 let gate = self.reference_gate(symbol);
432 let has_needles = !gate.idents.is_empty() || !gate.raw.is_empty();
433 let (mention_queries, mention_scanner) = if has_needles {
434 db.add_literal_mention_names(gate.idents.iter().map(|s| s.as_str()));
435 db.add_raw_mention_needles(gate.raw.iter().map(|s| s.as_str()));
436 let queries: Vec<_> = gate
437 .idents
438 .iter()
439 .chain(gate.raw.iter())
440 .filter_map(|s| db.prepare_class_mention_query(s))
441 .collect();
442 (queries, db.class_mention_scanner())
443 } else {
444 (Vec::new(), None)
445 };
446 let gate_complete = mention_queries.len() == gate.idents.len() + gate.raw.len()
450 && mention_scanner.is_some();
451 let committed_any: HashSet<Arc<str>> =
452 self.index.ref_committed_keys().into_iter().collect();
453 let current_gen = self.index_generation;
454 let mut stale = Vec::new();
455 let mut scanned: Vec<MentionScanRecord> = Vec::new();
456 for f in files {
457 let Some(sf) = db.lookup_source_file(f.as_ref()) else {
458 continue;
459 };
460 let text = sf.text(db as &dyn MirDatabase);
461 if self.index.is_ref_committed(f.as_ref(), text, current_gen) {
462 continue;
463 }
464 if committed_any.contains(f.as_ref())
465 && !self
466 .index
467 .ref_commit_stale_by_generation_only(f.as_ref(), text)
468 {
469 stale.push(f.clone());
470 continue;
471 }
472 if has_needles && gate_complete {
473 let mut answer = Some(false);
477 for q in &mention_queries {
478 match db.class_mention_answer(f.as_ref(), q, text) {
479 Some(true) => {
480 answer = Some(true);
481 break;
482 }
483 Some(false) => {}
484 None => answer = None,
485 }
486 }
487 match answer {
488 Some(true) => {}
489 Some(false) => continue,
490 None => {
491 let scanner = mention_scanner
492 .as_ref()
493 .expect("gate_complete implies a scanner");
494 let names = scanner.scan(text);
495 let hit = mention_queries
496 .iter()
497 .any(|q| names.binary_search(&q.name).is_ok());
498 scanned.push((f.clone(), text.clone(), names));
499 if hit {
500 stale.push(f.clone());
501 }
502 continue;
503 }
504 }
505 }
506 stale.push(f.clone());
507 }
508 if let Some(scanner) = &mention_scanner {
511 for (file, text, names) in scanned {
512 db.set_file_class_mentions(&file, &text, scanner.epoch(), names);
513 }
514 }
515 stale
516 }
517
518 pub(super) fn commit_reference_candidates(&self, stale: &[Arc<str>]) -> Result<(), Cancelled> {
520 self.warm_pass(stale, &crate::IndexCancel::new()).map(drop)
521 }
522
523 pub fn warm_files(
530 &self,
531 files: &[Arc<str>],
532 cancel: &crate::IndexCancel,
533 ) -> Result<bool, Cancelled> {
534 Ok(self.warm_pass(files, cancel)?.is_some())
535 }
536
537 pub(super) fn warm_pass(
538 &self,
539 files: &[Arc<str>],
540 cancel: &crate::IndexCancel,
541 ) -> Result<Option<Vec<AnalyzedFile>>, Cancelled> {
542 let Some(mut pass) = self.stage_warm(files, cancel)? else {
543 return Ok(None);
544 };
545 self.commit_warm(&mut pass)?;
546 Ok(Some(pass.analyzed))
547 }
548
549 fn stage_warm(
554 &self,
555 files: &[Arc<str>],
556 cancel: &crate::IndexCancel,
557 ) -> Result<Option<WarmPass>, Cancelled> {
558 catch(|| {
559 let mut db = self.db.clone();
560 db.freeze_workspace_index();
561 let mention_scanner = db.class_mention_scanner();
562 let cache = self.cache.as_deref();
563 let mut analyzed = Vec::with_capacity(files.len());
564 for path in files {
565 if cancel.is_cancelled() {
566 return None;
567 }
568 analyzed.extend(self.index.stage_analyzed(
569 &db,
570 cache,
571 mention_scanner.as_deref(),
572 path,
573 ));
574 }
575 Some(WarmPass {
576 mention_scanner,
577 analyzed,
578 })
579 })
580 }
581
582 fn commit_warm(&self, pass: &mut WarmPass) -> Result<(), Cancelled> {
585 self.index.commit_analyzed(
586 &self.db,
587 self.cache.as_deref(),
588 pass.mention_scanner.as_deref(),
589 &mut pass.analyzed,
590 self.stamp(),
591 )
592 }
593
594 pub(super) fn read_references(
607 &self,
608 symbol: &crate::Name,
609 files: &[Arc<str>],
610 include_declaration: bool,
611 includes: crate::ReferenceIncludes,
612 ) -> Vec<(Arc<str>, crate::Range)> {
613 let key = symbol.codebase_key();
614 let hierarchy: Vec<String> = match symbol {
615 crate::Name::Method { class, name } => {
616 if class.is_empty() {
617 Vec::new()
618 } else if name.as_ref() == "__construct" {
619 vec![class.trim_start_matches('\\').to_string()]
620 } else {
621 self.member_hierarchy_classes(class.as_ref())
622 }
623 }
624 crate::Name::Property { class, .. } | crate::Name::ClassConstant { class, .. } => {
625 if class.is_empty() {
626 Vec::new()
627 } else {
628 self.member_hierarchy_classes(class.as_ref())
629 }
630 }
631 _ => Vec::new(),
632 };
633 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
634 let read_symbol_key = |symbol_key: &str| -> Vec<(Arc<str>, crate::Range)> {
635 self.db
636 .reference_locations(symbol_key)
637 .into_iter()
638 .filter(|(file, ..)| scope.contains(file.as_ref()))
639 .map(|(file, line, col_start, col_end)| {
640 (file, span_range(line, col_start as u32, col_end as u32))
641 })
642 .collect()
643 };
644 let mut scratch_key = String::new();
645 let mut read_composed_key = |prefix: &str, middle: &str, separator: &str, suffix: &str| {
646 scratch_key.clear();
647 scratch_key.reserve(prefix.len() + middle.len() + separator.len() + suffix.len());
648 scratch_key.push_str(prefix);
649 scratch_key.push_str(middle);
650 scratch_key.push_str(separator);
651 scratch_key.push_str(suffix);
652 read_symbol_key(&scratch_key)
653 };
654 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
655 if !matches!(includes, crate::ReferenceIncludes::UseImports) {
656 let member_prefix = match symbol {
657 crate::Name::Method { name, .. } => Some(("meth:", name)),
658 crate::Name::Property { name, .. } => Some(("prop:", name)),
659 crate::Name::ClassConstant { name, .. } => Some(("cnst:", name)),
660 _ => None,
661 };
662 match member_prefix {
663 Some((prefix, name)) => {
664 for class in &hierarchy {
665 out.extend(read_composed_key(prefix, class, "::", name));
666 }
667 }
668 None => out.extend(read_symbol_key(key.as_str())),
669 }
670 }
671 if !matches!(includes, crate::ReferenceIncludes::Plain) {
672 out.extend(read_composed_key("use:", key.as_str(), "", ""));
673 }
674 if matches!(includes, crate::ReferenceIncludes::Plain) && out.is_empty() {
675 match symbol {
676 crate::Name::Method { name, .. } => {
677 out = read_composed_key("methname:", name.as_ref(), "", "");
678 }
679 crate::Name::Property { name, .. } => {
680 out = read_composed_key("propname:", name.as_ref(), "", "");
681 }
682 _ => {}
683 }
684 }
685 out.sort_by(|a, b| {
686 a.0.cmp(&b.0)
687 .then(a.1.start.line.cmp(&b.1.start.line))
688 .then(a.1.start.column.cmp(&b.1.start.column))
689 });
690 out.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
691
692 if include_declaration {
693 let decls: Vec<(Arc<str>, crate::Range)> = match symbol {
694 crate::Name::Method { class, name }
695 | crate::Name::Property { class, name }
696 | crate::Name::ClassConstant { class, name } => {
697 if class.is_empty() {
698 let prefix = match symbol {
702 crate::Name::Method { .. } => "methdecl:",
703 crate::Name::Property { .. } => "propdecl:",
704 _ => "cnstdecl:",
705 };
706 read_composed_key(prefix, name.as_ref(), "", "")
707 } else {
708 self.member_decl_sites(&hierarchy, symbol)
709 }
710 }
711 _ => self.declaration_name_range(symbol).into_iter().collect(),
712 };
713 for (file, range) in decls {
714 if scope.contains(file.as_ref())
715 && !out.iter().any(|(f, r)| *f == file && *r == range)
716 {
717 out.push((file, range));
718 }
719 }
720 }
721 out
722 }
723
724 fn member_hierarchy_classes(&self, class_fqn: &str) -> Vec<String> {
730 let target = class_fqn.trim_start_matches('\\');
731 let mut out = vec![target.to_string()];
732 let db = &self.db;
733 let here = crate::db::Fqcn::from_str(db, target);
734 let ancestors = crate::db::class_ancestors_by_fqcn(db, here)
736 .iter()
737 .skip(1)
738 .map(|a| a.trim_start_matches('\\').to_string())
739 .collect::<Vec<_>>();
740 out.extend(ancestors);
741 out.extend(
742 db.subtype_sites_of(target, true)
743 .into_iter()
744 .map(|s| s.fqcn.trim_start_matches('\\').to_string()),
745 );
746 let mut seen: HashSet<String> = HashSet::default();
747 out.retain(|c| seen.insert(c.to_ascii_lowercase()));
748 out
749 }
750
751 fn member_decl_sites(
757 &self,
758 classes: &[String],
759 symbol: &crate::Name,
760 ) -> Vec<(Arc<str>, crate::Range)> {
761 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
762 let db = &self.db;
763 for class in classes {
764 let here = crate::db::Fqcn::from_str(db, class);
765 let (loc, needle) = match symbol {
766 crate::Name::Method { name, .. } => {
767 let Some(m) = crate::db::find_method_in_class(db, here, name) else {
768 continue;
769 };
770 (m.location.clone(), name.to_string())
771 }
772 crate::Name::Property { name, .. } => {
773 let Some(p) = crate::db::find_property_in_class(db, here, name) else {
774 continue;
775 };
776 (p.location.clone(), name.to_string())
777 }
778 crate::Name::ClassConstant { name, .. } => {
779 let Some(c) = crate::db::find_class_constant_in_class(db, here, name) else {
780 continue;
781 };
782 (c.location.clone(), name.to_string())
783 }
784 _ => continue,
785 };
786 let Some(loc) = loc else { continue };
787 let range = self.refine_location_to_name(&loc, &needle);
788 out.push((loc.file.clone(), range));
789 }
790 out
791 }
792
793 pub(super) fn declaration_name_range(
797 &self,
798 symbol: &crate::Name,
799 ) -> Option<(Arc<str>, crate::Range)> {
800 if let crate::Name::GlobalConstant(fqn) = symbol {
801 return self.global_constant_decl_range(fqn);
802 }
803 let loc = self.definition_of_unguarded(symbol).ok()?;
804 let short = match symbol {
805 crate::Name::Class(f) | crate::Name::Function(f) | crate::Name::GlobalConstant(f) => {
806 crate::db::subtype_index::short_name_of(f)
807 }
808 crate::Name::Method { name, .. }
809 | crate::Name::Property { name, .. }
810 | crate::Name::ClassConstant { name, .. } => name.as_ref(),
811 };
812 let range = self.refine_location_to_name(&loc, short);
816 Some((loc.file.clone(), range))
817 }
818
819 fn refine_location_to_name(&self, loc: &mir_types::Location, needle: &str) -> crate::Range {
824 let fallback = span_range(loc.line, loc.col_start as u32, loc.col_end as u32);
825 let Some(text) = self
826 .db
827 .lookup_source_file(loc.file.as_ref())
828 .map(|sf| sf.text(&self.db as &dyn MirDatabase).clone())
829 else {
830 return fallback;
831 };
832 let needle_chars = needle.chars().count() as u32;
833 let first_line = loc.line.saturating_sub(1) as usize;
834 for case_insensitive in [false, true] {
839 for (idx, line_text) in text.lines().enumerate().skip(first_line) {
840 let line_no = idx as u32 + 1;
841 if line_no > loc.line_end {
842 break;
843 }
844 let min_col = if line_no == loc.line {
845 loc.col_start as usize
846 } else {
847 0
848 };
849 if let Some(col) = identifier_char_col(line_text, needle, min_col, case_insensitive)
850 {
851 return span_range(line_no, col, col + needle_chars);
852 }
853 }
854 }
855 fallback
856 }
857
858 fn global_constant_decl_range(&self, fqn: &str) -> Option<(Arc<str>, crate::Range)> {
863 let short = crate::db::subtype_index::short_name_of(fqn).to_string();
864 let db = &self.db;
865 let index = crate::db::workspace_index(db);
866 let loc = index.constant_loc(mir_types::Name::from(fqn.trim_start_matches('\\')))?;
867 let file = loc.file().path(db).clone();
868 let sf = db.lookup_source_file(file.as_ref())?;
869 let text = sf.text(db as &dyn MirDatabase);
870 for (idx, line) in text.lines().enumerate() {
871 let trimmed = line.trim_start();
872 let is_decl_line = trimmed.starts_with("const ")
873 || trimmed.contains("define(")
874 || trimmed.contains("define (");
875 if !is_decl_line {
876 continue;
877 }
878 if let Some(col) = identifier_char_col(line, &short, 0, false) {
879 let n = short.chars().count() as u32;
880 return Some((file, span_range(idx as u32 + 1, col, col + n)));
881 }
882 }
883 None
884 }
885
886 pub fn subtype_files(&self, class_fqn: &str) -> Result<Vec<Arc<str>>, Cancelled> {
891 let files = self.db.source_file_paths();
892 let mut out: Vec<Arc<str>> = self
893 .indexed_subtype_classes(class_fqn, &files, false)?
894 .into_iter()
895 .map(|s| s.file)
896 .collect();
897 out.sort();
898 out.dedup();
899 Ok(out)
900 }
901
902 pub fn indexed_subtype_classes(
921 &self,
922 class_fqn: &str,
923 files: &[Arc<str>],
924 include_trait_users: bool,
925 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
926 catch(|| {
927 let key = SubtypeQueryCacheKey {
928 class_fqn: class_fqn.trim_start_matches('\\').to_ascii_lowercase(),
929 include_trait_users,
930 generation: self.query_cache_generation(),
931 files_hash: hash_files(files),
932 };
933 if let Some(hit) = self.index.subtype_queries.get(&key) {
934 return hit;
935 }
936 let out = self.subtype_classes_uncached(class_fqn, files, include_trait_users);
937 if let Err(cancelled) = self.ensure_no_retire_since(key.generation) {
938 unwind(cancelled);
939 }
940 let generation = self.query_cache_generation();
942 if generation == key.generation {
943 self.index.subtype_queries.insert(generation, key, &out);
944 }
945 out
946 })
947 }
948
949 fn subtype_classes_uncached(
950 &self,
951 class_fqn: &str,
952 files: &[Arc<str>],
953 include_trait_users: bool,
954 ) -> Vec<SubtypeClassSite> {
955 let db = &self.db;
956 let mut scanned: HashSet<String> = HashSet::default();
957 let mut pending: Vec<String> = vec![class_fqn.trim_start_matches('\\').to_string()];
958 let mut sites: Vec<crate::db::SubtypeSite> = Vec::new();
959 while !pending.is_empty() {
960 let needles: Vec<String> = pending
963 .drain(..)
964 .filter(|f| scanned.insert(f.clone()))
965 .map(|f| crate::db::subtype_index::short_name_of(&f).to_string())
966 .collect();
967 if !needles.is_empty() {
968 self.commit_defs_for_matching(files, &needles);
969 }
970 sites = db.subtype_sites_of(class_fqn, include_trait_users);
971 pending = sites
972 .iter()
973 .map(|s| s.fqcn.trim_start_matches('\\').to_string())
974 .filter(|f| !scanned.contains(f))
975 .collect();
976 }
977 let mut out: Vec<SubtypeClassSite> = sites
978 .into_iter()
979 .filter_map(|s| {
980 let loc = s.location.as_ref()?;
981 let short = crate::db::subtype_index::short_name_of(&s.fqcn).to_string();
982 let range = self.refine_location_to_name(loc, &short);
983 Some(SubtypeClassSite {
984 fqcn: s.fqcn,
985 kind: s.kind,
986 is_abstract: s.is_abstract,
987 file: s.file,
988 range,
989 })
990 })
991 .collect();
992 let root_lc = class_fqn.trim_start_matches('\\').to_ascii_lowercase();
996 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
997 for (file, line, cs, ce) in db.reference_locations(&format!("impl:{root_lc}")) {
998 if !scope.contains(file.as_ref()) {
999 continue;
1000 }
1001 let range = span_range(line, cs as u32, ce as u32);
1002 if out.iter().any(|s| s.file == file && s.range == range) {
1003 continue;
1004 }
1005 out.push(SubtypeClassSite {
1006 fqcn: Arc::from("class@anonymous"),
1007 kind: crate::db::ClassLikeKind::Class,
1008 is_abstract: false,
1009 file,
1010 range,
1011 });
1012 }
1013 out
1014 }
1015
1016 #[allow(clippy::type_complexity)]
1022 pub fn indexed_method_implementations(
1023 &self,
1024 class_fqn: &str,
1025 method: &str,
1026 files: &[Arc<str>],
1027 ) -> Result<Vec<(Arc<str>, Arc<str>, crate::Range)>, Cancelled> {
1028 let subs = self.indexed_subtype_classes(class_fqn, files, false)?;
1029 catch(|| {
1030 let db = &self.db;
1031 let mut out: Vec<(Arc<str>, Arc<str>, crate::Range)> = Vec::new();
1032 for sub in &subs {
1033 let here = crate::db::Fqcn::from_str(db, sub.fqcn.as_ref());
1034 let Some((_, m)) = crate::db::find_method_in_chain(db, here, method) else {
1035 continue;
1036 };
1037 if m.is_abstract {
1038 continue;
1039 }
1040 let Some(loc) = m.location.as_ref() else {
1041 continue;
1042 };
1043 let range = self.refine_location_to_name(loc, method);
1044 out.push((sub.fqcn.clone(), loc.file.clone(), range));
1045 }
1046 out.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.start.line.cmp(&b.2.start.line)));
1047 out.dedup_by(|a, b| a.1 == b.1 && a.2 == b.2);
1048 out
1049 })
1050 }
1051
1052 fn commit_defs_for_matching(&self, files: &[Arc<str>], shorts: &[String]) {
1065 use rayon::prelude::*;
1066
1067 let committed_any: HashSet<Arc<str>> =
1068 self.index.defs_committed_keys().into_iter().collect();
1069 self.db
1073 .add_literal_mention_names(shorts.iter().map(|s| s.as_str()));
1074 let queries: Vec<_> = shorts
1075 .iter()
1076 .filter_map(|s| self.db.prepare_class_mention_query(s))
1077 .collect();
1078 let mention_scanner = self.db.class_mention_scanner();
1079 let use_mentions = queries.len() == shorts.len() && mention_scanner.is_some();
1083 type Work = (Arc<str>, Arc<str>, Vec<crate::db::SubtypeEntry>);
1084 let index = &*self.index;
1085 let results: Vec<(Option<Work>, Option<MentionScanRecord>)> = files
1086 .par_iter()
1087 .map_with(self.db.clone(), |db, path| {
1088 let Some(sf) = db.lookup_source_file(path.as_ref()) else {
1089 return (None, None);
1090 };
1091 let text = sf.text(&*db as &dyn MirDatabase).clone();
1092 if index.is_defs_committed(path.as_ref(), &text) {
1093 return (None, None);
1094 }
1095 let mut scan_rec: Option<MentionScanRecord> = None;
1099 if use_mentions && !committed_any.contains(path.as_ref()) {
1100 let mut answer = Some(false);
1101 for q in &queries {
1102 match db.class_mention_answer(path.as_ref(), q, &text) {
1103 Some(true) => {
1104 answer = Some(true);
1105 break;
1106 }
1107 Some(false) => {}
1108 None => answer = None,
1109 }
1110 }
1111 let hit = match answer {
1112 Some(hit) => hit,
1113 None => {
1114 let scanner = mention_scanner.as_ref().unwrap();
1117 let names = scanner.scan(&text);
1118 let hit = queries.iter().any(|q| names.binary_search(&q.name).is_ok());
1119 scan_rec = Some((path.clone(), text.clone(), names));
1120 hit
1121 }
1122 };
1123 if !hit {
1124 return (None, scan_rec);
1125 }
1126 }
1127 let defs = crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
1128 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
1129 (Some((path.clone(), text, entries)), scan_rec)
1130 })
1131 .collect();
1132 let mut refused = Ok(());
1136 for (work, scan_rec) in results {
1137 if let (Some(scanner), Some((file, text, names))) = (&mention_scanner, scan_rec) {
1138 self.db
1139 .set_file_class_mentions(&file, &text, scanner.epoch(), names);
1140 }
1141 if let Some((file, text, entries)) = work {
1142 if let Err(cancelled) = self.index.commit_class_edges(
1143 &self.db,
1144 &file,
1145 &text,
1146 entries,
1147 self.retire_epoch,
1148 ) {
1149 refused = Err(cancelled);
1150 }
1151 }
1152 }
1153 if let Err(cancelled) = refused {
1154 unwind(cancelled);
1155 }
1156 }
1157
1158 pub fn class_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1166 catch(|| {
1167 let db = &self.db;
1168 let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
1169 let file_data: Vec<(Arc<str>, Arc<str>)> = files
1170 .iter()
1171 .filter_map(|f| {
1172 let sf = db.lookup_source_file(f)?;
1173 Some((f.clone(), sf.text(db as &dyn MirDatabase).clone()))
1174 })
1175 .collect();
1176 crate::class::ClassAnalyzer::with_files(db, file_set, &file_data).analyze_all()
1177 })
1178 }
1179
1180 pub fn collector_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1189 catch(|| {
1190 files
1191 .iter()
1192 .filter_map(|f| self.db.lookup_source_file(f))
1193 .flat_map(|sf| {
1194 crate::db::collect_file_definitions(&self.db, sf)
1195 .issues
1196 .as_ref()
1197 .clone()
1198 })
1199 .collect()
1200 })
1201 }
1202
1203 pub(super) fn reference_gate(&self, symbol: &crate::Name) -> ReferenceGate {
1214 ReferenceGate::for_symbol(&self.db, symbol)
1215 }
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220 use super::*;
1221 use crate::{AnalysisSession, IndexCancel, Name, ReferenceIncludes};
1222 use salsa::Database as _;
1223
1224 const BASE: &str =
1225 "<?php\nclass Base { public function run(): void {} public function stop(): void {} }\n";
1226 const CALLS_RUN: &str = "<?php\nfunction go(Base $b): void { $b->run(); }\n";
1227 const CALLS_STOP: &str = "<?php\nfunction go(Base $b): void { $b->stop(); }\n";
1228
1229 #[test]
1233 fn warm_commit_racing_a_text_write_leaves_the_file_stale() {
1234 let (mut session, files) = prepared_workspace(CALLS_RUN);
1235 let snap = session.snapshot();
1236 let mut pass = snap
1237 .stage_warm(&files[1..], &IndexCancel::new())
1238 .unwrap()
1239 .unwrap();
1240
1241 let caller = files[1].clone();
1242 let writer = std::thread::spawn(move || {
1243 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1244 session
1245 });
1246 await_pending_write(&snap);
1247 snap.commit_warm(&mut pass).unwrap();
1248 drop(snap);
1249 let mut session = writer.join().unwrap();
1250
1251 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1252 assert!(callers_of(&mut session, &files, "run").is_empty());
1253 }
1254
1255 #[test]
1256 fn warm_commit_after_an_ingest_retired_the_file_is_refused() {
1257 let (session, files) = prepared_workspace(CALLS_RUN);
1258 let snap = session.snapshot();
1259 let mut pass = snap
1260 .stage_warm(&files[1..], &IndexCancel::new())
1261 .unwrap()
1262 .unwrap();
1263
1264 let caller = files[1].clone();
1265 let writer = std::thread::spawn(move || {
1266 let mut session = session;
1267 session.ingest_file(caller, Arc::from(CALLS_STOP));
1268 session
1269 });
1270 await_pending_write(&snap);
1271 assert!(snap.commit_warm(&mut pass).is_err());
1272 drop(snap);
1273 let mut session = writer.join().unwrap();
1274
1275 assert!(callers_of(&mut session, &files, "run").is_empty());
1276 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1277 }
1278
1279 #[test]
1280 fn commits_after_invalidate_file_retired_the_file_are_refused() {
1281 const CHILD_CALLS_RUN: &str =
1282 "<?php\nclass Child extends Base {}\nfunction go(Base $b): void { $b->run(); }\n";
1283 let (session, files) = prepared_workspace(CHILD_CALLS_RUN);
1284 let snap = session.snapshot();
1285 let mut pass = snap
1286 .stage_warm(&files[1..], &IndexCancel::new())
1287 .unwrap()
1288 .unwrap();
1289 let parsed = php_rs_parser::parse(CHILD_CALLS_RUN);
1290
1291 let caller = files[1].clone();
1292 let writer = std::thread::spawn(move || {
1293 let mut session = session;
1294 session.invalidate_file(&caller);
1295 session
1296 });
1297 await_pending_write(&snap);
1298 assert!(snap.commit_warm(&mut pass).is_err());
1299 assert!(snap
1300 .analyze(
1301 files[1].clone(),
1302 CHILD_CALLS_RUN,
1303 &parsed.program,
1304 &parsed.source_map
1305 )
1306 .is_err());
1307 drop(snap);
1308 let mut session = writer.join().unwrap();
1309
1310 assert!(callers_of(&mut session, &files, "run").is_empty());
1311 assert!(session
1312 .indexed_subtype_classes("Base", &files, false)
1313 .is_empty());
1314 }
1315
1316 #[test]
1317 fn cancelled_reference_commit_reports_cancellation() {
1318 let (mut session, files) = prepared_workspace(CALLS_RUN);
1319 let snap = session.snapshot();
1320 let stale = snap.stale_reference_candidates(&Name::method("Base", "run"), &files);
1321 assert!(stale.contains(&files[1]));
1322
1323 let caller = files[1].clone();
1324 let writer = std::thread::spawn(move || {
1325 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1326 session
1327 });
1328 await_pending_write(&snap);
1329 assert!(snap.commit_reference_candidates(&stale).is_err());
1330 drop(snap);
1331 writer.join().unwrap();
1332 }
1333
1334 const ROUTES_CALLS_RUN: &str = "<?php\n(new Base())->run();\n";
1337
1338 fn registered_routes_workspace() -> (AnalysisSession, Vec<Arc<str>>) {
1342 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1343 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("routes.php")];
1344 session.ingest_file(files[0].clone(), Arc::from(BASE));
1345 session.upsert_source_file(
1346 files[1].clone(),
1347 Arc::from(ROUTES_CALLS_RUN),
1348 salsa::Durability::LOW,
1349 );
1350 session.prepare_for_query(None);
1351 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1352 (session, files)
1353 }
1354
1355 #[test]
1359 fn ingest_of_unchanged_text_mid_query_keeps_references() {
1360 let (mut session, files) = registered_routes_workspace();
1361 let symbol = Name::method("Base", "run");
1362 let revision = session.text_revision();
1363
1364 let snap = session.snapshot();
1365 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1366 assert!(snap.stale_reference_candidates(&symbol, &files).is_empty());
1367 session.ingest_file(files[1].clone(), Arc::from(ROUTES_CALLS_RUN));
1368 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1369 assert!(snap.ensure_no_retire_since(key.generation).is_ok());
1370 snap.memoize_references(key, &out);
1371 drop(snap);
1372
1373 assert_eq!(session.text_revision(), revision, "ingest wrote salsa");
1374 assert_eq!(
1375 out.into_iter().map(|(file, _)| file).collect::<Vec<_>>(),
1376 [files[1].clone()]
1377 );
1378 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1379 }
1380
1381 #[test]
1385 fn query_spanning_a_retirement_is_cancelled_and_not_memoized() {
1386 let (mut session, files) = registered_routes_workspace();
1387 let symbol = Name::method("Base", "run");
1388
1389 let snap = session.snapshot();
1390 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1391 assert!(snap.stale_reference_candidates(&symbol, &files).is_empty());
1392 session
1393 .index
1394 .retire_references(&session.db.salsa, files[1].as_ref());
1395 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1396 assert!(out.is_empty());
1397 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1398 snap.memoize_references(key, &out);
1399 drop(snap);
1400
1401 let hits = session.ref_query_cache_hits();
1402 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1403 assert_eq!(session.ref_query_cache_hits(), hits);
1404 }
1405
1406 #[test]
1409 fn re_analyze_file_cache_hit_cancels_spanning_queries() {
1410 let dir = tempfile::tempdir().unwrap();
1411 let mut session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(dir.path());
1412 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1413 let opts = crate::BatchOptions::new();
1414 session.ingest_file(files[0].clone(), Arc::from(BASE));
1415 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1416 session.prepare_for_query(None);
1417 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1418
1419 let symbol = Name::method("Base", "run");
1420 let snap = session.snapshot();
1421 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1422 let revision = session.text_revision();
1423 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1424 assert_eq!(session.text_revision(), revision, "took the analysis path");
1425 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1426 drop(snap);
1427
1428 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1429 }
1430
1431 fn prepared_workspace(caller_text: &str) -> (AnalysisSession, Vec<Arc<str>>) {
1432 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1433 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1434 session.ingest_file(files[0].clone(), Arc::from(BASE));
1435 session.ingest_file(files[1].clone(), Arc::from(caller_text));
1436 session.prepare_for_query(Some(&files[1]));
1437 (session, files)
1438 }
1439
1440 fn await_pending_write(snap: &AnalysisSnapshot) {
1443 while catch(|| snap.db.unwind_if_revision_cancelled()).is_ok() {
1444 std::thread::yield_now();
1445 }
1446 }
1447
1448 fn callers_of(
1449 session: &mut AnalysisSession,
1450 files: &[Arc<str>],
1451 method: &str,
1452 ) -> Vec<Arc<str>> {
1453 session
1454 .indexed_references_to(
1455 &Name::method("Base", method),
1456 files,
1457 false,
1458 ReferenceIncludes::Plain,
1459 &|| false,
1460 )
1461 .unwrap()
1462 .into_iter()
1463 .map(|(file, _)| file)
1464 .collect()
1465 }
1466}