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, false)
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(
984 &self,
985 class_fqn: &str,
986 include_trait_users: bool,
987 ) -> Result<Vec<Arc<str>>, Cancelled> {
988 let files = self.db.source_file_paths();
989 let mut out: Vec<Arc<str>> = self
990 .indexed_subtype_classes(class_fqn, &files, include_trait_users)?
991 .into_iter()
992 .map(|s| s.file)
993 .collect();
994 out.sort();
995 out.dedup();
996 Ok(out)
997 }
998
999 pub fn indexed_subtype_classes(
1018 &self,
1019 class_fqn: &str,
1020 files: &[Arc<str>],
1021 include_trait_users: bool,
1022 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1023 self.subtype_classes_memoized(class_fqn, files, include_trait_users, false)
1024 }
1025
1026 pub fn indexed_direct_subtype_classes(
1030 &self,
1031 class_fqn: &str,
1032 files: &[Arc<str>],
1033 include_trait_users: bool,
1034 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1035 self.subtype_classes_memoized(class_fqn, files, include_trait_users, true)
1036 }
1037
1038 fn subtype_classes_memoized(
1039 &self,
1040 class_fqn: &str,
1041 files: &[Arc<str>],
1042 include_trait_users: bool,
1043 direct_only: bool,
1044 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1045 catch(|| {
1046 let key = SubtypeQueryCacheKey {
1047 class_fqn: class_fqn.trim_start_matches('\\').to_ascii_lowercase(),
1048 include_trait_users,
1049 direct_only,
1050 generation: self.query_cache_generation(),
1051 files_hash: hash_files(files),
1052 };
1053 if let Some(hit) = self.index.subtype_queries.get(&key) {
1054 return hit;
1055 }
1056 let out =
1057 self.subtype_classes_uncached(class_fqn, files, include_trait_users, direct_only);
1058 if let Err(cancelled) = self.ensure_no_retire_since(key.generation) {
1059 unwind(cancelled);
1060 }
1061 let generation = self.query_cache_generation();
1063 if generation == key.generation {
1064 self.index.subtype_queries.insert(generation, key, &out);
1065 }
1066 out
1067 })
1068 }
1069
1070 fn subtype_classes_uncached(
1071 &self,
1072 class_fqn: &str,
1073 files: &[Arc<str>],
1074 include_trait_users: bool,
1075 direct_only: bool,
1076 ) -> Vec<SubtypeClassSite> {
1077 let db = &self.db;
1078 let mut scanned: HashSet<String> = HashSet::default();
1079 let mut pending: Vec<String> = vec![class_fqn.trim_start_matches('\\').to_string()];
1080 let mut sites: Vec<crate::db::SubtypeSite> = Vec::new();
1081 while !pending.is_empty() {
1082 let needles: Vec<String> = pending
1085 .drain(..)
1086 .filter(|f| scanned.insert(f.clone()))
1087 .map(|f| crate::db::subtype_index::short_name_of(&f).to_string())
1088 .collect();
1089 if !needles.is_empty() {
1090 self.commit_defs_for_matching(files, &needles);
1091 }
1092 sites = db.subtype_sites_of(class_fqn, include_trait_users, direct_only);
1093 if direct_only {
1094 break;
1095 }
1096 pending = sites
1097 .iter()
1098 .map(|s| s.fqcn.trim_start_matches('\\').to_string())
1099 .filter(|f| !scanned.contains(f))
1100 .collect();
1101 }
1102 let mut out: Vec<SubtypeClassSite> = sites
1103 .into_iter()
1104 .filter_map(|s| {
1105 let loc = s.location.as_ref()?;
1106 let short = crate::db::subtype_index::short_name_of(&s.fqcn).to_string();
1107 let range = self.refine_location_to_name(loc, &short);
1108 Some(SubtypeClassSite {
1109 fqcn: s.fqcn,
1110 kind: s.kind,
1111 is_abstract: s.is_abstract,
1112 file: s.file,
1113 range,
1114 })
1115 })
1116 .collect();
1117 let root_lc = class_fqn.trim_start_matches('\\').to_ascii_lowercase();
1121 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
1122 for (file, line, cs, ce) in db.reference_locations(&format!("impl:{root_lc}")) {
1123 if !scope.contains(file.as_ref()) {
1124 continue;
1125 }
1126 let range = span_range(line, cs as u32, ce as u32);
1127 if out.iter().any(|s| s.file == file && s.range == range) {
1128 continue;
1129 }
1130 out.push(SubtypeClassSite {
1131 fqcn: Arc::from("class@anonymous"),
1132 kind: crate::db::ClassLikeKind::Class,
1133 is_abstract: false,
1134 file,
1135 range,
1136 });
1137 }
1138 out
1139 }
1140
1141 #[allow(clippy::type_complexity)]
1147 pub fn indexed_method_implementations(
1148 &self,
1149 class_fqn: &str,
1150 method: &str,
1151 files: &[Arc<str>],
1152 ) -> Result<Vec<(Arc<str>, Arc<str>, crate::Range)>, Cancelled> {
1153 let subs = self.indexed_subtype_classes(class_fqn, files, false)?;
1154 catch(|| {
1155 let db = &self.db;
1156 let mut out: Vec<(Arc<str>, Arc<str>, crate::Range)> = Vec::new();
1157 for sub in &subs {
1158 let here = crate::db::Fqcn::from_str(db, sub.fqcn.as_ref());
1159 let Some((_, m)) = crate::db::find_method_in_chain(db, here, method) else {
1160 continue;
1161 };
1162 if m.is_abstract {
1163 continue;
1164 }
1165 let Some(loc) = m.location.as_ref() else {
1166 continue;
1167 };
1168 let range = self.refine_location_to_name(loc, method);
1169 out.push((sub.fqcn.clone(), loc.file.clone(), range));
1170 }
1171 out.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.start.line.cmp(&b.2.start.line)));
1172 out.dedup_by(|a, b| a.1 == b.1 && a.2 == b.2);
1173 out
1174 })
1175 }
1176
1177 fn commit_defs_for_matching(&self, files: &[Arc<str>], shorts: &[String]) {
1190 use rayon::prelude::*;
1191
1192 self.db
1196 .add_literal_mention_names(shorts.iter().map(|s| s.as_str()));
1197 let queries: Vec<_> = shorts
1198 .iter()
1199 .filter_map(|s| self.db.prepare_class_mention_query(s))
1200 .collect();
1201 let mention_scanner = self.db.class_mention_scanner();
1202 let use_mentions = queries.len() == shorts.len() && mention_scanner.is_some();
1206 type Work = (Arc<str>, Arc<str>, Vec<crate::db::SubtypeEntry>);
1207 let index = &*self.index;
1208 let results: Vec<(Option<Work>, Option<MentionScanRecord>)> = files
1209 .par_iter()
1210 .map_with(self.db.clone(), |db, path| {
1211 let Some(sf) = db.lookup_source_file(path.as_ref()) else {
1212 return (None, None);
1213 };
1214 let text = sf.text(&*db as &dyn MirDatabase).clone();
1215 let freshness = index.defs_freshness(sf, &text);
1216 if freshness == DefsFreshness::Fresh {
1217 return (None, None);
1218 }
1219 let mut scan_rec: Option<MentionScanRecord> = None;
1223 if use_mentions && freshness == DefsFreshness::Uncommitted {
1224 let mut answer = Some(false);
1225 for q in &queries {
1226 match db.class_mention_answer(path.as_ref(), q, &text) {
1227 Some(true) => {
1228 answer = Some(true);
1229 break;
1230 }
1231 Some(false) => {}
1232 None => answer = None,
1233 }
1234 }
1235 let hit = match answer {
1236 Some(hit) => hit,
1237 None => {
1238 let scanner = mention_scanner.as_ref().unwrap();
1241 let names = scanner.scan(&text);
1242 let hit = queries.iter().any(|q| names.binary_search(&q.name).is_ok());
1243 scan_rec = Some((path.clone(), text.clone(), names));
1244 hit
1245 }
1246 };
1247 if !hit {
1248 return (None, scan_rec);
1249 }
1250 }
1251 let defs = crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
1252 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
1253 (Some((path.clone(), text, entries)), scan_rec)
1254 })
1255 .collect();
1256 let mut refused = Ok(());
1260 for (work, scan_rec) in results {
1261 if let (Some(scanner), Some((file, text, names))) = (&mention_scanner, scan_rec) {
1262 self.db
1263 .set_file_class_mentions(&file, &text, scanner.epoch(), names);
1264 }
1265 if let Some((file, text, entries)) = work {
1266 if let Err(cancelled) = self.index.commit_class_edges(
1267 &self.db,
1268 &file,
1269 &text,
1270 entries,
1271 self.retire_epoch,
1272 ) {
1273 refused = Err(cancelled);
1274 }
1275 }
1276 }
1277 if let Err(cancelled) = refused {
1278 unwind(cancelled);
1279 }
1280 }
1281
1282 pub fn class_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1290 catch(|| {
1291 let db = &self.db;
1292 let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
1293 let file_data: Vec<(Arc<str>, Arc<str>)> = files
1294 .iter()
1295 .filter_map(|f| {
1296 let sf = db.lookup_source_file(f)?;
1297 Some((f.clone(), sf.text(db as &dyn MirDatabase).clone()))
1298 })
1299 .collect();
1300 crate::class::ClassAnalyzer::with_files(db, file_set, &file_data).analyze_all()
1301 })
1302 }
1303
1304 pub fn collector_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1313 catch(|| {
1314 files
1315 .iter()
1316 .filter_map(|f| self.db.lookup_source_file(f))
1317 .flat_map(|sf| {
1318 crate::db::collect_file_definitions(&self.db, sf)
1319 .issues
1320 .as_ref()
1321 .clone()
1322 })
1323 .collect()
1324 })
1325 }
1326
1327 pub(super) fn reference_gate(&self, symbol: &crate::Name) -> ReferenceGate {
1338 ReferenceGate::for_symbol(&self.db, symbol)
1339 }
1340}
1341
1342#[cfg(test)]
1343mod tests {
1344 use super::*;
1345 use crate::{AnalysisSession, IndexCancel, Name, ReferenceIncludes};
1346 use salsa::Database as _;
1347
1348 const BASE: &str =
1349 "<?php\nclass Base { public function run(): void {} public function stop(): void {} }\n";
1350 const CALLS_RUN: &str = "<?php\nfunction go(Base $b): void { $b->run(); }\n";
1351 const CALLS_STOP: &str = "<?php\nfunction go(Base $b): void { $b->stop(); }\n";
1352
1353 #[test]
1357 fn warm_commit_racing_a_text_write_leaves_the_file_stale() {
1358 let (mut session, files) = prepared_workspace(CALLS_RUN);
1359 let snap = session.snapshot();
1360 let mut pass = snap
1361 .stage_warm(&files[1..], &IndexCancel::new())
1362 .unwrap()
1363 .unwrap();
1364
1365 let caller = files[1].clone();
1366 let writer = std::thread::spawn(move || {
1367 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1368 session
1369 });
1370 await_pending_write(&snap);
1371 snap.commit_warm(&mut pass).unwrap();
1372 drop(snap);
1373 let mut session = writer.join().unwrap();
1374
1375 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1376 assert!(callers_of(&mut session, &files, "run").is_empty());
1377 }
1378
1379 #[test]
1380 fn warm_commit_after_an_ingest_retired_the_file_is_refused() {
1381 let (session, files) = prepared_workspace(CALLS_RUN);
1382 let snap = session.snapshot();
1383 let mut pass = snap
1384 .stage_warm(&files[1..], &IndexCancel::new())
1385 .unwrap()
1386 .unwrap();
1387
1388 let caller = files[1].clone();
1389 let writer = std::thread::spawn(move || {
1390 let mut session = session;
1391 session.ingest_file(caller, Arc::from(CALLS_STOP));
1392 session
1393 });
1394 await_pending_write(&snap);
1395 assert!(snap.commit_warm(&mut pass).is_err());
1396 drop(snap);
1397 let mut session = writer.join().unwrap();
1398
1399 assert!(callers_of(&mut session, &files, "run").is_empty());
1400 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1401 }
1402
1403 #[test]
1404 fn commits_after_invalidate_file_retired_the_file_are_refused() {
1405 const CHILD_CALLS_RUN: &str =
1406 "<?php\nclass Child extends Base {}\nfunction go(Base $b): void { $b->run(); }\n";
1407 let (session, files) = prepared_workspace(CHILD_CALLS_RUN);
1408 let snap = session.snapshot();
1409 let mut pass = snap
1410 .stage_warm(&files[1..], &IndexCancel::new())
1411 .unwrap()
1412 .unwrap();
1413 let parsed = php_rs_parser::parse(CHILD_CALLS_RUN);
1414
1415 let caller = files[1].clone();
1416 let writer = std::thread::spawn(move || {
1417 let mut session = session;
1418 session.invalidate_file(&caller);
1419 session
1420 });
1421 await_pending_write(&snap);
1422 assert!(snap.commit_warm(&mut pass).is_err());
1423 assert!(snap
1424 .analyze(
1425 files[1].clone(),
1426 CHILD_CALLS_RUN,
1427 &parsed.program,
1428 &parsed.source_map
1429 )
1430 .is_err());
1431 drop(snap);
1432 let mut session = writer.join().unwrap();
1433
1434 assert!(callers_of(&mut session, &files, "run").is_empty());
1435 assert!(session
1436 .indexed_subtype_classes("Base", &files, false)
1437 .is_empty());
1438 }
1439
1440 #[test]
1441 fn cancelled_reference_commit_reports_cancellation() {
1442 let (mut session, files) = prepared_workspace(CALLS_RUN);
1443 let snap = session.snapshot();
1444 let stale = snap.stale_reference_candidates_unguarded(&Name::method("Base", "run"), &files);
1445 assert!(stale.contains(&files[1]));
1446
1447 let caller = files[1].clone();
1448 let writer = std::thread::spawn(move || {
1449 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1450 session
1451 });
1452 await_pending_write(&snap);
1453 assert!(snap.commit_reference_candidates(&stale).is_err());
1454 drop(snap);
1455 writer.join().unwrap();
1456 }
1457
1458 const ROUTES_CALLS_RUN: &str = "<?php\n(new Base())->run();\n";
1461
1462 fn registered_routes_workspace() -> (AnalysisSession, Vec<Arc<str>>) {
1466 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1467 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("routes.php")];
1468 session.ingest_file(files[0].clone(), Arc::from(BASE));
1469 session.upsert_source_file(
1470 files[1].clone(),
1471 Arc::from(ROUTES_CALLS_RUN),
1472 salsa::Durability::LOW,
1473 );
1474 session.prepare_for_query(None);
1475 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1476 (session, files)
1477 }
1478
1479 #[test]
1483 fn ingest_of_unchanged_text_mid_query_keeps_references() {
1484 let (mut session, files) = registered_routes_workspace();
1485 let symbol = Name::method("Base", "run");
1486 let revision = session.text_revision();
1487
1488 let snap = session.snapshot();
1489 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1490 assert!(snap
1491 .stale_reference_candidates_unguarded(&symbol, &files)
1492 .is_empty());
1493 session.ingest_file(files[1].clone(), Arc::from(ROUTES_CALLS_RUN));
1494 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1495 assert!(snap.ensure_no_retire_since(key.generation).is_ok());
1496 snap.memoize_references(key, &out);
1497 drop(snap);
1498
1499 assert_eq!(session.text_revision(), revision, "ingest wrote salsa");
1500 assert_eq!(
1501 out.into_iter().map(|(file, _)| file).collect::<Vec<_>>(),
1502 [files[1].clone()]
1503 );
1504 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1505 }
1506
1507 #[test]
1511 fn query_spanning_a_retirement_is_cancelled_and_not_memoized() {
1512 let (mut session, files) = registered_routes_workspace();
1513 let symbol = Name::method("Base", "run");
1514
1515 let snap = session.snapshot();
1516 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1517 assert!(snap
1518 .stale_reference_candidates_unguarded(&symbol, &files)
1519 .is_empty());
1520 session
1521 .index
1522 .retire_references(&session.db.salsa, files[1].as_ref());
1523 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1524 assert!(out.is_empty());
1525 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1526 snap.memoize_references(key, &out);
1527 drop(snap);
1528
1529 let hits = session.ref_query_cache_hits();
1530 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1531 assert_eq!(session.ref_query_cache_hits(), hits);
1532 }
1533
1534 #[test]
1537 fn re_analyze_file_cache_hit_cancels_spanning_queries() {
1538 let dir = tempfile::tempdir().unwrap();
1539 let mut session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(dir.path());
1540 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1541 let opts = crate::BatchOptions::new();
1542 session.ingest_file(files[0].clone(), Arc::from(BASE));
1543 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1544 session.prepare_for_query(None);
1545 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1546
1547 let symbol = Name::method("Base", "run");
1548 let snap = session.snapshot();
1549 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1550 let revision = session.text_revision();
1551 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1552 assert_eq!(session.text_revision(), revision, "took the analysis path");
1553 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1554 drop(snap);
1555
1556 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1557 }
1558
1559 fn prepared_workspace(caller_text: &str) -> (AnalysisSession, Vec<Arc<str>>) {
1560 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1561 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1562 session.ingest_file(files[0].clone(), Arc::from(BASE));
1563 session.ingest_file(files[1].clone(), Arc::from(caller_text));
1564 session.prepare_for_query(Some(&files[1]));
1565 (session, files)
1566 }
1567
1568 fn await_pending_write(snap: &AnalysisSnapshot) {
1571 while catch(|| snap.db.unwind_if_revision_cancelled()).is_ok() {
1572 std::thread::yield_now();
1573 }
1574 }
1575
1576 fn callers_of(
1577 session: &mut AnalysisSession,
1578 files: &[Arc<str>],
1579 method: &str,
1580 ) -> Vec<Arc<str>> {
1581 session
1582 .indexed_references_to(
1583 &Name::method("Base", method),
1584 files,
1585 false,
1586 ReferenceIncludes::Plain,
1587 &|| false,
1588 )
1589 .unwrap()
1590 .into_iter()
1591 .map(|(file, _)| file)
1592 .collect()
1593 }
1594
1595 #[derive(Debug, PartialEq)]
1598 struct AnalysisOutcome {
1599 issues: Vec<String>,
1600 symbols: usize,
1601 vendor_class_loaded: bool,
1602 vendor_function_loaded: bool,
1603 eager_files_drained: bool,
1604 stubs_loaded: usize,
1605 prepared: bool,
1606 prepare_generation: u64,
1607 references: Vec<String>,
1608 }
1609
1610 const DEPENDENT: &str = "<?php\nnamespace App;\nfunction go(\\Vendor\\Base $b): void { vendor_helper('x'); new \\ArrayObject(); }\n";
1611
1612 fn composer_project(root: &std::path::Path) -> AnalysisSession {
1613 let vendor = root.join("vendor");
1614 std::fs::create_dir_all(vendor.join("composer")).unwrap();
1615 std::fs::create_dir_all(vendor.join("helpers")).unwrap();
1616 std::fs::create_dir_all(root.join("lib")).unwrap();
1617 std::fs::write(
1618 vendor.join("composer/autoload_files.php"),
1619 "<?php\n$vendorDir = dirname(__DIR__);\n$baseDir = dirname($vendorDir);\nreturn array(\n 'abc123' => $vendorDir . '/helpers/functions.php',\n);\n",
1620 )
1621 .unwrap();
1622 for empty in ["psr4", "classmap", "namespaces"] {
1623 std::fs::write(
1624 vendor.join(format!("composer/autoload_{empty}.php")),
1625 "<?php\nreturn [];\n",
1626 )
1627 .unwrap();
1628 }
1629 std::fs::write(
1630 vendor.join("helpers/functions.php"),
1631 "<?php\nfunction vendor_helper(string $s): string { return $s; }\n",
1632 )
1633 .unwrap();
1634 std::fs::write(
1635 root.join("lib/Base.php"),
1636 "<?php\nnamespace Vendor;\nclass Base {}\n",
1637 )
1638 .unwrap();
1639 std::fs::write(
1640 root.join("composer.json"),
1641 r#"{"autoload":{"psr-4":{"App\\":"src/","Vendor\\":"lib/"}}}"#,
1642 )
1643 .unwrap();
1644 let psr4 = crate::composer::Psr4Map::from_composer(root).unwrap();
1645 AnalysisSession::new(PhpVersion::LATEST).with_psr4(Arc::new(psr4))
1646 }
1647
1648 fn outcome(
1649 session: &AnalysisSession,
1650 file: &Arc<str>,
1651 analysis: crate::FileAnalysis,
1652 ) -> AnalysisOutcome {
1653 let text = session.source_of(file).unwrap();
1654 AnalysisOutcome {
1655 issues: analysis
1656 .issues
1657 .iter()
1658 .map(|i| format!("{:?}", i.kind))
1659 .collect(),
1660 symbols: analysis.symbols.len(),
1661 vendor_class_loaded: {
1662 let view = session.db_view();
1663 let db = view.db();
1664 crate::db::class_like_indexed(db, crate::db::Fqcn::from_str(db, "Vendor\\Base"))
1665 },
1666 vendor_function_loaded: session.contains_function("vendor_helper"),
1667 eager_files_drained: session.pending_eager_function_files.is_none(),
1668 stubs_loaded: session.loaded_stub_count(),
1669 prepared: session.is_prepared_for_analysis(
1670 file,
1671 &text,
1672 session.prepare_generation_snapshot(),
1673 ),
1674 prepare_generation: session.prepare_generation_snapshot(),
1675 references: ["cls:Vendor\\Base", "fn:vendor_helper", "cls:ArrayObject"]
1676 .into_iter()
1677 .map(|key| format!("{key}={:?}", session.reference_locations(key)))
1678 .collect(),
1679 }
1680 }
1681
1682 #[test]
1686 fn prepare_for_query_then_snapshot_analyze_matches_file_analyzer() {
1687 let file: Arc<str> = Arc::from("consumer.php");
1688 let parsed = php_rs_parser::parse(DEPENDENT);
1689
1690 let root = tempfile::tempdir().unwrap();
1691 let mut via_analyzer = composer_project(root.path());
1692 via_analyzer.ingest_file(file.clone(), Arc::from(DEPENDENT));
1693 let analysis = crate::FileAnalyzer::new(&mut via_analyzer).analyze(
1694 file.clone(),
1695 DEPENDENT,
1696 &parsed.program,
1697 &parsed.source_map,
1698 );
1699 let expected = outcome(&via_analyzer, &file, analysis);
1700
1701 let root = tempfile::tempdir().unwrap();
1702 let mut via_snapshot = composer_project(root.path());
1703 via_snapshot.ingest_file(file.clone(), Arc::from(DEPENDENT));
1704 via_snapshot.prepare_for_query(Some(&file));
1705 let analysis = via_snapshot
1706 .snapshot()
1707 .analyze(file.clone(), DEPENDENT, &parsed.program, &parsed.source_map)
1708 .unwrap();
1709 let actual = outcome(&via_snapshot, &file, analysis);
1710
1711 assert!(
1712 expected.prepared && expected.vendor_function_loaded,
1713 "{expected:#?}"
1714 );
1715 assert_eq!(actual, expected);
1716 }
1717
1718 #[test]
1722 fn prepare_for_query_warms_nothing_for_a_file_that_was_never_ingested() {
1723 let file: Arc<str> = Arc::from("consumer.php");
1724 let parsed = php_rs_parser::parse(DEPENDENT);
1725 let unresolved = |analysis: &crate::FileAnalysis| {
1726 analysis
1727 .issues
1728 .iter()
1729 .any(|i| matches!(i.kind, crate::IssueKind::UndefinedFunction { .. }))
1730 };
1731
1732 let root = tempfile::tempdir().unwrap();
1733 let mut via_analyzer = composer_project(root.path());
1734 let analysis = crate::FileAnalyzer::new(&mut via_analyzer).analyze(
1735 file.clone(),
1736 DEPENDENT,
1737 &parsed.program,
1738 &parsed.source_map,
1739 );
1740 assert!(!unresolved(&analysis), "{:?}", analysis.issues);
1741
1742 let root = tempfile::tempdir().unwrap();
1743 let mut via_snapshot = composer_project(root.path());
1744 via_snapshot.prepare_for_query(Some(&file));
1745 let analysis = via_snapshot
1746 .snapshot()
1747 .analyze(file, DEPENDENT, &parsed.program, &parsed.source_map)
1748 .unwrap();
1749 assert!(unresolved(&analysis), "{:?}", analysis.issues);
1750 }
1751}