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
100pub(super) fn catch<T>(f: impl FnOnce() -> T) -> Result<T, Cancelled> {
101 Cancelled::catch(AssertUnwindSafe(f))
102}
103
104pub(super) fn unwind(cancelled: Cancelled) -> ! {
106 std::panic::resume_unwind(Box::new(cancelled))
107}
108
109impl AnalysisSnapshot {
110 #[doc(hidden)]
116 pub fn db(&self) -> &MirDbStorage {
117 &self.db
118 }
119
120 #[doc(hidden)]
125 pub fn read<R>(&self, f: impl FnOnce(&dyn MirDatabase) -> R) -> Result<R, Cancelled> {
126 catch(|| f(&self.db))
127 }
128
129 pub(crate) fn index(&self) -> &IndexState {
130 &self.index
131 }
132
133 pub(crate) fn stamp(&self) -> ViewStamp {
134 ViewStamp {
135 generation: self.index_generation,
136 retire_epoch: self.retire_epoch,
137 }
138 }
139
140 pub fn php_version(&self) -> PhpVersion {
141 self.php_version
142 }
143
144 pub fn index_generation(&self) -> u64 {
146 self.index_generation
147 }
148
149 pub fn text_revision(&self) -> salsa::Revision {
151 self.db.current_revision()
152 }
153
154 pub(crate) fn query_cache_generation(&self) -> QueryGeneration {
163 (
164 self.db.current_revision(),
165 self.db.subtype_edges_epoch(),
166 self.index.retire_seq(),
167 )
168 }
169
170 pub fn find_class_like(&self, fqcn: &str) -> Result<Option<crate::db::ClassLike>, Cancelled> {
171 self.read(|db| crate::db::find_class_like(db, crate::db::Fqcn::from_str(db, fqcn)))
172 }
173
174 pub fn is_builtin_class(&self, fqcn: &str) -> Result<bool, Cancelled> {
179 self.read(|db| {
180 let fqcn = fqcn.trim_start_matches('\\');
181 crate::db::class_like_decl_file(db, crate::db::Fqcn::from_str(db, fqcn))
182 .is_some_and(|file| crate::stubs::is_stub_path(&file))
183 })
184 }
185
186 pub fn find_function(&self, fqn: &str) -> Result<Option<Arc<crate::FunctionDef>>, Cancelled> {
187 self.read(|db| crate::db::find_function(db, crate::db::Fqcn::from_str(db, fqn)))
188 }
189
190 #[allow(clippy::type_complexity)]
193 pub fn find_method_in_chain(
194 &self,
195 class: &str,
196 method: &str,
197 ) -> Result<Option<(Arc<str>, Arc<mir_codebase::definitions::MethodDef>)>, Cancelled> {
198 self.read(|db| {
199 crate::db::find_method_in_chain(db, crate::db::Fqcn::from_str(db, class), method)
200 })
201 }
202
203 pub fn definition_of_cached(
206 &self,
207 symbol: &crate::Name,
208 ) -> Result<Result<mir_types::Location, crate::SymbolLookupError>, Cancelled> {
209 catch(|| self.definition_of_unguarded(symbol))
210 }
211
212 fn definition_of_unguarded(
213 &self,
214 symbol: &crate::Name,
215 ) -> Result<mir_types::Location, crate::SymbolLookupError> {
216 let db = &self.db;
217 match symbol {
218 crate::Name::Class(fqcn) => {
219 let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
220 let class = crate::db::find_class_like(db, here)
221 .ok_or(crate::SymbolLookupError::NotFound)?;
222 class
223 .location()
224 .cloned()
225 .ok_or(crate::SymbolLookupError::NoSourceLocation)
226 }
227 crate::Name::Function(fqn) => {
228 let here = crate::db::Fqcn::from_str(db, fqn.as_ref());
229 let f =
230 crate::db::find_function(db, here).ok_or(crate::SymbolLookupError::NotFound)?;
231 f.location
232 .clone()
233 .ok_or(crate::SymbolLookupError::NoSourceLocation)
234 }
235 crate::Name::Method { .. }
236 | crate::Name::Property { .. }
237 | crate::Name::ClassConstant { .. } => {
238 crate::db::member_location(db, symbol).ok_or(crate::SymbolLookupError::NotFound)
239 }
240 crate::Name::GlobalConstant(fqn) => self
241 .global_constant_location(fqn)
242 .ok_or(crate::SymbolLookupError::NoSourceLocation),
243 }
244 }
245
246 pub fn files_mentioning_class(
255 &self,
256 files: &[Arc<str>],
257 class_name: &str,
258 ) -> Result<Vec<Arc<str>>, Cancelled> {
259 self.files_mentioning_any(files, &[class_name])
260 }
261
262 pub fn files_mentioning_any(
267 &self,
268 files: &[Arc<str>],
269 needles: &[&str],
270 ) -> Result<Vec<Arc<str>>, Cancelled> {
271 catch(|| self.mentioning_any_unguarded(files, needles))
272 }
273
274 fn mentioning_any_unguarded(&self, files: &[Arc<str>], needles: &[&str]) -> Vec<Arc<str>> {
275 use rayon::prelude::*;
276
277 if needles.is_empty() {
278 return files.to_vec();
279 }
280 let db = self.db.clone();
281 db.add_literal_mention_names(needles.iter().copied());
282 let queries: Vec<_> = needles
284 .iter()
285 .filter_map(|n| db.prepare_class_mention_query(n))
286 .collect();
287 if queries.is_empty() {
288 return files.to_vec();
289 }
290 let Some(scanner) = db.class_mention_scanner() else {
291 return files.to_vec();
292 };
293
294 files
295 .par_iter()
296 .map_with(db, |db, f| -> Option<Arc<str>> {
297 let sf = db.lookup_source_file(f.as_ref())?;
298 let text = sf.text(&*db as &dyn MirDatabase).clone();
299 for q in &queries {
300 match db.class_mention_answer(f.as_ref(), q, &text) {
301 Some(true) => return Some(f.clone()),
302 Some(false) => continue,
303 None => {
304 let names = scanner.scan(text.as_ref());
305 let hit = queries
306 .iter()
307 .any(|q2| names.binary_search(&q2.name).is_ok());
308 db.set_file_class_mentions(f, &text, scanner.epoch(), names);
309 return hit.then(|| f.clone());
310 }
311 }
312 }
313 None
314 })
315 .flatten()
316 .collect()
317 }
318
319 pub fn indexed_references_to(
353 &self,
354 symbol: &crate::Name,
355 files: &[Arc<str>],
356 include_declaration: bool,
357 includes: crate::ReferenceIncludes,
358 ) -> Result<Vec<(Arc<str>, crate::Range)>, Cancelled> {
359 let key = self.reference_query_key(symbol, files, include_declaration, includes);
360 if let Some(hit) = self.index.ref_queries.get(&key) {
361 return Ok(hit);
362 }
363 let stale = catch(|| self.stale_reference_candidates_unguarded(symbol, files))?;
364 if !stale.is_empty() {
365 self.commit_reference_candidates(&stale)?;
366 }
367 let out = catch(|| self.read_references(symbol, files, include_declaration, includes))?;
368 self.ensure_no_retire_since(key.generation)?;
369 self.memoize_references(key, &out);
370 Ok(out)
371 }
372
373 pub(super) fn ensure_no_retire_since(
376 &self,
377 generation: QueryGeneration,
378 ) -> Result<(), Cancelled> {
379 if self.index.retire_seq() == generation.2 {
380 Ok(())
381 } else {
382 Err(Cancelled::PendingWrite)
383 }
384 }
385
386 pub fn indexed_use_import_locations(
390 &self,
391 symbol: &crate::Name,
392 files: &[Arc<str>],
393 ) -> Result<Vec<(Arc<str>, crate::Range)>, Cancelled> {
394 self.indexed_references_to(symbol, files, false, crate::ReferenceIncludes::UseImports)
395 }
396
397 pub(super) fn reference_query_key(
398 &self,
399 symbol: &crate::Name,
400 files: &[Arc<str>],
401 include_declaration: bool,
402 includes: crate::ReferenceIncludes,
403 ) -> RefQueryCacheKey {
404 RefQueryCacheKey {
405 symbol: symbol.codebase_key(),
406 include_declaration,
407 includes,
408 generation: self.query_cache_generation(),
409 files_hash: hash_files(files),
410 }
411 }
412
413 pub(super) fn memoize_references(
418 &self,
419 key: RefQueryCacheKey,
420 out: &[(Arc<str>, crate::Range)],
421 ) {
422 let generation = self.query_cache_generation();
423 if generation == key.generation {
424 self.index.ref_queries.insert(generation, key, out);
425 }
426 }
427
428 pub fn stale_reference_candidates(
436 &self,
437 symbol: &crate::Name,
438 files: &[Arc<str>],
439 ) -> Result<Vec<Arc<str>>, Cancelled> {
440 catch(|| self.stale_reference_candidates_unguarded(symbol, files))
441 }
442
443 pub(super) fn stale_reference_candidates_unguarded(
465 &self,
466 symbol: &crate::Name,
467 files: &[Arc<str>],
468 ) -> Vec<Arc<str>> {
469 let db = &self.db;
470 let gate = self.reference_gate(symbol);
471 let has_needles = !gate.idents.is_empty() || !gate.raw.is_empty();
472 let (mention_queries, mention_scanner) = if has_needles {
473 db.add_literal_mention_names(gate.idents.iter().map(|s| s.as_str()));
474 db.add_raw_mention_needles(gate.raw.iter().map(|s| s.as_str()));
475 let queries: Vec<_> = gate
476 .idents
477 .iter()
478 .chain(gate.raw.iter())
479 .filter_map(|s| db.prepare_class_mention_query(s))
480 .collect();
481 (queries, db.class_mention_scanner())
482 } else {
483 (Vec::new(), None)
484 };
485 let gate_complete = mention_queries.len() == gate.idents.len() + gate.raw.len()
489 && mention_scanner.is_some();
490 let current_gen = self.index_generation;
491 let mut stale = Vec::new();
492 let mut scanned: Vec<MentionScanRecord> = Vec::new();
493 for f in files {
494 let Some(sf) = db.lookup_source_file(f.as_ref()) else {
495 continue;
496 };
497 let text = sf.text(db as &dyn MirDatabase);
498 match self.index.ref_freshness(sf, text, current_gen) {
499 RefFreshness::Fresh => continue,
500 RefFreshness::Stale => {
501 stale.push(f.clone());
502 continue;
503 }
504 RefFreshness::StaleByGenerationOnly | RefFreshness::Uncommitted => {}
505 }
506 if has_needles && gate_complete {
507 let mut answer = Some(false);
511 for q in &mention_queries {
512 match db.class_mention_answer(f.as_ref(), q, text) {
513 Some(true) => {
514 answer = Some(true);
515 break;
516 }
517 Some(false) => {}
518 None => answer = None,
519 }
520 }
521 match answer {
522 Some(true) => {}
523 Some(false) => continue,
524 None => {
525 let scanner = mention_scanner
526 .as_ref()
527 .expect("gate_complete implies a scanner");
528 let names = scanner.scan(text);
529 let hit = mention_queries
530 .iter()
531 .any(|q| names.binary_search(&q.name).is_ok());
532 scanned.push((f.clone(), text.clone(), names));
533 if hit {
534 stale.push(f.clone());
535 }
536 continue;
537 }
538 }
539 }
540 stale.push(f.clone());
541 }
542 if let Some(scanner) = &mention_scanner {
545 for (file, text, names) in scanned {
546 db.set_file_class_mentions(&file, &text, scanner.epoch(), names);
547 }
548 }
549 stale
550 }
551
552 pub(super) fn commit_reference_candidates(&self, stale: &[Arc<str>]) -> Result<(), Cancelled> {
554 self.warm_pass(stale, &crate::IndexCancel::new()).map(drop)
555 }
556
557 pub fn warm_files(
564 &self,
565 files: &[Arc<str>],
566 cancel: &crate::IndexCancel,
567 ) -> Result<bool, Cancelled> {
568 Ok(self.warm_pass(files, cancel)?.is_some())
569 }
570
571 #[allow(clippy::type_complexity)]
581 pub fn reanalyze_files(
582 &self,
583 files: &[Arc<str>],
584 cancel: &crate::IndexCancel,
585 ) -> Result<Option<Vec<(Arc<str>, crate::FileAnalysis)>>, Cancelled> {
586 Ok(self.warm_pass(files, cancel)?.map(|analyzed| {
587 analyzed
588 .into_iter()
589 .map(|a| {
590 (
591 a.file,
592 crate::FileAnalysis {
593 issues: a.out.issues.to_vec(),
594 symbols: Vec::new(),
595 },
596 )
597 })
598 .collect()
599 }))
600 }
601
602 pub(super) fn warm_pass(
603 &self,
604 files: &[Arc<str>],
605 cancel: &crate::IndexCancel,
606 ) -> Result<Option<Vec<AnalyzedFile>>, Cancelled> {
607 let Some(mut pass) = self.stage_warm(files, cancel)? else {
608 return Ok(None);
609 };
610 self.commit_warm(&mut pass)?;
611 Ok(Some(pass.analyzed))
612 }
613
614 fn stage_warm(
619 &self,
620 files: &[Arc<str>],
621 cancel: &crate::IndexCancel,
622 ) -> Result<Option<WarmPass>, Cancelled> {
623 catch(|| {
624 let mut db = self.db.clone();
625 db.freeze_workspace_index();
626 let mention_scanner = db.class_mention_scanner();
627 let cache = self.cache.as_deref();
628 let mut analyzed = Vec::with_capacity(files.len());
629 for path in files {
630 if cancel.is_cancelled() {
631 return None;
632 }
633 analyzed.extend(self.index.stage_analyzed(
634 &db,
635 cache,
636 mention_scanner.as_deref(),
637 path,
638 ));
639 }
640 Some(WarmPass {
641 mention_scanner,
642 analyzed,
643 })
644 })
645 }
646
647 fn commit_warm(&self, pass: &mut WarmPass) -> Result<(), Cancelled> {
650 self.index.commit_analyzed(
651 &self.db,
652 self.cache.as_deref(),
653 pass.mention_scanner.as_deref(),
654 &mut pass.analyzed,
655 self.stamp(),
656 )
657 }
658
659 pub(super) fn read_references(
672 &self,
673 symbol: &crate::Name,
674 files: &[Arc<str>],
675 include_declaration: bool,
676 includes: crate::ReferenceIncludes,
677 ) -> Vec<(Arc<str>, crate::Range)> {
678 let key = symbol.codebase_key();
679 let hierarchy: Vec<String> = match symbol {
680 crate::Name::Method { class, name } => {
681 if class.is_empty() {
682 Vec::new()
683 } else if name.as_ref() == "__construct" {
684 vec![class.trim_start_matches('\\').to_string()]
685 } else {
686 self.member_hierarchy_classes(class.as_ref())
687 }
688 }
689 crate::Name::Property { class, .. } | crate::Name::ClassConstant { class, .. } => {
690 if class.is_empty() {
691 Vec::new()
692 } else {
693 self.member_hierarchy_classes(class.as_ref())
694 }
695 }
696 _ => Vec::new(),
697 };
698 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
699 let read_symbol_key = |symbol_key: &str| -> Vec<(Arc<str>, crate::Range)> {
700 self.db
701 .reference_locations(symbol_key)
702 .into_iter()
703 .filter(|(file, ..)| scope.contains(file.as_ref()))
704 .map(|(file, line, col_start, col_end)| {
705 (file, span_range(line, col_start as u32, col_end as u32))
706 })
707 .collect()
708 };
709 let mut scratch_key = String::new();
710 let mut read_composed_key = |prefix: &str, middle: &str, separator: &str, suffix: &str| {
711 scratch_key.clear();
712 scratch_key.reserve(prefix.len() + middle.len() + separator.len() + suffix.len());
713 scratch_key.push_str(prefix);
714 scratch_key.push_str(middle);
715 scratch_key.push_str(separator);
716 scratch_key.push_str(suffix);
717 read_symbol_key(&scratch_key)
718 };
719 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
720 if !matches!(includes, crate::ReferenceIncludes::UseImports) {
721 let member_prefix = match symbol {
722 crate::Name::Method { name, .. } => Some(("meth:", name)),
723 crate::Name::Property { name, .. } => Some(("prop:", name)),
724 crate::Name::ClassConstant { name, .. } => Some(("cnst:", name)),
725 _ => None,
726 };
727 match member_prefix {
728 Some((prefix, name)) => {
729 for class in &hierarchy {
730 out.extend(read_composed_key(prefix, class, "::", name));
731 }
732 }
733 None => out.extend(read_symbol_key(key.as_str())),
734 }
735 }
736 if !matches!(includes, crate::ReferenceIncludes::Plain) {
737 out.extend(read_composed_key("use:", key.as_str(), "", ""));
738 }
739 if matches!(includes, crate::ReferenceIncludes::Plain) && out.is_empty() {
740 match symbol {
741 crate::Name::Method { name, .. } => {
742 out = read_composed_key("methname:", name.as_ref(), "", "");
743 }
744 crate::Name::Property { name, .. } => {
745 out = read_composed_key("propname:", name.as_ref(), "", "");
746 }
747 _ => {}
748 }
749 }
750 out.sort_by(|a, b| {
751 a.0.cmp(&b.0)
752 .then(a.1.start.line.cmp(&b.1.start.line))
753 .then(a.1.start.column.cmp(&b.1.start.column))
754 });
755 out.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
756
757 if include_declaration {
758 let decls: Vec<(Arc<str>, crate::Range)> = match symbol {
759 crate::Name::Method { class, name }
760 | crate::Name::Property { class, name }
761 | crate::Name::ClassConstant { class, name } => {
762 if class.is_empty() {
763 let prefix = match symbol {
767 crate::Name::Method { .. } => "methdecl:",
768 crate::Name::Property { .. } => "propdecl:",
769 _ => "cnstdecl:",
770 };
771 read_composed_key(prefix, name.as_ref(), "", "")
772 } else {
773 self.member_decl_sites(&hierarchy, symbol)
774 }
775 }
776 _ => self.declaration_name_range(symbol).into_iter().collect(),
777 };
778 for (file, range) in decls {
779 if scope.contains(file.as_ref())
780 && !out.iter().any(|(f, r)| *f == file && *r == range)
781 {
782 out.push((file, range));
783 }
784 }
785 }
786 out
787 }
788
789 fn member_hierarchy_classes(&self, class_fqn: &str) -> Vec<String> {
795 let target = class_fqn.trim_start_matches('\\');
796 let mut out = vec![target.to_string()];
797 let db = &self.db;
798 let here = crate::db::Fqcn::from_str(db, target);
799 let ancestors = crate::db::class_ancestors_by_fqcn(db, here)
801 .iter()
802 .skip(1)
803 .map(|a| a.trim_start_matches('\\').to_string())
804 .collect::<Vec<_>>();
805 out.extend(ancestors);
806 out.extend(
807 db.subtype_sites_of(target, true)
808 .into_iter()
809 .map(|s| s.fqcn.trim_start_matches('\\').to_string()),
810 );
811 let mut seen: HashSet<String> = HashSet::default();
812 out.retain(|c| seen.insert(c.to_ascii_lowercase()));
813 out
814 }
815
816 fn member_decl_sites(
822 &self,
823 classes: &[String],
824 symbol: &crate::Name,
825 ) -> Vec<(Arc<str>, crate::Range)> {
826 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
827 let db = &self.db;
828 for class in classes {
829 let here = crate::db::Fqcn::from_str(db, class);
830 let (loc, needle) = match symbol {
831 crate::Name::Method { name, .. } => {
832 let Some(m) = crate::db::find_method_in_class(db, here, name) else {
833 continue;
834 };
835 (m.location.clone(), name.to_string())
836 }
837 crate::Name::Property { name, .. } => {
838 let Some(p) = crate::db::find_property_in_class(db, here, name) else {
839 continue;
840 };
841 (p.location.clone(), name.to_string())
842 }
843 crate::Name::ClassConstant { name, .. } => {
844 let Some(c) = crate::db::find_class_constant_in_class(db, here, name) else {
845 continue;
846 };
847 (c.location.clone(), name.to_string())
848 }
849 _ => continue,
850 };
851 let Some(loc) = loc else { continue };
852 let range = self.refine_location_to_name(&loc, &needle);
853 out.push((loc.file.clone(), range));
854 }
855 out
856 }
857
858 pub(super) fn declaration_name_range(
862 &self,
863 symbol: &crate::Name,
864 ) -> Option<(Arc<str>, crate::Range)> {
865 if let crate::Name::GlobalConstant(fqn) = symbol {
866 return self.global_constant_decl_range(fqn);
867 }
868 let loc = self.definition_of_unguarded(symbol).ok()?;
869 let short = match symbol {
870 crate::Name::Class(f) | crate::Name::Function(f) | crate::Name::GlobalConstant(f) => {
871 crate::db::subtype_index::short_name_of(f)
872 }
873 crate::Name::Method { name, .. }
874 | crate::Name::Property { name, .. }
875 | crate::Name::ClassConstant { name, .. } => name.as_ref(),
876 };
877 let range = self.refine_location_to_name(&loc, short);
881 Some((loc.file.clone(), range))
882 }
883
884 fn refine_location_to_name(&self, loc: &mir_types::Location, needle: &str) -> crate::Range {
889 let fallback = span_range(loc.line, loc.col_start as u32, loc.col_end as u32);
890 let Some(text) = self
891 .db
892 .lookup_source_file(loc.file.as_ref())
893 .map(|sf| sf.text(&self.db as &dyn MirDatabase).clone())
894 else {
895 return fallback;
896 };
897 let needle_chars = needle.chars().count() as u32;
898 let first_line = loc.line.saturating_sub(1) as usize;
899 for case_insensitive in [false, true] {
904 for (idx, line_text) in text.lines().enumerate().skip(first_line) {
905 let line_no = idx as u32 + 1;
906 if line_no > loc.line_end {
907 break;
908 }
909 let min_col = if line_no == loc.line {
910 loc.col_start as usize
911 } else {
912 0
913 };
914 if let Some(col) = identifier_char_col(line_text, needle, min_col, case_insensitive)
915 {
916 return span_range(line_no, col, col + needle_chars);
917 }
918 }
919 }
920 fallback
921 }
922
923 fn global_constant_decl_site(&self, fqn: &str) -> Option<ConstantDeclSite> {
927 let short = crate::db::subtype_index::short_name_of(fqn);
928 let db = &self.db;
929 let index = crate::db::workspace_index(db);
930 let loc = index.constant_loc(mir_types::Name::from(fqn.trim_start_matches('\\')))?;
931 let file = loc.file().path(db).clone();
932 let sf = db.lookup_source_file(file.as_ref())?;
933 let text = sf.text(db as &dyn MirDatabase);
934 for (idx, line) in text.lines().enumerate() {
935 let trimmed = line.trim_start();
936 let is_decl_line = trimmed.starts_with("const ")
937 || trimmed.contains("define(")
938 || trimmed.contains("define (");
939 if !is_decl_line {
940 continue;
941 }
942 if let Some(name_col) = identifier_char_col(line, short, 0, false) {
943 let stmt_end = match line.find(';') {
944 Some(byte) => line[..=byte].chars().count(),
945 None => line.chars().count(),
946 };
947 return Some(ConstantDeclSite {
948 file,
949 line: idx as u32 + 1,
950 stmt_col: (line.chars().count() - trimmed.chars().count()) as u32,
951 stmt_end: stmt_end as u32,
952 name_col,
953 name_len: short.chars().count() as u32,
954 });
955 }
956 }
957 None
958 }
959
960 fn global_constant_decl_range(&self, fqn: &str) -> Option<(Arc<str>, crate::Range)> {
961 let site = self.global_constant_decl_site(fqn)?;
962 let range = span_range(site.line, site.name_col, site.name_col + site.name_len);
963 Some((site.file, range))
964 }
965
966 fn global_constant_location(&self, fqn: &str) -> Option<mir_types::Location> {
968 let site = self.global_constant_decl_site(fqn)?;
969 Some(mir_types::Location {
970 file: site.file,
971 line: site.line,
972 line_end: site.line,
973 col_start: site.stmt_col as u16,
974 col_end: site.stmt_end as u16,
975 })
976 }
977
978 pub fn subtype_files(&self, class_fqn: &str) -> Result<Vec<Arc<str>>, Cancelled> {
983 let files = self.db.source_file_paths();
984 let mut out: Vec<Arc<str>> = self
985 .indexed_subtype_classes(class_fqn, &files, false)?
986 .into_iter()
987 .map(|s| s.file)
988 .collect();
989 out.sort();
990 out.dedup();
991 Ok(out)
992 }
993
994 pub fn indexed_subtype_classes(
1013 &self,
1014 class_fqn: &str,
1015 files: &[Arc<str>],
1016 include_trait_users: bool,
1017 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1018 catch(|| {
1019 let key = SubtypeQueryCacheKey {
1020 class_fqn: class_fqn.trim_start_matches('\\').to_ascii_lowercase(),
1021 include_trait_users,
1022 generation: self.query_cache_generation(),
1023 files_hash: hash_files(files),
1024 };
1025 if let Some(hit) = self.index.subtype_queries.get(&key) {
1026 return hit;
1027 }
1028 let out = self.subtype_classes_uncached(class_fqn, files, include_trait_users);
1029 if let Err(cancelled) = self.ensure_no_retire_since(key.generation) {
1030 unwind(cancelled);
1031 }
1032 let generation = self.query_cache_generation();
1034 if generation == key.generation {
1035 self.index.subtype_queries.insert(generation, key, &out);
1036 }
1037 out
1038 })
1039 }
1040
1041 fn subtype_classes_uncached(
1042 &self,
1043 class_fqn: &str,
1044 files: &[Arc<str>],
1045 include_trait_users: bool,
1046 ) -> Vec<SubtypeClassSite> {
1047 let db = &self.db;
1048 let mut scanned: HashSet<String> = HashSet::default();
1049 let mut pending: Vec<String> = vec![class_fqn.trim_start_matches('\\').to_string()];
1050 let mut sites: Vec<crate::db::SubtypeSite> = Vec::new();
1051 while !pending.is_empty() {
1052 let needles: Vec<String> = pending
1055 .drain(..)
1056 .filter(|f| scanned.insert(f.clone()))
1057 .map(|f| crate::db::subtype_index::short_name_of(&f).to_string())
1058 .collect();
1059 if !needles.is_empty() {
1060 self.commit_defs_for_matching(files, &needles);
1061 }
1062 sites = db.subtype_sites_of(class_fqn, include_trait_users);
1063 pending = sites
1064 .iter()
1065 .map(|s| s.fqcn.trim_start_matches('\\').to_string())
1066 .filter(|f| !scanned.contains(f))
1067 .collect();
1068 }
1069 let mut out: Vec<SubtypeClassSite> = sites
1070 .into_iter()
1071 .filter_map(|s| {
1072 let loc = s.location.as_ref()?;
1073 let short = crate::db::subtype_index::short_name_of(&s.fqcn).to_string();
1074 let range = self.refine_location_to_name(loc, &short);
1075 Some(SubtypeClassSite {
1076 fqcn: s.fqcn,
1077 kind: s.kind,
1078 is_abstract: s.is_abstract,
1079 file: s.file,
1080 range,
1081 })
1082 })
1083 .collect();
1084 let root_lc = class_fqn.trim_start_matches('\\').to_ascii_lowercase();
1088 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
1089 for (file, line, cs, ce) in db.reference_locations(&format!("impl:{root_lc}")) {
1090 if !scope.contains(file.as_ref()) {
1091 continue;
1092 }
1093 let range = span_range(line, cs as u32, ce as u32);
1094 if out.iter().any(|s| s.file == file && s.range == range) {
1095 continue;
1096 }
1097 out.push(SubtypeClassSite {
1098 fqcn: Arc::from("class@anonymous"),
1099 kind: crate::db::ClassLikeKind::Class,
1100 is_abstract: false,
1101 file,
1102 range,
1103 });
1104 }
1105 out
1106 }
1107
1108 #[allow(clippy::type_complexity)]
1114 pub fn indexed_method_implementations(
1115 &self,
1116 class_fqn: &str,
1117 method: &str,
1118 files: &[Arc<str>],
1119 ) -> Result<Vec<(Arc<str>, Arc<str>, crate::Range)>, Cancelled> {
1120 let subs = self.indexed_subtype_classes(class_fqn, files, false)?;
1121 catch(|| {
1122 let db = &self.db;
1123 let mut out: Vec<(Arc<str>, Arc<str>, crate::Range)> = Vec::new();
1124 for sub in &subs {
1125 let here = crate::db::Fqcn::from_str(db, sub.fqcn.as_ref());
1126 let Some((_, m)) = crate::db::find_method_in_chain(db, here, method) else {
1127 continue;
1128 };
1129 if m.is_abstract {
1130 continue;
1131 }
1132 let Some(loc) = m.location.as_ref() else {
1133 continue;
1134 };
1135 let range = self.refine_location_to_name(loc, method);
1136 out.push((sub.fqcn.clone(), loc.file.clone(), range));
1137 }
1138 out.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.start.line.cmp(&b.2.start.line)));
1139 out.dedup_by(|a, b| a.1 == b.1 && a.2 == b.2);
1140 out
1141 })
1142 }
1143
1144 fn commit_defs_for_matching(&self, files: &[Arc<str>], shorts: &[String]) {
1157 use rayon::prelude::*;
1158
1159 self.db
1163 .add_literal_mention_names(shorts.iter().map(|s| s.as_str()));
1164 let queries: Vec<_> = shorts
1165 .iter()
1166 .filter_map(|s| self.db.prepare_class_mention_query(s))
1167 .collect();
1168 let mention_scanner = self.db.class_mention_scanner();
1169 let use_mentions = queries.len() == shorts.len() && mention_scanner.is_some();
1173 type Work = (Arc<str>, Arc<str>, Vec<crate::db::SubtypeEntry>);
1174 let index = &*self.index;
1175 let results: Vec<(Option<Work>, Option<MentionScanRecord>)> = files
1176 .par_iter()
1177 .map_with(self.db.clone(), |db, path| {
1178 let Some(sf) = db.lookup_source_file(path.as_ref()) else {
1179 return (None, None);
1180 };
1181 let text = sf.text(&*db as &dyn MirDatabase).clone();
1182 let freshness = index.defs_freshness(sf, &text);
1183 if freshness == DefsFreshness::Fresh {
1184 return (None, None);
1185 }
1186 let mut scan_rec: Option<MentionScanRecord> = None;
1190 if use_mentions && freshness == DefsFreshness::Uncommitted {
1191 let mut answer = Some(false);
1192 for q in &queries {
1193 match db.class_mention_answer(path.as_ref(), q, &text) {
1194 Some(true) => {
1195 answer = Some(true);
1196 break;
1197 }
1198 Some(false) => {}
1199 None => answer = None,
1200 }
1201 }
1202 let hit = match answer {
1203 Some(hit) => hit,
1204 None => {
1205 let scanner = mention_scanner.as_ref().unwrap();
1208 let names = scanner.scan(&text);
1209 let hit = queries.iter().any(|q| names.binary_search(&q.name).is_ok());
1210 scan_rec = Some((path.clone(), text.clone(), names));
1211 hit
1212 }
1213 };
1214 if !hit {
1215 return (None, scan_rec);
1216 }
1217 }
1218 let defs = crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
1219 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
1220 (Some((path.clone(), text, entries)), scan_rec)
1221 })
1222 .collect();
1223 let mut refused = Ok(());
1227 for (work, scan_rec) in results {
1228 if let (Some(scanner), Some((file, text, names))) = (&mention_scanner, scan_rec) {
1229 self.db
1230 .set_file_class_mentions(&file, &text, scanner.epoch(), names);
1231 }
1232 if let Some((file, text, entries)) = work {
1233 if let Err(cancelled) = self.index.commit_class_edges(
1234 &self.db,
1235 &file,
1236 &text,
1237 entries,
1238 self.retire_epoch,
1239 ) {
1240 refused = Err(cancelled);
1241 }
1242 }
1243 }
1244 if let Err(cancelled) = refused {
1245 unwind(cancelled);
1246 }
1247 }
1248
1249 pub fn class_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1257 catch(|| {
1258 let db = &self.db;
1259 let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
1260 let file_data: Vec<(Arc<str>, Arc<str>)> = files
1261 .iter()
1262 .filter_map(|f| {
1263 let sf = db.lookup_source_file(f)?;
1264 Some((f.clone(), sf.text(db as &dyn MirDatabase).clone()))
1265 })
1266 .collect();
1267 crate::class::ClassAnalyzer::with_files(db, file_set, &file_data).analyze_all()
1268 })
1269 }
1270
1271 pub fn collector_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1280 catch(|| {
1281 files
1282 .iter()
1283 .filter_map(|f| self.db.lookup_source_file(f))
1284 .flat_map(|sf| {
1285 crate::db::collect_file_definitions(&self.db, sf)
1286 .issues
1287 .as_ref()
1288 .clone()
1289 })
1290 .collect()
1291 })
1292 }
1293
1294 pub(super) fn reference_gate(&self, symbol: &crate::Name) -> ReferenceGate {
1305 ReferenceGate::for_symbol(&self.db, symbol)
1306 }
1307}
1308
1309#[cfg(test)]
1310mod tests {
1311 use super::*;
1312 use crate::{AnalysisSession, IndexCancel, Name, ReferenceIncludes};
1313 use salsa::Database as _;
1314
1315 const BASE: &str =
1316 "<?php\nclass Base { public function run(): void {} public function stop(): void {} }\n";
1317 const CALLS_RUN: &str = "<?php\nfunction go(Base $b): void { $b->run(); }\n";
1318 const CALLS_STOP: &str = "<?php\nfunction go(Base $b): void { $b->stop(); }\n";
1319
1320 #[test]
1324 fn warm_commit_racing_a_text_write_leaves_the_file_stale() {
1325 let (mut session, files) = prepared_workspace(CALLS_RUN);
1326 let snap = session.snapshot();
1327 let mut pass = snap
1328 .stage_warm(&files[1..], &IndexCancel::new())
1329 .unwrap()
1330 .unwrap();
1331
1332 let caller = files[1].clone();
1333 let writer = std::thread::spawn(move || {
1334 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1335 session
1336 });
1337 await_pending_write(&snap);
1338 snap.commit_warm(&mut pass).unwrap();
1339 drop(snap);
1340 let mut session = writer.join().unwrap();
1341
1342 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1343 assert!(callers_of(&mut session, &files, "run").is_empty());
1344 }
1345
1346 #[test]
1347 fn warm_commit_after_an_ingest_retired_the_file_is_refused() {
1348 let (session, files) = prepared_workspace(CALLS_RUN);
1349 let snap = session.snapshot();
1350 let mut pass = snap
1351 .stage_warm(&files[1..], &IndexCancel::new())
1352 .unwrap()
1353 .unwrap();
1354
1355 let caller = files[1].clone();
1356 let writer = std::thread::spawn(move || {
1357 let mut session = session;
1358 session.ingest_file(caller, Arc::from(CALLS_STOP));
1359 session
1360 });
1361 await_pending_write(&snap);
1362 assert!(snap.commit_warm(&mut pass).is_err());
1363 drop(snap);
1364 let mut session = writer.join().unwrap();
1365
1366 assert!(callers_of(&mut session, &files, "run").is_empty());
1367 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1368 }
1369
1370 #[test]
1371 fn commits_after_invalidate_file_retired_the_file_are_refused() {
1372 const CHILD_CALLS_RUN: &str =
1373 "<?php\nclass Child extends Base {}\nfunction go(Base $b): void { $b->run(); }\n";
1374 let (session, files) = prepared_workspace(CHILD_CALLS_RUN);
1375 let snap = session.snapshot();
1376 let mut pass = snap
1377 .stage_warm(&files[1..], &IndexCancel::new())
1378 .unwrap()
1379 .unwrap();
1380 let parsed = php_rs_parser::parse(CHILD_CALLS_RUN);
1381
1382 let caller = files[1].clone();
1383 let writer = std::thread::spawn(move || {
1384 let mut session = session;
1385 session.invalidate_file(&caller);
1386 session
1387 });
1388 await_pending_write(&snap);
1389 assert!(snap.commit_warm(&mut pass).is_err());
1390 assert!(snap
1391 .analyze(
1392 files[1].clone(),
1393 CHILD_CALLS_RUN,
1394 &parsed.program,
1395 &parsed.source_map
1396 )
1397 .is_err());
1398 drop(snap);
1399 let mut session = writer.join().unwrap();
1400
1401 assert!(callers_of(&mut session, &files, "run").is_empty());
1402 assert!(session
1403 .indexed_subtype_classes("Base", &files, false)
1404 .is_empty());
1405 }
1406
1407 #[test]
1408 fn cancelled_reference_commit_reports_cancellation() {
1409 let (mut session, files) = prepared_workspace(CALLS_RUN);
1410 let snap = session.snapshot();
1411 let stale = snap.stale_reference_candidates_unguarded(&Name::method("Base", "run"), &files);
1412 assert!(stale.contains(&files[1]));
1413
1414 let caller = files[1].clone();
1415 let writer = std::thread::spawn(move || {
1416 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1417 session
1418 });
1419 await_pending_write(&snap);
1420 assert!(snap.commit_reference_candidates(&stale).is_err());
1421 drop(snap);
1422 writer.join().unwrap();
1423 }
1424
1425 const ROUTES_CALLS_RUN: &str = "<?php\n(new Base())->run();\n";
1428
1429 fn registered_routes_workspace() -> (AnalysisSession, Vec<Arc<str>>) {
1433 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1434 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("routes.php")];
1435 session.ingest_file(files[0].clone(), Arc::from(BASE));
1436 session.upsert_source_file(
1437 files[1].clone(),
1438 Arc::from(ROUTES_CALLS_RUN),
1439 salsa::Durability::LOW,
1440 );
1441 session.prepare_for_query(None);
1442 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1443 (session, files)
1444 }
1445
1446 #[test]
1450 fn ingest_of_unchanged_text_mid_query_keeps_references() {
1451 let (mut session, files) = registered_routes_workspace();
1452 let symbol = Name::method("Base", "run");
1453 let revision = session.text_revision();
1454
1455 let snap = session.snapshot();
1456 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1457 assert!(snap
1458 .stale_reference_candidates_unguarded(&symbol, &files)
1459 .is_empty());
1460 session.ingest_file(files[1].clone(), Arc::from(ROUTES_CALLS_RUN));
1461 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1462 assert!(snap.ensure_no_retire_since(key.generation).is_ok());
1463 snap.memoize_references(key, &out);
1464 drop(snap);
1465
1466 assert_eq!(session.text_revision(), revision, "ingest wrote salsa");
1467 assert_eq!(
1468 out.into_iter().map(|(file, _)| file).collect::<Vec<_>>(),
1469 [files[1].clone()]
1470 );
1471 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1472 }
1473
1474 #[test]
1478 fn query_spanning_a_retirement_is_cancelled_and_not_memoized() {
1479 let (mut session, files) = registered_routes_workspace();
1480 let symbol = Name::method("Base", "run");
1481
1482 let snap = session.snapshot();
1483 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1484 assert!(snap
1485 .stale_reference_candidates_unguarded(&symbol, &files)
1486 .is_empty());
1487 session
1488 .index
1489 .retire_references(&session.db.salsa, files[1].as_ref());
1490 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1491 assert!(out.is_empty());
1492 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1493 snap.memoize_references(key, &out);
1494 drop(snap);
1495
1496 let hits = session.ref_query_cache_hits();
1497 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1498 assert_eq!(session.ref_query_cache_hits(), hits);
1499 }
1500
1501 #[test]
1504 fn re_analyze_file_cache_hit_cancels_spanning_queries() {
1505 let dir = tempfile::tempdir().unwrap();
1506 let mut session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(dir.path());
1507 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1508 let opts = crate::BatchOptions::new();
1509 session.ingest_file(files[0].clone(), Arc::from(BASE));
1510 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1511 session.prepare_for_query(None);
1512 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1513
1514 let symbol = Name::method("Base", "run");
1515 let snap = session.snapshot();
1516 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1517 let revision = session.text_revision();
1518 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1519 assert_eq!(session.text_revision(), revision, "took the analysis path");
1520 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1521 drop(snap);
1522
1523 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1524 }
1525
1526 fn prepared_workspace(caller_text: &str) -> (AnalysisSession, Vec<Arc<str>>) {
1527 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1528 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1529 session.ingest_file(files[0].clone(), Arc::from(BASE));
1530 session.ingest_file(files[1].clone(), Arc::from(caller_text));
1531 session.prepare_for_query(Some(&files[1]));
1532 (session, files)
1533 }
1534
1535 fn await_pending_write(snap: &AnalysisSnapshot) {
1538 while catch(|| snap.db.unwind_if_revision_cancelled()).is_ok() {
1539 std::thread::yield_now();
1540 }
1541 }
1542
1543 fn callers_of(
1544 session: &mut AnalysisSession,
1545 files: &[Arc<str>],
1546 method: &str,
1547 ) -> Vec<Arc<str>> {
1548 session
1549 .indexed_references_to(
1550 &Name::method("Base", method),
1551 files,
1552 false,
1553 ReferenceIncludes::Plain,
1554 &|| false,
1555 )
1556 .unwrap()
1557 .into_iter()
1558 .map(|(file, _)| file)
1559 .collect()
1560 }
1561
1562 #[derive(Debug, PartialEq)]
1565 struct AnalysisOutcome {
1566 issues: Vec<String>,
1567 symbols: usize,
1568 vendor_class_loaded: bool,
1569 vendor_function_loaded: bool,
1570 eager_files_drained: bool,
1571 stubs_loaded: usize,
1572 prepared: bool,
1573 prepare_generation: u64,
1574 references: Vec<String>,
1575 }
1576
1577 const DEPENDENT: &str = "<?php\nnamespace App;\nfunction go(\\Vendor\\Base $b): void { vendor_helper('x'); new \\ArrayObject(); }\n";
1578
1579 fn composer_project(root: &std::path::Path) -> AnalysisSession {
1580 let vendor = root.join("vendor");
1581 std::fs::create_dir_all(vendor.join("composer")).unwrap();
1582 std::fs::create_dir_all(vendor.join("helpers")).unwrap();
1583 std::fs::create_dir_all(root.join("lib")).unwrap();
1584 std::fs::write(
1585 vendor.join("composer/autoload_files.php"),
1586 "<?php\n$vendorDir = dirname(__DIR__);\n$baseDir = dirname($vendorDir);\nreturn array(\n 'abc123' => $vendorDir . '/helpers/functions.php',\n);\n",
1587 )
1588 .unwrap();
1589 for empty in ["psr4", "classmap", "namespaces"] {
1590 std::fs::write(
1591 vendor.join(format!("composer/autoload_{empty}.php")),
1592 "<?php\nreturn [];\n",
1593 )
1594 .unwrap();
1595 }
1596 std::fs::write(
1597 vendor.join("helpers/functions.php"),
1598 "<?php\nfunction vendor_helper(string $s): string { return $s; }\n",
1599 )
1600 .unwrap();
1601 std::fs::write(
1602 root.join("lib/Base.php"),
1603 "<?php\nnamespace Vendor;\nclass Base {}\n",
1604 )
1605 .unwrap();
1606 std::fs::write(
1607 root.join("composer.json"),
1608 r#"{"autoload":{"psr-4":{"App\\":"src/","Vendor\\":"lib/"}}}"#,
1609 )
1610 .unwrap();
1611 let psr4 = crate::composer::Psr4Map::from_composer(root).unwrap();
1612 AnalysisSession::new(PhpVersion::LATEST).with_psr4(Arc::new(psr4))
1613 }
1614
1615 fn outcome(
1616 session: &AnalysisSession,
1617 file: &Arc<str>,
1618 analysis: crate::FileAnalysis,
1619 ) -> AnalysisOutcome {
1620 let text = session.source_of(file).unwrap();
1621 AnalysisOutcome {
1622 issues: analysis
1623 .issues
1624 .iter()
1625 .map(|i| format!("{:?}", i.kind))
1626 .collect(),
1627 symbols: analysis.symbols.len(),
1628 vendor_class_loaded: {
1629 let view = session.db_view();
1630 let db = view.db();
1631 crate::db::class_like_indexed(db, crate::db::Fqcn::from_str(db, "Vendor\\Base"))
1632 },
1633 vendor_function_loaded: session.contains_function("vendor_helper"),
1634 eager_files_drained: session.pending_eager_function_files.is_none(),
1635 stubs_loaded: session.loaded_stub_count(),
1636 prepared: session.is_prepared_for_analysis(
1637 file,
1638 &text,
1639 session.prepare_generation_snapshot(),
1640 ),
1641 prepare_generation: session.prepare_generation_snapshot(),
1642 references: ["cls:Vendor\\Base", "fn:vendor_helper", "cls:ArrayObject"]
1643 .into_iter()
1644 .map(|key| format!("{key}={:?}", session.reference_locations(key)))
1645 .collect(),
1646 }
1647 }
1648
1649 #[test]
1653 fn prepare_for_query_then_snapshot_analyze_matches_file_analyzer() {
1654 let file: Arc<str> = Arc::from("consumer.php");
1655 let parsed = php_rs_parser::parse(DEPENDENT);
1656
1657 let root = tempfile::tempdir().unwrap();
1658 let mut via_analyzer = composer_project(root.path());
1659 via_analyzer.ingest_file(file.clone(), Arc::from(DEPENDENT));
1660 let analysis = crate::FileAnalyzer::new(&mut via_analyzer).analyze(
1661 file.clone(),
1662 DEPENDENT,
1663 &parsed.program,
1664 &parsed.source_map,
1665 );
1666 let expected = outcome(&via_analyzer, &file, analysis);
1667
1668 let root = tempfile::tempdir().unwrap();
1669 let mut via_snapshot = composer_project(root.path());
1670 via_snapshot.ingest_file(file.clone(), Arc::from(DEPENDENT));
1671 via_snapshot.prepare_for_query(Some(&file));
1672 let analysis = via_snapshot
1673 .snapshot()
1674 .analyze(file.clone(), DEPENDENT, &parsed.program, &parsed.source_map)
1675 .unwrap();
1676 let actual = outcome(&via_snapshot, &file, analysis);
1677
1678 assert!(
1679 expected.prepared && expected.vendor_function_loaded,
1680 "{expected:#?}"
1681 );
1682 assert_eq!(actual, expected);
1683 }
1684
1685 #[test]
1689 fn prepare_for_query_warms_nothing_for_a_file_that_was_never_ingested() {
1690 let file: Arc<str> = Arc::from("consumer.php");
1691 let parsed = php_rs_parser::parse(DEPENDENT);
1692 let unresolved = |analysis: &crate::FileAnalysis| {
1693 analysis
1694 .issues
1695 .iter()
1696 .any(|i| matches!(i.kind, crate::IssueKind::UndefinedFunction { .. }))
1697 };
1698
1699 let root = tempfile::tempdir().unwrap();
1700 let mut via_analyzer = composer_project(root.path());
1701 let analysis = crate::FileAnalyzer::new(&mut via_analyzer).analyze(
1702 file.clone(),
1703 DEPENDENT,
1704 &parsed.program,
1705 &parsed.source_map,
1706 );
1707 assert!(!unresolved(&analysis), "{:?}", analysis.issues);
1708
1709 let root = tempfile::tempdir().unwrap();
1710 let mut via_snapshot = composer_project(root.path());
1711 via_snapshot.prepare_for_query(Some(&file));
1712 let analysis = via_snapshot
1713 .snapshot()
1714 .analyze(file, DEPENDENT, &parsed.program, &parsed.source_map)
1715 .unwrap();
1716 assert!(unresolved(&analysis), "{:?}", analysis.issues);
1717 }
1718}