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 implemented_method(
208 &self,
209 method: &crate::Name,
210 ) -> Result<Option<crate::Name>, Cancelled> {
211 let crate::Name::Method { class, name } = method else {
212 return Ok(None);
213 };
214 self.read(|db| {
215 let here = crate::db::Fqcn::from_str(db, class.as_ref());
216 crate::db::class_ancestors_by_fqcn(db, here)
217 .iter()
218 .skip(1)
219 .find_map(|ancestor| {
220 let fqcn = crate::db::Fqcn::from_str(db, ancestor.as_ref());
221 let is_interface = matches!(
222 crate::db::find_class_like(db, fqcn)?,
223 crate::db::ClassLike::Interface(_)
224 );
225 let m = crate::db::find_method_in_class(db, fqcn, name)?;
226 (is_interface || m.is_abstract)
227 .then(|| crate::Name::method(ancestor.clone(), name))
228 })
229 })
230 }
231
232 pub fn definition_of_cached(
235 &self,
236 symbol: &crate::Name,
237 ) -> Result<Result<mir_types::Location, crate::SymbolLookupError>, Cancelled> {
238 catch(|| self.definition_of_unguarded(symbol))
239 }
240
241 fn definition_of_unguarded(
242 &self,
243 symbol: &crate::Name,
244 ) -> Result<mir_types::Location, crate::SymbolLookupError> {
245 let db = &self.db;
246 match symbol {
247 crate::Name::Class(fqcn) => {
248 let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
249 let class = crate::db::find_class_like(db, here)
250 .ok_or(crate::SymbolLookupError::NotFound)?;
251 class
252 .location()
253 .cloned()
254 .ok_or(crate::SymbolLookupError::NoSourceLocation)
255 }
256 crate::Name::Function(fqn) => {
257 let here = crate::db::Fqcn::from_str(db, fqn.as_ref());
258 let f =
259 crate::db::find_function(db, here).ok_or(crate::SymbolLookupError::NotFound)?;
260 f.location
261 .clone()
262 .ok_or(crate::SymbolLookupError::NoSourceLocation)
263 }
264 crate::Name::Method { .. }
265 | crate::Name::Property { .. }
266 | crate::Name::ClassConstant { .. } => {
267 crate::db::member_location(db, symbol).ok_or(crate::SymbolLookupError::NotFound)
268 }
269 crate::Name::GlobalConstant(fqn) => self
270 .global_constant_location(fqn)
271 .ok_or(crate::SymbolLookupError::NoSourceLocation),
272 }
273 }
274
275 pub fn files_mentioning_class(
284 &self,
285 files: &[Arc<str>],
286 class_name: &str,
287 ) -> Result<Vec<Arc<str>>, Cancelled> {
288 self.files_mentioning_any(files, &[class_name])
289 }
290
291 pub fn files_mentioning_any(
296 &self,
297 files: &[Arc<str>],
298 needles: &[&str],
299 ) -> Result<Vec<Arc<str>>, Cancelled> {
300 catch(|| self.mentioning_any_unguarded(files, needles))
301 }
302
303 fn mentioning_any_unguarded(&self, files: &[Arc<str>], needles: &[&str]) -> Vec<Arc<str>> {
304 use rayon::prelude::*;
305
306 if needles.is_empty() {
307 return files.to_vec();
308 }
309 let db = self.db.clone();
310 db.add_literal_mention_names(needles.iter().copied());
311 let queries: Vec<_> = needles
313 .iter()
314 .filter_map(|n| db.prepare_class_mention_query(n))
315 .collect();
316 if queries.is_empty() {
317 return files.to_vec();
318 }
319 let Some(scanner) = db.class_mention_scanner() else {
320 return files.to_vec();
321 };
322
323 files
324 .par_iter()
325 .map_with(db, |db, f| -> Option<Arc<str>> {
326 let sf = db.lookup_source_file(f.as_ref())?;
327 let text = sf.text(&*db as &dyn MirDatabase).clone();
328 for q in &queries {
329 match db.class_mention_answer(f.as_ref(), q, &text) {
330 Some(true) => return Some(f.clone()),
331 Some(false) => continue,
332 None => {
333 let names = scanner.scan(text.as_ref());
334 let hit = queries
335 .iter()
336 .any(|q2| names.binary_search(&q2.name).is_ok());
337 db.set_file_class_mentions(f, &text, scanner.epoch(), names);
338 return hit.then(|| f.clone());
339 }
340 }
341 }
342 None
343 })
344 .flatten()
345 .collect()
346 }
347
348 pub fn indexed_references_to(
382 &self,
383 symbol: &crate::Name,
384 files: &[Arc<str>],
385 include_declaration: bool,
386 includes: crate::ReferenceIncludes,
387 ) -> Result<Vec<(Arc<str>, crate::Range)>, Cancelled> {
388 let key = self.reference_query_key(symbol, files, include_declaration, includes);
389 if let Some(hit) = self.index.ref_queries.get(&key) {
390 return Ok(hit);
391 }
392 let stale = catch(|| self.stale_reference_candidates_unguarded(symbol, files))?;
393 if !stale.is_empty() {
394 self.commit_reference_candidates(&stale)?;
395 }
396 let out = catch(|| self.read_references(symbol, files, include_declaration, includes))?;
397 self.ensure_no_retire_since(key.generation)?;
398 self.memoize_references(key, &out);
399 Ok(out)
400 }
401
402 pub(super) fn ensure_no_retire_since(
405 &self,
406 generation: QueryGeneration,
407 ) -> Result<(), Cancelled> {
408 if self.index.retire_seq() == generation.2 {
409 Ok(())
410 } else {
411 Err(Cancelled::PendingWrite)
412 }
413 }
414
415 pub fn indexed_use_import_locations(
419 &self,
420 symbol: &crate::Name,
421 files: &[Arc<str>],
422 ) -> Result<Vec<(Arc<str>, crate::Range)>, Cancelled> {
423 self.indexed_references_to(symbol, files, false, crate::ReferenceIncludes::UseImports)
424 }
425
426 pub(super) fn reference_query_key(
427 &self,
428 symbol: &crate::Name,
429 files: &[Arc<str>],
430 include_declaration: bool,
431 includes: crate::ReferenceIncludes,
432 ) -> RefQueryCacheKey {
433 RefQueryCacheKey {
434 symbol: symbol.codebase_key(),
435 include_declaration,
436 includes,
437 generation: self.query_cache_generation(),
438 files_hash: hash_files(files),
439 }
440 }
441
442 pub(super) fn memoize_references(
447 &self,
448 key: RefQueryCacheKey,
449 out: &[(Arc<str>, crate::Range)],
450 ) {
451 let generation = self.query_cache_generation();
452 if generation == key.generation {
453 self.index.ref_queries.insert(generation, key, out);
454 }
455 }
456
457 pub fn stale_reference_candidates(
465 &self,
466 symbol: &crate::Name,
467 files: &[Arc<str>],
468 ) -> Result<Vec<Arc<str>>, Cancelled> {
469 catch(|| self.stale_reference_candidates_unguarded(symbol, files))
470 }
471
472 pub(super) fn stale_reference_candidates_unguarded(
494 &self,
495 symbol: &crate::Name,
496 files: &[Arc<str>],
497 ) -> Vec<Arc<str>> {
498 let db = &self.db;
499 let gate = self.reference_gate(symbol);
500 let has_needles = !gate.idents.is_empty() || !gate.raw.is_empty();
501 let (mention_queries, mention_scanner) = if has_needles {
502 db.add_literal_mention_names(gate.idents.iter().map(|s| s.as_str()));
503 db.add_raw_mention_needles(gate.raw.iter().map(|s| s.as_str()));
504 let queries: Vec<_> = gate
505 .idents
506 .iter()
507 .chain(gate.raw.iter())
508 .filter_map(|s| db.prepare_class_mention_query(s))
509 .collect();
510 (queries, db.class_mention_scanner())
511 } else {
512 (Vec::new(), None)
513 };
514 let gate_complete = mention_queries.len() == gate.idents.len() + gate.raw.len()
518 && mention_scanner.is_some();
519 let current_gen = self.index_generation;
520 let mut stale = Vec::new();
521 let mut scanned: Vec<MentionScanRecord> = Vec::new();
522 for f in files {
523 let Some(sf) = db.lookup_source_file(f.as_ref()) else {
524 continue;
525 };
526 let text = sf.text(db as &dyn MirDatabase);
527 match self.index.ref_freshness(sf, text, current_gen) {
528 RefFreshness::Fresh => continue,
529 RefFreshness::Stale => {
530 stale.push(f.clone());
531 continue;
532 }
533 RefFreshness::StaleByGenerationOnly | RefFreshness::Uncommitted => {}
534 }
535 if has_needles && gate_complete {
536 let mut answer = Some(false);
540 for q in &mention_queries {
541 match db.class_mention_answer(f.as_ref(), q, text) {
542 Some(true) => {
543 answer = Some(true);
544 break;
545 }
546 Some(false) => {}
547 None => answer = None,
548 }
549 }
550 match answer {
551 Some(true) => {}
552 Some(false) => continue,
553 None => {
554 let scanner = mention_scanner
555 .as_ref()
556 .expect("gate_complete implies a scanner");
557 let names = scanner.scan(text);
558 let hit = mention_queries
559 .iter()
560 .any(|q| names.binary_search(&q.name).is_ok());
561 scanned.push((f.clone(), text.clone(), names));
562 if hit {
563 stale.push(f.clone());
564 }
565 continue;
566 }
567 }
568 }
569 stale.push(f.clone());
570 }
571 if let Some(scanner) = &mention_scanner {
574 for (file, text, names) in scanned {
575 db.set_file_class_mentions(&file, &text, scanner.epoch(), names);
576 }
577 }
578 stale
579 }
580
581 pub(super) fn commit_reference_candidates(&self, stale: &[Arc<str>]) -> Result<(), Cancelled> {
583 self.warm_pass(stale, &crate::IndexCancel::new()).map(drop)
584 }
585
586 pub fn warm_files(
593 &self,
594 files: &[Arc<str>],
595 cancel: &crate::IndexCancel,
596 ) -> Result<bool, Cancelled> {
597 Ok(self.warm_pass(files, cancel)?.is_some())
598 }
599
600 #[allow(clippy::type_complexity)]
610 pub fn reanalyze_files(
611 &self,
612 files: &[Arc<str>],
613 cancel: &crate::IndexCancel,
614 ) -> Result<Option<Vec<(Arc<str>, crate::FileAnalysis)>>, Cancelled> {
615 Ok(self.warm_pass(files, cancel)?.map(|analyzed| {
616 analyzed
617 .into_iter()
618 .map(|a| {
619 (
620 a.file,
621 crate::FileAnalysis {
622 issues: a.out.issues.to_vec(),
623 symbols: Vec::new(),
624 },
625 )
626 })
627 .collect()
628 }))
629 }
630
631 pub(super) fn warm_pass(
632 &self,
633 files: &[Arc<str>],
634 cancel: &crate::IndexCancel,
635 ) -> Result<Option<Vec<AnalyzedFile>>, Cancelled> {
636 let Some(mut pass) = self.stage_warm(files, cancel)? else {
637 return Ok(None);
638 };
639 self.commit_warm(&mut pass)?;
640 Ok(Some(pass.analyzed))
641 }
642
643 fn stage_warm(
648 &self,
649 files: &[Arc<str>],
650 cancel: &crate::IndexCancel,
651 ) -> Result<Option<WarmPass>, Cancelled> {
652 catch(|| {
653 let mut db = self.db.clone();
654 db.freeze_workspace_index();
655 let mention_scanner = db.class_mention_scanner();
656 let cache = self.cache.as_deref();
657 let mut analyzed = Vec::with_capacity(files.len());
658 for path in files {
659 if cancel.is_cancelled() {
660 return None;
661 }
662 analyzed.extend(self.index.stage_analyzed(
663 &db,
664 cache,
665 mention_scanner.as_deref(),
666 path,
667 ));
668 }
669 Some(WarmPass {
670 mention_scanner,
671 analyzed,
672 })
673 })
674 }
675
676 fn commit_warm(&self, pass: &mut WarmPass) -> Result<(), Cancelled> {
679 self.index.commit_analyzed(
680 &self.db,
681 self.cache.as_deref(),
682 pass.mention_scanner.as_deref(),
683 &mut pass.analyzed,
684 self.stamp(),
685 )
686 }
687
688 pub(super) fn read_references(
701 &self,
702 symbol: &crate::Name,
703 files: &[Arc<str>],
704 include_declaration: bool,
705 includes: crate::ReferenceIncludes,
706 ) -> Vec<(Arc<str>, crate::Range)> {
707 let key = symbol.codebase_key();
708 let hierarchy: Vec<String> = match symbol {
709 crate::Name::Method { class, name } => {
710 if class.is_empty() {
711 Vec::new()
712 } else if name.as_ref() == "__construct" {
713 vec![class.trim_start_matches('\\').to_string()]
714 } else {
715 self.member_hierarchy_classes(class.as_ref())
716 }
717 }
718 crate::Name::Property { class, .. } | crate::Name::ClassConstant { class, .. } => {
719 if class.is_empty() {
720 Vec::new()
721 } else {
722 self.member_hierarchy_classes(class.as_ref())
723 }
724 }
725 _ => Vec::new(),
726 };
727 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
728 let read_symbol_key = |symbol_key: &str| -> Vec<(Arc<str>, crate::Range)> {
729 self.db
730 .reference_locations(symbol_key)
731 .into_iter()
732 .filter(|(file, ..)| scope.contains(file.as_ref()))
733 .map(|(file, line, col_start, col_end)| {
734 (file, span_range(line, col_start as u32, col_end as u32))
735 })
736 .collect()
737 };
738 let mut scratch_key = String::new();
739 let mut read_composed_key = |prefix: &str, middle: &str, separator: &str, suffix: &str| {
740 scratch_key.clear();
741 scratch_key.reserve(prefix.len() + middle.len() + separator.len() + suffix.len());
742 scratch_key.push_str(prefix);
743 scratch_key.push_str(middle);
744 scratch_key.push_str(separator);
745 scratch_key.push_str(suffix);
746 read_symbol_key(&scratch_key)
747 };
748 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
749 if !matches!(includes, crate::ReferenceIncludes::UseImports) {
750 let member_prefix = match symbol {
751 crate::Name::Method { name, .. } => Some(("meth:", name)),
752 crate::Name::Property { name, .. } => Some(("prop:", name)),
753 crate::Name::ClassConstant { name, .. } => Some(("cnst:", name)),
754 _ => None,
755 };
756 match member_prefix {
757 Some((prefix, name)) => {
758 for class in &hierarchy {
759 out.extend(read_composed_key(prefix, class, "::", name));
760 }
761 }
762 None => out.extend(read_symbol_key(key.as_str())),
763 }
764 }
765 if !matches!(includes, crate::ReferenceIncludes::Plain) {
766 out.extend(read_composed_key("use:", key.as_str(), "", ""));
767 }
768 if matches!(includes, crate::ReferenceIncludes::Plain) && out.is_empty() {
769 match symbol {
770 crate::Name::Method { name, .. } => {
771 out = read_composed_key("methname:", name.as_ref(), "", "");
772 }
773 crate::Name::Property { name, .. } => {
774 out = read_composed_key("propname:", name.as_ref(), "", "");
775 }
776 _ => {}
777 }
778 }
779 out.sort_by(|a, b| {
780 a.0.cmp(&b.0)
781 .then(a.1.start.line.cmp(&b.1.start.line))
782 .then(a.1.start.column.cmp(&b.1.start.column))
783 });
784 out.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
785
786 if include_declaration {
787 let decls: Vec<(Arc<str>, crate::Range)> = match symbol {
788 crate::Name::Method { class, name }
789 | crate::Name::Property { class, name }
790 | crate::Name::ClassConstant { class, name } => {
791 if class.is_empty() {
792 let prefix = match symbol {
796 crate::Name::Method { .. } => "methdecl:",
797 crate::Name::Property { .. } => "propdecl:",
798 _ => "cnstdecl:",
799 };
800 read_composed_key(prefix, name.as_ref(), "", "")
801 } else {
802 self.member_decl_sites(&hierarchy, symbol)
803 }
804 }
805 _ => self.declaration_name_range(symbol).into_iter().collect(),
806 };
807 for (file, range) in decls {
808 if scope.contains(file.as_ref())
809 && !out.iter().any(|(f, r)| *f == file && *r == range)
810 {
811 out.push((file, range));
812 }
813 }
814 }
815 out
816 }
817
818 fn member_hierarchy_classes(&self, class_fqn: &str) -> Vec<String> {
824 let target = class_fqn.trim_start_matches('\\');
825 let mut out = vec![target.to_string()];
826 let db = &self.db;
827 let here = crate::db::Fqcn::from_str(db, target);
828 let ancestors = crate::db::class_ancestors_by_fqcn(db, here)
830 .iter()
831 .skip(1)
832 .map(|a| a.trim_start_matches('\\').to_string())
833 .collect::<Vec<_>>();
834 out.extend(ancestors);
835 out.extend(
836 db.subtype_sites_of(target, true, false)
837 .into_iter()
838 .map(|s| s.fqcn.trim_start_matches('\\').to_string()),
839 );
840 let mut seen: HashSet<String> = HashSet::default();
841 out.retain(|c| seen.insert(c.to_ascii_lowercase()));
842 out
843 }
844
845 fn member_decl_sites(
851 &self,
852 classes: &[String],
853 symbol: &crate::Name,
854 ) -> Vec<(Arc<str>, crate::Range)> {
855 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
856 let db = &self.db;
857 for class in classes {
858 let here = crate::db::Fqcn::from_str(db, class);
859 let (loc, needle) = match symbol {
860 crate::Name::Method { name, .. } => {
861 let Some(m) = crate::db::find_method_in_class(db, here, name) else {
862 continue;
863 };
864 (m.location.clone(), name.to_string())
865 }
866 crate::Name::Property { name, .. } => {
867 let Some(p) = crate::db::find_property_in_class(db, here, name) else {
868 continue;
869 };
870 (p.location.clone(), name.to_string())
871 }
872 crate::Name::ClassConstant { name, .. } => {
873 let Some(c) = crate::db::find_class_constant_in_class(db, here, name) else {
874 continue;
875 };
876 (c.location.clone(), name.to_string())
877 }
878 _ => continue,
879 };
880 let Some(loc) = loc else { continue };
881 let range = self.refine_location_to_name(&loc, &needle);
882 out.push((loc.file.clone(), range));
883 }
884 out
885 }
886
887 pub fn declaration_name_range_cached(
892 &self,
893 symbol: &crate::Name,
894 ) -> Result<Option<(Arc<str>, crate::Range)>, Cancelled> {
895 catch(|| self.declaration_name_range(symbol))
896 }
897
898 pub(super) fn declaration_name_range(
902 &self,
903 symbol: &crate::Name,
904 ) -> Option<(Arc<str>, crate::Range)> {
905 if let crate::Name::GlobalConstant(fqn) = symbol {
906 return self.global_constant_decl_range(fqn);
907 }
908 let loc = self.definition_of_unguarded(symbol).ok()?;
909 let declared_method_name: Arc<str>;
910 let short = match symbol {
911 crate::Name::Class(f) | crate::Name::Function(f) | crate::Name::GlobalConstant(f) => {
912 crate::db::subtype_index::short_name_of(f)
913 }
914 crate::Name::Method { class, name } => {
915 declared_method_name = crate::db::trait_alias_original_name(&self.db, class, name)
916 .unwrap_or_else(|| name.clone());
917 declared_method_name.as_ref()
918 }
919 crate::Name::Property { name, .. } | crate::Name::ClassConstant { name, .. } => {
920 name.as_ref()
921 }
922 };
923 let range = self.refine_location_to_name(&loc, short);
927 Some((loc.file.clone(), range))
928 }
929
930 fn refine_location_to_name(&self, loc: &mir_types::Location, needle: &str) -> crate::Range {
935 let fallback = span_range(loc.line, loc.col_start as u32, loc.col_end as u32);
936 let Some(text) = self
937 .db
938 .lookup_source_file(loc.file.as_ref())
939 .map(|sf| sf.text(&self.db as &dyn MirDatabase).clone())
940 else {
941 return fallback;
942 };
943 let needle_chars = needle.chars().count() as u32;
944 let first_line = loc.line.saturating_sub(1) as usize;
945 for case_insensitive in [false, true] {
950 for (idx, line_text) in text.lines().enumerate().skip(first_line) {
951 let line_no = idx as u32 + 1;
952 if line_no > loc.line_end {
953 break;
954 }
955 let min_col = if line_no == loc.line {
956 loc.col_start as usize
957 } else {
958 0
959 };
960 if let Some(col) = identifier_char_col(line_text, needle, min_col, case_insensitive)
961 {
962 return span_range(line_no, col, col + needle_chars);
963 }
964 }
965 }
966 fallback
967 }
968
969 fn global_constant_decl_site(&self, fqn: &str) -> Option<ConstantDeclSite> {
973 let short = crate::db::subtype_index::short_name_of(fqn);
974 let db = &self.db;
975 let index = crate::db::workspace_index(db);
976 let loc = index.constant_loc(mir_types::Name::from(fqn.trim_start_matches('\\')))?;
977 let file = loc.file().path(db).clone();
978 let sf = db.lookup_source_file(file.as_ref())?;
979 let text = sf.text(db as &dyn MirDatabase);
980 for (idx, line) in text.lines().enumerate() {
981 let trimmed = line.trim_start();
982 let is_decl_line = trimmed.starts_with("const ")
983 || trimmed.contains("define(")
984 || trimmed.contains("define (");
985 if !is_decl_line {
986 continue;
987 }
988 if let Some(name_col) = identifier_char_col(line, short, 0, false) {
989 let stmt_end = match line.find(';') {
990 Some(byte) => line[..=byte].chars().count(),
991 None => line.chars().count(),
992 };
993 return Some(ConstantDeclSite {
994 file,
995 line: idx as u32 + 1,
996 stmt_col: (line.chars().count() - trimmed.chars().count()) as u32,
997 stmt_end: stmt_end as u32,
998 name_col,
999 name_len: short.chars().count() as u32,
1000 });
1001 }
1002 }
1003 None
1004 }
1005
1006 fn global_constant_decl_range(&self, fqn: &str) -> Option<(Arc<str>, crate::Range)> {
1007 let site = self.global_constant_decl_site(fqn)?;
1008 let range = span_range(site.line, site.name_col, site.name_col + site.name_len);
1009 Some((site.file, range))
1010 }
1011
1012 fn global_constant_location(&self, fqn: &str) -> Option<mir_types::Location> {
1014 let site = self.global_constant_decl_site(fqn)?;
1015 Some(mir_types::Location {
1016 file: site.file,
1017 line: site.line,
1018 line_end: site.line,
1019 col_start: site.stmt_col as u16,
1020 col_end: site.stmt_end as u16,
1021 })
1022 }
1023
1024 pub fn subtype_files(
1030 &self,
1031 class_fqn: &str,
1032 include_trait_users: bool,
1033 ) -> Result<Vec<Arc<str>>, Cancelled> {
1034 let files = self.db.source_file_paths();
1035 let mut out: Vec<Arc<str>> = self
1036 .indexed_subtype_classes(class_fqn, &files, include_trait_users)?
1037 .into_iter()
1038 .map(|s| s.file)
1039 .collect();
1040 out.sort();
1041 out.dedup();
1042 Ok(out)
1043 }
1044
1045 pub fn indexed_subtype_classes(
1064 &self,
1065 class_fqn: &str,
1066 files: &[Arc<str>],
1067 include_trait_users: bool,
1068 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1069 self.subtype_classes_memoized(class_fqn, files, include_trait_users, false)
1070 }
1071
1072 pub fn indexed_direct_subtype_classes(
1076 &self,
1077 class_fqn: &str,
1078 files: &[Arc<str>],
1079 include_trait_users: bool,
1080 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1081 self.subtype_classes_memoized(class_fqn, files, include_trait_users, true)
1082 }
1083
1084 fn subtype_classes_memoized(
1085 &self,
1086 class_fqn: &str,
1087 files: &[Arc<str>],
1088 include_trait_users: bool,
1089 direct_only: bool,
1090 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1091 catch(|| {
1092 let key = SubtypeQueryCacheKey {
1093 class_fqn: class_fqn.trim_start_matches('\\').to_ascii_lowercase(),
1094 include_trait_users,
1095 direct_only,
1096 generation: self.query_cache_generation(),
1097 files_hash: hash_files(files),
1098 };
1099 if let Some(hit) = self.index.subtype_queries.get(&key) {
1100 return hit;
1101 }
1102 let out =
1103 self.subtype_classes_uncached(class_fqn, files, include_trait_users, direct_only);
1104 if let Err(cancelled) = self.ensure_no_retire_since(key.generation) {
1105 unwind(cancelled);
1106 }
1107 let generation = self.query_cache_generation();
1109 if generation == key.generation {
1110 self.index.subtype_queries.insert(generation, key, &out);
1111 }
1112 out
1113 })
1114 }
1115
1116 fn subtype_classes_uncached(
1117 &self,
1118 class_fqn: &str,
1119 files: &[Arc<str>],
1120 include_trait_users: bool,
1121 direct_only: bool,
1122 ) -> Vec<SubtypeClassSite> {
1123 let db = &self.db;
1124 let mut scanned: HashSet<String> = HashSet::default();
1125 let mut pending: Vec<String> = vec![class_fqn.trim_start_matches('\\').to_string()];
1126 let mut sites: Vec<crate::db::SubtypeSite> = Vec::new();
1127 while !pending.is_empty() {
1128 let needles: Vec<String> = pending
1131 .drain(..)
1132 .filter(|f| scanned.insert(f.clone()))
1133 .map(|f| crate::db::subtype_index::short_name_of(&f).to_string())
1134 .collect();
1135 if !needles.is_empty() {
1136 self.commit_defs_for_matching(files, &needles);
1137 }
1138 sites = db.subtype_sites_of(class_fqn, include_trait_users, direct_only);
1139 if direct_only {
1140 break;
1141 }
1142 pending = sites
1143 .iter()
1144 .map(|s| s.fqcn.trim_start_matches('\\').to_string())
1145 .filter(|f| !scanned.contains(f))
1146 .collect();
1147 }
1148 let mut out: Vec<SubtypeClassSite> = sites
1149 .into_iter()
1150 .filter_map(|s| {
1151 let loc = s.location.as_ref()?;
1152 let short = crate::db::subtype_index::short_name_of(&s.fqcn).to_string();
1153 let range = self.refine_location_to_name(loc, &short);
1154 Some(SubtypeClassSite {
1155 fqcn: s.fqcn,
1156 kind: s.kind,
1157 is_abstract: s.is_abstract,
1158 file: s.file,
1159 range,
1160 })
1161 })
1162 .collect();
1163 let root_lc = class_fqn.trim_start_matches('\\').to_ascii_lowercase();
1167 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
1168 for (file, line, cs, ce) in db.reference_locations(&format!("impl:{root_lc}")) {
1169 if !scope.contains(file.as_ref()) {
1170 continue;
1171 }
1172 let range = span_range(line, cs as u32, ce as u32);
1173 if out.iter().any(|s| s.file == file && s.range == range) {
1174 continue;
1175 }
1176 out.push(SubtypeClassSite {
1177 fqcn: Arc::from("class@anonymous"),
1178 kind: crate::db::ClassLikeKind::Class,
1179 is_abstract: false,
1180 file,
1181 range,
1182 });
1183 }
1184 out
1185 }
1186
1187 #[allow(clippy::type_complexity)]
1193 pub fn indexed_method_implementations(
1194 &self,
1195 class_fqn: &str,
1196 method: &str,
1197 files: &[Arc<str>],
1198 ) -> Result<Vec<(Arc<str>, Arc<str>, crate::Range)>, Cancelled> {
1199 let subs = self.indexed_subtype_classes(class_fqn, files, false)?;
1200 catch(|| {
1201 let db = &self.db;
1202 let mut out: Vec<(Arc<str>, Arc<str>, crate::Range)> = Vec::new();
1203 for sub in &subs {
1204 let here = crate::db::Fqcn::from_str(db, sub.fqcn.as_ref());
1205 let Some((_, m)) = crate::db::find_method_in_chain(db, here, method) else {
1206 continue;
1207 };
1208 if m.is_abstract {
1209 continue;
1210 }
1211 let Some(loc) = m.location.as_ref() else {
1212 continue;
1213 };
1214 let range = self.refine_location_to_name(loc, method);
1215 out.push((sub.fqcn.clone(), loc.file.clone(), range));
1216 }
1217 out.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.start.line.cmp(&b.2.start.line)));
1218 out.dedup_by(|a, b| a.1 == b.1 && a.2 == b.2);
1219 out
1220 })
1221 }
1222
1223 fn commit_defs_for_matching(&self, files: &[Arc<str>], shorts: &[String]) {
1236 use rayon::prelude::*;
1237
1238 self.db
1242 .add_literal_mention_names(shorts.iter().map(|s| s.as_str()));
1243 let queries: Vec<_> = shorts
1244 .iter()
1245 .filter_map(|s| self.db.prepare_class_mention_query(s))
1246 .collect();
1247 let mention_scanner = self.db.class_mention_scanner();
1248 let use_mentions = queries.len() == shorts.len() && mention_scanner.is_some();
1252 type Work = (Arc<str>, Arc<str>, Vec<crate::db::SubtypeEntry>);
1253 let index = &*self.index;
1254 let results: Vec<(Option<Work>, Option<MentionScanRecord>)> = files
1255 .par_iter()
1256 .map_with(self.db.clone(), |db, path| {
1257 let Some(sf) = db.lookup_source_file(path.as_ref()) else {
1258 return (None, None);
1259 };
1260 let text = sf.text(&*db as &dyn MirDatabase).clone();
1261 let freshness = index.defs_freshness(sf, &text);
1262 if freshness == DefsFreshness::Fresh {
1263 return (None, None);
1264 }
1265 let mut scan_rec: Option<MentionScanRecord> = None;
1269 if use_mentions && freshness == DefsFreshness::Uncommitted {
1270 let mut answer = Some(false);
1271 for q in &queries {
1272 match db.class_mention_answer(path.as_ref(), q, &text) {
1273 Some(true) => {
1274 answer = Some(true);
1275 break;
1276 }
1277 Some(false) => {}
1278 None => answer = None,
1279 }
1280 }
1281 let hit = match answer {
1282 Some(hit) => hit,
1283 None => {
1284 let scanner = mention_scanner.as_ref().unwrap();
1287 let names = scanner.scan(&text);
1288 let hit = queries.iter().any(|q| names.binary_search(&q.name).is_ok());
1289 scan_rec = Some((path.clone(), text.clone(), names));
1290 hit
1291 }
1292 };
1293 if !hit {
1294 return (None, scan_rec);
1295 }
1296 }
1297 let defs = crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
1298 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
1299 (Some((path.clone(), text, entries)), scan_rec)
1300 })
1301 .collect();
1302 let mut refused = Ok(());
1306 for (work, scan_rec) in results {
1307 if let (Some(scanner), Some((file, text, names))) = (&mention_scanner, scan_rec) {
1308 self.db
1309 .set_file_class_mentions(&file, &text, scanner.epoch(), names);
1310 }
1311 if let Some((file, text, entries)) = work {
1312 if let Err(cancelled) = self.index.commit_class_edges(
1313 &self.db,
1314 &file,
1315 &text,
1316 entries,
1317 self.retire_epoch,
1318 ) {
1319 refused = Err(cancelled);
1320 }
1321 }
1322 }
1323 if let Err(cancelled) = refused {
1324 unwind(cancelled);
1325 }
1326 }
1327
1328 pub fn class_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1336 catch(|| {
1337 let db = &self.db;
1338 let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
1339 let file_data: Vec<(Arc<str>, Arc<str>)> = files
1340 .iter()
1341 .filter_map(|f| {
1342 let sf = db.lookup_source_file(f)?;
1343 Some((f.clone(), sf.text(db as &dyn MirDatabase).clone()))
1344 })
1345 .collect();
1346 crate::class::ClassAnalyzer::with_files(db, file_set, &file_data).analyze_all()
1347 })
1348 }
1349
1350 pub fn collector_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1359 catch(|| {
1360 files
1361 .iter()
1362 .filter_map(|f| self.db.lookup_source_file(f))
1363 .flat_map(|sf| {
1364 crate::db::collect_file_definitions(&self.db, sf)
1365 .issues
1366 .as_ref()
1367 .clone()
1368 })
1369 .collect()
1370 })
1371 }
1372
1373 pub(super) fn reference_gate(&self, symbol: &crate::Name) -> ReferenceGate {
1384 ReferenceGate::for_symbol(&self.db, symbol)
1385 }
1386}
1387
1388#[cfg(test)]
1389mod tests {
1390 use super::*;
1391 use crate::{AnalysisSession, IndexCancel, Name, ReferenceIncludes};
1392 use salsa::Database as _;
1393
1394 const BASE: &str =
1395 "<?php\nclass Base { public function run(): void {} public function stop(): void {} }\n";
1396 const CALLS_RUN: &str = "<?php\nfunction go(Base $b): void { $b->run(); }\n";
1397 const CALLS_STOP: &str = "<?php\nfunction go(Base $b): void { $b->stop(); }\n";
1398
1399 #[test]
1403 fn warm_commit_racing_a_text_write_leaves_the_file_stale() {
1404 let (mut session, files) = prepared_workspace(CALLS_RUN);
1405 let snap = session.snapshot();
1406 let mut pass = snap
1407 .stage_warm(&files[1..], &IndexCancel::new())
1408 .unwrap()
1409 .unwrap();
1410
1411 let caller = files[1].clone();
1412 let writer = std::thread::spawn(move || {
1413 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1414 session
1415 });
1416 await_pending_write(&snap);
1417 snap.commit_warm(&mut pass).unwrap();
1418 drop(snap);
1419 let mut session = writer.join().unwrap();
1420
1421 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1422 assert!(callers_of(&mut session, &files, "run").is_empty());
1423 }
1424
1425 #[test]
1426 fn warm_commit_after_an_ingest_retired_the_file_is_refused() {
1427 let (session, files) = prepared_workspace(CALLS_RUN);
1428 let snap = session.snapshot();
1429 let mut pass = snap
1430 .stage_warm(&files[1..], &IndexCancel::new())
1431 .unwrap()
1432 .unwrap();
1433
1434 let caller = files[1].clone();
1435 let writer = std::thread::spawn(move || {
1436 let mut session = session;
1437 session.ingest_file(caller, Arc::from(CALLS_STOP));
1438 session
1439 });
1440 await_pending_write(&snap);
1441 assert!(snap.commit_warm(&mut pass).is_err());
1442 drop(snap);
1443 let mut session = writer.join().unwrap();
1444
1445 assert!(callers_of(&mut session, &files, "run").is_empty());
1446 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1447 }
1448
1449 #[test]
1450 fn commits_after_invalidate_file_retired_the_file_are_refused() {
1451 const CHILD_CALLS_RUN: &str =
1452 "<?php\nclass Child extends Base {}\nfunction go(Base $b): void { $b->run(); }\n";
1453 let (session, files) = prepared_workspace(CHILD_CALLS_RUN);
1454 let snap = session.snapshot();
1455 let mut pass = snap
1456 .stage_warm(&files[1..], &IndexCancel::new())
1457 .unwrap()
1458 .unwrap();
1459 let parsed = php_rs_parser::parse(CHILD_CALLS_RUN);
1460
1461 let caller = files[1].clone();
1462 let writer = std::thread::spawn(move || {
1463 let mut session = session;
1464 session.invalidate_file(&caller);
1465 session
1466 });
1467 await_pending_write(&snap);
1468 assert!(snap.commit_warm(&mut pass).is_err());
1469 assert!(snap
1470 .analyze(
1471 files[1].clone(),
1472 CHILD_CALLS_RUN,
1473 &parsed.program,
1474 &parsed.source_map
1475 )
1476 .is_err());
1477 drop(snap);
1478 let mut session = writer.join().unwrap();
1479
1480 assert!(callers_of(&mut session, &files, "run").is_empty());
1481 assert!(session
1482 .indexed_subtype_classes("Base", &files, false)
1483 .is_empty());
1484 }
1485
1486 #[test]
1487 fn cancelled_reference_commit_reports_cancellation() {
1488 let (mut session, files) = prepared_workspace(CALLS_RUN);
1489 let snap = session.snapshot();
1490 let stale = snap.stale_reference_candidates_unguarded(&Name::method("Base", "run"), &files);
1491 assert!(stale.contains(&files[1]));
1492
1493 let caller = files[1].clone();
1494 let writer = std::thread::spawn(move || {
1495 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1496 session
1497 });
1498 await_pending_write(&snap);
1499 assert!(snap.commit_reference_candidates(&stale).is_err());
1500 drop(snap);
1501 writer.join().unwrap();
1502 }
1503
1504 const ROUTES_CALLS_RUN: &str = "<?php\n(new Base())->run();\n";
1507
1508 fn registered_routes_workspace() -> (AnalysisSession, Vec<Arc<str>>) {
1512 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1513 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("routes.php")];
1514 session.ingest_file(files[0].clone(), Arc::from(BASE));
1515 session.upsert_source_file(
1516 files[1].clone(),
1517 Arc::from(ROUTES_CALLS_RUN),
1518 salsa::Durability::LOW,
1519 );
1520 session.prepare_for_query(None);
1521 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1522 (session, files)
1523 }
1524
1525 #[test]
1529 fn ingest_of_unchanged_text_mid_query_keeps_references() {
1530 let (mut session, files) = registered_routes_workspace();
1531 let symbol = Name::method("Base", "run");
1532 let revision = session.text_revision();
1533
1534 let snap = session.snapshot();
1535 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1536 assert!(snap
1537 .stale_reference_candidates_unguarded(&symbol, &files)
1538 .is_empty());
1539 session.ingest_file(files[1].clone(), Arc::from(ROUTES_CALLS_RUN));
1540 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1541 assert!(snap.ensure_no_retire_since(key.generation).is_ok());
1542 snap.memoize_references(key, &out);
1543 drop(snap);
1544
1545 assert_eq!(session.text_revision(), revision, "ingest wrote salsa");
1546 assert_eq!(
1547 out.into_iter().map(|(file, _)| file).collect::<Vec<_>>(),
1548 [files[1].clone()]
1549 );
1550 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1551 }
1552
1553 #[test]
1557 fn query_spanning_a_retirement_is_cancelled_and_not_memoized() {
1558 let (mut session, files) = registered_routes_workspace();
1559 let symbol = Name::method("Base", "run");
1560
1561 let snap = session.snapshot();
1562 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1563 assert!(snap
1564 .stale_reference_candidates_unguarded(&symbol, &files)
1565 .is_empty());
1566 session
1567 .index
1568 .retire_references(&session.db.salsa, files[1].as_ref());
1569 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1570 assert!(out.is_empty());
1571 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1572 snap.memoize_references(key, &out);
1573 drop(snap);
1574
1575 let hits = session.ref_query_cache_hits();
1576 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1577 assert_eq!(session.ref_query_cache_hits(), hits);
1578 }
1579
1580 #[test]
1583 fn re_analyze_file_cache_hit_cancels_spanning_queries() {
1584 let dir = tempfile::tempdir().unwrap();
1585 let mut session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(dir.path());
1586 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1587 let opts = crate::BatchOptions::new();
1588 session.ingest_file(files[0].clone(), Arc::from(BASE));
1589 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1590 session.prepare_for_query(None);
1591 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1592
1593 let symbol = Name::method("Base", "run");
1594 let snap = session.snapshot();
1595 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1596 let revision = session.text_revision();
1597 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1598 assert_eq!(session.text_revision(), revision, "took the analysis path");
1599 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1600 drop(snap);
1601
1602 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1603 }
1604
1605 fn prepared_workspace(caller_text: &str) -> (AnalysisSession, Vec<Arc<str>>) {
1606 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1607 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1608 session.ingest_file(files[0].clone(), Arc::from(BASE));
1609 session.ingest_file(files[1].clone(), Arc::from(caller_text));
1610 session.prepare_for_query(Some(&files[1]));
1611 (session, files)
1612 }
1613
1614 fn await_pending_write(snap: &AnalysisSnapshot) {
1617 while catch(|| snap.db.unwind_if_revision_cancelled()).is_ok() {
1618 std::thread::yield_now();
1619 }
1620 }
1621
1622 fn callers_of(
1623 session: &mut AnalysisSession,
1624 files: &[Arc<str>],
1625 method: &str,
1626 ) -> Vec<Arc<str>> {
1627 session
1628 .indexed_references_to(
1629 &Name::method("Base", method),
1630 files,
1631 false,
1632 ReferenceIncludes::Plain,
1633 &|| false,
1634 )
1635 .unwrap()
1636 .into_iter()
1637 .map(|(file, _)| file)
1638 .collect()
1639 }
1640
1641 #[derive(Debug, PartialEq)]
1644 struct AnalysisOutcome {
1645 issues: Vec<String>,
1646 symbols: usize,
1647 vendor_class_loaded: bool,
1648 vendor_function_loaded: bool,
1649 eager_files_drained: bool,
1650 stubs_loaded: usize,
1651 prepared: bool,
1652 prepare_generation: u64,
1653 references: Vec<String>,
1654 }
1655
1656 const DEPENDENT: &str = "<?php\nnamespace App;\nfunction go(\\Vendor\\Base $b): void { vendor_helper('x'); new \\ArrayObject(); }\n";
1657
1658 fn composer_project(root: &std::path::Path) -> AnalysisSession {
1659 let vendor = root.join("vendor");
1660 std::fs::create_dir_all(vendor.join("composer")).unwrap();
1661 std::fs::create_dir_all(vendor.join("helpers")).unwrap();
1662 std::fs::create_dir_all(root.join("lib")).unwrap();
1663 std::fs::write(
1664 vendor.join("composer/autoload_files.php"),
1665 "<?php\n$vendorDir = dirname(__DIR__);\n$baseDir = dirname($vendorDir);\nreturn array(\n 'abc123' => $vendorDir . '/helpers/functions.php',\n);\n",
1666 )
1667 .unwrap();
1668 for empty in ["psr4", "classmap", "namespaces"] {
1669 std::fs::write(
1670 vendor.join(format!("composer/autoload_{empty}.php")),
1671 "<?php\nreturn [];\n",
1672 )
1673 .unwrap();
1674 }
1675 std::fs::write(
1676 vendor.join("helpers/functions.php"),
1677 "<?php\nfunction vendor_helper(string $s): string { return $s; }\n",
1678 )
1679 .unwrap();
1680 std::fs::write(
1681 root.join("lib/Base.php"),
1682 "<?php\nnamespace Vendor;\nclass Base {}\n",
1683 )
1684 .unwrap();
1685 std::fs::write(
1686 root.join("composer.json"),
1687 r#"{"autoload":{"psr-4":{"App\\":"src/","Vendor\\":"lib/"}}}"#,
1688 )
1689 .unwrap();
1690 let psr4 = crate::composer::Psr4Map::from_composer(root).unwrap();
1691 AnalysisSession::new(PhpVersion::LATEST).with_psr4(Arc::new(psr4))
1692 }
1693
1694 fn outcome(
1695 session: &AnalysisSession,
1696 file: &Arc<str>,
1697 analysis: crate::FileAnalysis,
1698 ) -> AnalysisOutcome {
1699 let text = session.source_of(file).unwrap();
1700 AnalysisOutcome {
1701 issues: analysis
1702 .issues
1703 .iter()
1704 .map(|i| format!("{:?}", i.kind))
1705 .collect(),
1706 symbols: analysis.symbols.len(),
1707 vendor_class_loaded: {
1708 let view = session.db_view();
1709 let db = view.db();
1710 crate::db::class_like_indexed(db, crate::db::Fqcn::from_str(db, "Vendor\\Base"))
1711 },
1712 vendor_function_loaded: session.contains_function("vendor_helper"),
1713 eager_files_drained: session.pending_eager_function_files.is_none(),
1714 stubs_loaded: session.loaded_stub_count(),
1715 prepared: session.is_prepared_for_analysis(
1716 file,
1717 &text,
1718 session.prepare_generation_snapshot(),
1719 ),
1720 prepare_generation: session.prepare_generation_snapshot(),
1721 references: ["cls:Vendor\\Base", "fn:vendor_helper", "cls:ArrayObject"]
1722 .into_iter()
1723 .map(|key| format!("{key}={:?}", session.reference_locations(key)))
1724 .collect(),
1725 }
1726 }
1727
1728 #[test]
1732 fn prepare_for_query_then_snapshot_analyze_matches_file_analyzer() {
1733 let file: Arc<str> = Arc::from("consumer.php");
1734 let parsed = php_rs_parser::parse(DEPENDENT);
1735
1736 let root = tempfile::tempdir().unwrap();
1737 let mut via_analyzer = composer_project(root.path());
1738 via_analyzer.ingest_file(file.clone(), Arc::from(DEPENDENT));
1739 let analysis = crate::FileAnalyzer::new(&mut via_analyzer).analyze(
1740 file.clone(),
1741 DEPENDENT,
1742 &parsed.program,
1743 &parsed.source_map,
1744 );
1745 let expected = outcome(&via_analyzer, &file, analysis);
1746
1747 let root = tempfile::tempdir().unwrap();
1748 let mut via_snapshot = composer_project(root.path());
1749 via_snapshot.ingest_file(file.clone(), Arc::from(DEPENDENT));
1750 via_snapshot.prepare_for_query(Some(&file));
1751 let analysis = via_snapshot
1752 .snapshot()
1753 .analyze(file.clone(), DEPENDENT, &parsed.program, &parsed.source_map)
1754 .unwrap();
1755 let actual = outcome(&via_snapshot, &file, analysis);
1756
1757 assert!(
1758 expected.prepared && expected.vendor_function_loaded,
1759 "{expected:#?}"
1760 );
1761 assert_eq!(actual, expected);
1762 }
1763
1764 #[test]
1768 fn prepare_for_query_warms_nothing_for_a_file_that_was_never_ingested() {
1769 let file: Arc<str> = Arc::from("consumer.php");
1770 let parsed = php_rs_parser::parse(DEPENDENT);
1771 let unresolved = |analysis: &crate::FileAnalysis| {
1772 analysis
1773 .issues
1774 .iter()
1775 .any(|i| matches!(i.kind, crate::IssueKind::UndefinedFunction { .. }))
1776 };
1777
1778 let root = tempfile::tempdir().unwrap();
1779 let mut via_analyzer = composer_project(root.path());
1780 let analysis = crate::FileAnalyzer::new(&mut via_analyzer).analyze(
1781 file.clone(),
1782 DEPENDENT,
1783 &parsed.program,
1784 &parsed.source_map,
1785 );
1786 assert!(!unresolved(&analysis), "{:?}", analysis.issues);
1787
1788 let root = tempfile::tempdir().unwrap();
1789 let mut via_snapshot = composer_project(root.path());
1790 via_snapshot.prepare_for_query(Some(&file));
1791 let analysis = via_snapshot
1792 .snapshot()
1793 .analyze(file, DEPENDENT, &parsed.program, &parsed.source_map)
1794 .unwrap();
1795 assert!(unresolved(&analysis), "{:?}", analysis.issues);
1796 }
1797}