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