1use std::panic::AssertUnwindSafe;
2use std::sync::Arc;
3
4use rustc_hash::FxHashSet as HashSet;
5use salsa::Cancelled;
6
7use super::index_state::{
8 hash_files, AnalyzedFile, IndexState, QueryGeneration, RefQueryCacheKey, RetireEpoch,
9 SubtypeQueryCacheKey, ViewStamp,
10};
11use super::queries::{identifier_char_col, span_range, ReferenceGate};
12use super::SubtypeClassSite;
13use crate::cache::AnalysisCache;
14use crate::db::{MirDatabase, MirDbStorage};
15use crate::php_version::PhpVersion;
16
17#[derive(Clone)]
33pub struct AnalysisSnapshot {
34 pub(super) db: MirDbStorage,
35 pub(super) index: Arc<IndexState>,
36 pub(super) cache: Option<Arc<AnalysisCache>>,
37 pub(super) php_version: PhpVersion,
38 pub(super) index_generation: u64,
41 pub(super) retire_epoch: RetireEpoch,
44}
45
46const _: () = {
47 const fn assert_send_clone<T: Send + Clone>() {}
48 assert_send_clone::<AnalysisSnapshot>();
49};
50
51pub(crate) struct DbView<'a> {
55 snapshot: AnalysisSnapshot,
56 _owner: std::marker::PhantomData<&'a super::AnalysisSession>,
57}
58
59impl<'a> DbView<'a> {
60 pub(super) fn new(_owner: &'a super::AnalysisSession, snapshot: AnalysisSnapshot) -> Self {
61 Self {
62 snapshot,
63 _owner: std::marker::PhantomData,
64 }
65 }
66}
67
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 find_function(&self, fqn: &str) -> Result<Option<Arc<crate::FunctionDef>>, Cancelled> {
175 self.read(|db| crate::db::find_function(db, crate::db::Fqcn::from_str(db, fqn)))
176 }
177
178 #[allow(clippy::type_complexity)]
181 pub fn find_method_in_chain(
182 &self,
183 class: &str,
184 method: &str,
185 ) -> Result<Option<(Arc<str>, Arc<mir_codebase::definitions::MethodDef>)>, Cancelled> {
186 self.read(|db| {
187 crate::db::find_method_in_chain(db, crate::db::Fqcn::from_str(db, class), method)
188 })
189 }
190
191 pub fn definition_of_cached(
194 &self,
195 symbol: &crate::Name,
196 ) -> Result<Result<mir_types::Location, crate::SymbolLookupError>, Cancelled> {
197 catch(|| self.definition_of_unguarded(symbol))
198 }
199
200 fn definition_of_unguarded(
201 &self,
202 symbol: &crate::Name,
203 ) -> Result<mir_types::Location, crate::SymbolLookupError> {
204 let db = &self.db;
205 match symbol {
206 crate::Name::Class(fqcn) => {
207 let here = crate::db::Fqcn::from_str(db, fqcn.as_ref());
208 let class = crate::db::find_class_like(db, here)
209 .ok_or(crate::SymbolLookupError::NotFound)?;
210 class
211 .location()
212 .cloned()
213 .ok_or(crate::SymbolLookupError::NoSourceLocation)
214 }
215 crate::Name::Function(fqn) => {
216 let here = crate::db::Fqcn::from_str(db, fqn.as_ref());
217 let f =
218 crate::db::find_function(db, here).ok_or(crate::SymbolLookupError::NotFound)?;
219 f.location
220 .clone()
221 .ok_or(crate::SymbolLookupError::NoSourceLocation)
222 }
223 crate::Name::Method { .. }
224 | crate::Name::Property { .. }
225 | crate::Name::ClassConstant { .. } => {
226 crate::db::member_location(db, symbol).ok_or(crate::SymbolLookupError::NotFound)
227 }
228 crate::Name::GlobalConstant(fqn) => self
229 .global_constant_location(fqn)
230 .ok_or(crate::SymbolLookupError::NoSourceLocation),
231 }
232 }
233
234 pub fn files_mentioning_class(
243 &self,
244 files: &[Arc<str>],
245 class_name: &str,
246 ) -> Result<Vec<Arc<str>>, Cancelled> {
247 self.files_mentioning_any(files, &[class_name])
248 }
249
250 pub fn files_mentioning_any(
255 &self,
256 files: &[Arc<str>],
257 needles: &[&str],
258 ) -> Result<Vec<Arc<str>>, Cancelled> {
259 catch(|| self.mentioning_any_unguarded(files, needles))
260 }
261
262 fn mentioning_any_unguarded(&self, files: &[Arc<str>], needles: &[&str]) -> Vec<Arc<str>> {
263 use rayon::prelude::*;
264
265 if needles.is_empty() {
266 return files.to_vec();
267 }
268 let db = self.db.clone();
269 db.add_literal_mention_names(needles.iter().copied());
270 let queries: Vec<_> = needles
272 .iter()
273 .filter_map(|n| db.prepare_class_mention_query(n))
274 .collect();
275 if queries.is_empty() {
276 return files.to_vec();
277 }
278 let Some(scanner) = db.class_mention_scanner() else {
279 return files.to_vec();
280 };
281
282 files
283 .par_iter()
284 .map_with(db, |db, f| -> Option<Arc<str>> {
285 let sf = db.lookup_source_file(f.as_ref())?;
286 let text = sf.text(&*db as &dyn MirDatabase).clone();
287 for q in &queries {
288 match db.class_mention_answer(f.as_ref(), q, &text) {
289 Some(true) => return Some(f.clone()),
290 Some(false) => continue,
291 None => {
292 let names = scanner.scan(text.as_ref());
293 let hit = queries
294 .iter()
295 .any(|q2| names.binary_search(&q2.name).is_ok());
296 db.set_file_class_mentions(f, &text, scanner.epoch(), names);
297 return hit.then(|| f.clone());
298 }
299 }
300 }
301 None
302 })
303 .flatten()
304 .collect()
305 }
306
307 pub fn indexed_references_to(
339 &self,
340 symbol: &crate::Name,
341 files: &[Arc<str>],
342 include_declaration: bool,
343 includes: crate::ReferenceIncludes,
344 ) -> Result<Vec<(Arc<str>, crate::Range)>, Cancelled> {
345 let key = self.reference_query_key(symbol, files, include_declaration, includes);
346 if let Some(hit) = self.index.ref_queries.get(&key) {
347 return Ok(hit);
348 }
349 let stale = catch(|| self.stale_reference_candidates(symbol, files))?;
350 if !stale.is_empty() {
351 self.commit_reference_candidates(&stale)?;
352 }
353 let out = catch(|| self.read_references(symbol, files, include_declaration, includes))?;
354 self.ensure_no_retire_since(key.generation)?;
355 self.memoize_references(key, &out);
356 Ok(out)
357 }
358
359 pub(super) fn ensure_no_retire_since(
362 &self,
363 generation: QueryGeneration,
364 ) -> Result<(), Cancelled> {
365 if self.index.retire_seq() == generation.2 {
366 Ok(())
367 } else {
368 Err(Cancelled::PendingWrite)
369 }
370 }
371
372 pub fn indexed_use_import_locations(
376 &self,
377 symbol: &crate::Name,
378 files: &[Arc<str>],
379 ) -> Result<Vec<(Arc<str>, crate::Range)>, Cancelled> {
380 self.indexed_references_to(symbol, files, false, crate::ReferenceIncludes::UseImports)
381 }
382
383 pub(super) fn reference_query_key(
384 &self,
385 symbol: &crate::Name,
386 files: &[Arc<str>],
387 include_declaration: bool,
388 includes: crate::ReferenceIncludes,
389 ) -> RefQueryCacheKey {
390 RefQueryCacheKey {
391 symbol: symbol.codebase_key(),
392 include_declaration,
393 includes,
394 generation: self.query_cache_generation(),
395 files_hash: hash_files(files),
396 }
397 }
398
399 pub(super) fn memoize_references(
404 &self,
405 key: RefQueryCacheKey,
406 out: &[(Arc<str>, crate::Range)],
407 ) {
408 let generation = self.query_cache_generation();
409 if generation == key.generation {
410 self.index.ref_queries.insert(generation, key, out);
411 }
412 }
413
414 pub(super) fn stale_reference_candidates(
436 &self,
437 symbol: &crate::Name,
438 files: &[Arc<str>],
439 ) -> Vec<Arc<str>> {
440 let db = &self.db;
441 let gate = self.reference_gate(symbol);
442 let has_needles = !gate.idents.is_empty() || !gate.raw.is_empty();
443 let (mention_queries, mention_scanner) = if has_needles {
444 db.add_literal_mention_names(gate.idents.iter().map(|s| s.as_str()));
445 db.add_raw_mention_needles(gate.raw.iter().map(|s| s.as_str()));
446 let queries: Vec<_> = gate
447 .idents
448 .iter()
449 .chain(gate.raw.iter())
450 .filter_map(|s| db.prepare_class_mention_query(s))
451 .collect();
452 (queries, db.class_mention_scanner())
453 } else {
454 (Vec::new(), None)
455 };
456 let gate_complete = mention_queries.len() == gate.idents.len() + gate.raw.len()
460 && mention_scanner.is_some();
461 let committed_any: HashSet<Arc<str>> =
462 self.index.ref_committed_keys().into_iter().collect();
463 let current_gen = self.index_generation;
464 let mut stale = Vec::new();
465 let mut scanned: Vec<MentionScanRecord> = Vec::new();
466 for f in files {
467 let Some(sf) = db.lookup_source_file(f.as_ref()) else {
468 continue;
469 };
470 let text = sf.text(db as &dyn MirDatabase);
471 if self.index.is_ref_committed(f.as_ref(), text, current_gen) {
472 continue;
473 }
474 if committed_any.contains(f.as_ref())
475 && !self
476 .index
477 .ref_commit_stale_by_generation_only(f.as_ref(), text)
478 {
479 stale.push(f.clone());
480 continue;
481 }
482 if has_needles && gate_complete {
483 let mut answer = Some(false);
487 for q in &mention_queries {
488 match db.class_mention_answer(f.as_ref(), q, text) {
489 Some(true) => {
490 answer = Some(true);
491 break;
492 }
493 Some(false) => {}
494 None => answer = None,
495 }
496 }
497 match answer {
498 Some(true) => {}
499 Some(false) => continue,
500 None => {
501 let scanner = mention_scanner
502 .as_ref()
503 .expect("gate_complete implies a scanner");
504 let names = scanner.scan(text);
505 let hit = mention_queries
506 .iter()
507 .any(|q| names.binary_search(&q.name).is_ok());
508 scanned.push((f.clone(), text.clone(), names));
509 if hit {
510 stale.push(f.clone());
511 }
512 continue;
513 }
514 }
515 }
516 stale.push(f.clone());
517 }
518 if let Some(scanner) = &mention_scanner {
521 for (file, text, names) in scanned {
522 db.set_file_class_mentions(&file, &text, scanner.epoch(), names);
523 }
524 }
525 stale
526 }
527
528 pub(super) fn commit_reference_candidates(&self, stale: &[Arc<str>]) -> Result<(), Cancelled> {
530 self.warm_pass(stale, &crate::IndexCancel::new()).map(drop)
531 }
532
533 pub fn warm_files(
540 &self,
541 files: &[Arc<str>],
542 cancel: &crate::IndexCancel,
543 ) -> Result<bool, Cancelled> {
544 Ok(self.warm_pass(files, cancel)?.is_some())
545 }
546
547 pub(super) fn warm_pass(
548 &self,
549 files: &[Arc<str>],
550 cancel: &crate::IndexCancel,
551 ) -> Result<Option<Vec<AnalyzedFile>>, Cancelled> {
552 let Some(mut pass) = self.stage_warm(files, cancel)? else {
553 return Ok(None);
554 };
555 self.commit_warm(&mut pass)?;
556 Ok(Some(pass.analyzed))
557 }
558
559 fn stage_warm(
564 &self,
565 files: &[Arc<str>],
566 cancel: &crate::IndexCancel,
567 ) -> Result<Option<WarmPass>, Cancelled> {
568 catch(|| {
569 let mut db = self.db.clone();
570 db.freeze_workspace_index();
571 let mention_scanner = db.class_mention_scanner();
572 let cache = self.cache.as_deref();
573 let mut analyzed = Vec::with_capacity(files.len());
574 for path in files {
575 if cancel.is_cancelled() {
576 return None;
577 }
578 analyzed.extend(self.index.stage_analyzed(
579 &db,
580 cache,
581 mention_scanner.as_deref(),
582 path,
583 ));
584 }
585 Some(WarmPass {
586 mention_scanner,
587 analyzed,
588 })
589 })
590 }
591
592 fn commit_warm(&self, pass: &mut WarmPass) -> Result<(), Cancelled> {
595 self.index.commit_analyzed(
596 &self.db,
597 self.cache.as_deref(),
598 pass.mention_scanner.as_deref(),
599 &mut pass.analyzed,
600 self.stamp(),
601 )
602 }
603
604 pub(super) fn read_references(
617 &self,
618 symbol: &crate::Name,
619 files: &[Arc<str>],
620 include_declaration: bool,
621 includes: crate::ReferenceIncludes,
622 ) -> Vec<(Arc<str>, crate::Range)> {
623 let key = symbol.codebase_key();
624 let hierarchy: Vec<String> = match symbol {
625 crate::Name::Method { class, name } => {
626 if class.is_empty() {
627 Vec::new()
628 } else if name.as_ref() == "__construct" {
629 vec![class.trim_start_matches('\\').to_string()]
630 } else {
631 self.member_hierarchy_classes(class.as_ref())
632 }
633 }
634 crate::Name::Property { class, .. } | crate::Name::ClassConstant { class, .. } => {
635 if class.is_empty() {
636 Vec::new()
637 } else {
638 self.member_hierarchy_classes(class.as_ref())
639 }
640 }
641 _ => Vec::new(),
642 };
643 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
644 let read_symbol_key = |symbol_key: &str| -> Vec<(Arc<str>, crate::Range)> {
645 self.db
646 .reference_locations(symbol_key)
647 .into_iter()
648 .filter(|(file, ..)| scope.contains(file.as_ref()))
649 .map(|(file, line, col_start, col_end)| {
650 (file, span_range(line, col_start as u32, col_end as u32))
651 })
652 .collect()
653 };
654 let mut scratch_key = String::new();
655 let mut read_composed_key = |prefix: &str, middle: &str, separator: &str, suffix: &str| {
656 scratch_key.clear();
657 scratch_key.reserve(prefix.len() + middle.len() + separator.len() + suffix.len());
658 scratch_key.push_str(prefix);
659 scratch_key.push_str(middle);
660 scratch_key.push_str(separator);
661 scratch_key.push_str(suffix);
662 read_symbol_key(&scratch_key)
663 };
664 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
665 if !matches!(includes, crate::ReferenceIncludes::UseImports) {
666 let member_prefix = match symbol {
667 crate::Name::Method { name, .. } => Some(("meth:", name)),
668 crate::Name::Property { name, .. } => Some(("prop:", name)),
669 crate::Name::ClassConstant { name, .. } => Some(("cnst:", name)),
670 _ => None,
671 };
672 match member_prefix {
673 Some((prefix, name)) => {
674 for class in &hierarchy {
675 out.extend(read_composed_key(prefix, class, "::", name));
676 }
677 }
678 None => out.extend(read_symbol_key(key.as_str())),
679 }
680 }
681 if !matches!(includes, crate::ReferenceIncludes::Plain) {
682 out.extend(read_composed_key("use:", key.as_str(), "", ""));
683 }
684 if matches!(includes, crate::ReferenceIncludes::Plain) && out.is_empty() {
685 match symbol {
686 crate::Name::Method { name, .. } => {
687 out = read_composed_key("methname:", name.as_ref(), "", "");
688 }
689 crate::Name::Property { name, .. } => {
690 out = read_composed_key("propname:", name.as_ref(), "", "");
691 }
692 _ => {}
693 }
694 }
695 out.sort_by(|a, b| {
696 a.0.cmp(&b.0)
697 .then(a.1.start.line.cmp(&b.1.start.line))
698 .then(a.1.start.column.cmp(&b.1.start.column))
699 });
700 out.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
701
702 if include_declaration {
703 let decls: Vec<(Arc<str>, crate::Range)> = match symbol {
704 crate::Name::Method { class, name }
705 | crate::Name::Property { class, name }
706 | crate::Name::ClassConstant { class, name } => {
707 if class.is_empty() {
708 let prefix = match symbol {
712 crate::Name::Method { .. } => "methdecl:",
713 crate::Name::Property { .. } => "propdecl:",
714 _ => "cnstdecl:",
715 };
716 read_composed_key(prefix, name.as_ref(), "", "")
717 } else {
718 self.member_decl_sites(&hierarchy, symbol)
719 }
720 }
721 _ => self.declaration_name_range(symbol).into_iter().collect(),
722 };
723 for (file, range) in decls {
724 if scope.contains(file.as_ref())
725 && !out.iter().any(|(f, r)| *f == file && *r == range)
726 {
727 out.push((file, range));
728 }
729 }
730 }
731 out
732 }
733
734 fn member_hierarchy_classes(&self, class_fqn: &str) -> Vec<String> {
740 let target = class_fqn.trim_start_matches('\\');
741 let mut out = vec![target.to_string()];
742 let db = &self.db;
743 let here = crate::db::Fqcn::from_str(db, target);
744 let ancestors = crate::db::class_ancestors_by_fqcn(db, here)
746 .iter()
747 .skip(1)
748 .map(|a| a.trim_start_matches('\\').to_string())
749 .collect::<Vec<_>>();
750 out.extend(ancestors);
751 out.extend(
752 db.subtype_sites_of(target, true)
753 .into_iter()
754 .map(|s| s.fqcn.trim_start_matches('\\').to_string()),
755 );
756 let mut seen: HashSet<String> = HashSet::default();
757 out.retain(|c| seen.insert(c.to_ascii_lowercase()));
758 out
759 }
760
761 fn member_decl_sites(
767 &self,
768 classes: &[String],
769 symbol: &crate::Name,
770 ) -> Vec<(Arc<str>, crate::Range)> {
771 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
772 let db = &self.db;
773 for class in classes {
774 let here = crate::db::Fqcn::from_str(db, class);
775 let (loc, needle) = match symbol {
776 crate::Name::Method { name, .. } => {
777 let Some(m) = crate::db::find_method_in_class(db, here, name) else {
778 continue;
779 };
780 (m.location.clone(), name.to_string())
781 }
782 crate::Name::Property { name, .. } => {
783 let Some(p) = crate::db::find_property_in_class(db, here, name) else {
784 continue;
785 };
786 (p.location.clone(), name.to_string())
787 }
788 crate::Name::ClassConstant { name, .. } => {
789 let Some(c) = crate::db::find_class_constant_in_class(db, here, name) else {
790 continue;
791 };
792 (c.location.clone(), name.to_string())
793 }
794 _ => continue,
795 };
796 let Some(loc) = loc else { continue };
797 let range = self.refine_location_to_name(&loc, &needle);
798 out.push((loc.file.clone(), range));
799 }
800 out
801 }
802
803 pub(super) fn declaration_name_range(
807 &self,
808 symbol: &crate::Name,
809 ) -> Option<(Arc<str>, crate::Range)> {
810 if let crate::Name::GlobalConstant(fqn) = symbol {
811 return self.global_constant_decl_range(fqn);
812 }
813 let loc = self.definition_of_unguarded(symbol).ok()?;
814 let short = match symbol {
815 crate::Name::Class(f) | crate::Name::Function(f) | crate::Name::GlobalConstant(f) => {
816 crate::db::subtype_index::short_name_of(f)
817 }
818 crate::Name::Method { name, .. }
819 | crate::Name::Property { name, .. }
820 | crate::Name::ClassConstant { name, .. } => name.as_ref(),
821 };
822 let range = self.refine_location_to_name(&loc, short);
826 Some((loc.file.clone(), range))
827 }
828
829 fn refine_location_to_name(&self, loc: &mir_types::Location, needle: &str) -> crate::Range {
834 let fallback = span_range(loc.line, loc.col_start as u32, loc.col_end as u32);
835 let Some(text) = self
836 .db
837 .lookup_source_file(loc.file.as_ref())
838 .map(|sf| sf.text(&self.db as &dyn MirDatabase).clone())
839 else {
840 return fallback;
841 };
842 let needle_chars = needle.chars().count() as u32;
843 let first_line = loc.line.saturating_sub(1) as usize;
844 for case_insensitive in [false, true] {
849 for (idx, line_text) in text.lines().enumerate().skip(first_line) {
850 let line_no = idx as u32 + 1;
851 if line_no > loc.line_end {
852 break;
853 }
854 let min_col = if line_no == loc.line {
855 loc.col_start as usize
856 } else {
857 0
858 };
859 if let Some(col) = identifier_char_col(line_text, needle, min_col, case_insensitive)
860 {
861 return span_range(line_no, col, col + needle_chars);
862 }
863 }
864 }
865 fallback
866 }
867
868 fn global_constant_decl_site(&self, fqn: &str) -> Option<ConstantDeclSite> {
872 let short = crate::db::subtype_index::short_name_of(fqn);
873 let db = &self.db;
874 let index = crate::db::workspace_index(db);
875 let loc = index.constant_loc(mir_types::Name::from(fqn.trim_start_matches('\\')))?;
876 let file = loc.file().path(db).clone();
877 let sf = db.lookup_source_file(file.as_ref())?;
878 let text = sf.text(db as &dyn MirDatabase);
879 for (idx, line) in text.lines().enumerate() {
880 let trimmed = line.trim_start();
881 let is_decl_line = trimmed.starts_with("const ")
882 || trimmed.contains("define(")
883 || trimmed.contains("define (");
884 if !is_decl_line {
885 continue;
886 }
887 if let Some(name_col) = identifier_char_col(line, short, 0, false) {
888 let stmt_end = match line.find(';') {
889 Some(byte) => line[..=byte].chars().count(),
890 None => line.chars().count(),
891 };
892 return Some(ConstantDeclSite {
893 file,
894 line: idx as u32 + 1,
895 stmt_col: (line.chars().count() - trimmed.chars().count()) as u32,
896 stmt_end: stmt_end as u32,
897 name_col,
898 name_len: short.chars().count() as u32,
899 });
900 }
901 }
902 None
903 }
904
905 fn global_constant_decl_range(&self, fqn: &str) -> Option<(Arc<str>, crate::Range)> {
906 let site = self.global_constant_decl_site(fqn)?;
907 let range = span_range(site.line, site.name_col, site.name_col + site.name_len);
908 Some((site.file, range))
909 }
910
911 fn global_constant_location(&self, fqn: &str) -> Option<mir_types::Location> {
913 let site = self.global_constant_decl_site(fqn)?;
914 Some(mir_types::Location {
915 file: site.file,
916 line: site.line,
917 line_end: site.line,
918 col_start: site.stmt_col as u16,
919 col_end: site.stmt_end as u16,
920 })
921 }
922
923 pub fn subtype_files(&self, class_fqn: &str) -> Result<Vec<Arc<str>>, Cancelled> {
928 let files = self.db.source_file_paths();
929 let mut out: Vec<Arc<str>> = self
930 .indexed_subtype_classes(class_fqn, &files, false)?
931 .into_iter()
932 .map(|s| s.file)
933 .collect();
934 out.sort();
935 out.dedup();
936 Ok(out)
937 }
938
939 pub fn indexed_subtype_classes(
958 &self,
959 class_fqn: &str,
960 files: &[Arc<str>],
961 include_trait_users: bool,
962 ) -> Result<Vec<SubtypeClassSite>, Cancelled> {
963 catch(|| {
964 let key = SubtypeQueryCacheKey {
965 class_fqn: class_fqn.trim_start_matches('\\').to_ascii_lowercase(),
966 include_trait_users,
967 generation: self.query_cache_generation(),
968 files_hash: hash_files(files),
969 };
970 if let Some(hit) = self.index.subtype_queries.get(&key) {
971 return hit;
972 }
973 let out = self.subtype_classes_uncached(class_fqn, files, include_trait_users);
974 if let Err(cancelled) = self.ensure_no_retire_since(key.generation) {
975 unwind(cancelled);
976 }
977 let generation = self.query_cache_generation();
979 if generation == key.generation {
980 self.index.subtype_queries.insert(generation, key, &out);
981 }
982 out
983 })
984 }
985
986 fn subtype_classes_uncached(
987 &self,
988 class_fqn: &str,
989 files: &[Arc<str>],
990 include_trait_users: bool,
991 ) -> Vec<SubtypeClassSite> {
992 let db = &self.db;
993 let mut scanned: HashSet<String> = HashSet::default();
994 let mut pending: Vec<String> = vec![class_fqn.trim_start_matches('\\').to_string()];
995 let mut sites: Vec<crate::db::SubtypeSite> = Vec::new();
996 while !pending.is_empty() {
997 let needles: Vec<String> = pending
1000 .drain(..)
1001 .filter(|f| scanned.insert(f.clone()))
1002 .map(|f| crate::db::subtype_index::short_name_of(&f).to_string())
1003 .collect();
1004 if !needles.is_empty() {
1005 self.commit_defs_for_matching(files, &needles);
1006 }
1007 sites = db.subtype_sites_of(class_fqn, include_trait_users);
1008 pending = sites
1009 .iter()
1010 .map(|s| s.fqcn.trim_start_matches('\\').to_string())
1011 .filter(|f| !scanned.contains(f))
1012 .collect();
1013 }
1014 let mut out: Vec<SubtypeClassSite> = sites
1015 .into_iter()
1016 .filter_map(|s| {
1017 let loc = s.location.as_ref()?;
1018 let short = crate::db::subtype_index::short_name_of(&s.fqcn).to_string();
1019 let range = self.refine_location_to_name(loc, &short);
1020 Some(SubtypeClassSite {
1021 fqcn: s.fqcn,
1022 kind: s.kind,
1023 is_abstract: s.is_abstract,
1024 file: s.file,
1025 range,
1026 })
1027 })
1028 .collect();
1029 let root_lc = class_fqn.trim_start_matches('\\').to_ascii_lowercase();
1033 let scope: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
1034 for (file, line, cs, ce) in db.reference_locations(&format!("impl:{root_lc}")) {
1035 if !scope.contains(file.as_ref()) {
1036 continue;
1037 }
1038 let range = span_range(line, cs as u32, ce as u32);
1039 if out.iter().any(|s| s.file == file && s.range == range) {
1040 continue;
1041 }
1042 out.push(SubtypeClassSite {
1043 fqcn: Arc::from("class@anonymous"),
1044 kind: crate::db::ClassLikeKind::Class,
1045 is_abstract: false,
1046 file,
1047 range,
1048 });
1049 }
1050 out
1051 }
1052
1053 #[allow(clippy::type_complexity)]
1059 pub fn indexed_method_implementations(
1060 &self,
1061 class_fqn: &str,
1062 method: &str,
1063 files: &[Arc<str>],
1064 ) -> Result<Vec<(Arc<str>, Arc<str>, crate::Range)>, Cancelled> {
1065 let subs = self.indexed_subtype_classes(class_fqn, files, false)?;
1066 catch(|| {
1067 let db = &self.db;
1068 let mut out: Vec<(Arc<str>, Arc<str>, crate::Range)> = Vec::new();
1069 for sub in &subs {
1070 let here = crate::db::Fqcn::from_str(db, sub.fqcn.as_ref());
1071 let Some((_, m)) = crate::db::find_method_in_chain(db, here, method) else {
1072 continue;
1073 };
1074 if m.is_abstract {
1075 continue;
1076 }
1077 let Some(loc) = m.location.as_ref() else {
1078 continue;
1079 };
1080 let range = self.refine_location_to_name(loc, method);
1081 out.push((sub.fqcn.clone(), loc.file.clone(), range));
1082 }
1083 out.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.start.line.cmp(&b.2.start.line)));
1084 out.dedup_by(|a, b| a.1 == b.1 && a.2 == b.2);
1085 out
1086 })
1087 }
1088
1089 fn commit_defs_for_matching(&self, files: &[Arc<str>], shorts: &[String]) {
1102 use rayon::prelude::*;
1103
1104 let committed_any: HashSet<Arc<str>> =
1105 self.index.defs_committed_keys().into_iter().collect();
1106 self.db
1110 .add_literal_mention_names(shorts.iter().map(|s| s.as_str()));
1111 let queries: Vec<_> = shorts
1112 .iter()
1113 .filter_map(|s| self.db.prepare_class_mention_query(s))
1114 .collect();
1115 let mention_scanner = self.db.class_mention_scanner();
1116 let use_mentions = queries.len() == shorts.len() && mention_scanner.is_some();
1120 type Work = (Arc<str>, Arc<str>, Vec<crate::db::SubtypeEntry>);
1121 let index = &*self.index;
1122 let results: Vec<(Option<Work>, Option<MentionScanRecord>)> = files
1123 .par_iter()
1124 .map_with(self.db.clone(), |db, path| {
1125 let Some(sf) = db.lookup_source_file(path.as_ref()) else {
1126 return (None, None);
1127 };
1128 let text = sf.text(&*db as &dyn MirDatabase).clone();
1129 if index.is_defs_committed(path.as_ref(), &text) {
1130 return (None, None);
1131 }
1132 let mut scan_rec: Option<MentionScanRecord> = None;
1136 if use_mentions && !committed_any.contains(path.as_ref()) {
1137 let mut answer = Some(false);
1138 for q in &queries {
1139 match db.class_mention_answer(path.as_ref(), q, &text) {
1140 Some(true) => {
1141 answer = Some(true);
1142 break;
1143 }
1144 Some(false) => {}
1145 None => answer = None,
1146 }
1147 }
1148 let hit = match answer {
1149 Some(hit) => hit,
1150 None => {
1151 let scanner = mention_scanner.as_ref().unwrap();
1154 let names = scanner.scan(&text);
1155 let hit = queries.iter().any(|q| names.binary_search(&q.name).is_ok());
1156 scan_rec = Some((path.clone(), text.clone(), names));
1157 hit
1158 }
1159 };
1160 if !hit {
1161 return (None, scan_rec);
1162 }
1163 }
1164 let defs = crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
1165 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
1166 (Some((path.clone(), text, entries)), scan_rec)
1167 })
1168 .collect();
1169 let mut refused = Ok(());
1173 for (work, scan_rec) in results {
1174 if let (Some(scanner), Some((file, text, names))) = (&mention_scanner, scan_rec) {
1175 self.db
1176 .set_file_class_mentions(&file, &text, scanner.epoch(), names);
1177 }
1178 if let Some((file, text, entries)) = work {
1179 if let Err(cancelled) = self.index.commit_class_edges(
1180 &self.db,
1181 &file,
1182 &text,
1183 entries,
1184 self.retire_epoch,
1185 ) {
1186 refused = Err(cancelled);
1187 }
1188 }
1189 }
1190 if let Err(cancelled) = refused {
1191 unwind(cancelled);
1192 }
1193 }
1194
1195 pub fn class_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1203 catch(|| {
1204 let db = &self.db;
1205 let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
1206 let file_data: Vec<(Arc<str>, Arc<str>)> = files
1207 .iter()
1208 .filter_map(|f| {
1209 let sf = db.lookup_source_file(f)?;
1210 Some((f.clone(), sf.text(db as &dyn MirDatabase).clone()))
1211 })
1212 .collect();
1213 crate::class::ClassAnalyzer::with_files(db, file_set, &file_data).analyze_all()
1214 })
1215 }
1216
1217 pub fn collector_issues(&self, files: &[Arc<str>]) -> Result<Vec<crate::Issue>, Cancelled> {
1226 catch(|| {
1227 files
1228 .iter()
1229 .filter_map(|f| self.db.lookup_source_file(f))
1230 .flat_map(|sf| {
1231 crate::db::collect_file_definitions(&self.db, sf)
1232 .issues
1233 .as_ref()
1234 .clone()
1235 })
1236 .collect()
1237 })
1238 }
1239
1240 pub(super) fn reference_gate(&self, symbol: &crate::Name) -> ReferenceGate {
1251 ReferenceGate::for_symbol(&self.db, symbol)
1252 }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use super::*;
1258 use crate::{AnalysisSession, IndexCancel, Name, ReferenceIncludes};
1259 use salsa::Database as _;
1260
1261 const BASE: &str =
1262 "<?php\nclass Base { public function run(): void {} public function stop(): void {} }\n";
1263 const CALLS_RUN: &str = "<?php\nfunction go(Base $b): void { $b->run(); }\n";
1264 const CALLS_STOP: &str = "<?php\nfunction go(Base $b): void { $b->stop(); }\n";
1265
1266 #[test]
1270 fn warm_commit_racing_a_text_write_leaves_the_file_stale() {
1271 let (mut session, files) = prepared_workspace(CALLS_RUN);
1272 let snap = session.snapshot();
1273 let mut pass = snap
1274 .stage_warm(&files[1..], &IndexCancel::new())
1275 .unwrap()
1276 .unwrap();
1277
1278 let caller = files[1].clone();
1279 let writer = std::thread::spawn(move || {
1280 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1281 session
1282 });
1283 await_pending_write(&snap);
1284 snap.commit_warm(&mut pass).unwrap();
1285 drop(snap);
1286 let mut session = writer.join().unwrap();
1287
1288 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1289 assert!(callers_of(&mut session, &files, "run").is_empty());
1290 }
1291
1292 #[test]
1293 fn warm_commit_after_an_ingest_retired_the_file_is_refused() {
1294 let (session, files) = prepared_workspace(CALLS_RUN);
1295 let snap = session.snapshot();
1296 let mut pass = snap
1297 .stage_warm(&files[1..], &IndexCancel::new())
1298 .unwrap()
1299 .unwrap();
1300
1301 let caller = files[1].clone();
1302 let writer = std::thread::spawn(move || {
1303 let mut session = session;
1304 session.ingest_file(caller, Arc::from(CALLS_STOP));
1305 session
1306 });
1307 await_pending_write(&snap);
1308 assert!(snap.commit_warm(&mut pass).is_err());
1309 drop(snap);
1310 let mut session = writer.join().unwrap();
1311
1312 assert!(callers_of(&mut session, &files, "run").is_empty());
1313 assert_eq!(callers_of(&mut session, &files, "stop"), [files[1].clone()]);
1314 }
1315
1316 #[test]
1317 fn commits_after_invalidate_file_retired_the_file_are_refused() {
1318 const CHILD_CALLS_RUN: &str =
1319 "<?php\nclass Child extends Base {}\nfunction go(Base $b): void { $b->run(); }\n";
1320 let (session, files) = prepared_workspace(CHILD_CALLS_RUN);
1321 let snap = session.snapshot();
1322 let mut pass = snap
1323 .stage_warm(&files[1..], &IndexCancel::new())
1324 .unwrap()
1325 .unwrap();
1326 let parsed = php_rs_parser::parse(CHILD_CALLS_RUN);
1327
1328 let caller = files[1].clone();
1329 let writer = std::thread::spawn(move || {
1330 let mut session = session;
1331 session.invalidate_file(&caller);
1332 session
1333 });
1334 await_pending_write(&snap);
1335 assert!(snap.commit_warm(&mut pass).is_err());
1336 assert!(snap
1337 .analyze(
1338 files[1].clone(),
1339 CHILD_CALLS_RUN,
1340 &parsed.program,
1341 &parsed.source_map
1342 )
1343 .is_err());
1344 drop(snap);
1345 let mut session = writer.join().unwrap();
1346
1347 assert!(callers_of(&mut session, &files, "run").is_empty());
1348 assert!(session
1349 .indexed_subtype_classes("Base", &files, false)
1350 .is_empty());
1351 }
1352
1353 #[test]
1354 fn cancelled_reference_commit_reports_cancellation() {
1355 let (mut session, files) = prepared_workspace(CALLS_RUN);
1356 let snap = session.snapshot();
1357 let stale = snap.stale_reference_candidates(&Name::method("Base", "run"), &files);
1358 assert!(stale.contains(&files[1]));
1359
1360 let caller = files[1].clone();
1361 let writer = std::thread::spawn(move || {
1362 session.upsert_source_file(caller, Arc::from(CALLS_STOP), salsa::Durability::LOW);
1363 session
1364 });
1365 await_pending_write(&snap);
1366 assert!(snap.commit_reference_candidates(&stale).is_err());
1367 drop(snap);
1368 writer.join().unwrap();
1369 }
1370
1371 const ROUTES_CALLS_RUN: &str = "<?php\n(new Base())->run();\n";
1374
1375 fn registered_routes_workspace() -> (AnalysisSession, Vec<Arc<str>>) {
1379 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1380 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("routes.php")];
1381 session.ingest_file(files[0].clone(), Arc::from(BASE));
1382 session.upsert_source_file(
1383 files[1].clone(),
1384 Arc::from(ROUTES_CALLS_RUN),
1385 salsa::Durability::LOW,
1386 );
1387 session.prepare_for_query(None);
1388 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1389 (session, files)
1390 }
1391
1392 #[test]
1396 fn ingest_of_unchanged_text_mid_query_keeps_references() {
1397 let (mut session, files) = registered_routes_workspace();
1398 let symbol = Name::method("Base", "run");
1399 let revision = session.text_revision();
1400
1401 let snap = session.snapshot();
1402 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1403 assert!(snap.stale_reference_candidates(&symbol, &files).is_empty());
1404 session.ingest_file(files[1].clone(), Arc::from(ROUTES_CALLS_RUN));
1405 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1406 assert!(snap.ensure_no_retire_since(key.generation).is_ok());
1407 snap.memoize_references(key, &out);
1408 drop(snap);
1409
1410 assert_eq!(session.text_revision(), revision, "ingest wrote salsa");
1411 assert_eq!(
1412 out.into_iter().map(|(file, _)| file).collect::<Vec<_>>(),
1413 [files[1].clone()]
1414 );
1415 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1416 }
1417
1418 #[test]
1422 fn query_spanning_a_retirement_is_cancelled_and_not_memoized() {
1423 let (mut session, files) = registered_routes_workspace();
1424 let symbol = Name::method("Base", "run");
1425
1426 let snap = session.snapshot();
1427 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1428 assert!(snap.stale_reference_candidates(&symbol, &files).is_empty());
1429 session
1430 .index
1431 .retire_references(&session.db.salsa, files[1].as_ref());
1432 let out = snap.read_references(&symbol, &files, false, ReferenceIncludes::Plain);
1433 assert!(out.is_empty());
1434 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1435 snap.memoize_references(key, &out);
1436 drop(snap);
1437
1438 let hits = session.ref_query_cache_hits();
1439 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1440 assert_eq!(session.ref_query_cache_hits(), hits);
1441 }
1442
1443 #[test]
1446 fn re_analyze_file_cache_hit_cancels_spanning_queries() {
1447 let dir = tempfile::tempdir().unwrap();
1448 let mut session = AnalysisSession::new(PhpVersion::LATEST).with_cache_dir(dir.path());
1449 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1450 let opts = crate::BatchOptions::new();
1451 session.ingest_file(files[0].clone(), Arc::from(BASE));
1452 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1453 session.prepare_for_query(None);
1454 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1455
1456 let symbol = Name::method("Base", "run");
1457 let snap = session.snapshot();
1458 let key = snap.reference_query_key(&symbol, &files, false, ReferenceIncludes::Plain);
1459 let revision = session.text_revision();
1460 session.re_analyze_file(&files[1], CALLS_RUN, &opts);
1461 assert_eq!(session.text_revision(), revision, "took the analysis path");
1462 assert!(snap.ensure_no_retire_since(key.generation).is_err());
1463 drop(snap);
1464
1465 assert_eq!(callers_of(&mut session, &files, "run"), [files[1].clone()]);
1466 }
1467
1468 fn prepared_workspace(caller_text: &str) -> (AnalysisSession, Vec<Arc<str>>) {
1469 let mut session = AnalysisSession::new(PhpVersion::LATEST);
1470 let files: Vec<Arc<str>> = vec![Arc::from("base.php"), Arc::from("caller.php")];
1471 session.ingest_file(files[0].clone(), Arc::from(BASE));
1472 session.ingest_file(files[1].clone(), Arc::from(caller_text));
1473 session.prepare_for_query(Some(&files[1]));
1474 (session, files)
1475 }
1476
1477 fn await_pending_write(snap: &AnalysisSnapshot) {
1480 while catch(|| snap.db.unwind_if_revision_cancelled()).is_ok() {
1481 std::thread::yield_now();
1482 }
1483 }
1484
1485 fn callers_of(
1486 session: &mut AnalysisSession,
1487 files: &[Arc<str>],
1488 method: &str,
1489 ) -> Vec<Arc<str>> {
1490 session
1491 .indexed_references_to(
1492 &Name::method("Base", method),
1493 files,
1494 false,
1495 ReferenceIncludes::Plain,
1496 &|| false,
1497 )
1498 .unwrap()
1499 .into_iter()
1500 .map(|(file, _)| file)
1501 .collect()
1502 }
1503}