use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range, Url};
use crate::analysis::diagnostics::PHP_LSP_SOURCE;
use crate::ast::ParsedDoc;
use crate::config::DiagnosticsConfig;
pub fn semantic_diagnostics(
uri: &Url,
doc: &ParsedDoc,
session: &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());
session.ingest_file(file.clone(), doc.source_arc());
let source_map = php_rs_parser::source_map::SourceMap::new(doc.source());
let owned_program = php_ast::owned::to_owned_program(doc.program());
let analyzer = mir_analyzer::FileAnalyzer::new(session);
let analysis = analyzer.analyze(file.clone(), doc.source(), &owned_program, &source_map);
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: &Url,
cfg: &DiagnosticsConfig,
) -> Vec<Diagnostic> {
if !cfg.enabled {
return vec![];
}
issues
.iter()
.filter(|i| issue_passes_filter(i, cfg))
.cloned()
.map(to_lsp_diagnostic)
.collect()
}
fn issue_passes_filter(issue: &mir_issues::Issue, cfg: &DiagnosticsConfig) -> bool {
use mir_issues::IssueKind;
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 => 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 => cfg.mixed_usage,
_ => true,
}
}
fn uses_codebase_location(kind: &mir_issues::IssueKind) -> bool {
use mir_issues::IssueKind;
matches!(
kind,
IssueKind::CircularInheritance { .. }
| IssueKind::InvalidExtendClass { .. }
| IssueKind::UnimplementedAbstractMethod { .. }
| IssueKind::UnimplementedInterfaceMethod { .. }
| IssueKind::FinalMethodOverridden { .. }
| IssueKind::OverriddenMethodAccess { .. }
| IssueKind::MethodSignatureMismatch { .. }
| IssueKind::InvalidTraitUse { .. }
)
}
fn to_lsp_diagnostic(issue: mir_issues::Issue) -> Diagnostic {
let line = issue.location.line.saturating_sub(1);
let (col_start, col_end) = if uses_codebase_location(&issue.kind) {
(
issue.location.col_start as u32,
issue.location.col_end as u32,
)
} else {
(
issue.location.col_start.saturating_sub(1) as u32,
issue.location.col_end.saturating_sub(1) as u32,
)
};
Diagnostic {
range: Range {
start: Position {
line,
character: col_start,
},
end: Position {
line,
character: col_end.max(col_start + 1),
},
},
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::lsp_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"
);
}
}