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