Skip to main content

php_lsp/db/
codebase.rs

1//! `codebase` salsa query — aggregates every file's `StubSlice` into a
2//! finalized `mir_codebase::Codebase` via `codebase_from_parts`.
3//!
4//! When any file's `file_definitions` output changes, salsa marks this query
5//! dirty and re-runs it. Re-running calls `file_definitions` for each file in
6//! the workspace; unchanged files return their memoized slice instantly, so
7//! the work per edit is `O(N * merge_cost)` (plus one `finalize()`).
8//!
9//! Phase C step 2: query exists and has correctness tests; Backend still uses
10//! the imperative `remove/collect/finalize` path. Step 3 migrates Backend.
11
12use std::sync::Arc;
13
14use mir_codebase::Codebase;
15use salsa::{Database, Update};
16
17use crate::db::definitions::file_definitions;
18use crate::db::input::Workspace;
19
20/// Opaque handle to a finalized Codebase. `Arc::ptr_eq` for the `Update`
21/// contract — every re-run produces a new `Arc`, matching `ParsedArc`'s
22/// pattern.
23#[derive(Clone)]
24pub struct CodebaseArc(pub Arc<Codebase>);
25
26impl CodebaseArc {
27    pub fn get(&self) -> &Codebase {
28        &self.0
29    }
30}
31
32// SAFETY: identical contract to other `*Arc` newtypes in this module.
33unsafe impl Update for CodebaseArc {
34    unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
35        let old_ref = unsafe { &mut *old_pointer };
36        if Arc::ptr_eq(&old_ref.0, &new_value.0) {
37            false
38        } else {
39            *old_ref = new_value;
40            true
41        }
42    }
43}
44
45/// Build a finalized Codebase from the bundled PHP stubs (string/array/etc.
46/// builtins) plus every user file's `StubSlice`. Depends on `Workspace.files`
47/// and transitively on every file's `file_definitions` query. Stubs are
48/// treated as constant — they don't participate in salsa invalidation.
49///
50/// Load order matches today's imperative path (`Backend::new`): stubs first,
51/// user definitions second — so user classes with an FQN matching a stub
52/// overwrite the stub entry. `finalize()` runs once at the end.
53#[salsa::tracked(no_eq)]
54pub fn codebase(db: &dyn Database, ws: Workspace) -> CodebaseArc {
55    let mut builder = mir_codebase::CodebaseBuilder::new();
56    mir_analyzer::stubs::load_stubs(builder.codebase());
57    let files = ws.files(db);
58    for sf in files.iter() {
59        builder.add((*file_definitions(db, *sf).0).clone());
60    }
61    // TODO: when PHP-version-dependent stubs land, thread a PhpVersion input
62    // through this query so different versions don't share memoization.
63    CodebaseArc(Arc::new(builder.finalize()))
64}
65
66#[cfg(test)]
67mod tests {
68    use std::sync::Arc;
69
70    use super::*;
71    use crate::db::analysis::AnalysisHost;
72    use crate::db::input::{FileId, SourceFile};
73    use salsa::Setter;
74
75    #[test]
76    fn codebase_aggregates_classes_across_files() {
77        let host = AnalysisHost::new();
78        let f1 = SourceFile::new(
79            host.db(),
80            FileId(0),
81            Arc::<str>::from("file:///a.php"),
82            Arc::<str>::from("<?php\nnamespace A;\nclass Foo {}"),
83            None,
84        );
85        let f2 = SourceFile::new(
86            host.db(),
87            FileId(1),
88            Arc::<str>::from("file:///b.php"),
89            Arc::<str>::from("<?php\nnamespace B;\nclass Bar {}"),
90            None,
91        );
92        let ws = Workspace::new(
93            host.db(),
94            Arc::from([f1, f2]),
95            mir_analyzer::PhpVersion::LATEST,
96        );
97
98        let cb = codebase(host.db(), ws);
99        assert!(cb.get().type_exists("A\\Foo"));
100        assert!(cb.get().type_exists("B\\Bar"));
101    }
102
103    #[test]
104    fn codebase_reruns_after_file_edit() {
105        let mut host = AnalysisHost::new();
106        let f1 = SourceFile::new(
107            host.db(),
108            FileId(0),
109            Arc::<str>::from("file:///t.php"),
110            Arc::<str>::from("<?php\nclass Before {}"),
111            None,
112        );
113        let ws = Workspace::new(host.db(), Arc::from([f1]), mir_analyzer::PhpVersion::LATEST);
114
115        let a1 = codebase(host.db(), ws);
116        assert!(a1.get().type_exists("Before"));
117        let first_ptr = Arc::as_ptr(&a1.0);
118
119        f1.set_text(host.db_mut())
120            .to(Arc::<str>::from("<?php\nclass After {}"));
121        let a2 = codebase(host.db(), ws);
122        assert_ne!(first_ptr, Arc::as_ptr(&a2.0), "edit should invalidate");
123        assert!(a2.get().type_exists("After"));
124        assert!(!a2.get().type_exists("Before"));
125    }
126
127    #[test]
128    fn codebase_memoizes_when_nothing_changes() {
129        let host = AnalysisHost::new();
130        let f1 = SourceFile::new(
131            host.db(),
132            FileId(0),
133            Arc::<str>::from("file:///t.php"),
134            Arc::<str>::from("<?php\nclass X {}"),
135            None,
136        );
137        let ws = Workspace::new(host.db(), Arc::from([f1]), mir_analyzer::PhpVersion::LATEST);
138
139        let a1 = codebase(host.db(), ws);
140        let a2 = codebase(host.db(), ws);
141        assert!(
142            Arc::ptr_eq(&a1.0, &a2.0),
143            "no input change — second call should return the memoized Arc"
144        );
145    }
146}