use std::sync::Arc;
use mir_codebase::storage::StubSlice;
use salsa::{Database, Update};
use crate::db::input::SourceFile;
use crate::db::parse::parsed_doc;
#[derive(Clone)]
pub struct SliceArc(pub Arc<StubSlice>);
impl SliceArc {
pub fn get(&self) -> &StubSlice {
&self.0
}
}
unsafe impl Update for SliceArc {
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_definitions(db: &dyn Database, file: SourceFile) -> SliceArc {
if let Some(cached) = file.cached_slice(db) {
let _ = file.text(db);
return SliceArc(cached);
}
let doc = parsed_doc(db, file);
let text = file.text(db);
let file_path: Arc<str> = file.uri(db);
let source_map = php_rs_parser::source_map::SourceMap::new(&text);
let collector =
mir_analyzer::collector::DefinitionCollector::new_for_slice(file_path, &text, &source_map);
let (slice, _issues) = collector.collect_slice(doc.get().program());
SliceArc(Arc::new(slice))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::db::analysis::AnalysisHost;
use crate::db::input::{FileId, SourceFile};
use salsa::Setter;
#[test]
fn file_definitions_extracts_class() {
let host = AnalysisHost::new();
let file = SourceFile::new(
host.db(),
FileId(0),
Arc::<str>::from("file:///t.php"),
Arc::<str>::from("<?php\nnamespace App;\nclass Foo {}"),
None,
);
let slice = file_definitions(host.db(), file);
let classes: Vec<&str> = slice
.get()
.classes
.iter()
.map(|c| c.fqcn.as_ref())
.collect();
assert_eq!(classes, vec!["App\\Foo"]);
}
#[test]
fn file_definitions_reruns_after_edit() {
let mut host = AnalysisHost::new();
let file = SourceFile::new(
host.db(),
FileId(1),
Arc::<str>::from("file:///t.php"),
Arc::<str>::from("<?php\nclass A {}"),
None,
);
let a1 = file_definitions(host.db(), file);
let first_ptr = Arc::as_ptr(&a1.0);
file.set_text(host.db_mut())
.to(Arc::<str>::from("<?php\nclass B {}"));
let a2 = file_definitions(host.db(), file);
assert_ne!(first_ptr, Arc::as_ptr(&a2.0));
let classes: Vec<&str> = a2.get().classes.iter().map(|c| c.fqcn.as_ref()).collect();
assert_eq!(classes, vec!["B"]);
}
#[test]
fn file_definitions_returns_seeded_slice_without_parsing() {
let mut host = AnalysisHost::new();
let file = SourceFile::new(
host.db(),
FileId(2),
Arc::<str>::from("file:///t.php"),
Arc::<str>::from("<?php\nclass Text {}"),
None,
);
let seeded = {
let src = "<?php\nclass Cached {}";
let source_map = php_rs_parser::source_map::SourceMap::new(src);
let (doc, _) = crate::diagnostics::parse_document(src);
let collector = mir_analyzer::collector::DefinitionCollector::new_for_slice(
Arc::<str>::from("file:///t.php"),
src,
&source_map,
);
let (slice, _) = collector.collect_slice(doc.program());
Arc::new(slice)
};
file.set_cached_slice(host.db_mut()).to(Some(seeded));
let out = file_definitions(host.db(), file);
let classes: Vec<&str> = out.get().classes.iter().map(|c| c.fqcn.as_ref()).collect();
assert_eq!(
classes,
vec!["Cached"],
"seeded cached_slice must short-circuit parse + collect"
);
}
#[test]
fn edit_invalidates_seeded_slice() {
let mut host = AnalysisHost::new();
let file = SourceFile::new(
host.db(),
FileId(3),
Arc::<str>::from("file:///t.php"),
Arc::<str>::from("<?php\nclass Original {}"),
None,
);
let misleading = {
let src = "<?php\nclass Misleading {}";
let source_map = php_rs_parser::source_map::SourceMap::new(src);
let (doc, _) = crate::diagnostics::parse_document(src);
let collector = mir_analyzer::collector::DefinitionCollector::new_for_slice(
Arc::<str>::from("file:///t.php"),
src,
&source_map,
);
let (s, _) = collector.collect_slice(doc.program());
Arc::new(s)
};
file.set_cached_slice(host.db_mut()).to(Some(misleading));
let out1 = file_definitions(host.db(), file);
let names: Vec<&str> = out1.get().classes.iter().map(|c| c.fqcn.as_ref()).collect();
assert_eq!(names, vec!["Misleading"]);
file.set_text(host.db_mut())
.to(Arc::<str>::from("<?php\nclass Edited {}"));
file.set_cached_slice(host.db_mut()).to(None);
let out2 = file_definitions(host.db(), file);
let names: Vec<&str> = out2.get().classes.iter().map(|c| c.fqcn.as_ref()).collect();
assert_eq!(
names,
vec!["Edited"],
"edit must invalidate cached slice — fresh parse of new text"
);
}
}