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`. `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`).
29crate::impl_arc_update!(IndexArc);
30
31/// Build the compact symbol index for a file. `no_eq` so salsa doesn't try to
32/// compare `IndexArc` structurally; invalidation flows from `parsed_doc`.
33#[salsa::tracked(no_eq)]
34pub fn file_index(db: &dyn Database, file: SourceFile) -> IndexArc {
35    let doc = parsed_doc(db, file);
36    IndexArc(Arc::new(FileIndex::extract(doc.get())))
37}
38
39#[cfg(test)]
40mod tests {
41    use std::sync::Arc;
42    use std::sync::atomic::{AtomicUsize, Ordering};
43
44    use super::*;
45    use crate::db::analysis::AnalysisHost;
46    use crate::db::input::{FileId, SourceFile};
47    use crate::db::parse::parsed_doc;
48    use salsa::Setter;
49
50    static CALLS: AtomicUsize = AtomicUsize::new(0);
51
52    /// Wrap `file_index` with a counter to verify salsa shares the `parsed_doc`
53    /// memoization between `file_index` and other downstream queries.
54    #[salsa::tracked]
55    fn counted_index_len(db: &dyn Database, file: SourceFile) -> usize {
56        CALLS.fetch_add(1, Ordering::SeqCst);
57        file_index(db, file).get().classes.len()
58    }
59
60    #[test]
61    fn file_index_extracts_class() {
62        let host = AnalysisHost::new();
63        let file = SourceFile::new(
64            host.db(),
65            FileId(0),
66            Arc::<str>::from("file:///t.php"),
67            Arc::<str>::from("<?php\nclass Foo { public function bar() {} }"),
68            None,
69        );
70        let idx = file_index(host.db(), file);
71        assert_eq!(idx.get().classes.len(), 1);
72        assert_eq!(idx.get().classes[0].name, "Foo".into());
73    }
74
75    #[test]
76    fn file_index_memoizes_and_shares_parse_with_downstream() {
77        CALLS.store(0, Ordering::SeqCst);
78        let mut host = AnalysisHost::new();
79        let file = SourceFile::new(
80            host.db(),
81            FileId(1),
82            Arc::<str>::from("file:///t.php"),
83            Arc::<str>::from("<?php\nclass A {} class B {}"),
84            None,
85        );
86
87        // Fetch the parsed doc, then the index — salsa should parse once.
88        let _ = parsed_doc(host.db(), file);
89        let _ = counted_index_len(host.db(), file);
90        let _ = counted_index_len(host.db(), file);
91        assert_eq!(
92            CALLS.load(Ordering::SeqCst),
93            1,
94            "index query should memoize within a revision"
95        );
96
97        // Edit the file — both the parse and the index should re-run.
98        file.set_text(host.db_mut())
99            .to(Arc::<str>::from("<?php\nclass A {}"));
100        let _ = counted_index_len(host.db(), file);
101        assert_eq!(CALLS.load(Ordering::SeqCst), 2);
102
103        let idx = file_index(host.db(), file);
104        assert_eq!(idx.get().classes.len(), 1);
105    }
106}