Skip to main content

php_lsp/db/
index.rs

1//! `file_index` salsa query — derives a compact `FileIndex` from a parsed
2//! document. Depends on `parsed_doc`, so editing a file reparses once and the
3//! index re-extracts from the new AST.
4
5use std::sync::Arc;
6
7use salsa::Database;
8
9use crate::db::input::SourceFile;
10use crate::db::parse::parsed_doc;
11use crate::file_index::FileIndex;
12
13/// Arc wrapper for `FileIndex`. Uses structural equality on the inner
14/// `FileIndex` so salsa can short-circuit downstream queries (e.g.
15/// `workspace_index`) when a body-only edit produces an identical index.
16#[derive(Clone, PartialEq, Debug)]
17pub struct IndexArc(pub Arc<FileIndex>);
18
19impl IndexArc {
20    pub fn get(&self) -> &FileIndex {
21        &self.0
22    }
23}
24
25// SAFETY: writes through `old_pointer` only when returning `true`. Uses
26// structural equality on `FileIndex` so that body-only edits (no declaration
27// change) return `false` and don't cascade to `workspace_index`.
28unsafe impl salsa::Update for IndexArc {
29    unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
30        let old_ref = unsafe { &mut *old_pointer };
31        if *old_ref.0 == *new_value.0 {
32            false
33        } else {
34            *old_ref = new_value;
35            true
36        }
37    }
38}
39
40/// Build the compact symbol index for a file.  Salsa compares the returned
41/// `IndexArc` structurally via `FileIndex::PartialEq`; if declarations are
42/// unchanged (body-only edit) the comparison returns equal and `workspace_index`
43/// is not re-run.
44///
45/// Fast path: if the workspace scan seeded a `cached_index` (loaded from the
46/// on-disk cache), return it directly — no parse, no extract.
47#[salsa::tracked]
48pub fn file_index(db: &dyn Database, file: SourceFile<'_>) -> IndexArc {
49    if let Some(cached) = file.text_input(db).cached_index(db) {
50        return IndexArc(cached);
51    }
52    let doc = parsed_doc(db, file);
53    IndexArc(Arc::new(FileIndex::extract(doc.get())))
54}
55
56#[cfg(test)]
57mod tests {
58    use std::sync::Arc;
59    use std::sync::atomic::{AtomicUsize, Ordering};
60
61    use super::*;
62    use crate::db::analysis::AnalysisHost;
63    use crate::db::input::{FileText, Workspace, workspace_files};
64    use crate::db::parse::parsed_doc;
65    use salsa::Setter;
66
67    static CALLS: AtomicUsize = AtomicUsize::new(0);
68
69    /// Wrap `file_index` with a counter to verify salsa shares the `parsed_doc`
70    /// memoization between `file_index` and other downstream queries.
71    #[salsa::tracked]
72    fn counted_index_len(db: &dyn Database, file: SourceFile<'_>) -> usize {
73        CALLS.fetch_add(1, Ordering::SeqCst);
74        file_index(db, file).get().classes.len()
75    }
76
77    fn make_ws(host: &AnalysisHost, uri: &str, ft: FileText) -> Workspace {
78        Workspace::new(
79            host.db(),
80            std::sync::Arc::from([(Arc::<str>::from(uri), ft)]),
81            mir_analyzer::PhpVersion::LATEST,
82        )
83    }
84
85    #[test]
86    fn file_index_extracts_class() {
87        let host = AnalysisHost::new();
88        let ft = FileText::new(
89            host.db(),
90            Arc::<str>::from("<?php\nclass Foo { public function bar() {} }"),
91            None,
92        );
93        let ws = make_ws(&host, "file:///t.php", ft);
94        let files = workspace_files(host.db(), ws);
95        let idx = file_index(host.db(), files[0]);
96        assert_eq!(idx.get().classes.len(), 1);
97        assert_eq!(idx.get().classes[0].name, "Foo".into());
98    }
99
100    #[test]
101    fn file_index_memoizes_and_shares_parse_with_downstream() {
102        CALLS.store(0, Ordering::SeqCst);
103        let mut host = AnalysisHost::new();
104        let ft = FileText::new(
105            host.db(),
106            Arc::<str>::from("<?php\nclass A {} class B {}"),
107            None,
108        );
109        let ws = make_ws(&host, "file:///t.php", ft);
110        {
111            let files = workspace_files(host.db(), ws);
112            // Fetch the parsed doc, then the index — salsa should parse once.
113            let _ = parsed_doc(host.db(), files[0]);
114            let _ = counted_index_len(host.db(), files[0]);
115            let _ = counted_index_len(host.db(), files[0]);
116            assert_eq!(
117                CALLS.load(Ordering::SeqCst),
118                1,
119                "index query should memoize within a revision"
120            );
121        }
122
123        // Edit the file — both the parse and the index should re-run.
124        ft.set_text(host.db_mut())
125            .to(Arc::<str>::from("<?php\nclass A {}"));
126        {
127            let files = workspace_files(host.db(), ws);
128            let _ = counted_index_len(host.db(), files[0]);
129            assert_eq!(CALLS.load(Ordering::SeqCst), 2);
130            let idx = file_index(host.db(), files[0]);
131            assert_eq!(idx.get().classes.len(), 1);
132        }
133    }
134
135    #[test]
136    fn body_only_edit_produces_equal_index_arc() {
137        let mut host = AnalysisHost::new();
138        let ft = FileText::new(
139            host.db(),
140            Arc::<str>::from("<?php\nclass Foo { public function bar(): int { return 1; } }"),
141            None,
142        );
143        let ws = make_ws(&host, "file:///t.php", ft);
144        let before = {
145            let files = workspace_files(host.db(), ws);
146            file_index(host.db(), files[0])
147        };
148
149        // Change the method body only — no declaration-level change.
150        ft.set_text(host.db_mut()).to(Arc::<str>::from(
151            "<?php\nclass Foo { public function bar(): int { return 2; } }",
152        ));
153        let after = {
154            let files = workspace_files(host.db(), ws);
155            file_index(host.db(), files[0])
156        };
157
158        assert_eq!(
159            before, after,
160            "body-only edit must produce an equal IndexArc so salsa can short-circuit workspace_index"
161        );
162    }
163}