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        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    /// When a dependency is absent from the workspace (background scan hasn't
141    /// reached it yet), UndefinedClass is emitted at the salsa layer. The LSP
142    /// fixes this via PSR-4 lazy-loading before `semantic_issues` runs.
143    #[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    /// Regression: `new Alias()` must not emit UndefinedClass when the aliased
169    /// class is present in the workspace. Requires mir 0.14.0+ which populates
170    /// `Codebase.file_imports` from `StubSlice.imports`.
171    #[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}