1use std::sync::Arc;
6
7use salsa::Database;
8
9use crate::db::input::SourceFile;
10use crate::db::parse::parsed_doc;
11use crate::file_index::FileIndex;
12
13#[derive(Clone, PartialEq, Debug)]
17pub struct IndexArc(pub Arc<FileIndex>);
18
19impl IndexArc {
20 pub fn get(&self) -> &FileIndex {
21 &self.0
22 }
23}
24
25unsafe impl salsa::Update for IndexArc {
29 unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
30 let old_ref = unsafe { &mut *old_pointer };
31 if *old_ref.0 == *new_value.0 {
32 false
33 } else {
34 *old_ref = new_value;
35 true
36 }
37 }
38}
39
40#[salsa::tracked]
48pub fn file_index(db: &dyn Database, file: SourceFile) -> IndexArc {
49 if let Some(cached) = file.cached_index(db) {
50 return IndexArc(cached);
51 }
52 let doc = parsed_doc(db, file);
53 IndexArc(Arc::new(FileIndex::extract(doc.get())))
54}
55
56#[cfg(test)]
57mod tests {
58 use std::sync::Arc;
59 use std::sync::atomic::{AtomicUsize, Ordering};
60
61 use super::*;
62 use crate::db::analysis::AnalysisHost;
63 use crate::db::input::{FileId, SourceFile};
64 use crate::db::parse::parsed_doc;
65 use salsa::Setter;
66
67 static CALLS: AtomicUsize = AtomicUsize::new(0);
68
69 #[salsa::tracked]
72 fn counted_index_len(db: &dyn Database, file: SourceFile) -> usize {
73 CALLS.fetch_add(1, Ordering::SeqCst);
74 file_index(db, file).get().classes.len()
75 }
76
77 #[test]
78 fn file_index_extracts_class() {
79 let host = AnalysisHost::new();
80 let file = SourceFile::new(
81 host.db(),
82 FileId(0),
83 Arc::<str>::from("file:///t.php"),
84 Arc::<str>::from("<?php\nclass Foo { public function bar() {} }"),
85 None,
86 );
87 let idx = file_index(host.db(), file);
88 assert_eq!(idx.get().classes.len(), 1);
89 assert_eq!(idx.get().classes[0].name, "Foo".into());
90 }
91
92 #[test]
93 fn file_index_memoizes_and_shares_parse_with_downstream() {
94 CALLS.store(0, Ordering::SeqCst);
95 let mut host = AnalysisHost::new();
96 let file = SourceFile::new(
97 host.db(),
98 FileId(1),
99 Arc::<str>::from("file:///t.php"),
100 Arc::<str>::from("<?php\nclass A {} class B {}"),
101 None,
102 );
103
104 let _ = parsed_doc(host.db(), file);
106 let _ = counted_index_len(host.db(), file);
107 let _ = counted_index_len(host.db(), file);
108 assert_eq!(
109 CALLS.load(Ordering::SeqCst),
110 1,
111 "index query should memoize within a revision"
112 );
113
114 file.set_text(host.db_mut())
116 .to(Arc::<str>::from("<?php\nclass A {}"));
117 let _ = counted_index_len(host.db(), file);
118 assert_eq!(CALLS.load(Ordering::SeqCst), 2);
119
120 let idx = file_index(host.db(), file);
121 assert_eq!(idx.get().classes.len(), 1);
122 }
123
124 #[test]
125 fn body_only_edit_produces_equal_index_arc() {
126 let mut host = AnalysisHost::new();
130 let file = SourceFile::new(
131 host.db(),
132 FileId(2),
133 Arc::<str>::from("file:///t.php"),
134 Arc::<str>::from("<?php\nclass Foo { public function bar(): int { return 1; } }"),
135 None,
136 );
137
138 let before = file_index(host.db(), file);
139
140 file.set_text(host.db_mut()).to(Arc::<str>::from(
142 "<?php\nclass Foo { public function bar(): int { return 2; } }",
143 ));
144 let after = file_index(host.db(), file);
145
146 assert_eq!(
147 before, after,
148 "body-only edit must produce an equal IndexArc so salsa can short-circuit workspace_index"
149 );
150 }
151}