php_lsp/analysis/
semantic_diagnostics.rs1use 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(
17 uri: &Uri,
18 doc: &ParsedDoc,
19 session: &mir_analyzer::AnalysisSession,
20 cfg: &DiagnosticsConfig,
21) -> Vec<Diagnostic> {
22 if !cfg.enabled {
23 return vec![];
24 }
25 let file: std::sync::Arc<str> = std::sync::Arc::from(uri.as_str());
26 session.ingest_file(file.clone(), doc.source_arc());
27 let source_map = php_rs_parser::source_map::SourceMap::new(doc.source());
28 let owned_program = php_ast::owned::to_owned_program(doc.program());
29 let analyzer = mir_analyzer::FileAnalyzer::new(session);
30 let analysis = analyzer.analyze(file.clone(), doc.source(), &owned_program, &source_map);
31 let class_issues = session.class_issues(std::slice::from_ref(&file));
32 analysis
33 .issues
34 .into_iter()
35 .chain(class_issues)
36 .filter(|i| !i.suppressed)
37 .filter(|i| issue_passes_filter(i, cfg))
38 .map(to_lsp_diagnostic)
39 .collect()
40}
41
42pub fn issues_to_diagnostics(
47 issues: &[mir_issues::Issue],
48 _uri: &Uri,
49 cfg: &DiagnosticsConfig,
50) -> Vec<Diagnostic> {
51 if !cfg.enabled {
52 return vec![];
53 }
54 issues
55 .iter()
56 .filter(|i| issue_passes_filter(i, cfg))
57 .cloned()
58 .map(to_lsp_diagnostic)
59 .collect()
60}
61
62pub fn issues_to_diagnostics_gated(
68 issues: &[mir_issues::Issue],
69 uri: &Uri,
70 cfg: &DiagnosticsConfig,
71 index_ready: bool,
72) -> Vec<Diagnostic> {
73 if index_ready {
74 return issues_to_diagnostics(issues, uri, cfg);
75 }
76 let filtered: Vec<mir_issues::Issue> = issues
77 .iter()
78 .filter(|i| !issue_needs_workspace_index(&i.kind))
79 .cloned()
80 .collect();
81 issues_to_diagnostics(&filtered, uri, cfg)
82}
83
84pub fn issue_needs_workspace_index(kind: &mir_issues::IssueKind) -> bool {
96 use mir_issues::IssueKind;
97 matches!(
98 kind,
99 IssueKind::UndefinedFunction { .. }
100 | IssueKind::UndefinedMethod { .. }
101 | IssueKind::UndefinedClass { .. }
102 | IssueKind::UndefinedTrait { .. }
103 )
104}
105
106fn issue_passes_filter(issue: &mir_issues::Issue, cfg: &DiagnosticsConfig) -> bool {
108 use mir_issues::IssueKind;
109 match &issue.kind {
110 IssueKind::UndefinedVariable { .. } | IssueKind::PossiblyUndefinedVariable { .. } => {
111 cfg.undefined_variables
112 }
113 IssueKind::UndefinedFunction { .. } | IssueKind::UndefinedMethod { .. } => {
114 cfg.undefined_functions
115 }
116 IssueKind::UndefinedClass { .. } | IssueKind::UndefinedTrait { .. } => {
117 cfg.undefined_classes
118 }
119 IssueKind::InvalidTraitUse { .. } => cfg.type_errors,
120 IssueKind::TooFewArguments { .. }
121 | IssueKind::TooManyArguments { .. }
122 | IssueKind::InvalidPassByReference { .. }
123 | IssueKind::InvalidNamedArgument { .. } => cfg.arity_errors,
124 IssueKind::InvalidArgument { .. } | IssueKind::PossiblyInvalidArgument { .. } => {
127 cfg.arity_errors || cfg.type_errors
128 }
129 IssueKind::InvalidReturnType { .. }
130 | IssueKind::NullMethodCall { .. }
131 | IssueKind::NullPropertyFetch { .. }
132 | IssueKind::NullArrayAccess
133 | IssueKind::NullArgument { .. }
134 | IssueKind::PossiblyNullMethodCall { .. }
135 | IssueKind::PossiblyNullPropertyFetch { .. }
136 | IssueKind::PossiblyNullArrayAccess
137 | IssueKind::PossiblyNullArgument { .. }
138 | IssueKind::NullableReturnStatement { .. }
139 | IssueKind::InvalidPropertyAssignment { .. }
140 | IssueKind::InvalidOperand { .. }
141 | IssueKind::InvalidCast { .. }
142 | IssueKind::AbstractInstantiation { .. }
143 | IssueKind::MixedClone
144 | IssueKind::ReadonlyPropertyAssignment { .. }
145 | IssueKind::ArgumentTypeCoercion { .. } => cfg.type_errors,
146 IssueKind::DeprecatedCall { .. }
147 | IssueKind::DeprecatedMethodCall { .. }
148 | IssueKind::DeprecatedMethod { .. }
149 | IssueKind::DeprecatedClass { .. } => cfg.deprecated_calls,
150 IssueKind::CircularInheritance { .. } => cfg.type_errors,
151 IssueKind::DuplicateClass { .. }
152 | IssueKind::DuplicateInterface { .. }
153 | IssueKind::DuplicateTrait { .. }
154 | IssueKind::DuplicateEnum { .. }
155 | IssueKind::DuplicateFunction { .. } => cfg.duplicate_declarations,
156 IssueKind::UnusedVariable { .. }
157 | IssueKind::UnusedParam { .. }
158 | IssueKind::UnusedMethod { .. }
159 | IssueKind::UnusedProperty { .. }
160 | IssueKind::UnusedFunction { .. } => cfg.unused_symbols,
161 IssueKind::MissingReturnType { .. }
162 | IssueKind::MissingParamType { .. }
163 | IssueKind::MissingPropertyType { .. } => cfg.missing_types,
164 IssueKind::MixedArgument { .. }
165 | IssueKind::MixedAssignment { .. }
166 | IssueKind::MixedMethodCall { .. }
167 | IssueKind::MixedPropertyFetch { .. }
168 | IssueKind::MixedPropertyAssignment { .. }
169 | IssueKind::MixedArrayAccess
170 | IssueKind::MixedArrayOffset
171 | IssueKind::MixedReturnStatement { .. } => cfg.mixed_usage,
172 IssueKind::DocblockTypeContradiction { .. }
173 | IssueKind::UnevaluatedCode { .. }
174 | IssueKind::IfThisIsMismatch { .. } => true,
175 _ => true,
176 }
177}
178
179fn to_lsp_diagnostic(issue: mir_issues::Issue) -> Diagnostic {
180 let line = issue.location.line.saturating_sub(1);
182 let col_start = issue.location.col_start as u32;
183 let col_end = issue.location.col_end as u32;
184 Diagnostic {
185 range: Range {
186 start: Position {
187 line,
188 character: col_start,
189 },
190 end: Position {
191 line,
192 character: col_end.max(col_start + 1),
193 },
194 },
195 severity: Some(match issue.severity {
196 mir_issues::Severity::Error => DiagnosticSeverity::ERROR,
197 mir_issues::Severity::Warning => DiagnosticSeverity::WARNING,
198 mir_issues::Severity::Info => DiagnosticSeverity::INFORMATION,
199 }),
200 code: Some(NumberOrString::String(issue.kind.name().to_string())),
201 source: Some(PHP_LSP_SOURCE.to_string()),
202 message: issue.kind.message(),
203 ..Default::default()
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn to_lsp_diagnostic_sets_code_to_issue_kind_name() {
213 use mir_issues::{Issue, IssueKind, Location};
214 use std::sync::Arc;
215 use tower_lsp_server::ls_types::NumberOrString;
216
217 let location = Location {
218 file: Arc::from("file:///test.php"),
219 line: 1,
220 line_end: 1,
221 col_start: 0,
222 col_end: 3,
223 };
224 let issue = Issue::new(
225 IssueKind::UndefinedClass {
226 name: "Foo".to_string(),
227 },
228 location,
229 );
230 let diag = to_lsp_diagnostic(issue);
231 assert_eq!(
232 diag.code,
233 Some(NumberOrString::String("UndefinedClass".to_string())),
234 "diagnostic code must be the IssueKind name so code actions can match by type"
235 );
236 assert!(
237 diag.message.contains("Foo"),
238 "diagnostic message should mention the class name"
239 );
240 }
241}