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    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}