use std::sync::Arc;
use salsa::{Database, Update};
use crate::db::codebase::codebase;
use crate::db::input::{SourceFile, Workspace};
use crate::db::parse::parsed_doc;
#[derive(Debug, Clone)]
pub struct FileRefRecord {
pub key: Arc<str>,
pub line: u32,
pub col_start: u16,
pub col_end: u16,
}
#[derive(Clone)]
pub struct FileRefsArc(pub Arc<Vec<FileRefRecord>>);
impl FileRefsArc {
pub fn get(&self) -> &[FileRefRecord] {
&self.0
}
}
unsafe impl Update for FileRefsArc {
unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
let old_ref = unsafe { &mut *old_pointer };
if Arc::ptr_eq(&old_ref.0, &new_value.0) {
false
} else {
*old_ref = new_value;
true
}
}
}
type SymbolRefsInner = Arc<Vec<(Arc<str>, u32, u16, u16)>>;
#[derive(Clone)]
pub struct SymbolRefsArc(pub SymbolRefsInner);
impl SymbolRefsArc {
pub fn get(&self) -> &[(Arc<str>, u32, u16, u16)] {
&self.0
}
}
unsafe impl Update for SymbolRefsArc {
unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
let old_ref = unsafe { &mut *old_pointer };
if Arc::ptr_eq(&old_ref.0, &new_value.0) {
false
} else {
*old_ref = new_value;
true
}
}
}
#[salsa::tracked(no_eq)]
pub fn file_refs(db: &dyn Database, ws: Workspace, file: SourceFile) -> FileRefsArc {
let cb = codebase(db, ws);
let doc = parsed_doc(db, file);
let uri = file.uri(db);
let source = file.text(db);
let map = php_rs_parser::source_map::SourceMap::new(&source);
let mut issue_buffer = mir_issues::IssueBuffer::new();
let mut symbols = Vec::new();
{
let mut analyzer = mir_analyzer::stmt::StatementsAnalyzer::new(
cb.get(),
uri,
&source,
&map,
&mut issue_buffer,
&mut symbols,
ws.php_version(db),
false,
);
let mut ctx = mir_analyzer::context::Context::new();
analyzer.analyze_stmts(&doc.get().program().stmts, &mut ctx);
}
let records: Vec<FileRefRecord> = symbols
.into_iter()
.filter_map(|s| {
let key = s.codebase_key()?;
let (line, col_start) = map.offset_to_line_col(s.span.start).to_one_based();
let (_, col_end) = map.offset_to_line_col(s.span.end).to_one_based();
Some(FileRefRecord {
key: Arc::from(key),
line,
col_start: col_start.saturating_sub(1) as u16,
col_end: col_end.saturating_sub(1) as u16,
})
})
.collect();
FileRefsArc(Arc::new(records))
}
#[salsa::tracked(no_eq)]
pub fn symbol_refs(db: &dyn Database, ws: Workspace, key: String) -> SymbolRefsArc {
let files = ws.files(db);
let mut out: Vec<(Arc<str>, u32, u16, u16)> = Vec::new();
for sf in files.iter() {
let refs = file_refs(db, ws, *sf);
let uri = sf.uri(db);
for r in refs.get() {
if r.key.as_ref() == key.as_str() {
out.push((uri.clone(), r.line, r.col_start, r.col_end));
}
}
}
SymbolRefsArc(Arc::new(out))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::db::analysis::AnalysisHost;
use crate::db::input::{FileId, SourceFile};
#[test]
fn symbol_refs_finds_function_call_across_files() {
let host = AnalysisHost::new();
let f1 = SourceFile::new(
host.db(),
FileId(0),
Arc::<str>::from("file:///a.php"),
Arc::<str>::from("<?php\nfunction greet(): void {}"),
None,
);
let f2 = SourceFile::new(
host.db(),
FileId(1),
Arc::<str>::from("file:///b.php"),
Arc::<str>::from("<?php\ngreet();"),
None,
);
let ws = Workspace::new(
host.db(),
Arc::from([f1, f2]),
mir_analyzer::PhpVersion::LATEST,
);
let locs = symbol_refs(host.db(), ws, "greet".to_string());
let found: Vec<&str> = locs.get().iter().map(|(u, _, _, _)| u.as_ref()).collect();
assert!(
found.iter().any(|u| *u == "file:///b.php"),
"expected a reference from b.php, got {:?}",
found
);
}
#[test]
fn symbol_refs_memoizes_per_key() {
let host = AnalysisHost::new();
let f1 = SourceFile::new(
host.db(),
FileId(0),
Arc::<str>::from("file:///a.php"),
Arc::<str>::from("<?php\nfunction hi(): void {}\nhi();"),
None,
);
let ws = Workspace::new(host.db(), Arc::from([f1]), mir_analyzer::PhpVersion::LATEST);
let a = symbol_refs(host.db(), ws, "hi".to_string());
let b = symbol_refs(host.db(), ws, "hi".to_string());
assert!(Arc::ptr_eq(&a.0, &b.0), "second call should be memoized");
}
}