mir_analyzer/session/loading.rs
1use super::*;
2
3impl AnalysisSession {
4 /// Returns `true` if a function with `fqn` is registered and active in
5 /// the codebase. Case-insensitive lookup with optional leading backslash.
6 pub fn contains_function(&self, fqn: &str) -> bool {
7 let db = self.snapshot_db();
8 crate::db::function_exists(&db, fqn)
9 }
10
11 /// Returns `true` if a class / interface / trait / enum with `fqcn` is
12 /// registered and active in the codebase.
13 pub fn contains_class(&self, fqcn: &str) -> bool {
14 let db = self.snapshot_db();
15 crate::db::class_exists(&db, fqcn)
16 }
17
18 fn class_indexed(&self, fqcn: &str) -> bool {
19 let db = self.snapshot_db();
20 crate::db::class_like_indexed(&db, crate::db::Fqcn::from_str(&db, fqcn))
21 }
22
23 /// Returns `true` if `class` has a method named `name` registered. Method
24 /// names are matched case-insensitively (PHP method dispatch semantics).
25 pub fn contains_method(&self, class: &str, name: &str) -> bool {
26 let db = self.snapshot_db();
27 crate::db::has_method_in_chain(&db, class, name)
28 }
29
30 /// Resolve `fqcn` via the configured [`crate::ClassResolver`] and ingest
31 /// the mapped file into the symbol index. Lookups already find such a
32 /// class on demand; ingesting also makes workspace enumeration see it.
33 /// The session keeps a negative cache so repeated calls for an
34 /// unresolvable name don't re-hit the resolver; the cache is invalidated
35 /// on any [`Self::ingest_file`] / [`Self::invalidate_file`].
36 ///
37 /// This is the LSP-friendly entry point: the analyzer never touches
38 /// `vendor/` on its own, but consumers can ask it to resolve individual
39 /// symbols on demand. Designed to be called when a diagnostic would
40 /// otherwise report `UndefinedClass`.
41 ///
42 /// Returns a [`crate::LoadOutcome`] distinguishing
43 /// already-loaded / freshly-loaded / not-resolvable. Use
44 /// [`crate::LoadOutcome::is_loaded`] when only success matters.
45 pub fn load_class(&mut self, fqcn: &str) -> crate::LoadOutcome {
46 if self.class_indexed(fqcn) {
47 return crate::LoadOutcome::AlreadyLoaded;
48 }
49 if self.unresolvable_fqcns.contains_key(fqcn) {
50 return crate::LoadOutcome::NotResolvable;
51 }
52 if self.try_resolve_and_ingest(fqcn) {
53 crate::LoadOutcome::Loaded
54 } else {
55 // Cache the failure with the resolver-mapped path (if any) so
56 // future file edits can selectively evict.
57 let resolved_path: Option<Arc<str>> = self
58 .resolver
59 .as_ref()
60 .and_then(|r| r.resolve(fqcn))
61 .map(|p| Arc::from(p.to_string_lossy().as_ref()));
62 let key: Arc<str> = Arc::from(fqcn);
63 if self.unresolvable_fqcns.len() >= UNRESOLVABLE_CACHE_CAP {
64 self.unresolvable_fqcns.clear();
65 }
66 self.unresolvable_fqcns.insert(key, resolved_path);
67 crate::LoadOutcome::NotResolvable
68 }
69 }
70
71 /// Inner load path: resolver lookup + ingest, no caching. Returns `true`
72 /// iff `fqcn` ends up registered. Failure buckets are recorded for
73 /// telemetry.
74 fn try_resolve_and_ingest(&mut self, fqcn: &str) -> bool {
75 use crate::metrics::{record_lazy_load_failure, LazyLoadFailure};
76 let Some(resolver) = &self.resolver else {
77 record_lazy_load_failure(LazyLoadFailure::NoResolver, fqcn);
78 return false;
79 };
80 let Some(path) = resolver.resolve(fqcn) else {
81 record_lazy_load_failure(LazyLoadFailure::ResolverNone, fqcn);
82 return false;
83 };
84 let file: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
85 // Prefer in-memory text from a prior `set_file_text` /
86 // `set_workspace_files` call; fall back to disk. This makes the LSP's
87 // unsaved-edit buffer authoritative over the on-disk content for the
88 // same path.
89 let src: Arc<str> = match self.source_of(&file) {
90 Some(text) => text,
91 None => match self
92 .db
93 .salsa
94 .source_provider()
95 .and_then(|p| p.read(&path.to_string_lossy()))
96 {
97 Some(text) => text,
98 None => {
99 record_lazy_load_failure(LazyLoadFailure::SourceUnreadable, fqcn);
100 return false;
101 }
102 },
103 };
104 self.ingest_file(file, src);
105 if self.class_indexed(fqcn) {
106 true
107 } else {
108 record_lazy_load_failure(LazyLoadFailure::IngestThenMissing, fqcn);
109 false
110 }
111 }
112
113 /// Evict every negative-cache entry whose stored resolver-mapped path
114 /// equals `file`. FQCNs cached as never-resolvable (path `None`) are left
115 /// alone — no source-text change can make them resolvable.
116 pub(super) fn evict_unresolvable_for_file(&mut self, file: &str) {
117 self.unresolvable_fqcns
118 .retain(|_fqcn, path| path.as_deref() != Some(file));
119 }
120
121 /// Bulk variant of [`Self::evict_unresolvable_for_file`]. One `HashSet`
122 /// build + one pass over the cache; no resolver calls.
123 pub(super) fn evict_unresolvable_for_files(&mut self, files: &[Arc<str>]) {
124 if self.unresolvable_fqcns.is_empty() {
125 return;
126 }
127 let registered: HashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
128 self.unresolvable_fqcns.retain(|_fqcn, path| match path {
129 Some(p) => !registered.contains(p.as_ref()),
130 None => true,
131 });
132 }
133}