Skip to main content

perl_diagnostics/codes/
metadata.rs

1use super::{DiagnosticCode, DiagnosticSeverity, DiagnosticTag};
2
3fn is_undefined_variable_message(msg_lower: &str) -> bool {
4    contains_diagnostic_phrase(msg_lower, "not declared")
5        || contains_diagnostic_phrase(msg_lower, "undefined variable")
6        || (contains_diagnostic_phrase(msg_lower, "undefined")
7            && (contains_diagnostic_phrase(msg_lower, "variable")
8                || contains_diagnostic_phrase(msg_lower, "global symbol")))
9        // Real Perl: "Global symbol "$x" requires explicit package name at file.pl line N."
10        || contains_diagnostic_phrase(msg_lower, "requires explicit package name")
11}
12
13fn contains_diagnostic_phrase(haystack: &str, needle: &str) -> bool {
14    haystack.match_indices(needle).any(|(start, matched)| {
15        let end = start + matched.len();
16        has_phrase_boundary(haystack, start, end)
17    })
18}
19
20fn has_phrase_boundary(haystack: &str, start: usize, end: usize) -> bool {
21    let bytes = haystack.as_bytes();
22    let before_is_word = start
23        .checked_sub(1)
24        .and_then(|index| bytes.get(index))
25        .is_some_and(|byte| is_phrase_word_byte(*byte));
26    let after_is_word = bytes.get(end).is_some_and(|byte| is_phrase_word_byte(*byte));
27
28    !before_is_word && !after_is_word
29}
30
31fn is_phrase_word_byte(byte: u8) -> bool {
32    byte.is_ascii_alphanumeric() || byte == b'_'
33}
34
35impl DiagnosticCode {
36    /// Get the string representation of this code.
37    pub fn as_str(&self) -> &'static str {
38        match self {
39            Self::ParseError => "PL001",
40            Self::SyntaxError => "PL002",
41            Self::UnexpectedEof => "PL003",
42            Self::MissingStrict => "PL100",
43            Self::MissingWarnings => "PL101",
44            Self::UnusedVariable => "PL102",
45            Self::UndefinedVariable => "PL103",
46            Self::VariableShadowing => "PL104",
47            Self::VariableRedeclaration => "PL105",
48            Self::DuplicateParameter => "PL106",
49            Self::ParameterShadowsGlobal => "PL107",
50            Self::UnusedParameter => "PL108",
51            Self::UnquotedBareword => "PL109",
52            Self::UninitializedVariable => "PL110",
53            Self::MisspelledPragma => "PL111",
54            Self::CaptureVarWithoutRegexMatch => "PL112",
55            Self::MissingPackageDeclaration => "PL200",
56            Self::DuplicatePackage => "PL201",
57            Self::DuplicateSubroutine => "PL300",
58            Self::MissingReturn => "PL301",
59            Self::InvalidPrototype => "PL302",
60            Self::RoleConflict => "PL303",
61            Self::MissingPodCoverage => "PL304",
62            Self::BarewordFilehandle => "PL400",
63            Self::TwoArgOpen => "PL401",
64            Self::ImplicitReturn => "PL402",
65            Self::AssignmentInCondition => "PL403",
66            Self::NumericComparisonWithUndef => "PL404",
67            Self::PrintfFormatMismatch => "PL405",
68            Self::UnreachableCode => "PL406",
69            Self::EvalErrorFlow => "PL407",
70            Self::DuplicateHashKey => "PL408",
71            Self::GotoUndefinedLabel => "PL409",
72            Self::LoopControlUndefinedLabel => "PL410",
73            Self::DeprecatedDefined => "PL500",
74            Self::DeprecatedArrayBase => "PL501",
75            Self::PhaseScopedStrictPragma => "PL502",
76            Self::PhaseScopedWarningsPragma => "PL503",
77            Self::SecurityStringEval => "PL600",
78            Self::SecurityBacktickExec => "PL601",
79            Self::SecuritySignalHandler => "PL602",
80            Self::SecuritySystemCall => "PL603",
81            Self::SecurityExecCall => "PL604",
82            Self::SecurityPipeOpen => "PL605",
83            Self::SecurityReadpipe => "PL606",
84            Self::UnusedImport => "PL700",
85            Self::ModuleNotFound => "PL701",
86            Self::HeredocInFormat => "PL800",
87            Self::HeredocInBegin => "PL801",
88            Self::HeredocDynamicDelimiter => "PL802",
89            Self::HeredocInSourceFilter => "PL803",
90            Self::HeredocInRegexCode => "PL804",
91            Self::HeredocInEval => "PL805",
92            Self::HeredocTiedHandle => "PL806",
93            Self::VersionIncompatFeature => "PL900",
94            Self::CriticSeverity1 => "PC001",
95            Self::CriticSeverity2 => "PC002",
96            Self::CriticSeverity3 => "PC003",
97            Self::CriticSeverity4 => "PC004",
98            Self::CriticSeverity5 => "PC005",
99        }
100    }
101
102    /// Get the documentation URL for this code, if available.
103    pub fn documentation_url(&self) -> Option<&'static str> {
104        let code = self.as_str();
105        // Perl::Critic codes don't have centralized documentation
106        if code.starts_with("PC") {
107            return None;
108        }
109        // Build URL from stable code string for all PL codes
110        Some(match code {
111            "PL001" => "https://docs.perl-lsp.org/errors/PL001",
112            "PL002" => "https://docs.perl-lsp.org/errors/PL002",
113            "PL003" => "https://docs.perl-lsp.org/errors/PL003",
114            "PL100" => "https://docs.perl-lsp.org/errors/PL100",
115            "PL101" => "https://docs.perl-lsp.org/errors/PL101",
116            "PL102" => "https://docs.perl-lsp.org/errors/PL102",
117            "PL103" => "https://docs.perl-lsp.org/errors/PL103",
118            "PL104" => "https://docs.perl-lsp.org/errors/PL104",
119            "PL105" => "https://docs.perl-lsp.org/errors/PL105",
120            "PL106" => "https://docs.perl-lsp.org/errors/PL106",
121            "PL107" => "https://docs.perl-lsp.org/errors/PL107",
122            "PL108" => "https://docs.perl-lsp.org/errors/PL108",
123            "PL109" => "https://docs.perl-lsp.org/errors/PL109",
124            "PL110" => "https://docs.perl-lsp.org/errors/PL110",
125            "PL111" => "https://docs.perl-lsp.org/errors/PL111",
126            "PL112" => "https://docs.perl-lsp.org/errors/PL112",
127            "PL200" => "https://docs.perl-lsp.org/errors/PL200",
128            "PL201" => "https://docs.perl-lsp.org/errors/PL201",
129            "PL300" => "https://docs.perl-lsp.org/errors/PL300",
130            "PL301" => "https://docs.perl-lsp.org/errors/PL301",
131            "PL302" => "https://docs.perl-lsp.org/errors/PL302",
132            "PL303" => "https://docs.perl-lsp.org/errors/PL303",
133            "PL304" => "https://docs.perl-lsp.org/errors/PL304",
134            "PL400" => "https://docs.perl-lsp.org/errors/PL400",
135            "PL401" => "https://docs.perl-lsp.org/errors/PL401",
136            "PL402" => "https://docs.perl-lsp.org/errors/PL402",
137            "PL403" => "https://docs.perl-lsp.org/errors/PL403",
138            "PL404" => "https://docs.perl-lsp.org/errors/PL404",
139            "PL405" => "https://docs.perl-lsp.org/errors/PL405",
140            "PL406" => "https://docs.perl-lsp.org/errors/PL406",
141            "PL407" => "https://docs.perl-lsp.org/errors/PL407",
142            "PL408" => "https://docs.perl-lsp.org/errors/PL408",
143            "PL409" => "https://docs.perl-lsp.org/errors/PL409",
144            "PL410" => "https://docs.perl-lsp.org/errors/PL410",
145            "PL500" => "https://docs.perl-lsp.org/errors/PL500",
146            "PL501" => "https://docs.perl-lsp.org/errors/PL501",
147            "PL502" => "https://docs.perl-lsp.org/errors/PL502",
148            "PL503" => "https://docs.perl-lsp.org/errors/PL503",
149            "PL600" => "https://docs.perl-lsp.org/errors/PL600",
150            "PL601" => "https://docs.perl-lsp.org/errors/PL601",
151            "PL602" => "https://docs.perl-lsp.org/errors/PL602",
152            "PL603" => "https://docs.perl-lsp.org/errors/PL603",
153            "PL604" => "https://docs.perl-lsp.org/errors/PL604",
154            "PL605" => "https://docs.perl-lsp.org/errors/PL605",
155            "PL606" => "https://docs.perl-lsp.org/errors/PL606",
156            "PL700" => "https://docs.perl-lsp.org/errors/PL700",
157            "PL701" => "https://docs.perl-lsp.org/errors/PL701",
158            "PL800" => "https://docs.perl-lsp.org/errors/PL800",
159            "PL801" => "https://docs.perl-lsp.org/errors/PL801",
160            "PL802" => "https://docs.perl-lsp.org/errors/PL802",
161            "PL803" => "https://docs.perl-lsp.org/errors/PL803",
162            "PL804" => "https://docs.perl-lsp.org/errors/PL804",
163            "PL805" => "https://docs.perl-lsp.org/errors/PL805",
164            "PL806" => "https://docs.perl-lsp.org/errors/PL806",
165            "PL900" => "https://docs.perl-lsp.org/errors/PL900",
166            _ => return None,
167        })
168    }
169
170    /// Get the default severity for this diagnostic code.
171    pub fn severity(&self) -> DiagnosticSeverity {
172        match self {
173            // Errors
174            Self::ParseError
175            | Self::SyntaxError
176            | Self::UnexpectedEof
177            | Self::UndefinedVariable
178            | Self::VariableRedeclaration
179            | Self::DuplicateParameter
180            | Self::UnquotedBareword => DiagnosticSeverity::Error,
181
182            // Warnings
183            Self::MissingStrict
184            | Self::MissingWarnings
185            | Self::UnusedVariable
186            | Self::VariableShadowing
187            | Self::ParameterShadowsGlobal
188            | Self::UnusedParameter
189            | Self::UninitializedVariable
190            | Self::MisspelledPragma
191            | Self::MissingPackageDeclaration
192            | Self::DuplicatePackage
193            | Self::DuplicateSubroutine
194            | Self::MissingReturn
195            | Self::InvalidPrototype
196            | Self::RoleConflict
197            | Self::BarewordFilehandle
198            | Self::TwoArgOpen
199            | Self::ImplicitReturn
200            | Self::AssignmentInCondition
201            | Self::NumericComparisonWithUndef
202            | Self::PrintfFormatMismatch
203            | Self::DuplicateHashKey
204            | Self::GotoUndefinedLabel
205            | Self::LoopControlUndefinedLabel
206            | Self::DeprecatedDefined
207            | Self::DeprecatedArrayBase
208            | Self::PhaseScopedStrictPragma
209            | Self::PhaseScopedWarningsPragma
210            | Self::SecurityStringEval
211            | Self::SecurityBacktickExec
212            | Self::SecuritySignalHandler
213            | Self::SecuritySystemCall
214            | Self::SecurityExecCall
215            | Self::SecurityPipeOpen
216            | Self::SecurityReadpipe
217            | Self::ModuleNotFound
218            | Self::VersionIncompatFeature
219            | Self::EvalErrorFlow
220            | Self::CriticSeverity1
221            | Self::CriticSeverity2 => DiagnosticSeverity::Warning,
222
223            // Information
224            Self::CaptureVarWithoutRegexMatch
225            | Self::HeredocInFormat
226            | Self::HeredocInBegin
227            | Self::HeredocDynamicDelimiter
228            | Self::HeredocInSourceFilter
229            | Self::HeredocInRegexCode
230            | Self::HeredocInEval
231            | Self::HeredocTiedHandle => DiagnosticSeverity::Information,
232
233            // Hints
234            Self::MissingPodCoverage
235            | Self::UnusedImport
236            | Self::UnreachableCode
237            | Self::CriticSeverity3
238            | Self::CriticSeverity4
239            | Self::CriticSeverity5 => DiagnosticSeverity::Hint,
240        }
241    }
242
243    /// Get any diagnostic tags associated with this code.
244    pub fn tags(&self) -> &'static [DiagnosticTag] {
245        match self {
246            Self::UnusedVariable
247            | Self::UnusedParameter
248            | Self::UnusedImport
249            | Self::UnreachableCode => &[DiagnosticTag::Unnecessary],
250            Self::DeprecatedDefined | Self::DeprecatedArrayBase => &[DiagnosticTag::Deprecated],
251            _ => &[],
252        }
253    }
254
255    /// Return a human-readable context hint for this diagnostic code.
256    ///
257    /// Hints are short, actionable explanations that help users understand
258    /// what the diagnostic means and how to resolve it.  Perl::Critic codes
259    /// return `None` because their per-policy descriptions already serve this
260    /// purpose.
261    pub fn context_hint(&self) -> Option<&'static str> {
262        match self {
263            Self::ParseError => Some(
264                "The parser could not understand this code. \
265                Check for missing semicolons, unmatched brackets, or incorrect syntax.",
266            ),
267            Self::SyntaxError => Some(
268                "Perl syntax error. Check for typos, missing operators, \
269                or unbalanced parentheses near this location.",
270            ),
271            Self::UnexpectedEof => Some(
272                "The file ended unexpectedly. Check for unclosed blocks `{}`, \
273                heredocs, or multi-line strings.",
274            ),
275            Self::MissingStrict => Some(
276                "Add `use strict;` at the top of your file. \
277                Strict mode catches common variable mistakes at compile time.",
278            ),
279            Self::MissingWarnings => Some(
280                "Add `use warnings;` at the top of your file. \
281                Warnings highlight many common programming mistakes.",
282            ),
283            Self::UnusedVariable => Some(
284                "This variable is declared but never used. \
285                Remove it, or prefix with `_` (e.g., `$_unused`) to suppress.",
286            ),
287            Self::UndefinedVariable => Some(
288                "This variable was not declared with `my`, `our`, or `local`. \
289                Add `use strict;` and declare all variables before use.",
290            ),
291            Self::MissingPackageDeclaration => Some(
292                "This file has no `package` declaration. \
293                Add `package MyModule;` at the top for module files.",
294            ),
295            Self::DuplicatePackage => Some(
296                "This package name is declared more than once in the same file. \
297                Each package should appear once, or split into separate files.",
298            ),
299            Self::DuplicateSubroutine => Some(
300                "A subroutine with this name is defined more than once. \
301                The later definition silently replaces the earlier one.",
302            ),
303            Self::MissingReturn => Some(
304                "This subroutine has no explicit `return` statement. \
305                Add `return $value;` to make the return value clear.",
306            ),
307            Self::RoleConflict => Some(
308                "Two or more consumed Moo/Moose roles provide the same method. \
309                Define the method in the class or remove one of the conflicting roles.",
310            ),
311            Self::MissingPodCoverage => Some(
312                "This exported subroutine has no corresponding `=head2` or `=item` POD section. \
313                Add documentation so users of your module can discover its API.",
314            ),
315            Self::InvalidPrototype => Some(
316                "The prototype contains a character that Perl does not recognise. \
317                Valid prototype characters are: $, @, %, &, *, \\, ;, +, _ and spaces. \
318                See perlsub for the full prototype syntax.",
319            ),
320            Self::BarewordFilehandle => Some(
321                "Bareword filehandles (e.g., `open FH, ...`) are global and unsafe. \
322                Use a lexical filehandle instead: `open my $fh, '<', $file or die $!;`",
323            ),
324            Self::TwoArgOpen => Some(
325                "Two-argument `open()` is vulnerable to injection. \
326                Use three-argument form: `open my $fh, '<', $filename or die $!;`",
327            ),
328            Self::ImplicitReturn => Some(
329                "The return value of this expression is used implicitly. \
330                Make it explicit with `return` or assign it to a variable.",
331            ),
332            Self::AssignmentInCondition => Some(
333                "This looks like an assignment `=` inside a condition where \
334                a comparison `==` or `eq` was likely intended.",
335            ),
336            Self::NumericComparisonWithUndef => Some(
337                "Comparing a potentially undefined value with a numeric operator \
338                produces a warning at runtime. Check for definedness first with `defined()`.",
339            ),
340            Self::EvalErrorFlow => Some(
341                "Read `$@` or `$EVAL_ERROR` immediately after an `eval` or `try` \
342                block; intervening statements can clobber the exception state.",
343            ),
344            Self::UnreachableCode => Some(
345                "This statement cannot be executed because a preceding statement \
346                unconditionally exits (return, die, exit, croak). Remove or relocate it.",
347            ),
348            Self::DuplicateHashKey => Some(
349                "This hash key appears more than once in the same literal. \
350                Only the last value will be used; the earlier assignment is silently discarded.",
351            ),
352            Self::GotoUndefinedLabel => Some(
353                "This goto target label is not defined in the current file. \
354                Define the label or use a dynamic goto form only when the target is known at runtime.",
355            ),
356            Self::LoopControlUndefinedLabel => Some(
357                "This `next`, `last`, or `redo` references a label that is not defined in the current file. \
358                Add a matching `LABEL:` on an enclosing loop, or remove the label to target the innermost loop.",
359            ),
360            Self::PrintfFormatMismatch => Some(
361                "The number of format specifiers does not match the number of arguments. \
362                Each %s/%d/%f/etc. consumes one argument (except %% which consumes none).",
363            ),
364            Self::VariableShadowing => Some(
365                "This variable shadows an outer variable with the same name. \
366                Rename it to avoid confusion, or use the outer variable directly.",
367            ),
368            Self::VariableRedeclaration => Some(
369                "This variable is declared again in the same scope. \
370                Remove the duplicate `my` declaration and reuse the existing variable.",
371            ),
372            Self::DuplicateParameter => Some(
373                "This subroutine signature has a duplicate parameter name. \
374                Each parameter must have a unique name.",
375            ),
376            Self::ParameterShadowsGlobal => Some(
377                "This subroutine parameter shadows a global (`our`) variable. \
378                Rename the parameter to avoid confusion with the global.",
379            ),
380            Self::UnusedParameter => Some(
381                "This subroutine parameter is declared but never used. \
382                Remove it or prefix with `_` (e.g., `$_unused`) to suppress.",
383            ),
384            Self::UnquotedBareword => Some(
385                "This bareword is used where a quoted string is expected. \
386                Under `use strict`, barewords are not allowed. Quote it: `'word'`.",
387            ),
388            Self::UninitializedVariable => Some(
389                "This variable is used before being assigned a value. \
390                Initialize it before use to avoid `Use of uninitialized value` warnings.",
391            ),
392            Self::MisspelledPragma => Some(
393                "This pragma name appears to be misspelled. \
394                Check the spelling and ensure the module is installed.",
395            ),
396            Self::CaptureVarWithoutRegexMatch => Some(
397                "Capture variables ($1, $2, etc.) are only meaningful after a successful regex match. \
398                Perform a regex match with =~ /.../ before using $1 or $2.",
399            ),
400            Self::DeprecatedDefined => Some(
401                "`defined(@array)` and `defined(%hash)` are deprecated since Perl 5.6. \
402                Use `@array` or `%hash` directly in boolean context instead.",
403            ),
404            Self::DeprecatedArrayBase => Some(
405                "The `$[` variable is deprecated. Array indices always start at 0 \
406                in modern Perl. Remove any assignment to `$[`.",
407            ),
408            Self::PhaseScopedStrictPragma => Some(
409                "`use strict` inside a phase block only applies inside that block. \
410                Move `use strict;` to file scope for file-wide strict enforcement.",
411            ),
412            Self::PhaseScopedWarningsPragma => Some(
413                "`use warnings` inside a phase block only applies inside that block. \
414                Move `use warnings;` to file scope for file-wide warnings coverage.",
415            ),
416            Self::SecurityStringEval => Some(
417                "String `eval` executes arbitrary code and is a security risk. \
418                Use block eval `eval { ... }` or safer alternatives.",
419            ),
420            Self::SecurityBacktickExec => Some(
421                "Backticks/`qx()` execute shell commands and can be exploited. \
422                Use `system()` with a list form or IPC::Run for safer execution.",
423            ),
424            Self::SecuritySignalHandler => Some(
425                "Assigning to $SIG{__DIE__} or $SIG{__WARN__} globally changes exception \
426                and warning handling for the whole process. Use `local` to scope the handler.",
427            ),
428            Self::SecuritySystemCall => Some(
429                "`system()` executes a shell command. If the arguments include user input, \
430                use the list form `system($cmd, @args)` to avoid shell injection.",
431            ),
432            Self::SecurityExecCall => Some(
433                "`exec()` replaces the current process with a shell command. If arguments \
434                include user input, use the list form `exec($cmd, @args)` to avoid shell injection.",
435            ),
436            Self::SecurityPipeOpen => Some(
437                "Pipe-open executes a shell command. Pass a list to `open` for safe argument \
438                handling: `open(my $fh, '-|', $cmd, @args)` instead of `open(my $fh, \"|$cmd\")`.",
439            ),
440            Self::SecurityReadpipe => Some(
441                "`readpipe()` executes a shell command (equivalent to backticks/qx//). \
442                Use `open(my $fh, '-|', $cmd, @args)` or IPC::Run for safer command execution.",
443            ),
444            Self::UnusedImport => Some(
445                "This module is imported but none of its exports appear to be used. \
446                Remove the `use` statement to reduce unnecessary dependencies.",
447            ),
448            Self::ModuleNotFound => Some(
449                "This module was not found in the workspace or configured include paths. \
450                Install it with cpanm or add it to cpanfile.",
451            ),
452            Self::HeredocInFormat => Some(
453                "Heredocs inside `format` blocks can cause subtle parsing issues. \
454                Extract the heredoc content into a variable before the format.",
455            ),
456            Self::HeredocInBegin => Some(
457                "Heredocs inside `BEGIN` blocks may behave unexpectedly due to \
458                compile-time execution. Move the heredoc outside the BEGIN block.",
459            ),
460            Self::HeredocDynamicDelimiter => Some(
461                "The heredoc delimiter contains a variable, making it dynamic. \
462                Use a static delimiter string to avoid surprising behavior.",
463            ),
464            Self::HeredocInSourceFilter => Some(
465                "Heredocs inside source-filtered code may be mangled by the filter. \
466                Avoid combining heredocs with source filters.",
467            ),
468            Self::HeredocInRegexCode => Some(
469                "Heredocs inside regex code blocks `(?{ ... })` can cause parsing failures. \
470                Move the heredoc content outside the regex.",
471            ),
472            Self::HeredocInEval => Some(
473                "Heredocs inside string `eval` are fragile and error-prone. \
474                Use a variable or block eval instead.",
475            ),
476            Self::HeredocTiedHandle => Some(
477                "Heredocs written to tied filehandles may not behave as expected. \
478                The tie interface may not handle multi-line heredoc output correctly.",
479            ),
480            Self::VersionIncompatFeature => Some(
481                "This Perl feature requires a newer Perl version than declared. \
482                Update 'use vN.NN' or 'use feature' to enable it.",
483            ),
484            // Perl::Critic codes carry per-policy descriptions; no generic hint needed.
485            Self::CriticSeverity1
486            | Self::CriticSeverity2
487            | Self::CriticSeverity3
488            | Self::CriticSeverity4
489            | Self::CriticSeverity5 => None,
490        }
491    }
492
493    /// Try to infer a diagnostic code from a message.
494    pub fn from_message(msg: &str) -> Option<Self> {
495        let msg_lower = msg.to_lowercase();
496        if contains_diagnostic_phrase(&msg_lower, "inside a begin block does not enable strict")
497            || contains_diagnostic_phrase(&msg_lower, "inside a phase block does not enable strict")
498        {
499            Some(Self::PhaseScopedStrictPragma)
500        } else if contains_diagnostic_phrase(
501            &msg_lower,
502            "inside a begin block does not enable warnings",
503        ) || contains_diagnostic_phrase(
504            &msg_lower,
505            "inside a phase block does not enable warnings",
506        ) {
507            Some(Self::PhaseScopedWarningsPragma)
508        } else if contains_diagnostic_phrase(&msg_lower, "use strict") {
509            Some(Self::MissingStrict)
510        } else if contains_diagnostic_phrase(&msg_lower, "use warnings") {
511            Some(Self::MissingWarnings)
512        } else if contains_diagnostic_phrase(&msg_lower, "unused variable")
513            || contains_diagnostic_phrase(&msg_lower, "never used")
514        {
515            Some(Self::UnusedVariable)
516        } else if is_undefined_variable_message(&msg_lower) {
517            Some(Self::UndefinedVariable)
518        } else if contains_diagnostic_phrase(&msg_lower, "bareword filehandle") {
519            Some(Self::BarewordFilehandle)
520        } else if contains_diagnostic_phrase(&msg_lower, "two-argument")
521            || contains_diagnostic_phrase(&msg_lower, "2-argument")
522            || contains_diagnostic_phrase(&msg_lower, "2-arg")
523        {
524            Some(Self::TwoArgOpen)
525        // Real Perl: "Subroutine foo redefined at file.pl line N."
526        } else if contains_diagnostic_phrase(&msg_lower, "subroutine")
527            && contains_diagnostic_phrase(&msg_lower, "redefined")
528        {
529            Some(Self::DuplicateSubroutine)
530        } else if contains_diagnostic_phrase(&msg_lower, "invalid prototype character")
531            || contains_diagnostic_phrase(&msg_lower, "illegal character in prototype")
532            // Real Perl: "Prototype mismatch: sub foo ($$) vs sub foo ($) at file.pl line N."
533            || contains_diagnostic_phrase(&msg_lower, "prototype mismatch")
534        {
535            Some(Self::InvalidPrototype)
536        } else if contains_diagnostic_phrase(&msg_lower, "parse error")
537            || contains_diagnostic_phrase(&msg_lower, "syntax error")
538        {
539            Some(Self::ParseError)
540        // Real Perl: "Use of uninitialized value $x in string at file.pl line N."
541        // Also: "Use of uninitialized value in concatenation (.) or string at file.pl line N."
542        } else if contains_diagnostic_phrase(&msg_lower, "uninitialized value") {
543            Some(Self::UninitializedVariable)
544        // Real Perl: "Can't locate Foo/Bar.pm in @INC (...) at file.pl line N."
545        } else if contains_diagnostic_phrase(&msg_lower, "can't locate")
546            && msg_lower.contains(".pm")
547        {
548            Some(Self::ModuleNotFound)
549        // Real Perl: `Bareword "foo" not allowed while 'strict subs' in use at file.pl line N.`
550        // Real Perl: "Unquoted string "foo" may clash with future reserved word at file.pl line N."
551        // Note: the actual bareword name is interpolated between "Bareword" and "not allowed",
552        // so we match on "strict subs" (unique to this warning) or "unquoted string".
553        } else if contains_diagnostic_phrase(&msg_lower, "strict subs")
554            || contains_diagnostic_phrase(&msg_lower, "unquoted string")
555            || contains_diagnostic_phrase(&msg_lower, "bareword not allowed")
556        {
557            Some(Self::UnquotedBareword)
558        // Real Perl: "defined(@array) is deprecated (it's always defined) at file.pl line N."
559        // Real Perl: "defined(%hash) is deprecated (it's always defined) at file.pl line N."
560        } else if msg_lower.contains("defined(")
561            && contains_diagnostic_phrase(&msg_lower, "deprecated")
562        {
563            Some(Self::DeprecatedDefined)
564        } else {
565            None
566        }
567    }
568
569    /// Try to parse a code string into a DiagnosticCode.
570    pub fn parse_code(code: &str) -> Option<Self> {
571        match code {
572            "PL001" => Some(Self::ParseError),
573            "PL002" => Some(Self::SyntaxError),
574            "PL003" => Some(Self::UnexpectedEof),
575            "PL100" => Some(Self::MissingStrict),
576            "PL101" => Some(Self::MissingWarnings),
577            "PL102" => Some(Self::UnusedVariable),
578            "PL103" => Some(Self::UndefinedVariable),
579            "PL104" => Some(Self::VariableShadowing),
580            "PL105" => Some(Self::VariableRedeclaration),
581            "PL106" => Some(Self::DuplicateParameter),
582            "PL107" => Some(Self::ParameterShadowsGlobal),
583            "PL108" => Some(Self::UnusedParameter),
584            "PL109" => Some(Self::UnquotedBareword),
585            "PL110" => Some(Self::UninitializedVariable),
586            "PL111" => Some(Self::MisspelledPragma),
587            "PL112" => Some(Self::CaptureVarWithoutRegexMatch),
588            "PL200" => Some(Self::MissingPackageDeclaration),
589            "PL201" => Some(Self::DuplicatePackage),
590            "PL300" => Some(Self::DuplicateSubroutine),
591            "PL301" => Some(Self::MissingReturn),
592            "PL302" => Some(Self::InvalidPrototype),
593            "PL303" => Some(Self::RoleConflict),
594            "PL304" => Some(Self::MissingPodCoverage),
595            "PL400" => Some(Self::BarewordFilehandle),
596            "PL401" => Some(Self::TwoArgOpen),
597            "PL402" => Some(Self::ImplicitReturn),
598            "PL403" => Some(Self::AssignmentInCondition),
599            "PL404" => Some(Self::NumericComparisonWithUndef),
600            "PL405" => Some(Self::PrintfFormatMismatch),
601            "PL406" => Some(Self::UnreachableCode),
602            "PL407" => Some(Self::EvalErrorFlow),
603            "PL408" => Some(Self::DuplicateHashKey),
604            "PL409" => Some(Self::GotoUndefinedLabel),
605            "PL410" => Some(Self::LoopControlUndefinedLabel),
606            "PL500" => Some(Self::DeprecatedDefined),
607            "PL501" => Some(Self::DeprecatedArrayBase),
608            "PL502" => Some(Self::PhaseScopedStrictPragma),
609            "PL503" => Some(Self::PhaseScopedWarningsPragma),
610            "PL600" => Some(Self::SecurityStringEval),
611            "PL601" => Some(Self::SecurityBacktickExec),
612            "PL602" => Some(Self::SecuritySignalHandler),
613            "PL603" => Some(Self::SecuritySystemCall),
614            "PL604" => Some(Self::SecurityExecCall),
615            "PL605" => Some(Self::SecurityPipeOpen),
616            "PL606" => Some(Self::SecurityReadpipe),
617            "PL700" => Some(Self::UnusedImport),
618            "PL701" => Some(Self::ModuleNotFound),
619            "PL800" => Some(Self::HeredocInFormat),
620            "PL801" => Some(Self::HeredocInBegin),
621            "PL802" => Some(Self::HeredocDynamicDelimiter),
622            "PL803" => Some(Self::HeredocInSourceFilter),
623            "PL804" => Some(Self::HeredocInRegexCode),
624            "PL805" => Some(Self::HeredocInEval),
625            "PL806" => Some(Self::HeredocTiedHandle),
626            "PL900" => Some(Self::VersionIncompatFeature),
627            "PC001" => Some(Self::CriticSeverity1),
628            "PC002" => Some(Self::CriticSeverity2),
629            "PC003" => Some(Self::CriticSeverity3),
630            "PC004" => Some(Self::CriticSeverity4),
631            "PC005" => Some(Self::CriticSeverity5),
632            _ => None,
633        }
634    }
635}