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.text_input(db).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::{FileText, Workspace, workspace_files};
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 fn make_ws(host: &AnalysisHost, uri: &str, ft: FileText) -> Workspace {
78 Workspace::new(
79 host.db(),
80 std::sync::Arc::from([(Arc::<str>::from(uri), ft)]),
81 mir_analyzer::PhpVersion::LATEST,
82 )
83 }
84
85 #[test]
86 fn file_index_extracts_class() {
87 let host = AnalysisHost::new();
88 let ft = FileText::new(
89 host.db(),
90 Arc::<str>::from("<?php\nclass Foo { public function bar() {} }"),
91 None,
92 );
93 let ws = make_ws(&host, "file:///t.php", ft);
94 let files = workspace_files(host.db(), ws);
95 let idx = file_index(host.db(), files[0]);
96 assert_eq!(idx.get().classes.len(), 1);
97 assert_eq!(idx.get().classes[0].name, "Foo".into());
98 }
99
100 #[test]
101 fn file_index_memoizes_and_shares_parse_with_downstream() {
102 CALLS.store(0, Ordering::SeqCst);
103 let mut host = AnalysisHost::new();
104 let ft = FileText::new(
105 host.db(),
106 Arc::<str>::from("<?php\nclass A {} class B {}"),
107 None,
108 );
109 let ws = make_ws(&host, "file:///t.php", ft);
110 {
111 let files = workspace_files(host.db(), ws);
112 let _ = parsed_doc(host.db(), files[0]);
114 let _ = counted_index_len(host.db(), files[0]);
115 let _ = counted_index_len(host.db(), files[0]);
116 assert_eq!(
117 CALLS.load(Ordering::SeqCst),
118 1,
119 "index query should memoize within a revision"
120 );
121 }
122
123 ft.set_text(host.db_mut())
125 .to(Arc::<str>::from("<?php\nclass A {}"));
126 {
127 let files = workspace_files(host.db(), ws);
128 let _ = counted_index_len(host.db(), files[0]);
129 assert_eq!(CALLS.load(Ordering::SeqCst), 2);
130 let idx = file_index(host.db(), files[0]);
131 assert_eq!(idx.get().classes.len(), 1);
132 }
133 }
134
135 #[test]
136 fn body_only_edit_produces_equal_index_arc() {
137 let mut host = AnalysisHost::new();
138 let ft = FileText::new(
139 host.db(),
140 Arc::<str>::from("<?php\nclass Foo { public function bar(): int { return 1; } }"),
141 None,
142 );
143 let ws = make_ws(&host, "file:///t.php", ft);
144 let before = {
145 let files = workspace_files(host.db(), ws);
146 file_index(host.db(), files[0])
147 };
148
149 ft.set_text(host.db_mut()).to(Arc::<str>::from(
151 "<?php\nclass Foo { public function bar(): int { return 2; } }",
152 ));
153 let after = {
154 let files = workspace_files(host.db(), ws);
155 file_index(host.db(), files[0])
156 };
157
158 assert_eq!(
159 before, after,
160 "body-only edit must produce an equal IndexArc so salsa can short-circuit workspace_index"
161 );
162 }
163}