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, Update};
8
9use crate::db::input::SourceFile;
10use crate::db::parse::parsed_doc;
11use crate::file_index::FileIndex;
12
13/// Arc wrapper for `FileIndex`. `FileIndex` is structurally clone-able but
14/// doesn't implement salsa's `Update` — we wrap in `Arc` and compare pointers,
15/// mirroring the `ParsedArc` approach. A new extract always produces a fresh
16/// `Arc`, so pointer inequality is a safe "changed" signal.
17#[derive(Clone)]
18pub struct IndexArc(pub Arc<FileIndex>);
19
20impl IndexArc {
21    pub fn get(&self) -> &FileIndex {
22        &self.0
23    }
24}
25
26// SAFETY: same contract as `ParsedArc::maybe_update` — only writes through
27// `old_pointer` when returning `true`. `FileIndex` is `Send + Sync` by virtue
28// of its fields (all owned `String`/`Vec`).
29unsafe impl Update for IndexArc {
30    unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
31        let old_ref = unsafe { &mut *old_pointer };
32        if Arc::ptr_eq(&old_ref.0, &new_value.0) {
33            false
34        } else {
35            *old_ref = new_value;
36            true
37        }
38    }
39}
40
41/// Build the compact symbol index for a file. `no_eq` so salsa doesn't try to
42/// compare `IndexArc` structurally; invalidation flows from `parsed_doc`.
43#[salsa::tracked(no_eq)]
44pub fn file_index(db: &dyn Database, file: SourceFile) -> IndexArc {
45    let doc = parsed_doc(db, file);
46    IndexArc(Arc::new(FileIndex::extract(doc.get())))
47}
48
49#[cfg(test)]
50mod tests {
51    use std::sync::Arc;
52    use std::sync::atomic::{AtomicUsize, Ordering};
53
54    use super::*;
55    use crate::db::analysis::AnalysisHost;
56    use crate::db::input::{FileId, SourceFile};
57    use crate::db::parse::parsed_doc;
58    use salsa::Setter;
59
60    static CALLS: AtomicUsize = AtomicUsize::new(0);
61
62    /// Wrap `file_index` with a counter to verify salsa shares the `parsed_doc`
63    /// memoization between `file_index` and other downstream queries.
64    #[salsa::tracked]
65    fn counted_index_len(db: &dyn Database, file: SourceFile) -> usize {
66        CALLS.fetch_add(1, Ordering::SeqCst);
67        file_index(db, file).get().classes.len()
68    }
69
70    #[test]
71    fn file_index_extracts_class() {
72        let host = AnalysisHost::new();
73        let file = SourceFile::new(
74            host.db(),
75            FileId(0),
76            Arc::<str>::from("file:///t.php"),
77            Arc::<str>::from("<?php\nclass Foo { public function bar() {} }"),
78            None,
79        );
80        let idx = file_index(host.db(), file);
81        assert_eq!(idx.get().classes.len(), 1);
82        assert_eq!(idx.get().classes[0].name, "Foo");
83    }
84
85    #[test]
86    fn file_index_memoizes_and_shares_parse_with_downstream() {
87        CALLS.store(0, Ordering::SeqCst);
88        let mut host = AnalysisHost::new();
89        let file = SourceFile::new(
90            host.db(),
91            FileId(1),
92            Arc::<str>::from("file:///t.php"),
93            Arc::<str>::from("<?php\nclass A {} class B {}"),
94            None,
95        );
96
97        // Fetch the parsed doc, then the index — salsa should parse once.
98        let _ = parsed_doc(host.db(), file);
99        let _ = counted_index_len(host.db(), file);
100        let _ = counted_index_len(host.db(), file);
101        assert_eq!(
102            CALLS.load(Ordering::SeqCst),
103            1,
104            "index query should memoize within a revision"
105        );
106
107        // Edit the file — both the parse and the index should re-run.
108        file.set_text(host.db_mut())
109            .to(Arc::<str>::from("<?php\nclass A {}"));
110        let _ = counted_index_len(host.db(), file);
111        assert_eq!(CALLS.load(Ordering::SeqCst), 2);
112
113        let idx = file_index(host.db(), file);
114        assert_eq!(idx.get().classes.len(), 1);
115    }
116}