use perl_diagnostics_codes::DiagnosticCode;
use perl_parser_core::ast::{Node, NodeKind};
use super::super::walker::walk_node;
use perl_lsp_diagnostic_types::{
Diagnostic, DiagnosticSeverity, DiagnosticTag, RelatedInformation,
};
const PRAGMA_SKIP_LIST: &[&str] = &[
"strict",
"warnings",
"utf8",
"feature",
"constant",
"lib",
"parent",
"base",
"Exporter",
"vars",
"subs",
"overload",
"overloading",
"integer",
"bigint",
"bignum",
"bigrat",
"bytes",
"charnames",
"encoding",
"locale",
"mro",
"open",
"ops",
"re",
"sigtrap",
"sort",
"threads",
"threads::shared",
"autodie",
"autouse",
"diagnostics",
"English",
"experimental",
"fields",
"filetest",
"if",
"less",
"POSIX",
];
const IMPLICIT_EXPORT_SKIP_LIST: &[&str] = &[
"Moose",
"Moose::Role",
"Moose::Util::TypeConstraints",
"MooseX::StrictConstructor",
"MooseX::Types",
"Moo",
"Moo::Role",
"Mouse",
"Mouse::Role",
"Modern::Perl",
"Dancer",
"Dancer2",
"Catalyst",
"Mojolicious",
"Mojolicious::Lite",
"Mojo::Base",
"Test::More",
"Test::Most",
"Test::Simple",
"Test::Exception",
"Test::Fatal",
"Test::Warnings",
"Test::Deep",
"Test::Differences",
"Test::Class",
"Test::MockModule",
"Test::MockObject",
"Test::Spec",
"Test::Builder",
"Test2::V0",
"Test2::Bundle::More",
"Carp",
"Carp::Always",
"Scalar::Util",
"List::Util",
"List::MoreUtils",
"Data::Dumper",
"Try::Tiny",
"Getopt::Long",
"File::Basename",
"FindBin",
"Fcntl",
"UNIVERSAL",
];
pub fn check_unused_imports(node: &Node, source: &str, diagnostics: &mut Vec<Diagnostic>) {
let mut use_statements: Vec<(String, usize, usize)> = Vec::new();
walk_node(node, &mut |n| {
if let NodeKind::Use { module, .. } = &n.kind {
use_statements.push((module.clone(), n.location.start, n.location.end));
}
});
for (module, start, end) in &use_statements {
let module = module.as_str();
let start = *start;
let end = *end;
if PRAGMA_SKIP_LIST.contains(&module) {
continue;
}
if IMPLICIT_EXPORT_SKIP_LIST.contains(&module) {
continue;
}
if module.chars().next().is_some_and(|c| c.is_ascii_digit()) {
continue;
}
if has_use_args(node, module) {
continue;
}
if !is_module_referenced(source, module, start, end) {
diagnostics.push(Diagnostic {
range: (start, end),
severity: DiagnosticSeverity::Hint,
code: Some(DiagnosticCode::UnusedImport.as_str().to_string()),
message: format!("Module '{}' appears to be unused", module),
related_information: vec![RelatedInformation {
location: (start, end),
message: format!(
"If '{}' is imported for side effects, you can ignore this hint",
module
),
}],
tags: vec![DiagnosticTag::Unnecessary],
suggestion: Some(format!("Remove 'use {};' if it is not needed", module)),
});
}
}
}
fn has_use_args(node: &Node, target_module: &str) -> bool {
let mut found = false;
walk_node(node, &mut |n| {
if let NodeKind::Use { module, args, .. } = &n.kind
&& module == target_module
&& !args.is_empty()
{
found = true;
}
});
found
}
fn is_module_referenced(source: &str, module: &str, use_start: usize, use_end: usize) -> bool {
let mut search_start = 0;
while let Some(pos) = source[search_start..].find(module) {
let abs_pos = search_start + pos;
let match_end = abs_pos + module.len();
if abs_pos >= use_start && abs_pos < use_end {
search_start = match_end;
continue;
}
let before_ok = abs_pos == 0 || {
let ch = source.as_bytes()[abs_pos - 1];
!ch.is_ascii_alphanumeric() && ch != b'_'
};
let after_ok = match_end >= source.len() || {
let ch = source.as_bytes()[match_end];
!ch.is_ascii_alphanumeric() && ch != b'_'
};
if before_ok && after_ok {
return true;
}
search_start = match_end;
}
false
}