Skip to main content

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}