Skip to main content

php_lsp/analysis/
semantic_diagnostics.rs

1/// Semantic diagnostics bridge.
2///
3/// Delegates all analysis to the `mir-analyzer` crate and converts its `Issue`
4/// type into the `tower-lsp` `Diagnostic` type expected by the LSP backend.
5use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range, Url};
6
7use crate::analysis::diagnostics::PHP_LSP_SOURCE;
8use crate::document::ast::ParsedDoc;
9use crate::lang::config::DiagnosticsConfig;
10
11/// Run semantic checks on `doc` against the supplied `AnalysisSession`.
12/// Ingests the current file, runs Pass 2 via `FileAnalyzer`, and returns LSP
13/// diagnostics filtered by `DiagnosticsConfig`.
14pub fn semantic_diagnostics(
15    uri: &Url,
16    doc: &ParsedDoc,
17    session: &mir_analyzer::AnalysisSession,
18    cfg: &DiagnosticsConfig,
19) -> Vec<Diagnostic> {
20    if !cfg.enabled {
21        return vec![];
22    }
23    let file: std::sync::Arc<str> = std::sync::Arc::from(uri.as_str());
24    session.ingest_file(file.clone(), doc.source_arc());
25    let source_map = php_rs_parser::source_map::SourceMap::new(doc.source());
26    let owned_program = php_ast::owned::to_owned_program(doc.program());
27    let analyzer = mir_analyzer::FileAnalyzer::new(session);
28    let analysis = analyzer.analyze(file.clone(), doc.source(), &owned_program, &source_map);
29    let class_issues = session.class_issues(std::slice::from_ref(&file));
30    analysis
31        .issues
32        .into_iter()
33        .chain(class_issues)
34        .filter(|i| !i.suppressed)
35        .filter(|i| issue_passes_filter(i, cfg))
36        .map(to_lsp_diagnostic)
37        .collect()
38}
39
40/// Convert pre-computed raw issues (from `db::semantic::semantic_issues`) into
41/// LSP diagnostics, applying the user's `DiagnosticsConfig` filter. Keeping
42/// filter + conversion outside the salsa query preserves memoization across
43/// config toggles (the user flipping a category must not rerun the analyzer).
44pub fn issues_to_diagnostics(
45    issues: &[mir_issues::Issue],
46    _uri: &Url,
47    cfg: &DiagnosticsConfig,
48) -> Vec<Diagnostic> {
49    if !cfg.enabled {
50        return vec![];
51    }
52    issues
53        .iter()
54        .filter(|i| issue_passes_filter(i, cfg))
55        .cloned()
56        .map(to_lsp_diagnostic)
57        .collect()
58}
59
60/// Returns `true` if the mir-analyzer issue is allowed through by the config.
61fn issue_passes_filter(issue: &mir_issues::Issue, cfg: &DiagnosticsConfig) -> bool {
62    use mir_issues::IssueKind;
63    match &issue.kind {
64        IssueKind::UndefinedVariable { .. } | IssueKind::PossiblyUndefinedVariable { .. } => {
65            cfg.undefined_variables
66        }
67        IssueKind::UndefinedFunction { .. } | IssueKind::UndefinedMethod { .. } => {
68            cfg.undefined_functions
69        }
70        IssueKind::UndefinedClass { .. } | IssueKind::UndefinedTrait { .. } => {
71            cfg.undefined_classes
72        }
73        IssueKind::InvalidTraitUse { .. } => cfg.type_errors,
74        IssueKind::TooFewArguments { .. }
75        | IssueKind::TooManyArguments { .. }
76        | IssueKind::InvalidPassByReference { .. }
77        | IssueKind::InvalidNamedArgument { .. } => cfg.arity_errors,
78        // InvalidArgument covers both arity errors and type mismatches in mir-analyzer;
79        // show it if either toggle is on.
80        IssueKind::InvalidArgument { .. } | IssueKind::PossiblyInvalidArgument { .. } => {
81            cfg.arity_errors || cfg.type_errors
82        }
83        IssueKind::InvalidReturnType { .. }
84        | IssueKind::NullMethodCall { .. }
85        | IssueKind::NullPropertyFetch { .. }
86        | IssueKind::NullArrayAccess
87        | IssueKind::NullArgument { .. }
88        | IssueKind::PossiblyNullMethodCall { .. }
89        | IssueKind::PossiblyNullPropertyFetch { .. }
90        | IssueKind::PossiblyNullArrayAccess
91        | IssueKind::PossiblyNullArgument { .. }
92        | IssueKind::NullableReturnStatement { .. }
93        | IssueKind::InvalidPropertyAssignment { .. }
94        | IssueKind::InvalidOperand { .. }
95        | IssueKind::InvalidCast { .. }
96        | IssueKind::AbstractInstantiation { .. }
97        | IssueKind::MixedClone
98        | IssueKind::ReadonlyPropertyAssignment { .. }
99        | IssueKind::ArgumentTypeCoercion { .. } => cfg.type_errors,
100        IssueKind::DeprecatedCall { .. }
101        | IssueKind::DeprecatedMethodCall { .. }
102        | IssueKind::DeprecatedMethod { .. }
103        | IssueKind::DeprecatedClass { .. } => cfg.deprecated_calls,
104        IssueKind::CircularInheritance { .. } => cfg.type_errors,
105        IssueKind::DuplicateClass { .. }
106        | IssueKind::DuplicateInterface { .. }
107        | IssueKind::DuplicateTrait { .. }
108        | IssueKind::DuplicateEnum { .. }
109        | IssueKind::DuplicateFunction { .. } => cfg.duplicate_declarations,
110        IssueKind::UnusedVariable { .. }
111        | IssueKind::UnusedParam { .. }
112        | IssueKind::UnusedMethod { .. }
113        | IssueKind::UnusedProperty { .. }
114        | IssueKind::UnusedFunction { .. } => cfg.unused_symbols,
115        IssueKind::MissingReturnType { .. }
116        | IssueKind::MissingParamType { .. }
117        | IssueKind::MissingPropertyType { .. } => cfg.missing_types,
118        IssueKind::MixedArgument { .. }
119        | IssueKind::MixedAssignment { .. }
120        | IssueKind::MixedMethodCall { .. }
121        | IssueKind::MixedPropertyFetch { .. }
122        | IssueKind::MixedPropertyAssignment { .. }
123        | IssueKind::MixedArrayAccess
124        | IssueKind::MixedArrayOffset
125        | IssueKind::MixedReturnStatement { .. } => cfg.mixed_usage,
126        IssueKind::DocblockTypeContradiction { .. }
127        | IssueKind::UnevaluatedCode { .. }
128        | IssueKind::IfThisIsMismatch { .. } => true,
129        _ => true,
130    }
131}
132
133fn to_lsp_diagnostic(issue: mir_issues::Issue) -> Diagnostic {
134    // mir uses 1-based lines; LSP uses 0-based. Columns are 0-based on both sides.
135    let line = issue.location.line.saturating_sub(1);
136    let col_start = issue.location.col_start as u32;
137    let col_end = issue.location.col_end as u32;
138    Diagnostic {
139        range: Range {
140            start: Position {
141                line,
142                character: col_start,
143            },
144            end: Position {
145                line,
146                character: col_end.max(col_start + 1),
147            },
148        },
149        severity: Some(match issue.severity {
150            mir_issues::Severity::Error => DiagnosticSeverity::ERROR,
151            mir_issues::Severity::Warning => DiagnosticSeverity::WARNING,
152            mir_issues::Severity::Info => DiagnosticSeverity::INFORMATION,
153        }),
154        code: Some(NumberOrString::String(issue.kind.name().to_string())),
155        source: Some(PHP_LSP_SOURCE.to_string()),
156        message: issue.kind.message(),
157        ..Default::default()
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn to_lsp_diagnostic_sets_code_to_issue_kind_name() {
167        use mir_issues::{Issue, IssueKind, Location};
168        use std::sync::Arc;
169        use tower_lsp::lsp_types::NumberOrString;
170
171        let location = Location {
172            file: Arc::from("file:///test.php"),
173            line: 1,
174            line_end: 1,
175            col_start: 0,
176            col_end: 3,
177        };
178        let issue = Issue::new(
179            IssueKind::UndefinedClass {
180                name: "Foo".to_string(),
181            },
182            location,
183        );
184        let diag = to_lsp_diagnostic(issue);
185        assert_eq!(
186            diag.code,
187            Some(NumberOrString::String("UndefinedClass".to_string())),
188            "diagnostic code must be the IssueKind name so code actions can match by type"
189        );
190        assert!(
191            diag.message.contains("Foo"),
192            "diagnostic message should mention the class name"
193        );
194    }
195}