1use std::sync::Arc;
13
14use mir_issues::Issue;
15use salsa::{Database, Update};
16
17use crate::db::codebase::codebase;
18use crate::db::input::{SourceFile, Workspace};
19use crate::db::parse::parsed_doc;
20
21#[derive(Clone)]
25pub struct IssuesArc(pub Arc<[Issue]>);
26
27impl IssuesArc {
28 pub fn get(&self) -> &[Issue] {
29 &self.0
30 }
31}
32
33unsafe impl Update for IssuesArc {
35 unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
36 let old_ref = unsafe { &mut *old_pointer };
37 if Arc::ptr_eq(&old_ref.0, &new_value.0) {
38 false
39 } else {
40 *old_ref = new_value;
41 true
42 }
43 }
44}
45
46#[salsa::tracked(no_eq)]
52pub fn semantic_issues(db: &dyn Database, ws: Workspace, file: SourceFile) -> IssuesArc {
53 let cb = codebase(db, ws);
54 let doc_arc = parsed_doc(db, file);
55 let doc = doc_arc.get();
56 let uri_arc: Arc<str> = file.uri(db);
57 let source = doc.source();
58 let source_map = php_rs_parser::source_map::SourceMap::new(source);
59
60 let mut issue_buffer = mir_issues::IssueBuffer::new();
61 let mut symbols = Vec::new();
62 let php_version = ws.php_version(db);
63 let mut analyzer = mir_analyzer::stmt::StatementsAnalyzer::new(
64 cb.get(),
65 uri_arc,
66 source,
67 &source_map,
68 &mut issue_buffer,
69 &mut symbols,
70 php_version,
71 false,
72 );
73 let mut ctx = mir_analyzer::context::Context::new();
74 analyzer.analyze_stmts(&doc.program().stmts, &mut ctx);
75
76 let issues: Vec<Issue> = issue_buffer
77 .into_issues()
78 .into_iter()
79 .filter(|i| !i.suppressed)
80 .collect();
81 IssuesArc(Arc::from(issues))
82}
83
84#[cfg(test)]
85mod tests {
86 use std::sync::Arc;
87
88 use super::*;
89 use crate::db::analysis::AnalysisHost;
90 use crate::db::input::{FileId, SourceFile};
91 use salsa::Setter;
92
93 fn new_file(host: &AnalysisHost, id: u32, uri: &str, src: &str) -> SourceFile {
94 SourceFile::new(
95 host.db(),
96 FileId(id),
97 Arc::<str>::from(uri),
98 Arc::<str>::from(src),
99 None,
100 )
101 }
102
103 #[test]
104 fn semantic_issues_flags_undefined_function() {
105 let host = AnalysisHost::new();
106 let file = new_file(&host, 0, "file:///a.php", "<?php\nfoo_bar_baz();");
107 let ws = Workspace::new(
108 host.db(),
109 Arc::from([file]),
110 mir_analyzer::PhpVersion::LATEST,
111 );
112 let issues = semantic_issues(host.db(), ws, file);
113 assert!(
114 issues
115 .get()
116 .iter()
117 .any(|i| matches!(i.kind, mir_issues::IssueKind::UndefinedFunction { .. })),
118 "expected an UndefinedFunction issue, got {:?}",
119 issues.get()
120 );
121 }
122
123 #[test]
124 fn semantic_issues_memoizes_across_calls() {
125 let host = AnalysisHost::new();
126 let file = new_file(&host, 0, "file:///a.php", "<?php\nfoo_bar_baz();");
127 let ws = Workspace::new(
128 host.db(),
129 Arc::from([file]),
130 mir_analyzer::PhpVersion::LATEST,
131 );
132 let a = semantic_issues(host.db(), ws, file);
133 let b = semantic_issues(host.db(), ws, file);
134 assert!(
135 Arc::ptr_eq(&a.0, &b.0),
136 "second call with unchanged inputs should return the memoized Arc"
137 );
138 }
139
140 #[test]
144 fn use_imported_class_absent_from_workspace_emits_undefined_class() {
145 let host = AnalysisHost::new();
146 let consuming = new_file(
147 &host,
148 0,
149 "file:///src/Service/Handler.php",
150 "<?php\nnamespace App\\Service;\nuse App\\Model\\Entity;\nfunction handle(): void { $e = new Entity(); }",
151 );
152 let ws = Workspace::new(
153 host.db(),
154 Arc::from([consuming]),
155 mir_analyzer::PhpVersion::LATEST,
156 );
157 let issues = semantic_issues(host.db(), ws, consuming);
158 assert!(
159 issues
160 .get()
161 .iter()
162 .any(|i| matches!(i.kind, mir_issues::IssueKind::UndefinedClass { .. })),
163 "expected UndefinedClass when dependency is absent from workspace; got: {:?}",
164 issues.get()
165 );
166 }
167
168 #[test]
172 fn new_expr_with_use_alias_resolved_in_workspace() {
173 let host = AnalysisHost::new();
174 let entity = new_file(
175 &host,
176 0,
177 "file:///src/Model/Entity.php",
178 "<?php\nnamespace App\\Model;\nclass Entity {}",
179 );
180 let handler = new_file(
181 &host,
182 1,
183 "file:///src/Service/Handler.php",
184 "<?php\nnamespace App\\Service;\nuse App\\Model\\Entity;\nfunction handle(): void { $e = new Entity(); }",
185 );
186 let ws = Workspace::new(
187 host.db(),
188 Arc::from([entity, handler]),
189 mir_analyzer::PhpVersion::LATEST,
190 );
191 let issues = semantic_issues(host.db(), ws, handler);
192 let undef: Vec<_> = issues
193 .get()
194 .iter()
195 .filter(|i| matches!(i.kind, mir_issues::IssueKind::UndefinedClass { .. }))
196 .collect();
197 assert!(
198 undef.is_empty(),
199 "new Alias() must not emit UndefinedClass when class is in workspace; got: {undef:?}"
200 );
201 }
202
203 #[test]
204 fn semantic_issues_reruns_after_edit() {
205 let mut host = AnalysisHost::new();
206 let file = new_file(&host, 0, "file:///a.php", "<?php\nfoo_bar_baz();");
207 let ws = Workspace::new(
208 host.db(),
209 Arc::from([file]),
210 mir_analyzer::PhpVersion::LATEST,
211 );
212 let a = semantic_issues(host.db(), ws, file);
213 let first_ptr = Arc::as_ptr(&a.0);
214 file.set_text(host.db_mut())
215 .to(Arc::<str>::from("<?php\necho 1;"));
216 let b = semantic_issues(host.db(), ws, file);
217 assert_ne!(
218 first_ptr,
219 Arc::as_ptr(&b.0),
220 "edit should invalidate memoized issues"
221 );
222 }
223}