use std::path::Path;
use perl_diagnostics_codes::DiagnosticCode;
use perl_parser_core::Node;
use perl_parser_core::error::ParseError;
use perl_pragma::PragmaTracker;
use perl_semantic_analyzer::scope_analyzer::ScopeAnalyzer;
use perl_semantic_analyzer::symbol::SymbolExtractor;
use crate::dedup::deduplicate_diagnostics;
use crate::lints::common_mistakes::check_common_mistakes;
use crate::lints::deprecated::check_deprecated_syntax;
use crate::lints::package_subroutine::{
check_duplicate_package, check_duplicate_subroutine, check_missing_package_declaration,
};
use crate::lints::printf_format::check_printf_format;
use crate::lints::security::check_security;
use crate::lints::strict_warnings::check_strict_warnings;
use crate::lints::unreachable_code::check_unreachable_code;
use crate::lints::unused_imports::check_unused_imports;
use crate::lints::version_compat::check_version_compat;
use crate::scope::scope_issues_to_diagnostics;
pub use perl_lsp_diagnostic_types::{Diagnostic, DiagnosticSeverity, RelatedInformation};
pub struct DiagnosticsProvider {
_ast: std::sync::Arc<Node>,
_source: String,
}
impl DiagnosticsProvider {
pub fn new(ast: &std::sync::Arc<Node>, source: String) -> Self {
Self { _ast: ast.clone(), _source: source }
}
pub fn get_diagnostics(
&self,
ast: &std::sync::Arc<Node>,
parse_errors: &[ParseError],
source: &str,
module_resolver: Option<&dyn Fn(&str) -> bool>,
) -> Vec<Diagnostic> {
self.get_diagnostics_with_path(ast, parse_errors, source, module_resolver, None)
}
pub fn get_diagnostics_with_path(
&self,
ast: &std::sync::Arc<Node>,
parse_errors: &[ParseError],
source: &str,
module_resolver: Option<&dyn Fn(&str) -> bool>,
source_path: Option<&Path>,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let source_len = source.len();
for error in parse_errors {
let (location, message) = match error {
ParseError::UnexpectedToken { location, expected, found } => {
let found_display = format_found_token(found);
let msg = build_enhanced_message(expected, found, &found_display);
(*location, msg)
}
ParseError::SyntaxError { location, message } => (*location, message.clone()),
ParseError::UnexpectedEof => (source.len(), "Unexpected end of input".to_string()),
ParseError::LexerError { message } => (0, message.clone()),
_ => (0, error.to_string()),
};
let range_start = location.min(source_len);
let range_end = range_start.saturating_add(1).min(source_len.saturating_add(1));
let suggestion = build_parse_error_suggestion(error);
let related_information = suggestion
.as_ref()
.map(|s| {
vec![RelatedInformation {
location: (range_start, range_end),
message: format!("Suggestion: {s}"),
}]
})
.unwrap_or_default();
let code = match error {
ParseError::UnexpectedEof => DiagnosticCode::UnexpectedEof,
ParseError::SyntaxError { .. } => DiagnosticCode::SyntaxError,
_ => DiagnosticCode::ParseError,
};
diagnostics.push(Diagnostic {
range: (range_start, range_end),
severity: DiagnosticSeverity::Error,
code: Some(code.as_str().to_string()),
message,
related_information,
tags: Vec::new(),
suggestion,
});
}
let pragma_map = PragmaTracker::build(ast);
let scope_analyzer = ScopeAnalyzer::new();
let scope_issues = scope_analyzer.analyze(ast, source, &pragma_map);
diagnostics.extend(scope_issues_to_diagnostics(scope_issues));
let heredoc_diags = crate::heredoc_antipatterns::detect_heredoc_antipatterns(source);
diagnostics.extend(heredoc_diags);
check_strict_warnings(ast, &mut diagnostics);
check_deprecated_syntax(ast, &mut diagnostics);
let symbol_table = SymbolExtractor::new_with_source(source).extract(ast);
check_common_mistakes(ast, &symbol_table, &mut diagnostics);
check_printf_format(ast, &mut diagnostics);
check_missing_package_declaration(ast, source, source_path, &mut diagnostics);
check_duplicate_package(ast, &mut diagnostics);
check_duplicate_subroutine(ast, &mut diagnostics);
check_security(ast, &mut diagnostics);
check_unused_imports(ast, source, &mut diagnostics);
check_version_compat(ast, &mut diagnostics);
check_unreachable_code(ast, &mut diagnostics);
if let Some(resolver) = module_resolver {
crate::lints::missing_module::check_missing_modules(
ast,
source,
resolver,
&mut diagnostics,
);
}
deduplicate_diagnostics(&mut diagnostics);
diagnostics
}
}
fn format_found_token(found: &str) -> String {
if found.is_empty() || found == "<EOF>" {
"end of input".to_string()
} else {
format!("`{found}`")
}
}
fn build_enhanced_message(expected: &str, found: &str, found_display: &str) -> String {
let expected_lower = expected.to_lowercase();
if expected.contains(';') || expected_lower.contains("semicolon") {
return format!("Missing semicolon after statement. Add `;` here (found {found_display})");
}
if expected_lower.contains("variable") {
return format!(
"Expected a variable like `$foo`, `@bar`, or `%hash` here, found {found_display}"
);
}
if found == "}" || found == ")" || found == "]" {
let opener = match found {
"}" => "{",
")" => "(",
"]" => "[",
_ => "",
};
return format!(
"Unexpected `{found}` -- possible unmatched brace. \
Check the opening `{opener}` earlier in this scope"
);
}
format!("Expected {expected}, found {found_display}")
}
fn build_parse_error_suggestion(error: &ParseError) -> Option<String> {
match error {
ParseError::UnexpectedToken { expected, found, .. } => {
if expected.contains(';') || expected.contains("semicolon") {
return Some("Missing semicolon after statement. Add `;` here.".to_string());
}
if found == ";" {
return Some(format!(
"A {expected} is required here -- the statement appears incomplete"
));
}
if found == "}" || found == ")" || found == "]" {
return Some(format!("Check for a missing {expected} before '{found}'"));
}
if expected.contains('{') || expected.contains("block") {
return Some(format!(
"Add an opening '{{' to start the block (found {found})"
));
}
if expected.contains(')') {
return Some(
"Add a closing ')' -- there may be an unmatched opening '('".to_string(),
);
}
if expected.contains(']') {
return Some(
"Add a closing ']' -- there may be an unmatched opening '['".to_string(),
);
}
if expected.to_lowercase().contains("variable") {
return Some(
"Expected a variable like `$foo`, `@bar`, or `%hash` after the declaration keyword".to_string(),
);
}
None
}
ParseError::UnexpectedEof => Some(
"The file ended unexpectedly -- check for unclosed delimiters or missing semicolons"
.to_string(),
),
ParseError::UnclosedDelimiter { delimiter } => {
Some(format!("Add a matching closing '{delimiter}'"))
}
ParseError::SyntaxError { message, .. } => {
let msg_lower = message.to_lowercase();
if msg_lower.contains("semicolon") || msg_lower.contains("missing ;") {
Some("Add a ';' at the end of the statement".to_string())
} else if msg_lower.contains("heredoc") {
Some(
"Check that the heredoc terminator appears on its own line with no extra whitespace"
.to_string(),
)
} else {
None
}
}
ParseError::LexerError { message } => {
let msg_lower = message.to_lowercase();
if msg_lower.contains("unterminated") || msg_lower.contains("unclosed") {
Some(
"Check for an unclosed string, regex, or heredoc near this position"
.to_string(),
)
} else if msg_lower.contains("invalid") && msg_lower.contains("character") {
Some(
"Remove or replace the invalid character -- Perl source should be valid UTF-8 or the encoding declared with 'use utf8;'"
.to_string(),
)
} else {
None
}
}
ParseError::RecursionLimit => Some(
"The code is too deeply nested -- consider refactoring into smaller subroutines"
.to_string(),
),
ParseError::InvalidNumber { literal } => Some(format!(
"'{literal}' is not a valid number -- check for misplaced underscores or invalid digits"
)),
ParseError::InvalidString => Some(
"Check for a missing closing quote or an invalid escape sequence".to_string(),
),
ParseError::InvalidRegex { .. } => Some(
"Check the regex pattern for unmatched delimiters, invalid quantifiers, or unescaped metacharacters"
.to_string(),
),
ParseError::NestingTooDeep { .. } => Some(
"Reduce nesting depth by extracting inner logic into named subroutines".to_string(),
),
ParseError::Cancelled => None,
ParseError::Recovered { .. } => None,
}
}