Skip to main content

php_lsp/db/
semantic.rs

1//! `semantic_issues` salsa query — runs `mir_analyzer` Pass-2 body analysis
2//! on a single file against the finalized workspace codebase. Depends on
3//! `codebase(ws)` and `parsed_doc(file)`, so invalidation happens automatically
4//! when the file's text or any file that contributes to the shared codebase
5//! changes.
6//!
7//! The query returns raw `mir_issues::Issue` values. Config-level filtering
8//! (`DiagnosticsConfig`) and LSP conversion (`to_lsp_diagnostic`) live outside
9//! the query — the user toggling a diagnostic category must not invalidate
10//! the expensive analysis.
11
12use 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/// Opaque handle to the per-file raw issue list. `Arc<[Issue]>` so clones
22/// are cheap; `Update` uses `Arc::ptr_eq` like other `*Arc` newtypes in this
23/// module.
24#[derive(Clone)]
25pub struct IssuesArc(pub Arc<[Issue]>);
26
27impl IssuesArc {
28    pub fn get(&self) -> &[Issue] {
29        &self.0
30    }
31}
32
33// SAFETY: identical contract to `ParsedArc::maybe_update`.
34unsafe 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/// Run Pass-2 (body analysis) on a single file against the workspace codebase
47/// and return the raw issue list with `suppressed` entries already dropped.
48///
49/// `no_eq` because `IssuesArc` has no structural equality — invalidation is
50/// driven by the upstream inputs (codebase, parsed_doc).
51#[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}