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 );
72 let mut ctx = mir_analyzer::context::Context::new();
73 analyzer.analyze_stmts(&doc.program().stmts, &mut ctx);
74
75 let issues: Vec<Issue> = issue_buffer
76 .into_issues()
77 .into_iter()
78 .filter(|i| !i.suppressed)
79 .collect();
80 IssuesArc(Arc::from(issues))
81}
82
83#[cfg(test)]
84mod tests {
85 use std::sync::Arc;
86
87 use super::*;
88 use crate::db::analysis::AnalysisHost;
89 use crate::db::input::{FileId, SourceFile};
90 use salsa::Setter;
91
92 fn new_file(host: &AnalysisHost, id: u32, uri: &str, src: &str) -> SourceFile {
93 SourceFile::new(
94 host.db(),
95 FileId(id),
96 Arc::<str>::from(uri),
97 Arc::<str>::from(src),
98 None,
99 )
100 }
101
102 #[test]
103 fn semantic_issues_flags_undefined_function() {
104 let host = AnalysisHost::new();
105 let file = new_file(&host, 0, "file:///a.php", "<?php\nfoo_bar_baz();");
106 let ws = Workspace::new(
107 host.db(),
108 Arc::from([file]),
109 mir_analyzer::PhpVersion::LATEST,
110 );
111 let issues = semantic_issues(host.db(), ws, file);
112 assert!(
113 issues
114 .get()
115 .iter()
116 .any(|i| matches!(i.kind, mir_issues::IssueKind::UndefinedFunction { .. })),
117 "expected an UndefinedFunction issue, got {:?}",
118 issues.get()
119 );
120 }
121
122 #[test]
123 fn semantic_issues_memoizes_across_calls() {
124 let host = AnalysisHost::new();
125 let file = new_file(&host, 0, "file:///a.php", "<?php\nfoo_bar_baz();");
126 let ws = Workspace::new(
127 host.db(),
128 Arc::from([file]),
129 mir_analyzer::PhpVersion::LATEST,
130 );
131 let a = semantic_issues(host.db(), ws, file);
132 let b = semantic_issues(host.db(), ws, file);
133 assert!(
134 Arc::ptr_eq(&a.0, &b.0),
135 "second call with unchanged inputs should return the memoized Arc"
136 );
137 }
138
139 #[test]
140 fn semantic_issues_reruns_after_edit() {
141 let mut host = AnalysisHost::new();
142 let file = new_file(&host, 0, "file:///a.php", "<?php\nfoo_bar_baz();");
143 let ws = Workspace::new(
144 host.db(),
145 Arc::from([file]),
146 mir_analyzer::PhpVersion::LATEST,
147 );
148 let a = semantic_issues(host.db(), ws, file);
149 let first_ptr = Arc::as_ptr(&a.0);
150 file.set_text(host.db_mut())
151 .to(Arc::<str>::from("<?php\necho 1;"));
152 let b = semantic_issues(host.db(), ws, file);
153 assert_ne!(
154 first_ptr,
155 Arc::as_ptr(&b.0),
156 "edit should invalidate memoized issues"
157 );
158 }
159}