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