php_lsp/analysis/
semantic_diagnostics.rs1use 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
11pub 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
40pub 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
60fn 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 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 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}