Skip to main content

php_lsp/db/
refs.rs

1//! `file_refs` / `symbol_refs` salsa queries — Phase D.
2//!
3//! Replaces the imperative `build_reference_index` scan that ran on workspace
4//! startup. References are now computed lazily on first `textDocument/references`
5//! call and memoized thereafter. Body-only edits to a single file invalidate
6//! only that file's `file_refs`; structural edits also invalidate `codebase(ws)`
7//! which cascades into every `file_refs` because StatementsAnalyzer depends on
8//! the finalized codebase.
9
10use std::sync::Arc;
11
12use salsa::{Database, Update};
13
14use crate::db::codebase::codebase;
15use crate::db::input::{SourceFile, Workspace};
16use crate::db::parse::parsed_doc;
17
18/// A single Pass-2 reference observed during StatementsAnalyzer.
19/// `key` mirrors `Codebase::symbol_reference_locations` keys so that consumers
20/// can aggregate by the same scheme `mark_*_referenced_at` would have used.
21#[derive(Debug, Clone)]
22pub struct FileRefRecord {
23    pub key: Arc<str>,
24    pub line: u32,
25    pub col_start: u16,
26    pub col_end: u16,
27}
28
29#[derive(Clone)]
30pub struct FileRefsArc(pub Arc<Vec<FileRefRecord>>);
31
32impl FileRefsArc {
33    pub fn get(&self) -> &[FileRefRecord] {
34        &self.0
35    }
36}
37
38// SAFETY: same contract as other `*Arc` newtypes — `Arc::ptr_eq` is sufficient
39// because every re-run of the tracked query allocates a fresh `Arc`.
40unsafe impl Update for FileRefsArc {
41    unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
42        let old_ref = unsafe { &mut *old_pointer };
43        if Arc::ptr_eq(&old_ref.0, &new_value.0) {
44            false
45        } else {
46            *old_ref = new_value;
47            true
48        }
49    }
50}
51
52type SymbolRefsInner = Arc<Vec<(Arc<str>, u32, u16, u16)>>;
53
54#[derive(Clone)]
55pub struct SymbolRefsArc(pub SymbolRefsInner);
56
57impl SymbolRefsArc {
58    pub fn get(&self) -> &[(Arc<str>, u32, u16, u16)] {
59        &self.0
60    }
61}
62
63unsafe impl Update for SymbolRefsArc {
64    unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
65        let old_ref = unsafe { &mut *old_pointer };
66        if Arc::ptr_eq(&old_ref.0, &new_value.0) {
67            false
68        } else {
69            *old_ref = new_value;
70            true
71        }
72    }
73}
74
75/// Run Pass-2 analysis on `file` against the workspace codebase and return
76/// every resolved reference with its codebase key and byte span.
77///
78/// The analyzer internally also calls `mark_*_referenced_at` on the Codebase
79/// Arc from `codebase(ws)` — we deliberately ignore those mutations here and
80/// build our own aggregation via `symbol_refs`. This keeps the data flow
81/// purely functional from salsa's perspective even though the underlying
82/// Codebase uses interior mutability.
83#[salsa::tracked(no_eq)]
84pub fn file_refs(db: &dyn Database, ws: Workspace, file: SourceFile) -> FileRefsArc {
85    let cb = codebase(db, ws);
86    let doc = parsed_doc(db, file);
87    let uri = file.uri(db);
88    let source = file.text(db);
89    let map = php_rs_parser::source_map::SourceMap::new(&source);
90    let mut issue_buffer = mir_issues::IssueBuffer::new();
91    let mut symbols = Vec::new();
92    {
93        let mut analyzer = mir_analyzer::stmt::StatementsAnalyzer::new(
94            cb.get(),
95            uri,
96            &source,
97            &map,
98            &mut issue_buffer,
99            &mut symbols,
100            ws.php_version(db),
101            false,
102        );
103        let mut ctx = mir_analyzer::context::Context::new();
104        analyzer.analyze_stmts(&doc.get().program().stmts, &mut ctx);
105    }
106
107    let records: Vec<FileRefRecord> = symbols
108        .into_iter()
109        .filter_map(|s| {
110            let key = s.codebase_key()?;
111            let (line, col_start) = map.offset_to_line_col(s.span.start).to_one_based();
112            let (_, col_end) = map.offset_to_line_col(s.span.end).to_one_based();
113            Some(FileRefRecord {
114                key: Arc::from(key),
115                line,
116                col_start: col_start.saturating_sub(1) as u16,
117                col_end: col_end.saturating_sub(1) as u16,
118            })
119        })
120        .collect();
121    FileRefsArc(Arc::new(records))
122}
123
124/// Aggregate every file's `file_refs` filtered by `key` into a flat
125/// `(uri, start, end)` list — drop-in replacement for
126/// `Codebase::get_reference_locations`.
127#[salsa::tracked(no_eq)]
128pub fn symbol_refs(db: &dyn Database, ws: Workspace, key: String) -> SymbolRefsArc {
129    let files = ws.files(db);
130    let mut out: Vec<(Arc<str>, u32, u16, u16)> = Vec::new();
131    for sf in files.iter() {
132        let refs = file_refs(db, ws, *sf);
133        let uri = sf.uri(db);
134        for r in refs.get() {
135            if r.key.as_ref() == key.as_str() {
136                out.push((uri.clone(), r.line, r.col_start, r.col_end));
137            }
138        }
139    }
140    SymbolRefsArc(Arc::new(out))
141}
142
143#[cfg(test)]
144mod tests {
145    use std::sync::Arc;
146
147    use super::*;
148    use crate::db::analysis::AnalysisHost;
149    use crate::db::input::{FileId, SourceFile};
150
151    #[test]
152    fn symbol_refs_finds_function_call_across_files() {
153        let host = AnalysisHost::new();
154        let f1 = SourceFile::new(
155            host.db(),
156            FileId(0),
157            Arc::<str>::from("file:///a.php"),
158            Arc::<str>::from("<?php\nfunction greet(): void {}"),
159            None,
160        );
161        let f2 = SourceFile::new(
162            host.db(),
163            FileId(1),
164            Arc::<str>::from("file:///b.php"),
165            Arc::<str>::from("<?php\ngreet();"),
166            None,
167        );
168        let ws = Workspace::new(
169            host.db(),
170            Arc::from([f1, f2]),
171            mir_analyzer::PhpVersion::LATEST,
172        );
173
174        let locs = symbol_refs(host.db(), ws, "greet".to_string());
175        let found: Vec<&str> = locs.get().iter().map(|(u, _, _, _)| u.as_ref()).collect();
176        assert!(
177            found.iter().any(|u| *u == "file:///b.php"),
178            "expected a reference from b.php, got {:?}",
179            found
180        );
181    }
182
183    #[test]
184    fn symbol_refs_memoizes_per_key() {
185        let host = AnalysisHost::new();
186        let f1 = SourceFile::new(
187            host.db(),
188            FileId(0),
189            Arc::<str>::from("file:///a.php"),
190            Arc::<str>::from("<?php\nfunction hi(): void {}\nhi();"),
191            None,
192        );
193        let ws = Workspace::new(host.db(), Arc::from([f1]), mir_analyzer::PhpVersion::LATEST);
194
195        let a = symbol_refs(host.db(), ws, "hi".to_string());
196        let b = symbol_refs(host.db(), ws, "hi".to_string());
197        assert!(Arc::ptr_eq(&a.0, &b.0), "second call should be memoized");
198    }
199}