1use 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#[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
38unsafe 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#[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#[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}