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