use tower_lsp_server::ls_types::{
Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range, Uri,
};
use crate::analysis::diagnostics::PHP_LSP_SOURCE;
use crate::document::ast::ParsedDoc;
use crate::lang::config::DiagnosticsConfig;
pub fn semantic_diagnostics(
uri: &Uri,
doc: &ParsedDoc,
session: &mut mir_analyzer::AnalysisSession,
cfg: &DiagnosticsConfig,
) -> Vec<Diagnostic> {
if !cfg.enabled {
return vec![];
}
let file: std::sync::Arc<str> = std::sync::Arc::from(uri.as_str());
let analysis = session.analyze_file_diagnostics(uri.as_str(), doc.source());
let class_issues = session.class_issues(std::slice::from_ref(&file));
analysis
.issues
.into_iter()
.chain(class_issues)
.filter(|i| !i.suppressed)
.filter(|i| issue_passes_filter(i, cfg))
.map(to_lsp_diagnostic)
.collect()
}
pub fn issues_to_diagnostics(
issues: &[mir_issues::Issue],
_uri: &Uri,
cfg: &DiagnosticsConfig,
) -> Vec<Diagnostic> {
if !cfg.enabled {
return vec![];
}
issues
.iter()
.filter(|i| !matches!(i.kind, mir_issues::IssueKind::ParseError { .. }))
.filter(|i| issue_passes_filter(i, cfg))
.cloned()
.map(to_lsp_diagnostic)
.collect()
}
pub fn issues_to_diagnostics_gated(
issues: &[mir_issues::Issue],
uri: &Uri,
cfg: &DiagnosticsConfig,
index_ready: bool,
) -> Vec<Diagnostic> {
if index_ready {
return issues_to_diagnostics(issues, uri, cfg);
}
let filtered: Vec<mir_issues::Issue> = issues
.iter()
.filter(|i| !issue_needs_workspace_index(&i.kind))
.cloned()
.collect();
issues_to_diagnostics(&filtered, uri, cfg)
}
pub fn issue_needs_workspace_index(kind: &mir_issues::IssueKind) -> bool {
use mir_issues::IssueKind;
matches!(
kind,
IssueKind::UndefinedFunction { .. }
| IssueKind::UndefinedMethod { .. }
| IssueKind::UndefinedClass { .. }
| IssueKind::UndefinedTrait { .. }
)
}
fn issue_passes_filter(issue: &mir_issues::Issue, cfg: &DiagnosticsConfig) -> bool {
use mir_issues::IssueKind;
let name = issue.kind.name();
if cfg.disabled.iter().any(|d| d == name) {
return false;
}
match &issue.kind {
IssueKind::UndefinedVariable { .. } | IssueKind::PossiblyUndefinedVariable { .. } => {
cfg.undefined_variables
}
IssueKind::UndefinedFunction { .. } | IssueKind::UndefinedMethod { .. } => {
cfg.undefined_functions
}
IssueKind::UndefinedClass { .. } | IssueKind::UndefinedTrait { .. } => {
cfg.undefined_classes
}
IssueKind::InvalidTraitUse { .. } => cfg.type_errors,
IssueKind::TooFewArguments { .. }
| IssueKind::TooManyArguments { .. }
| IssueKind::InvalidPassByReference { .. }
| IssueKind::InvalidNamedArgument { .. } => cfg.arity_errors,
IssueKind::InvalidArgument { .. } | IssueKind::PossiblyInvalidArgument { .. } => {
cfg.arity_errors || cfg.type_errors
}
IssueKind::InvalidReturnType { .. }
| IssueKind::NullMethodCall { .. }
| IssueKind::NullPropertyFetch { .. }
| IssueKind::NullArrayAccess
| IssueKind::NullArgument { .. }
| IssueKind::PossiblyNullMethodCall { .. }
| IssueKind::PossiblyNullPropertyFetch { .. }
| IssueKind::PossiblyNullArrayAccess
| IssueKind::PossiblyNullArgument { .. }
| IssueKind::NullableReturnStatement { .. }
| IssueKind::InvalidPropertyAssignment { .. }
| IssueKind::InvalidOperand { .. }
| IssueKind::InvalidCast { .. }
| IssueKind::AbstractInstantiation { .. }
| IssueKind::MixedClone
| IssueKind::ReadonlyPropertyAssignment { .. }
| IssueKind::ArgumentTypeCoercion { .. } => cfg.type_errors,
IssueKind::DeprecatedCall { .. }
| IssueKind::DeprecatedMethodCall { .. }
| IssueKind::DeprecatedMethod { .. }
| IssueKind::DeprecatedClass { .. } => cfg.deprecated_calls,
IssueKind::CircularInheritance { .. } => cfg.type_errors,
IssueKind::DuplicateClass { .. }
| IssueKind::DuplicateInterface { .. }
| IssueKind::DuplicateTrait { .. }
| IssueKind::DuplicateEnum { .. }
| IssueKind::DuplicateFunction { .. } => cfg.duplicate_declarations,
IssueKind::UnusedVariable { .. }
| IssueKind::UnusedParam { .. }
| IssueKind::UnusedMethod { .. }
| IssueKind::UnusedProperty { .. }
| IssueKind::UnusedFunction { .. } => cfg.unused_symbols,
IssueKind::MissingReturnType { .. }
| IssueKind::MissingParamType { .. }
| IssueKind::MissingPropertyType { .. } => cfg.missing_types,
IssueKind::MixedArgument { .. }
| IssueKind::MixedAssignment { .. }
| IssueKind::MixedMethodCall { .. }
| IssueKind::MixedPropertyFetch { .. }
| IssueKind::MixedPropertyAssignment { .. }
| IssueKind::MixedArrayAccess
| IssueKind::MixedArrayOffset
| IssueKind::MixedReturnStatement { .. } => cfg.mixed_usage,
IssueKind::DocblockTypeContradiction { .. }
| IssueKind::UnevaluatedCode { .. }
| IssueKind::IfThisIsMismatch { .. } => true,
_ => true,
}
}
fn to_lsp_diagnostic(issue: mir_issues::Issue) -> Diagnostic {
let line = issue.location.line.saturating_sub(1);
let line_end = issue.location.line_end.saturating_sub(1).max(line);
let col_start = issue.location.col_start as u32;
let col_end = issue.location.col_end as u32;
let end_character = if line_end == line {
col_end.max(col_start + 1)
} else {
col_end
};
Diagnostic {
range: Range {
start: Position {
line,
character: col_start,
},
end: Position {
line: line_end,
character: end_character,
},
},
severity: Some(match issue.severity {
mir_issues::Severity::Error => DiagnosticSeverity::ERROR,
mir_issues::Severity::Warning => DiagnosticSeverity::WARNING,
mir_issues::Severity::Info => DiagnosticSeverity::INFORMATION,
}),
code: Some(NumberOrString::String(issue.kind.name().to_string())),
source: Some(PHP_LSP_SOURCE.to_string()),
message: issue.kind.message(),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_lsp_diagnostic_sets_code_to_issue_kind_name() {
use mir_issues::{Issue, IssueKind, Location};
use std::sync::Arc;
use tower_lsp_server::ls_types::NumberOrString;
let location = Location {
file: Arc::from("file:///test.php"),
line: 1,
line_end: 1,
col_start: 0,
col_end: 3,
};
let issue = Issue::new(
IssueKind::UndefinedClass {
name: "Foo".to_string(),
},
location,
);
let diag = to_lsp_diagnostic(issue);
assert_eq!(
diag.code,
Some(NumberOrString::String("UndefinedClass".to_string())),
"diagnostic code must be the IssueKind name so code actions can match by type"
);
assert!(
diag.message.contains("Foo"),
"diagnostic message should mention the class name"
);
}
#[test]
fn disabled_list_filters_issue_by_name() {
use mir_issues::{Issue, IssueKind, Location};
use std::sync::Arc;
let issue = Issue::new(
IssueKind::UndefinedClass {
name: "Foo".to_string(),
},
Location {
file: Arc::from("file:///test.php"),
line: 1,
line_end: 1,
col_start: 0,
col_end: 3,
},
);
let uri: Uri = "file:///test.php".parse().unwrap();
let mut cfg = DiagnosticsConfig::default();
assert_eq!(
issues_to_diagnostics(std::slice::from_ref(&issue), &uri, &cfg).len(),
1
);
cfg.disabled = vec!["UndefinedClass".to_string()];
assert!(issues_to_diagnostics(&[issue], &uri, &cfg).is_empty());
}
#[test]
fn to_lsp_diagnostic_spans_multiple_lines() {
use mir_issues::{Issue, IssueKind, Location};
use std::sync::Arc;
let issue = Issue::new(
IssueKind::UndefinedClass {
name: "Foo".to_string(),
},
Location {
file: Arc::from("file:///test.php"),
line: 2,
line_end: 4,
col_start: 8,
col_end: 1,
},
);
let range = to_lsp_diagnostic(issue).range;
assert_eq!(range.start, Position::new(1, 8));
assert_eq!(range.end, Position::new(3, 1));
}
}