Skip to main content

php_lsp/db/
symbol_map.rs

1//! `symbol_map` salsa query — derives a [`SymbolMap`] from a parsed document.
2//!
3//! Depends on `parsed_doc`, so editing a file reparses once and then the symbol
4//! map rebuilds. Between edits all lookups are served from the cache in O(1).
5
6use std::sync::Arc;
7
8use salsa::Database;
9
10use crate::db::input::SourceFile;
11use crate::db::parse::parsed_doc;
12use crate::symbol_map::SymbolMap;
13
14/// Arc wrapper for [`SymbolMap`]. Pointer equality drives salsa invalidation:
15/// every `build` call produces a new `Arc`, so a changed parse always propagates.
16#[derive(Clone)]
17pub struct SymbolMapArc(pub Arc<SymbolMap>);
18
19impl SymbolMapArc {
20    pub fn get(&self) -> &SymbolMap {
21        &self.0
22    }
23}
24
25crate::impl_arc_update!(SymbolMapArc);
26
27/// Build the symbol map for a file. `no_eq` because `SymbolMapArc` has no
28/// structural equality — invalidation flows from `parsed_doc`.
29#[salsa::tracked(no_eq)]
30pub fn symbol_map(db: &dyn Database, file: SourceFile) -> SymbolMapArc {
31    let doc = parsed_doc(db, file);
32    SymbolMapArc(Arc::new(SymbolMap::build(doc.get())))
33}
34
35#[cfg(test)]
36mod tests {
37    use std::sync::Arc;
38    use std::sync::atomic::{AtomicUsize, Ordering};
39
40    use salsa::Setter;
41
42    use super::*;
43    use crate::db::analysis::AnalysisHost;
44    use crate::db::input::{FileId, SourceFile};
45
46    static MEMO_CALLS: AtomicUsize = AtomicUsize::new(0);
47    static INVAL_CALLS: AtomicUsize = AtomicUsize::new(0);
48
49    #[salsa::tracked]
50    fn counted_memo(db: &dyn Database, file: SourceFile) -> usize {
51        MEMO_CALLS.fetch_add(1, Ordering::SeqCst);
52        symbol_map(db, file)
53            .get()
54            .lookup("greet", |_| true)
55            .is_some() as usize
56    }
57
58    #[salsa::tracked]
59    fn counted_inval(db: &dyn Database, file: SourceFile) -> usize {
60        INVAL_CALLS.fetch_add(1, Ordering::SeqCst);
61        symbol_map(db, file)
62            .get()
63            .lookup("greet", |_| true)
64            .is_some() as usize
65    }
66
67    #[test]
68    fn symbol_map_builds_and_memoizes() {
69        MEMO_CALLS.store(0, Ordering::SeqCst);
70        let mut host = AnalysisHost::new();
71        let file = SourceFile::new(
72            host.db(),
73            FileId(100),
74            Arc::<str>::from("file:///memo.php"),
75            Arc::<str>::from("<?php\nfunction greet(): void {}"),
76            None,
77        );
78
79        let _ = counted_memo(host.db(), file);
80        let _ = counted_memo(host.db(), file);
81        assert_eq!(
82            MEMO_CALLS.load(Ordering::SeqCst),
83            1,
84            "salsa should memoize the second call with unchanged input"
85        );
86    }
87
88    #[test]
89    fn symbol_map_invalidates_on_edit() {
90        INVAL_CALLS.store(0, Ordering::SeqCst);
91        let mut host = AnalysisHost::new();
92        let file = SourceFile::new(
93            host.db(),
94            FileId(101),
95            Arc::<str>::from("file:///inval.php"),
96            Arc::<str>::from("<?php\nfunction greet(): void {}"),
97            None,
98        );
99
100        let _ = counted_inval(host.db(), file);
101        assert_eq!(INVAL_CALLS.load(Ordering::SeqCst), 1);
102
103        file.set_text(host.db_mut())
104            .to(Arc::<str>::from("<?php\nfunction farewell(): void {}"));
105        let _ = counted_inval(host.db(), file);
106        assert_eq!(
107            INVAL_CALLS.load(Ordering::SeqCst),
108            2,
109            "symbol_map should re-run after source text change"
110        );
111    }
112}