1use tower_lsp_server::ls_types::{
6 Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range, Uri,
7};
8
9use crate::analysis::diagnostics::PHP_LSP_SOURCE;
10use crate::document::ast::ParsedDoc;
11use crate::lang::config::DiagnosticsConfig;
12
13pub fn semantic_diagnostics(
23 uri: &Uri,
24 doc: &ParsedDoc,
25 session: &mut mir_analyzer::AnalysisSession,
26 cfg: &DiagnosticsConfig,
27) -> Vec<Diagnostic> {
28 if !cfg.enabled {
29 return vec![];
30 }
31 let file: std::sync::Arc<str> = std::sync::Arc::from(uri.as_str());
32 let analysis = session.analyze_file_diagnostics(uri.as_str(), doc.source());
33 let class_issues = session.class_issues(std::slice::from_ref(&file));
34 analysis
35 .issues
36 .into_iter()
37 .chain(class_issues)
38 .filter(|i| !i.suppressed)
39 .filter(|i| issue_passes_filter(i, cfg))
40 .map(to_lsp_diagnostic)
41 .collect()
42}
43
44pub fn issues_to_diagnostics(
55 issues: &[mir_issues::Issue],
56 _uri: &Uri,
57 cfg: &DiagnosticsConfig,
58) -> Vec<Diagnostic> {
59 if !cfg.enabled {
60 return vec![];
61 }
62 issues
63 .iter()
64 .filter(|i| !matches!(i.kind, mir_issues::IssueKind::ParseError { .. }))
65 .filter(|i| issue_passes_filter(i, cfg))
66 .cloned()
67 .map(to_lsp_diagnostic)
68 .collect()
69}
70
71pub fn issues_to_diagnostics_gated(
77 issues: &[mir_issues::Issue],
78 uri: &Uri,
79 cfg: &DiagnosticsConfig,
80 index_ready: bool,
81) -> Vec<Diagnostic> {
82 if index_ready {
83 return issues_to_diagnostics(issues, uri, cfg);
84 }
85 let filtered: Vec<mir_issues::Issue> = issues
86 .iter()
87 .filter(|i| !issue_needs_workspace_index(&i.kind))
88 .cloned()
89 .collect();
90 issues_to_diagnostics(&filtered, uri, cfg)
91}
92
93pub fn issue_needs_workspace_index(kind: &mir_issues::IssueKind) -> bool {
105 use mir_issues::IssueKind;
106 matches!(
107 kind,
108 IssueKind::UndefinedFunction { .. }
109 | IssueKind::UndefinedMethod { .. }
110 | IssueKind::UndefinedClass { .. }
111 | IssueKind::UndefinedTrait { .. }
112 )
113}
114
115fn issue_passes_filter(issue: &mir_issues::Issue, cfg: &DiagnosticsConfig) -> bool {
117 use mir_issues::IssueKind;
118 let name = issue.kind.name();
119 if cfg.disabled.iter().any(|d| d == name) {
120 return false;
121 }
122 match &issue.kind {
123 IssueKind::UndefinedVariable { .. } | IssueKind::PossiblyUndefinedVariable { .. } => {
124 cfg.undefined_variables
125 }
126 IssueKind::UndefinedFunction { .. } | IssueKind::UndefinedMethod { .. } => {
127 cfg.undefined_functions
128 }
129 IssueKind::UndefinedClass { .. } | IssueKind::UndefinedTrait { .. } => {
130 cfg.undefined_classes
131 }
132 IssueKind::InvalidTraitUse { .. } => cfg.type_errors,
133 IssueKind::TooFewArguments { .. }
134 | IssueKind::TooManyArguments { .. }
135 | IssueKind::InvalidPassByReference { .. }
136 | IssueKind::InvalidNamedArgument { .. } => cfg.arity_errors,
137 IssueKind::InvalidArgument { .. } | IssueKind::PossiblyInvalidArgument { .. } => {
140 cfg.arity_errors || cfg.type_errors
141 }
142 IssueKind::InvalidReturnType { .. }
143 | IssueKind::NullMethodCall { .. }
144 | IssueKind::NullPropertyFetch { .. }
145 | IssueKind::NullArrayAccess
146 | IssueKind::NullArgument { .. }
147 | IssueKind::PossiblyNullMethodCall { .. }
148 | IssueKind::PossiblyNullPropertyFetch { .. }
149 | IssueKind::PossiblyNullArrayAccess
150 | IssueKind::PossiblyNullArgument { .. }
151 | IssueKind::NullableReturnStatement { .. }
152 | IssueKind::InvalidPropertyAssignment { .. }
153 | IssueKind::InvalidOperand { .. }
154 | IssueKind::InvalidCast { .. }
155 | IssueKind::AbstractInstantiation { .. }
156 | IssueKind::MixedClone
157 | IssueKind::ReadonlyPropertyAssignment { .. }
158 | IssueKind::ArgumentTypeCoercion { .. } => cfg.type_errors,
159 IssueKind::DeprecatedCall { .. }
160 | IssueKind::DeprecatedMethodCall { .. }
161 | IssueKind::DeprecatedMethod { .. }
162 | IssueKind::DeprecatedClass { .. } => cfg.deprecated_calls,
163 IssueKind::CircularInheritance { .. } => cfg.type_errors,
164 IssueKind::DuplicateClass { .. }
165 | IssueKind::DuplicateInterface { .. }
166 | IssueKind::DuplicateTrait { .. }
167 | IssueKind::DuplicateEnum { .. }
168 | IssueKind::DuplicateFunction { .. } => cfg.duplicate_declarations,
169 IssueKind::UnusedVariable { .. }
170 | IssueKind::UnusedParam { .. }
171 | IssueKind::UnusedMethod { .. }
172 | IssueKind::UnusedProperty { .. }
173 | IssueKind::UnusedFunction { .. } => cfg.unused_symbols,
174 IssueKind::MissingReturnType { .. }
175 | IssueKind::MissingParamType { .. }
176 | IssueKind::MissingPropertyType { .. } => cfg.missing_types,
177 IssueKind::MixedArgument { .. }
178 | IssueKind::MixedAssignment { .. }
179 | IssueKind::MixedMethodCall { .. }
180 | IssueKind::MixedPropertyFetch { .. }
181 | IssueKind::MixedPropertyAssignment { .. }
182 | IssueKind::MixedArrayAccess
183 | IssueKind::MixedArrayOffset
184 | IssueKind::MixedReturnStatement { .. } => cfg.mixed_usage,
185 IssueKind::DocblockTypeContradiction { .. }
186 | IssueKind::UnevaluatedCode { .. }
187 | IssueKind::IfThisIsMismatch { .. } => true,
188 _ => true,
189 }
190}
191
192fn to_lsp_diagnostic(issue: mir_issues::Issue) -> Diagnostic {
193 let line = issue.location.line.saturating_sub(1);
195 let line_end = issue.location.line_end.saturating_sub(1).max(line);
196 let col_start = issue.location.col_start as u32;
197 let col_end = issue.location.col_end as u32;
198 let end_character = if line_end == line {
199 col_end.max(col_start + 1)
200 } else {
201 col_end
202 };
203 Diagnostic {
204 range: Range {
205 start: Position {
206 line,
207 character: col_start,
208 },
209 end: Position {
210 line: line_end,
211 character: end_character,
212 },
213 },
214 severity: Some(match issue.severity {
215 mir_issues::Severity::Error => DiagnosticSeverity::ERROR,
216 mir_issues::Severity::Warning => DiagnosticSeverity::WARNING,
217 mir_issues::Severity::Info => DiagnosticSeverity::INFORMATION,
218 }),
219 code: Some(NumberOrString::String(issue.kind.name().to_string())),
220 source: Some(PHP_LSP_SOURCE.to_string()),
221 message: issue.kind.message(),
222 ..Default::default()
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn to_lsp_diagnostic_sets_code_to_issue_kind_name() {
232 use mir_issues::{Issue, IssueKind, Location};
233 use std::sync::Arc;
234 use tower_lsp_server::ls_types::NumberOrString;
235
236 let location = Location {
237 file: Arc::from("file:///test.php"),
238 line: 1,
239 line_end: 1,
240 col_start: 0,
241 col_end: 3,
242 };
243 let issue = Issue::new(
244 IssueKind::UndefinedClass {
245 name: "Foo".to_string(),
246 },
247 location,
248 );
249 let diag = to_lsp_diagnostic(issue);
250 assert_eq!(
251 diag.code,
252 Some(NumberOrString::String("UndefinedClass".to_string())),
253 "diagnostic code must be the IssueKind name so code actions can match by type"
254 );
255 assert!(
256 diag.message.contains("Foo"),
257 "diagnostic message should mention the class name"
258 );
259 }
260
261 #[test]
262 fn disabled_list_filters_issue_by_name() {
263 use mir_issues::{Issue, IssueKind, Location};
264 use std::sync::Arc;
265
266 let issue = Issue::new(
267 IssueKind::UndefinedClass {
268 name: "Foo".to_string(),
269 },
270 Location {
271 file: Arc::from("file:///test.php"),
272 line: 1,
273 line_end: 1,
274 col_start: 0,
275 col_end: 3,
276 },
277 );
278 let uri: Uri = "file:///test.php".parse().unwrap();
279 let mut cfg = DiagnosticsConfig::default();
280 assert_eq!(
281 issues_to_diagnostics(std::slice::from_ref(&issue), &uri, &cfg).len(),
282 1
283 );
284 cfg.disabled = vec!["UndefinedClass".to_string()];
285 assert!(issues_to_diagnostics(&[issue], &uri, &cfg).is_empty());
286 }
287
288 #[test]
289 fn to_lsp_diagnostic_spans_multiple_lines() {
290 use mir_issues::{Issue, IssueKind, Location};
291 use std::sync::Arc;
292
293 let issue = Issue::new(
294 IssueKind::UndefinedClass {
295 name: "Foo".to_string(),
296 },
297 Location {
298 file: Arc::from("file:///test.php"),
299 line: 2,
300 line_end: 4,
301 col_start: 8,
302 col_end: 1,
303 },
304 );
305 let range = to_lsp_diagnostic(issue).range;
306 assert_eq!(range.start, Position::new(1, 8));
307 assert_eq!(range.end, Position::new(3, 1));
308 }
309}