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 short = match symbol {
910 crate::Name::Class(f) | crate::Name::Function(f) | crate::Name::GlobalConstant(f) => {
911 crate::db::subtype_index::short_name_of(f)
912 }
913 crate::Name::Method { name, .. }
914 | crate::Name::Property { name, .. }
915 | crate::Name::ClassConstant { name, .. } => name.as_ref(),
916 };
917 let range = self.refine_location_to_name(&loc, short);
921 Some((loc.file.clone(), range))
922 }
923
924 fn refine_location_to_name(&self, loc: &mir_types::Location, needle: &str) -> crate::Range {
929 let fallback = span_range(loc.line, loc.col_start as u32, loc.col_end as u32);
930 let Some(text) = self
931 .db
932 .lookup_source_file(loc.file.as_ref())
933 .map(|sf| sf.text(&self.db as &dyn MirDatabase).clone())
934 else {
935 return fallback;
936 };
937 let needle_chars = needle.chars().count() as u32;
938 let first_line = loc.line.saturating_sub(1) as usize;
939 for case_insensitive in [false, true] {
944 for (idx, line_text) in text.lines().enumerate().skip(first_line) {
945 let line_no = idx as u32 + 1;
946 if line_no > loc.line_end {
947 break;
948 }
949 let min_col = if line_no == loc.line {
950 loc.col_start as usize
951 } else {
952 0
953 };
954 if let Some(col) = identifier_char_col(line_text, needle, min_col, case_insensitive)
955 {
956 return span_range(line_no, col, col + needle_chars);
957 }
958 }
959 }
960 fallback
961 }
962
963 fn global_constant_decl_site(&self, fqn: &str) -> Option<ConstantDeclSite> {
967 let short = crate::db::subtype_index::short_name_of(fqn);
968 let db = &self.db;
969 let index = crate::db::workspace_index(db);
970 let loc = index.constant_loc(mir_types::Name::from(fqn.trim_start_matches('\\')))?;
971 let file = loc.file().path(db).clone();
972 let sf = db.lookup_source_file(file.as_ref())?;
973 let text = sf.text(db as &dyn MirDatabase);
974 for (idx, line) in text.lines().enumerate() {
975 let trimmed = line.trim_start();
976 let is_decl_line = trimmed.starts_with("const ")
977 || trimmed.contains("define(")
978 || trimmed.contains("define (");
979 if !is_decl_line {
980 continue;
981 }
982 if let Some(name_col) = identifier_char_col(line, short, 0, false) {
983 let stmt_end = match line.find(';') {
984 Some(byte) => line[..=byte].chars().count(),
985 None => line.chars().count(),
986 };
987 return Some(ConstantDeclSite {
988 file,
989 line: idx as u32 + 1,
990 stmt_col: (line.chars().count() - trimmed.chars().count()) as u32,
991 stmt_end: stmt_end as u32,
992 name_col,
993 name_len: short.chars().count() as u32,
994 });
995 }
996 }
997 None
998 }
999
1000 fn global_constant_decl_range(&self, fqn: &str) -> Option<(Arc<str>, crate::Range)> {
1001 let site = self.global_constant_decl_site(fqn)?;
1002 let range = span_range(site.line, site.name_col, site.name_col + site.name_len);
1003 Some((site.file, range))
1004 }
1005
1006 fn global_constant_location(&self, fqn: &str) -> Option<mir_types::Location> {
1008 let site = self.global_constant_decl_site(fqn)?;
1009 Some(mir_types::Location {
1010 file: site.file,
1011 line: site.line,
1012 line_end: site.line,
1013 col_start: site.stmt_col as u16,
1014 col_end: site.stmt_end as u16,
1015 })
1016 }
1017
1018 pub fn subtype_files(
1024 &self,
1025 class_fqn: &str,
1026 include_trait_users: bool,
1027 ) -> Result<Vec<Arc<str>>, Cancelled> {
1028 let files = self.db.source_file_paths();
1029 let mut out: Vec<Arc<str>> = self
1030 .indexed_subtype_classes(class_fqn, &files, include_trait_users)?
1031 .into_iter()
1032 .map(|s| s.file)
1033 .collect();
1034 out.sort();
1035 out.dedup();
1036 Ok(out)
1037 }
1038
1039 pub fn indexed_subtype_classes(
1058 &self,
1059 class_fqn: &str,
1060 files: &[Arc<str>],
1061 include_trait_users: bool,
1062 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1063 self.subtype_classes_memoized(class_fqn, files, include_trait_users, false)
1064 }
1065
1066 pub fn indexed_direct_subtype_classes(
1070 &self,
1071 class_fqn: &str,
1072 files: &[Arc<str>],
1073 include_trait_users: bool,
1074 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1075 self.subtype_classes_memoized(class_fqn, files, include_trait_users, true)
1076 }
1077
1078 fn subtype_classes_memoized(
1079 &self,
1080 class_fqn: &str,
1081 files: &[Arc<str>],
1082 include_trait_users: bool,
1083 direct_only: bool,
1084 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
1085 catch(|| {
1086 let key = SubtypeQueryCacheKey {
1087 class_fqn: class_fqn.trim_start_matches('\\').to_ascii_lowercase(),
1088 include_trait_users,
1089 direct_only,
1090 generation: self.query_cache_generation(),
1091 files_hash: hash_files(files),
1092 };
1093 if let Some(hit) = self.index.subtype_queries.get(&key) {
1094 return hit;
1095 }
1096 let out =
1097 self.subtype_classes_uncached(class_fqn, files, include_trait_users, direct_only);
1098 if let Err(cancelled) = self.ensure_no_retire_since(key.generation) {
1099 unwind(cancelled);
1100 }
1101 let generation = self.query_cache_generation();
1103 if generation == key.generation {
1104 self.index.subtype_queries.insert(generation, key, &out);
1105 }
1106 out
1107 })
1108 }
1109
1110 fn subtype_classes_uncached(
1111 &self,
1112 class_fqn: &str,
1113 files: &[Arc<str>],
1114 include_trait_users: bool,
1115 direct_only: bool,
1116 ) -> Vec<SubtypeClassSite> {
1117 let db = &self.db;
1118 let mut scanned: HashSet<String> = HashSet::default();
1119 let mut pending: Vec<String> = vec![class_fqn.trim_start_matches('\\').to_string()];
1120 let mut sites: Vec<crate::db::SubtypeSite> = Vec::new();
1121 while !pending.is_empty() {
1122 let needles: Vec<String> = pending
1125 .drain(..)
1126 .filter(|f| scanned.insert(f.clone()))
1127 .map(|f| crate::db::subtype_index::short_name_of(&f).to_string())
1128 .collect();
1129 if !needles.is_empty() {
1130 self.commit_defs_for_matching(files, &needles);
1131 }
1132 sites = db.subtype_sites_of(class_fqn, include_trait_users, direct_only);
1133 if direct_only {
1134 break;
1135 }
1136 pending = sites
1137 .iter()
1138 .map(|s| s.fqcn.trim_start_matches('\\').to_string())
1139 .filter(|f| !scanned.contains(f))
1140 .collect();
1141 }
1142 let mut out: Vec<SubtypeClassSite> = sites
1143 .into_iter()
1144 .filter_map(|s| {
1145 let loc = s.location.as_ref()?;
1146 let short = crate::db::subtype_index::short_name_of(&s.fqcn).to_string();
1147 let range = self.refine_location_to_name(loc, &short);
1148 Some(SubtypeClassSite {
1149 fqcn: s.fqcn,
1150 kind: s.kind,
1151 is_abstract: s.is_abstract,
1152 file: s.file,
1153 range,
1154 })
1155 })
1156 .collect();
1157 let root_lc = class_fqn.trim_start_matches('\\').to_ascii_lowercase();
1161 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
1162 for (file, line, cs, ce) in db.reference_locations(&format!("impl:{root_lc}")) {
1163 if !scope.contains(file.as_ref()) {
1164 continue;
1165 }
1166 let range = span_range(line, cs as u32, ce as u32);
1167 if out.iter().any(|s| s.file == file && s.range == range) {
1168 continue;
1169 }
1170 out.push(SubtypeClassSite {
1171 fqcn: Arc::from("class@anonymous"),
1172 kind: crate::db::ClassLikeKind::Class,
1173 is_abstract: false,
1174 file,
1175 range,
1176 });
1177 }
1178 out
1179 }
1180
1181 #[allow(clippy::type_complexity)]
1187 pub fn indexed_method_implementations(
1188 &self,
1189 class_fqn: &str,
1190 method: &str,
1191 files: &[Arc<str>],
1192 ) -> Result<Vec<(Arc<str>, Arc<str>, crate::Range)>, Cancelled> {
1193 let subs = self.indexed_subtype_classes(class_fqn, files, false)?;
1194 catch(|| {
1195 let db = &self.db;
1196 let mut out: Vec<(Arc<str>, Arc<str>, crate::Range)> = Vec::new();
1197 for sub in &subs {
1198 let here = crate::db::Fqcn::from_str(db, sub.fqcn.as_ref());
1199 let Some((_, m)) = crate::db::find_method_in_chain(db, here, method) else {
1200 continue;
1201 };
1202 if m.is_abstract {
1203 continue;
1204 }
1205 let Some(loc) = m.location.as_ref() else {
1206 continue;
1207 };
1208 let range = self.refine_location_to_name(loc, method);
1209 out.push((sub.fqcn.clone(), loc.file.clone(), range));
1210 }
1211 out.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.start.line.cmp(&b.2.start.line)));
1212 out.dedup_by(|a, b| a.1 == b.1 && a.2 == b.2);
1213 out
1214 })
1215 }
1216
1217 fn commit_defs_for_matching(&self, files: &[Arc<str>], shorts: &[String]) {
1230 use rayon::prelude::*;
1231
1232 self.db
1236 .add_literal_mention_names(shorts.iter().map(|s| s.as_str()));
1237 let queries: Vec<_> = shorts
1238 .iter()
1239 .filter_map(|s| self.db.prepare_class_mention_query(s))
1240 .collect();
1241 let mention_scanner = self.db.class_mention_scanner();
1242 let use_mentions = queries.len() == shorts.len() && mention_scanner.is_some();
1246 type Work = (Arc<str>, Arc<str>, Vec<crate::db::SubtypeEntry>);
1247 let index = &*self.index;
1248 let results: Vec<(Option<Work>, Option<MentionScanRecord>)> = files
1249 .par_iter()
1250 .map_with(self.db.clone(), |db, path| {
1251 let Some(sf) = db.lookup_source_file(path.as_ref()) else {
1252 return (None, None);
1253 };
1254 let text = sf.text(&*db as &dyn MirDatabase).clone();
1255 let freshness = index.defs_freshness(sf, &text);
1256 if freshness == DefsFreshness::Fresh {
1257 return (None, None);
1258 }
1259 let mut scan_rec: Option<MentionScanRecord> = None;
1263 if use_mentions && freshness == DefsFreshness::Uncommitted {
1264 let mut answer = Some(false);
1265 for q in &queries {
1266 match db.class_mention_answer(path.as_ref(), q, &text) {
1267 Some(true) => {
1268 answer = Some(true);
1269 break;
1270 }
1271 Some(false) => {}
1272 None => answer = None,
1273 }
1274 }
1275 let hit = match answer {
1276 Some(hit) => hit,
1277 None => {
1278 let scanner = mention_scanner.as_ref().unwrap();
1281 let names = scanner.scan(&text);
1282 let hit = queries.iter().any(|q| names.binary_search(&q.name).is_ok());
1283 scan_rec = Some((path.clone(), text.clone(), names));
1284 hit
1285 }
1286 };
1287 if !hit {
1288 return (None, scan_rec);
1289 }
1290 }
1291 let defs = crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
1292 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
1293 (Some((path.clone(), text, entries)), scan_rec)
1294 })
1295 .collect();
1296 let mut refused = Ok(());
1300 for (work, scan_rec) in results {
1301 if let (Some(scanner), Some((file, text, names))) = (&mention_scanner, scan_rec) {
1302 self.db
1303 .set_file_class_mentions(&file, &text, scanner.epoch(), names);
1304 }
1305 if let Some((file, text, entries)) = work {
1306 if let Err(cancelled) = self.index.commit_class_edges(
1307 &self.db,
1308 &file,
1309 &text,
1310 entries,
1311 self.retire_epoch,
1312 ) {
1313 refused = Err(cancelled);
1314 }
1315 }
1316 }
1317 if let Err(cancelled) = refused {
1318 unwind(cancelled);
1319 }
1320 }
1321
1322 pub fn class_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1330 catch(|| {
1331 let db = &self.db;
1332 let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
1333 let file_data: Vec<(Arc<str>, Arc<str>)> = files
1334 .iter()
1335 .filter_map(|f| {
1336 let sf = db.lookup_source_file(f)?;
1337 Some((f.clone(), sf.text(db as &dyn MirDatabase).clone()))
1338 })
1339 .collect();
1340 crate::class::ClassAnalyzer::with_files(db, file_set, &file_data).analyze_all()
1341 })
1342 }
1343
1344 pub fn collector_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1353 catch(|| {
1354 files
1355 .iter()
1356 .filter_map(|f| self.db.lookup_source_file(f))
1357 .flat_map(|sf| {
1358 crate::db::collect_file_definitions(&self.db, sf)
1359 .issues
1360 .as_ref()
1361 .clone()
1362 })
1363 .collect()
1364 })
1365 }
1366
1367 pub(super) fn reference_gate(&self, symbol: &crate::Name) -> ReferenceGate {
1378 ReferenceGate::for_symbol(&self.db, symbol)
1379 }
1380}
1381
1382#[cfg(test)]
1383mod tests {
1384 use super::*;
1385 use crate::{AnalysisSession, IndexCancel, Name, ReferenceIncludes};
1386 use salsa::Database as _;
1387
1388 const BASE: &str =
1389 "<?php\nclass Base { public function run(): void {} public function stop(): void {} }\n";
1390 const CALLS_RUN: &str = "<?php\nfunction go(Base $b): void { $b->run(); }\n";
1391 const CALLS_STOP: &str = "<?php\nfunction go(Base $b): void { $b->stop(); }\n";
1392
1393 #[test]
1397 fn warm_commit_racing_a_text_write_leaves_the_file_stale() {
1398 let (mut session, files) = prepared_workspace(CALLS_RUN);
1399 let snap = session.snapshot();
1400 let mut pass = snap
1401 .stage_warm(&files[1..], &IndexCancel::new())
1402 .unwrap()
1403 .unwrap();
1404
1405 let caller = files[1].clone();
1406 let writer = std::thread::spawn(move || {
1407 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1408 session
1409 });
1410 await_pending_write(&snap);
1411 snap.commit_warm(&mut pass).unwrap();
1412 drop(snap);
1413 let mut session = writer.join().unwrap();
1414
1415 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1416 assert!(callers_of(&mut session, &files, "run").is_empty());
1417 }
1418
1419 #[test]
1420 fn warm_commit_after_an_ingest_retired_the_file_is_refused() {
1421 let (session, files) = prepared_workspace(CALLS_RUN);
1422 let snap = session.snapshot();
1423 let mut pass = snap
1424 .stage_warm(&files[1..], &IndexCancel::new())
1425 .unwrap()
1426 .unwrap();
1427
1428 let caller = files[1].clone();
1429 let writer = std::thread::spawn(move || {
1430 let mut session = session;
1431 session.ingest_file(caller, Arc::from(CALLS_STOP));
1432 session
1433 });
1434 await_pending_write(&snap);
1435 assert!(snap.commit_warm(&mut pass).is_err());
1436 drop(snap);
1437 let mut session = writer.join().unwrap();
1438
1439 assert!(callers_of(&mut session, &files, "run").is_empty());
1440 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1441 }
1442
1443 #[test]
1444 fn commits_after_invalidate_file_retired_the_file_are_refused() {
1445 const CHILD_CALLS_RUN: &str =
1446 "<?php\nclass Child extends Base {}\nfunction go(Base $b): void { $b->run(); }\n";
1447 let (session, files) = prepared_workspace(CHILD_CALLS_RUN);
1448 let snap = session.snapshot();
1449 let mut pass = snap
1450 .stage_warm(&files[1..], &IndexCancel::new())
1451 .unwrap()
1452 .unwrap();
1453 let parsed = php_rs_parser::parse(CHILD_CALLS_RUN);
1454
1455 let caller = files[1].clone();
1456 let writer = std::thread::spawn(move || {
1457 let mut session = session;
1458 session.invalidate_file(&caller);
1459 session
1460 });
1461 await_pending_write(&snap);
1462 assert!(snap.commit_warm(&mut pass).is_err());
1463 assert!(snap
1464 .analyze(
1465 files[1].clone(),
1466 CHILD_CALLS_RUN,
1467 &parsed.program,
1468 &parsed.source_map
1469 )
1470 .is_err());
1471 drop(snap);
1472 let mut session = writer.join().unwrap();
1473
1474 assert!(callers_of(&mut session, &files, "run").is_empty());
1475 assert!(session
1476 .indexed_subtype_classes("Base", &files, false)
1477 .is_empty());
1478 }
1479
1480 #[test]
1481 fn cancelled_reference_commit_reports_cancellation() {
1482 let (mut session, files) = prepared_workspace(CALLS_RUN);
1483 let snap = session.snapshot();
1484 let stale = snap.stale_reference_candidates_unguarded(&Name::method("Base", "run"), &files);
1485 assert!(stale.contains(&files[1]));
1486
1487 let caller = files[1].clone();
1488 let writer = std::thread::spawn(move || {
1489 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1490 session
1491 });
1492 await_pending_write(&snap);
1493 assert!(snap.commit_reference_candidates(&stale).is_err());
1494 drop(snap);
1495 writer.join().unwrap();
1496 }
1497
1498 const ROUTES_CALLS_RUN: &str = "<?php\n(new Base())->run();\n";
1501
1502 fn registered_routes_workspace() -> (AnalysisSession, Vec<Arc<str>>) {
1506 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1507 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("routes.php")];
1508 session.ingest_file(files[0].clone(), Arc::from(BASE));
1509 session.upsert_source_file(
1510 files[1].clone(),
1511 Arc::from(ROUTES_CALLS_RUN),
1512 salsa::Durability::LOW,
1513 );
1514 session.prepare_for_query(None);
1515 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1516 (session, files)
1517 }
1518
1519 #[test]
1523 fn ingest_of_unchanged_text_mid_query_keeps_references() {
1524 let (mut session, files) = registered_routes_workspace();
1525 let symbol = Name::method("Base", "run");
1526 let revision = session.text_revision();
1527
1528 let snap = session.snapshot();
1529 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1530 assert!(snap
1531 .stale_reference_candidates_unguarded(&symbol, &files)
1532 .is_empty());
1533 session.ingest_file(files[1].clone(), Arc::from(ROUTES_CALLS_RUN));
1534 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1535 assert!(snap.ensure_no_retire_since(key.generation).is_ok());
1536 snap.memoize_references(key, &out);
1537 drop(snap);
1538
1539 assert_eq!(session.text_revision(), revision, "ingest wrote salsa");
1540 assert_eq!(
1541 out.into_iter().map(|(file, _)| file).collect::<Vec<_>>(),
1542 [files[1].clone()]
1543 );
1544 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1545 }
1546
1547 #[test]
1551 fn query_spanning_a_retirement_is_cancelled_and_not_memoized() {
1552 let (mut session, files) = registered_routes_workspace();
1553 let symbol = Name::method("Base", "run");
1554
1555 let snap = session.snapshot();
1556 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1557 assert!(snap
1558 .stale_reference_candidates_unguarded(&symbol, &files)
1559 .is_empty());
1560 session
1561 .index
1562 .retire_references(&session.db.salsa, files[1].as_ref());
1563 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1564 assert!(out.is_empty());
1565 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1566 snap.memoize_references(key, &out);
1567 drop(snap);
1568
1569 let hits = session.ref_query_cache_hits();
1570 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1571 assert_eq!(session.ref_query_cache_hits(), hits);
1572 }
1573
1574 #[test]
1577 fn re_analyze_file_cache_hit_cancels_spanning_queries() {
1578 let dir = tempfile::tempdir().unwrap();
1579 let mut session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(dir.path());
1580 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1581 let opts = crate::BatchOptions::new();
1582 session.ingest_file(files[0].clone(), Arc::from(BASE));
1583 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1584 session.prepare_for_query(None);
1585 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1586
1587 let symbol = Name::method("Base", "run");
1588 let snap = session.snapshot();
1589 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1590 let revision = session.text_revision();
1591 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1592 assert_eq!(session.text_revision(), revision, "took the analysis path");
1593 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1594 drop(snap);
1595
1596 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1597 }
1598
1599 fn prepared_workspace(caller_text: &str) -> (AnalysisSession, Vec<Arc<str>>) {
1600 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1601 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1602 session.ingest_file(files[0].clone(), Arc::from(BASE));
1603 session.ingest_file(files[1].clone(), Arc::from(caller_text));
1604 session.prepare_for_query(Some(&files[1]));
1605 (session, files)
1606 }
1607
1608 fn await_pending_write(snap: &AnalysisSnapshot) {
1611 while catch(|| snap.db.unwind_if_revision_cancelled()).is_ok() {
1612 std::thread::yield_now();
1613 }
1614 }
1615
1616 fn callers_of(
1617 session: &mut AnalysisSession,
1618 files: &[Arc<str>],
1619 method: &str,
1620 ) -> Vec<Arc<str>> {
1621 session
1622 .indexed_references_to(
1623 &Name::method("Base", method),
1624 files,
1625 false,
1626 ReferenceIncludes::Plain,
1627 &|| false,
1628 )
1629 .unwrap()
1630 .into_iter()
1631 .map(|(file, _)| file)
1632 .collect()
1633 }
1634
1635 #[derive(Debug, PartialEq)]
1638 struct AnalysisOutcome {
1639 issues: Vec<String>,
1640 symbols: usize,
1641 vendor_class_loaded: bool,
1642 vendor_function_loaded: bool,
1643 eager_files_drained: bool,
1644 stubs_loaded: usize,
1645 prepared: bool,
1646 prepare_generation: u64,
1647 references: Vec<String>,
1648 }
1649
1650 const DEPENDENT: &str = "<?php\nnamespace App;\nfunction go(\\Vendor\\Base $b): void { vendor_helper('x'); new \\ArrayObject(); }\n";
1651
1652 fn composer_project(root: &std::path::Path) -> AnalysisSession {
1653 let vendor = root.join("vendor");
1654 std::fs::create_dir_all(vendor.join("composer")).unwrap();
1655 std::fs::create_dir_all(vendor.join("helpers")).unwrap();
1656 std::fs::create_dir_all(root.join("lib")).unwrap();
1657 std::fs::write(
1658 vendor.join("composer/autoload_files.php"),
1659 "<?php\n$vendorDir = dirname(__DIR__);\n$baseDir = dirname($vendorDir);\nreturn array(\n 'abc123' => $vendorDir . '/helpers/functions.php',\n);\n",
1660 )
1661 .unwrap();
1662 for empty in ["psr4", "classmap", "namespaces"] {
1663 std::fs::write(
1664 vendor.join(format!("composer/autoload_{empty}.php")),
1665 "<?php\nreturn [];\n",
1666 )
1667 .unwrap();
1668 }
1669 std::fs::write(
1670 vendor.join("helpers/functions.php"),
1671 "<?php\nfunction vendor_helper(string $s): string { return $s; }\n",
1672 )
1673 .unwrap();
1674 std::fs::write(
1675 root.join("lib/Base.php"),
1676 "<?php\nnamespace Vendor;\nclass Base {}\n",
1677 )
1678 .unwrap();
1679 std::fs::write(
1680 root.join("composer.json"),
1681 r#"{"autoload":{"psr-4":{"App\\":"src/","Vendor\\":"lib/"}}}"#,
1682 )
1683 .unwrap();
1684 let psr4 = crate::composer::Psr4Map::from_composer(root).unwrap();
1685 AnalysisSession::new(PhpVersion::LATEST).with_psr4(Arc::new(psr4))
1686 }
1687
1688 fn outcome(
1689 session: &AnalysisSession,
1690 file: &Arc<str>,
1691 analysis: crate::FileAnalysis,
1692 ) -> AnalysisOutcome {
1693 let text = session.source_of(file).unwrap();
1694 AnalysisOutcome {
1695 issues: analysis
1696 .issues
1697 .iter()
1698 .map(|i| format!("{:?}", i.kind))
1699 .collect(),
1700 symbols: analysis.symbols.len(),
1701 vendor_class_loaded: {
1702 let view = session.db_view();
1703 let db = view.db();
1704 crate::db::class_like_indexed(db, crate::db::Fqcn::from_str(db, "Vendor\\Base"))
1705 },
1706 vendor_function_loaded: session.contains_function("vendor_helper"),
1707 eager_files_drained: session.pending_eager_function_files.is_none(),
1708 stubs_loaded: session.loaded_stub_count(),
1709 prepared: session.is_prepared_for_analysis(
1710 file,
1711 &text,
1712 session.prepare_generation_snapshot(),
1713 ),
1714 prepare_generation: session.prepare_generation_snapshot(),
1715 references: ["cls:Vendor\\Base", "fn:vendor_helper", "cls:ArrayObject"]
1716 .into_iter()
1717 .map(|key| format!("{key}={:?}", session.reference_locations(key)))
1718 .collect(),
1719 }
1720 }
1721
1722 #[test]
1726 fn prepare_for_query_then_snapshot_analyze_matches_file_analyzer() {
1727 let file: Arc<str> = Arc::from("consumer.php");
1728 let parsed = php_rs_parser::parse(DEPENDENT);
1729
1730 let root = tempfile::tempdir().unwrap();
1731 let mut via_analyzer = composer_project(root.path());
1732 via_analyzer.ingest_file(file.clone(), Arc::from(DEPENDENT));
1733 let analysis = crate::FileAnalyzer::new(&mut via_analyzer).analyze(
1734 file.clone(),
1735 DEPENDENT,
1736 &parsed.program,
1737 &parsed.source_map,
1738 );
1739 let expected = outcome(&via_analyzer, &file, analysis);
1740
1741 let root = tempfile::tempdir().unwrap();
1742 let mut via_snapshot = composer_project(root.path());
1743 via_snapshot.ingest_file(file.clone(), Arc::from(DEPENDENT));
1744 via_snapshot.prepare_for_query(Some(&file));
1745 let analysis = via_snapshot
1746 .snapshot()
1747 .analyze(file.clone(), DEPENDENT, &parsed.program, &parsed.source_map)
1748 .unwrap();
1749 let actual = outcome(&via_snapshot, &file, analysis);
1750
1751 assert!(
1752 expected.prepared && expected.vendor_function_loaded,
1753 "{expected:#?}"
1754 );
1755 assert_eq!(actual, expected);
1756 }
1757
1758 #[test]
1762 fn prepare_for_query_warms_nothing_for_a_file_that_was_never_ingested() {
1763 let file: Arc<str> = Arc::from("consumer.php");
1764 let parsed = php_rs_parser::parse(DEPENDENT);
1765 let unresolved = |analysis: &crate::FileAnalysis| {
1766 analysis
1767 .issues
1768 .iter()
1769 .any(|i| matches!(i.kind, crate::IssueKind::UndefinedFunction { .. }))
1770 };
1771
1772 let root = tempfile::tempdir().unwrap();
1773 let mut via_analyzer = composer_project(root.path());
1774 let analysis = crate::FileAnalyzer::new(&mut via_analyzer).analyze(
1775 file.clone(),
1776 DEPENDENT,
1777 &parsed.program,
1778 &parsed.source_map,
1779 );
1780 assert!(!unresolved(&analysis), "{:?}", analysis.issues);
1781
1782 let root = tempfile::tempdir().unwrap();
1783 let mut via_snapshot = composer_project(root.path());
1784 via_snapshot.prepare_for_query(Some(&file));
1785 let analysis = via_snapshot
1786 .snapshot()
1787 .analyze(file, DEPENDENT, &parsed.program, &parsed.source_map)
1788 .unwrap();
1789 assert!(unresolved(&analysis), "{:?}", analysis.issues);
1790 }
1791}