1use std::path::Path;
6
7use perl_diagnostics_codes::DiagnosticCode;
8use perl_parser_core::Node;
9use perl_parser_core::error::ParseError;
10use perl_pragma::PragmaTracker;
11use perl_semantic_analyzer::scope_analyzer::ScopeAnalyzer;
12use perl_semantic_analyzer::symbol::SymbolExtractor;
13
14use crate::dedup::deduplicate_diagnostics;
15use crate::lints::common_mistakes::check_common_mistakes;
16use crate::lints::deprecated::check_deprecated_syntax;
17use crate::lints::package_subroutine::{
18 check_duplicate_package, check_duplicate_subroutine, check_missing_package_declaration,
19};
20use crate::lints::printf_format::check_printf_format;
21use crate::lints::security::check_security;
22use crate::lints::strict_warnings::check_strict_warnings;
23use crate::lints::unreachable_code::check_unreachable_code;
24use crate::lints::unused_imports::check_unused_imports;
25use crate::lints::version_compat::check_version_compat;
26use crate::scope::scope_issues_to_diagnostics;
27
28pub use perl_lsp_diagnostic_types::{Diagnostic, DiagnosticSeverity, RelatedInformation};
30
31pub struct DiagnosticsProvider {
36 _ast: std::sync::Arc<Node>,
37 _source: String,
38}
39
40impl DiagnosticsProvider {
41 pub fn new(ast: &std::sync::Arc<Node>, source: String) -> Self {
43 Self { _ast: ast.clone(), _source: source }
44 }
45
46 pub fn get_diagnostics(
56 &self,
57 ast: &std::sync::Arc<Node>,
58 parse_errors: &[ParseError],
59 source: &str,
60 module_resolver: Option<&dyn Fn(&str) -> bool>,
61 ) -> Vec<Diagnostic> {
62 self.get_diagnostics_with_path(ast, parse_errors, source, module_resolver, None)
63 }
64
65 pub fn get_diagnostics_with_path(
67 &self,
68 ast: &std::sync::Arc<Node>,
69 parse_errors: &[ParseError],
70 source: &str,
71 module_resolver: Option<&dyn Fn(&str) -> bool>,
72 source_path: Option<&Path>,
73 ) -> Vec<Diagnostic> {
74 let mut diagnostics = Vec::new();
75 let source_len = source.len();
76
77 for error in parse_errors {
79 let (location, message) = match error {
80 ParseError::UnexpectedToken { location, expected, found } => {
81 let found_display = format_found_token(found);
82 let msg = build_enhanced_message(expected, found, &found_display);
83 (*location, msg)
84 }
85 ParseError::SyntaxError { location, message } => (*location, message.clone()),
86 ParseError::UnexpectedEof => (source.len(), "Unexpected end of input".to_string()),
87 ParseError::LexerError { message } => (0, message.clone()),
88 _ => (0, error.to_string()),
89 };
90
91 let range_start = location.min(source_len);
92 let range_end = range_start.saturating_add(1).min(source_len.saturating_add(1));
93
94 let suggestion = build_parse_error_suggestion(error);
95
96 let related_information = suggestion
98 .as_ref()
99 .map(|s| {
100 vec![RelatedInformation {
101 location: (range_start, range_end),
102 message: format!("Suggestion: {s}"),
103 }]
104 })
105 .unwrap_or_default();
106
107 let code = match error {
108 ParseError::UnexpectedEof => DiagnosticCode::UnexpectedEof,
109 ParseError::SyntaxError { .. } => DiagnosticCode::SyntaxError,
110 _ => DiagnosticCode::ParseError,
111 };
112
113 diagnostics.push(Diagnostic {
114 range: (range_start, range_end),
115 severity: DiagnosticSeverity::Error,
116 code: Some(code.as_str().to_string()),
117 message,
118 related_information,
119 tags: Vec::new(),
120 suggestion,
121 });
122 }
123
124 let pragma_map = PragmaTracker::build(ast);
126 let scope_analyzer = ScopeAnalyzer::new();
127 let scope_issues = scope_analyzer.analyze(ast, source, &pragma_map);
128 diagnostics.extend(scope_issues_to_diagnostics(scope_issues));
129
130 let heredoc_diags = crate::heredoc_antipatterns::detect_heredoc_antipatterns(source);
132 diagnostics.extend(heredoc_diags);
133
134 check_strict_warnings(ast, &mut diagnostics);
136 check_deprecated_syntax(ast, &mut diagnostics);
137 let symbol_table = SymbolExtractor::new_with_source(source).extract(ast);
138 check_common_mistakes(ast, &symbol_table, &mut diagnostics);
139 check_printf_format(ast, &mut diagnostics);
140
141 check_missing_package_declaration(ast, source, source_path, &mut diagnostics);
143 check_duplicate_package(ast, &mut diagnostics);
144 check_duplicate_subroutine(ast, &mut diagnostics);
145
146 check_security(ast, &mut diagnostics);
148
149 check_unused_imports(ast, source, &mut diagnostics);
151
152 check_version_compat(ast, &mut diagnostics);
154
155 check_unreachable_code(ast, &mut diagnostics);
157
158 if let Some(resolver) = module_resolver {
160 crate::lints::missing_module::check_missing_modules(
161 ast,
162 source,
163 resolver,
164 &mut diagnostics,
165 );
166 }
167
168 deduplicate_diagnostics(&mut diagnostics);
170
171 diagnostics
172 }
173}
174
175fn format_found_token(found: &str) -> String {
176 if found.is_empty() || found == "<EOF>" {
177 "end of input".to_string()
178 } else {
179 format!("`{found}`")
180 }
181}
182
183fn build_enhanced_message(expected: &str, found: &str, found_display: &str) -> String {
185 let expected_lower = expected.to_lowercase();
186
187 if expected.contains(';') || expected_lower.contains("semicolon") {
189 return format!("Missing semicolon after statement. Add `;` here (found {found_display})");
190 }
191
192 if expected_lower.contains("variable") {
194 return format!(
195 "Expected a variable like `$foo`, `@bar`, or `%hash` here, found {found_display}"
196 );
197 }
198
199 if found == "}" || found == ")" || found == "]" {
201 let opener = match found {
202 "}" => "{",
203 ")" => "(",
204 "]" => "[",
205 _ => "",
206 };
207 return format!(
208 "Unexpected `{found}` -- possible unmatched brace. \
209 Check the opening `{opener}` earlier in this scope"
210 );
211 }
212
213 format!("Expected {expected}, found {found_display}")
215}
216
217fn build_parse_error_suggestion(error: &ParseError) -> Option<String> {
222 match error {
223 ParseError::UnexpectedToken { expected, found, .. } => {
224 if expected.contains(';') || expected.contains("semicolon") {
226 return Some("Missing semicolon after statement. Add `;` here.".to_string());
227 }
228 if found == ";" {
230 return Some(format!(
231 "A {expected} is required here -- the statement appears incomplete"
232 ));
233 }
234 if found == "}" || found == ")" || found == "]" {
236 return Some(format!("Check for a missing {expected} before '{found}'"));
237 }
238 if expected.contains('{') || expected.contains("block") {
240 return Some(format!(
241 "Add an opening '{{' to start the block (found {found})"
242 ));
243 }
244 if expected.contains(')') {
246 return Some(
247 "Add a closing ')' -- there may be an unmatched opening '('".to_string(),
248 );
249 }
250 if expected.contains(']') {
252 return Some(
253 "Add a closing ']' -- there may be an unmatched opening '['".to_string(),
254 );
255 }
256 if expected.to_lowercase().contains("variable") {
258 return Some(
259 "Expected a variable like `$foo`, `@bar`, or `%hash` after the declaration keyword".to_string(),
260 );
261 }
262 None
263 }
264 ParseError::UnexpectedEof => Some(
265 "The file ended unexpectedly -- check for unclosed delimiters or missing semicolons"
266 .to_string(),
267 ),
268 ParseError::UnclosedDelimiter { delimiter } => {
269 Some(format!("Add a matching closing '{delimiter}'"))
270 }
271 ParseError::SyntaxError { message, .. } => {
272 let msg_lower = message.to_lowercase();
274 if msg_lower.contains("semicolon") || msg_lower.contains("missing ;") {
275 Some("Add a ';' at the end of the statement".to_string())
276 } else if msg_lower.contains("heredoc") {
277 Some(
278 "Check that the heredoc terminator appears on its own line with no extra whitespace"
279 .to_string(),
280 )
281 } else {
282 None
283 }
284 }
285 ParseError::LexerError { message } => {
286 let msg_lower = message.to_lowercase();
287 if msg_lower.contains("unterminated") || msg_lower.contains("unclosed") {
288 Some(
289 "Check for an unclosed string, regex, or heredoc near this position"
290 .to_string(),
291 )
292 } else if msg_lower.contains("invalid") && msg_lower.contains("character") {
293 Some(
294 "Remove or replace the invalid character -- Perl source should be valid UTF-8 or the encoding declared with 'use utf8;'"
295 .to_string(),
296 )
297 } else {
298 None
299 }
300 }
301 ParseError::RecursionLimit => Some(
302 "The code is too deeply nested -- consider refactoring into smaller subroutines"
303 .to_string(),
304 ),
305 ParseError::InvalidNumber { literal } => Some(format!(
306 "'{literal}' is not a valid number -- check for misplaced underscores or invalid digits"
307 )),
308 ParseError::InvalidString => Some(
309 "Check for a missing closing quote or an invalid escape sequence".to_string(),
310 ),
311 ParseError::InvalidRegex { .. } => Some(
312 "Check the regex pattern for unmatched delimiters, invalid quantifiers, or unescaped metacharacters"
313 .to_string(),
314 ),
315 ParseError::NestingTooDeep { .. } => Some(
316 "Reduce nesting depth by extracting inner logic into named subroutines".to_string(),
317 ),
318 ParseError::Cancelled => None,
319 ParseError::Recovered { .. } => None,
322 }
323}