Skip to main content

perl_diagnostics/codes/
mod.rs

1//! Diagnostic codes, severity levels, tags, and categories.
2//!
3//! This module contains the canonical definitions of all diagnostic codes used
4//! throughout the Perl LSP ecosystem. These codes are stable and can be
5//! referenced in documentation and error messages.
6//!
7//! # Code Ranges
8//!
9//! | Range       | Category                  |
10//! |-------------|---------------------------|
11//! | PL001-PL099 | Parser diagnostics        |
12//! | PL100-PL199 | Strict/warnings           |
13//! | PL200-PL299 | Package/module            |
14//! | PL300-PL399 | Subroutine                |
15//! | PL400-PL499 | Best practices            |
16//! | PL500-PL599 | Deprecated syntax         |
17//! | PL600-PL699 | Security                  |
18//! | PL700-PL799 | Import                    |
19//! | PL800-PL899 | Heredoc anti-patterns     |
20//! | PL900-PL999 | Version compatibility     |
21//! | PC001-PC005 | Perl::Critic violations   |
22
23use std::fmt;
24
25/// Severity level of a diagnostic.
26///
27/// Maps to LSP DiagnosticSeverity values (1=Error, 2=Warning, 3=Info, 4=Hint).
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30#[repr(u8)]
31pub enum DiagnosticSeverity {
32    /// Critical error that prevents parsing/execution.
33    #[default]
34    Error = 1,
35    /// Non-critical issue that should be addressed.
36    Warning = 2,
37    /// Informational message.
38    Information = 3,
39    /// Subtle suggestion or hint.
40    Hint = 4,
41}
42
43impl DiagnosticSeverity {
44    /// Get the LSP numeric value for this severity.
45    pub fn to_lsp_value(self) -> u8 {
46        self as u8
47    }
48}
49
50impl fmt::Display for DiagnosticSeverity {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::Error => write!(f, "error"),
54            Self::Warning => write!(f, "warning"),
55            Self::Information => write!(f, "info"),
56            Self::Hint => write!(f, "hint"),
57        }
58    }
59}
60
61/// Diagnostic tags for additional classification.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64pub enum DiagnosticTag {
65    /// Code that can be safely removed (unused variables, imports).
66    Unnecessary = 1,
67    /// Code using deprecated features.
68    Deprecated = 2,
69}
70
71impl DiagnosticTag {
72    /// Get the LSP numeric value for this tag.
73    pub fn to_lsp_value(self) -> u8 {
74        self as u8
75    }
76}
77
78impl fmt::Display for DiagnosticTag {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            Self::Unnecessary => write!(f, "unnecessary"),
82            Self::Deprecated => write!(f, "deprecated"),
83        }
84    }
85}
86
87/// Stable diagnostic codes for Perl LSP.
88///
89/// Each code has a fixed string representation and associated metadata.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub enum DiagnosticCode {
93    // Parser diagnostics (PL001-PL099)
94    /// General parse error
95    #[default]
96    ParseError,
97    /// Syntax error
98    SyntaxError,
99    /// Unexpected end-of-file
100    UnexpectedEof,
101
102    // Strict/warnings (PL100-PL199)
103    /// Missing 'use strict' pragma
104    MissingStrict,
105    /// Missing 'use warnings' pragma
106    MissingWarnings,
107    /// Unused variable
108    UnusedVariable,
109    /// Undefined variable
110    UndefinedVariable,
111    /// Variable shadowing an outer declaration
112    VariableShadowing,
113    /// Variable redeclared in the same scope
114    VariableRedeclaration,
115    /// Duplicate parameter in a subroutine signature
116    DuplicateParameter,
117    /// Subroutine parameter shadows a global variable
118    ParameterShadowsGlobal,
119    /// Subroutine parameter is declared but never used
120    UnusedParameter,
121    /// Bareword used where a quoted string is expected (under strict)
122    UnquotedBareword,
123    /// Variable used before being initialized
124    UninitializedVariable,
125    /// Pragma name appears to be misspelled
126    MisspelledPragma,
127    /// Capture variable ($1, $2, etc.) used without a preceding regex match in scope
128    CaptureVarWithoutRegexMatch,
129
130    // Package/module (PL200-PL299)
131    /// Missing package declaration
132    MissingPackageDeclaration,
133    /// Duplicate package declaration
134    DuplicatePackage,
135
136    // Subroutine (PL300-PL399)
137    /// Duplicate subroutine definition
138    DuplicateSubroutine,
139    /// Missing explicit return statement
140    MissingReturn,
141    /// Invalid character(s) in a subroutine prototype
142    ///
143    /// Perl only allows `$`, `@`, `%`, `&`, `*`, `\`, `;`, `+`, `_`, and
144    /// spaces in old-style prototypes.  Any other character triggers Perl's
145    /// "Illegal character in prototype" warning.
146    InvalidPrototype,
147    /// Same-file Moo/Moose roles provide conflicting methods
148    RoleConflict,
149    /// Exported subroutine lacks POD documentation
150    MissingPodCoverage,
151
152    // Best practices (PL400-PL499)
153    /// Bareword filehandle usage
154    BarewordFilehandle,
155    /// Two-argument open() call
156    TwoArgOpen,
157    /// Implicit return value
158    ImplicitReturn,
159    /// Assignment used where a comparison was likely intended
160    AssignmentInCondition,
161    /// Numeric comparison against a potentially undefined value
162    NumericComparisonWithUndef,
163    /// printf/sprintf format specifier count does not match argument count
164    PrintfFormatMismatch,
165    /// Statement that cannot be reached due to preceding unconditional exit
166    UnreachableCode,
167    /// `$@` / `$EVAL_ERROR` reads that are not paired with a nearby `eval`/`try`
168    EvalErrorFlow,
169    /// Duplicate key in a hash literal or hash reference constructor
170    DuplicateHashKey,
171    /// `goto LABEL` references a label that does not exist in this file
172    GotoUndefinedLabel,
173    /// `next`/`last`/`redo LABEL` references a label that does not exist in this file
174    LoopControlUndefinedLabel,
175
176    // Pragma pitfalls / deprecated syntax (PL500-PL599)
177    /// Use of deprecated defined(@array) / defined(%hash)
178    DeprecatedDefined,
179    /// Use of deprecated $[ array base variable
180    DeprecatedArrayBase,
181    /// `use strict` appears only inside a phase block and does not affect file scope
182    PhaseScopedStrictPragma,
183    /// `use warnings` appears only inside a phase block and does not affect file scope
184    PhaseScopedWarningsPragma,
185
186    // Security (PL600-PL699)
187    /// String eval is a security risk
188    SecurityStringEval,
189    /// Backtick/qx command execution detected
190    SecurityBacktickExec,
191    /// Global assignment to `$SIG{__DIE__}` / `$SIG{__WARN__}`
192    SecuritySignalHandler,
193    /// `system()` call executes shell commands
194    SecuritySystemCall,
195    /// `exec()` call replaces the current process with a shell command
196    SecurityExecCall,
197    /// Pipe-open `open(FH, "|-", ...)` / `open(FH, "-|", ...)` executes shell commands
198    SecurityPipeOpen,
199    /// `readpipe()` function call executes shell commands (equivalent to qx//)
200    SecurityReadpipe,
201
202    // Import (PL700-PL799)
203    /// Module appears to be unused
204    UnusedImport,
205    /// Module not found in workspace or configured include paths
206    ModuleNotFound,
207
208    // Heredoc anti-patterns (PL800-PL899)
209    /// Heredoc used inside a format block
210    HeredocInFormat,
211    /// Heredoc used inside a BEGIN block
212    HeredocInBegin,
213    /// Heredoc delimiter is dynamic (variable interpolation)
214    HeredocDynamicDelimiter,
215    /// Heredoc used inside a source filter
216    HeredocInSourceFilter,
217    /// Heredoc used inside a regex code block
218    HeredocInRegexCode,
219    /// Heredoc used inside string eval
220    HeredocInEval,
221    /// Heredoc used with a tied filehandle
222    HeredocTiedHandle,
223
224    // Version compatibility (PL900-PL999)
225    /// Use of a Perl feature not available in the declared version
226    VersionIncompatFeature,
227
228    // Perl::Critic violations (PC001-PC005)
229    /// Perl::Critic brutal (severity 1) violation
230    CriticSeverity1,
231    /// Perl::Critic cruel (severity 2) violation
232    CriticSeverity2,
233    /// Perl::Critic harsh (severity 3) violation
234    CriticSeverity3,
235    /// Perl::Critic stern (severity 4) violation
236    CriticSeverity4,
237    /// Perl::Critic gentle (severity 5) violation
238    CriticSeverity5,
239}
240
241impl DiagnosticCode {
242    /// Get the string representation of this code.
243    pub fn as_str(&self) -> &'static str {
244        match self {
245            Self::ParseError => "PL001",
246            Self::SyntaxError => "PL002",
247            Self::UnexpectedEof => "PL003",
248            Self::MissingStrict => "PL100",
249            Self::MissingWarnings => "PL101",
250            Self::UnusedVariable => "PL102",
251            Self::UndefinedVariable => "PL103",
252            Self::VariableShadowing => "PL104",
253            Self::VariableRedeclaration => "PL105",
254            Self::DuplicateParameter => "PL106",
255            Self::ParameterShadowsGlobal => "PL107",
256            Self::UnusedParameter => "PL108",
257            Self::UnquotedBareword => "PL109",
258            Self::UninitializedVariable => "PL110",
259            Self::MisspelledPragma => "PL111",
260            Self::CaptureVarWithoutRegexMatch => "PL112",
261            Self::MissingPackageDeclaration => "PL200",
262            Self::DuplicatePackage => "PL201",
263            Self::DuplicateSubroutine => "PL300",
264            Self::MissingReturn => "PL301",
265            Self::InvalidPrototype => "PL302",
266            Self::RoleConflict => "PL303",
267            Self::MissingPodCoverage => "PL304",
268            Self::BarewordFilehandle => "PL400",
269            Self::TwoArgOpen => "PL401",
270            Self::ImplicitReturn => "PL402",
271            Self::AssignmentInCondition => "PL403",
272            Self::NumericComparisonWithUndef => "PL404",
273            Self::PrintfFormatMismatch => "PL405",
274            Self::UnreachableCode => "PL406",
275            Self::EvalErrorFlow => "PL407",
276            Self::DuplicateHashKey => "PL408",
277            Self::GotoUndefinedLabel => "PL409",
278            Self::LoopControlUndefinedLabel => "PL410",
279            Self::DeprecatedDefined => "PL500",
280            Self::DeprecatedArrayBase => "PL501",
281            Self::PhaseScopedStrictPragma => "PL502",
282            Self::PhaseScopedWarningsPragma => "PL503",
283            Self::SecurityStringEval => "PL600",
284            Self::SecurityBacktickExec => "PL601",
285            Self::SecuritySignalHandler => "PL602",
286            Self::SecuritySystemCall => "PL603",
287            Self::SecurityExecCall => "PL604",
288            Self::SecurityPipeOpen => "PL605",
289            Self::SecurityReadpipe => "PL606",
290            Self::UnusedImport => "PL700",
291            Self::ModuleNotFound => "PL701",
292            Self::HeredocInFormat => "PL800",
293            Self::HeredocInBegin => "PL801",
294            Self::HeredocDynamicDelimiter => "PL802",
295            Self::HeredocInSourceFilter => "PL803",
296            Self::HeredocInRegexCode => "PL804",
297            Self::HeredocInEval => "PL805",
298            Self::HeredocTiedHandle => "PL806",
299            Self::VersionIncompatFeature => "PL900",
300            Self::CriticSeverity1 => "PC001",
301            Self::CriticSeverity2 => "PC002",
302            Self::CriticSeverity3 => "PC003",
303            Self::CriticSeverity4 => "PC004",
304            Self::CriticSeverity5 => "PC005",
305        }
306    }
307
308    /// Get the documentation URL for this code, if available.
309    pub fn documentation_url(&self) -> Option<&'static str> {
310        let code = self.as_str();
311        // Perl::Critic codes don't have centralized documentation
312        if code.starts_with("PC") {
313            return None;
314        }
315        // Build URL from stable code string for all PL codes
316        Some(match code {
317            "PL001" => "https://docs.perl-lsp.org/errors/PL001",
318            "PL002" => "https://docs.perl-lsp.org/errors/PL002",
319            "PL003" => "https://docs.perl-lsp.org/errors/PL003",
320            "PL100" => "https://docs.perl-lsp.org/errors/PL100",
321            "PL101" => "https://docs.perl-lsp.org/errors/PL101",
322            "PL102" => "https://docs.perl-lsp.org/errors/PL102",
323            "PL103" => "https://docs.perl-lsp.org/errors/PL103",
324            "PL104" => "https://docs.perl-lsp.org/errors/PL104",
325            "PL105" => "https://docs.perl-lsp.org/errors/PL105",
326            "PL106" => "https://docs.perl-lsp.org/errors/PL106",
327            "PL107" => "https://docs.perl-lsp.org/errors/PL107",
328            "PL108" => "https://docs.perl-lsp.org/errors/PL108",
329            "PL109" => "https://docs.perl-lsp.org/errors/PL109",
330            "PL110" => "https://docs.perl-lsp.org/errors/PL110",
331            "PL111" => "https://docs.perl-lsp.org/errors/PL111",
332            "PL112" => "https://docs.perl-lsp.org/errors/PL112",
333            "PL200" => "https://docs.perl-lsp.org/errors/PL200",
334            "PL201" => "https://docs.perl-lsp.org/errors/PL201",
335            "PL300" => "https://docs.perl-lsp.org/errors/PL300",
336            "PL301" => "https://docs.perl-lsp.org/errors/PL301",
337            "PL302" => "https://docs.perl-lsp.org/errors/PL302",
338            "PL303" => "https://docs.perl-lsp.org/errors/PL303",
339            "PL304" => "https://docs.perl-lsp.org/errors/PL304",
340            "PL400" => "https://docs.perl-lsp.org/errors/PL400",
341            "PL401" => "https://docs.perl-lsp.org/errors/PL401",
342            "PL402" => "https://docs.perl-lsp.org/errors/PL402",
343            "PL403" => "https://docs.perl-lsp.org/errors/PL403",
344            "PL404" => "https://docs.perl-lsp.org/errors/PL404",
345            "PL405" => "https://docs.perl-lsp.org/errors/PL405",
346            "PL406" => "https://docs.perl-lsp.org/errors/PL406",
347            "PL407" => "https://docs.perl-lsp.org/errors/PL407",
348            "PL408" => "https://docs.perl-lsp.org/errors/PL408",
349            "PL409" => "https://docs.perl-lsp.org/errors/PL409",
350            "PL410" => "https://docs.perl-lsp.org/errors/PL410",
351            "PL500" => "https://docs.perl-lsp.org/errors/PL500",
352            "PL501" => "https://docs.perl-lsp.org/errors/PL501",
353            "PL502" => "https://docs.perl-lsp.org/errors/PL502",
354            "PL503" => "https://docs.perl-lsp.org/errors/PL503",
355            "PL600" => "https://docs.perl-lsp.org/errors/PL600",
356            "PL601" => "https://docs.perl-lsp.org/errors/PL601",
357            "PL602" => "https://docs.perl-lsp.org/errors/PL602",
358            "PL603" => "https://docs.perl-lsp.org/errors/PL603",
359            "PL604" => "https://docs.perl-lsp.org/errors/PL604",
360            "PL605" => "https://docs.perl-lsp.org/errors/PL605",
361            "PL606" => "https://docs.perl-lsp.org/errors/PL606",
362            "PL700" => "https://docs.perl-lsp.org/errors/PL700",
363            "PL701" => "https://docs.perl-lsp.org/errors/PL701",
364            "PL800" => "https://docs.perl-lsp.org/errors/PL800",
365            "PL801" => "https://docs.perl-lsp.org/errors/PL801",
366            "PL802" => "https://docs.perl-lsp.org/errors/PL802",
367            "PL803" => "https://docs.perl-lsp.org/errors/PL803",
368            "PL804" => "https://docs.perl-lsp.org/errors/PL804",
369            "PL805" => "https://docs.perl-lsp.org/errors/PL805",
370            "PL806" => "https://docs.perl-lsp.org/errors/PL806",
371            "PL900" => "https://docs.perl-lsp.org/errors/PL900",
372            _ => return None,
373        })
374    }
375
376    /// Get the default severity for this diagnostic code.
377    pub fn severity(&self) -> DiagnosticSeverity {
378        match self {
379            // Errors
380            Self::ParseError
381            | Self::SyntaxError
382            | Self::UnexpectedEof
383            | Self::UndefinedVariable
384            | Self::VariableRedeclaration
385            | Self::DuplicateParameter
386            | Self::UnquotedBareword => DiagnosticSeverity::Error,
387
388            // Warnings
389            Self::MissingStrict
390            | Self::MissingWarnings
391            | Self::UnusedVariable
392            | Self::VariableShadowing
393            | Self::ParameterShadowsGlobal
394            | Self::UnusedParameter
395            | Self::UninitializedVariable
396            | Self::MisspelledPragma
397            | Self::MissingPackageDeclaration
398            | Self::DuplicatePackage
399            | Self::DuplicateSubroutine
400            | Self::MissingReturn
401            | Self::InvalidPrototype
402            | Self::RoleConflict
403            | Self::BarewordFilehandle
404            | Self::TwoArgOpen
405            | Self::ImplicitReturn
406            | Self::AssignmentInCondition
407            | Self::NumericComparisonWithUndef
408            | Self::PrintfFormatMismatch
409            | Self::DuplicateHashKey
410            | Self::GotoUndefinedLabel
411            | Self::LoopControlUndefinedLabel
412            | Self::DeprecatedDefined
413            | Self::DeprecatedArrayBase
414            | Self::PhaseScopedStrictPragma
415            | Self::PhaseScopedWarningsPragma
416            | Self::SecurityStringEval
417            | Self::SecurityBacktickExec
418            | Self::SecuritySignalHandler
419            | Self::SecuritySystemCall
420            | Self::SecurityExecCall
421            | Self::SecurityPipeOpen
422            | Self::SecurityReadpipe
423            | Self::ModuleNotFound
424            | Self::VersionIncompatFeature
425            | Self::EvalErrorFlow
426            | Self::CriticSeverity1
427            | Self::CriticSeverity2 => DiagnosticSeverity::Warning,
428
429            // Information
430            Self::CaptureVarWithoutRegexMatch
431            | Self::HeredocInFormat
432            | Self::HeredocInBegin
433            | Self::HeredocDynamicDelimiter
434            | Self::HeredocInSourceFilter
435            | Self::HeredocInRegexCode
436            | Self::HeredocInEval
437            | Self::HeredocTiedHandle => DiagnosticSeverity::Information,
438
439            // Hints
440            Self::MissingPodCoverage
441            | Self::UnusedImport
442            | Self::UnreachableCode
443            | Self::CriticSeverity3
444            | Self::CriticSeverity4
445            | Self::CriticSeverity5 => DiagnosticSeverity::Hint,
446        }
447    }
448
449    /// Get any diagnostic tags associated with this code.
450    pub fn tags(&self) -> &'static [DiagnosticTag] {
451        match self {
452            Self::UnusedVariable
453            | Self::UnusedParameter
454            | Self::UnusedImport
455            | Self::UnreachableCode => &[DiagnosticTag::Unnecessary],
456            Self::DeprecatedDefined | Self::DeprecatedArrayBase => &[DiagnosticTag::Deprecated],
457            _ => &[],
458        }
459    }
460
461    /// Return a human-readable context hint for this diagnostic code.
462    ///
463    /// Hints are short, actionable explanations that help users understand
464    /// what the diagnostic means and how to resolve it.  Perl::Critic codes
465    /// return `None` because their per-policy descriptions already serve this
466    /// purpose.
467    pub fn context_hint(&self) -> Option<&'static str> {
468        match self {
469            Self::ParseError => Some(
470                "The parser could not understand this code. \
471                Check for missing semicolons, unmatched brackets, or incorrect syntax.",
472            ),
473            Self::SyntaxError => Some(
474                "Perl syntax error. Check for typos, missing operators, \
475                or unbalanced parentheses near this location.",
476            ),
477            Self::UnexpectedEof => Some(
478                "The file ended unexpectedly. Check for unclosed blocks `{}`, \
479                heredocs, or multi-line strings.",
480            ),
481            Self::MissingStrict => Some(
482                "Add `use strict;` at the top of your file. \
483                Strict mode catches common variable mistakes at compile time.",
484            ),
485            Self::MissingWarnings => Some(
486                "Add `use warnings;` at the top of your file. \
487                Warnings highlight many common programming mistakes.",
488            ),
489            Self::UnusedVariable => Some(
490                "This variable is declared but never used. \
491                Remove it, or prefix with `_` (e.g., `$_unused`) to suppress.",
492            ),
493            Self::UndefinedVariable => Some(
494                "This variable was not declared with `my`, `our`, or `local`. \
495                Add `use strict;` and declare all variables before use.",
496            ),
497            Self::MissingPackageDeclaration => Some(
498                "This file has no `package` declaration. \
499                Add `package MyModule;` at the top for module files.",
500            ),
501            Self::DuplicatePackage => Some(
502                "This package name is declared more than once in the same file. \
503                Each package should appear once, or split into separate files.",
504            ),
505            Self::DuplicateSubroutine => Some(
506                "A subroutine with this name is defined more than once. \
507                The later definition silently replaces the earlier one.",
508            ),
509            Self::MissingReturn => Some(
510                "This subroutine has no explicit `return` statement. \
511                Add `return $value;` to make the return value clear.",
512            ),
513            Self::RoleConflict => Some(
514                "Two or more consumed Moo/Moose roles provide the same method. \
515                Define the method in the class or remove one of the conflicting roles.",
516            ),
517            Self::MissingPodCoverage => Some(
518                "This exported subroutine has no corresponding `=head2` or `=item` POD section. \
519                Add documentation so users of your module can discover its API.",
520            ),
521            Self::InvalidPrototype => Some(
522                "The prototype contains a character that Perl does not recognise. \
523                Valid prototype characters are: $, @, %, &, *, \\, ;, +, _ and spaces. \
524                See perlsub for the full prototype syntax.",
525            ),
526            Self::BarewordFilehandle => Some(
527                "Bareword filehandles (e.g., `open FH, ...`) are global and unsafe. \
528                Use a lexical filehandle instead: `open my $fh, '<', $file or die $!;`",
529            ),
530            Self::TwoArgOpen => Some(
531                "Two-argument `open()` is vulnerable to injection. \
532                Use three-argument form: `open my $fh, '<', $filename or die $!;`",
533            ),
534            Self::ImplicitReturn => Some(
535                "The return value of this expression is used implicitly. \
536                Make it explicit with `return` or assign it to a variable.",
537            ),
538            Self::AssignmentInCondition => Some(
539                "This looks like an assignment `=` inside a condition where \
540                a comparison `==` or `eq` was likely intended.",
541            ),
542            Self::NumericComparisonWithUndef => Some(
543                "Comparing a potentially undefined value with a numeric operator \
544                produces a warning at runtime. Check for definedness first with `defined()`.",
545            ),
546            Self::EvalErrorFlow => Some(
547                "Read `$@` or `$EVAL_ERROR` immediately after an `eval` or `try` \
548                block; intervening statements can clobber the exception state.",
549            ),
550            Self::UnreachableCode => Some(
551                "This statement cannot be executed because a preceding statement \
552                unconditionally exits (return, die, exit, croak). Remove or relocate it.",
553            ),
554            Self::DuplicateHashKey => Some(
555                "This hash key appears more than once in the same literal. \
556                Only the last value will be used; the earlier assignment is silently discarded.",
557            ),
558            Self::GotoUndefinedLabel => Some(
559                "This goto target label is not defined in the current file. \
560                Define the label or use a dynamic goto form only when the target is known at runtime.",
561            ),
562            Self::LoopControlUndefinedLabel => Some(
563                "This `next`, `last`, or `redo` references a label that is not defined in the current file. \
564                Add a matching `LABEL:` on an enclosing loop, or remove the label to target the innermost loop.",
565            ),
566            Self::PrintfFormatMismatch => Some(
567                "The number of format specifiers does not match the number of arguments. \
568                Each %s/%d/%f/etc. consumes one argument (except %% which consumes none).",
569            ),
570            Self::VariableShadowing => Some(
571                "This variable shadows an outer variable with the same name. \
572                Rename it to avoid confusion, or use the outer variable directly.",
573            ),
574            Self::VariableRedeclaration => Some(
575                "This variable is declared again in the same scope. \
576                Remove the duplicate `my` declaration and reuse the existing variable.",
577            ),
578            Self::DuplicateParameter => Some(
579                "This subroutine signature has a duplicate parameter name. \
580                Each parameter must have a unique name.",
581            ),
582            Self::ParameterShadowsGlobal => Some(
583                "This subroutine parameter shadows a global (`our`) variable. \
584                Rename the parameter to avoid confusion with the global.",
585            ),
586            Self::UnusedParameter => Some(
587                "This subroutine parameter is declared but never used. \
588                Remove it or prefix with `_` (e.g., `$_unused`) to suppress.",
589            ),
590            Self::UnquotedBareword => Some(
591                "This bareword is used where a quoted string is expected. \
592                Under `use strict`, barewords are not allowed. Quote it: `'word'`.",
593            ),
594            Self::UninitializedVariable => Some(
595                "This variable is used before being assigned a value. \
596                Initialize it before use to avoid `Use of uninitialized value` warnings.",
597            ),
598            Self::MisspelledPragma => Some(
599                "This pragma name appears to be misspelled. \
600                Check the spelling and ensure the module is installed.",
601            ),
602            Self::CaptureVarWithoutRegexMatch => Some(
603                "Capture variables ($1, $2, etc.) are only meaningful after a successful regex match. \
604                Perform a regex match with =~ /.../ before using $1 or $2.",
605            ),
606            Self::DeprecatedDefined => Some(
607                "`defined(@array)` and `defined(%hash)` are deprecated since Perl 5.6. \
608                Use `@array` or `%hash` directly in boolean context instead.",
609            ),
610            Self::DeprecatedArrayBase => Some(
611                "The `$[` variable is deprecated. Array indices always start at 0 \
612                in modern Perl. Remove any assignment to `$[`.",
613            ),
614            Self::PhaseScopedStrictPragma => Some(
615                "`use strict` inside a phase block only applies inside that block. \
616                Move `use strict;` to file scope for file-wide strict enforcement.",
617            ),
618            Self::PhaseScopedWarningsPragma => Some(
619                "`use warnings` inside a phase block only applies inside that block. \
620                Move `use warnings;` to file scope for file-wide warnings coverage.",
621            ),
622            Self::SecurityStringEval => Some(
623                "String `eval` executes arbitrary code and is a security risk. \
624                Use block eval `eval { ... }` or safer alternatives.",
625            ),
626            Self::SecurityBacktickExec => Some(
627                "Backticks/`qx()` execute shell commands and can be exploited. \
628                Use `system()` with a list form or IPC::Run for safer execution.",
629            ),
630            Self::SecuritySignalHandler => Some(
631                "Assigning to $SIG{__DIE__} or $SIG{__WARN__} globally changes exception \
632                and warning handling for the whole process. Use `local` to scope the handler.",
633            ),
634            Self::SecuritySystemCall => Some(
635                "`system()` executes a shell command. If the arguments include user input, \
636                use the list form `system($cmd, @args)` to avoid shell injection.",
637            ),
638            Self::SecurityExecCall => Some(
639                "`exec()` replaces the current process with a shell command. If arguments \
640                include user input, use the list form `exec($cmd, @args)` to avoid shell injection.",
641            ),
642            Self::SecurityPipeOpen => Some(
643                "Pipe-open executes a shell command. Pass a list to `open` for safe argument \
644                handling: `open(my $fh, '-|', $cmd, @args)` instead of `open(my $fh, \"|$cmd\")`.",
645            ),
646            Self::SecurityReadpipe => Some(
647                "`readpipe()` executes a shell command (equivalent to backticks/qx//). \
648                Use `open(my $fh, '-|', $cmd, @args)` or IPC::Run for safer command execution.",
649            ),
650            Self::UnusedImport => Some(
651                "This module is imported but none of its exports appear to be used. \
652                Remove the `use` statement to reduce unnecessary dependencies.",
653            ),
654            Self::ModuleNotFound => Some(
655                "This module was not found in the workspace or configured include paths. \
656                Install it with cpanm or add it to cpanfile.",
657            ),
658            Self::HeredocInFormat => Some(
659                "Heredocs inside `format` blocks can cause subtle parsing issues. \
660                Extract the heredoc content into a variable before the format.",
661            ),
662            Self::HeredocInBegin => Some(
663                "Heredocs inside `BEGIN` blocks may behave unexpectedly due to \
664                compile-time execution. Move the heredoc outside the BEGIN block.",
665            ),
666            Self::HeredocDynamicDelimiter => Some(
667                "The heredoc delimiter contains a variable, making it dynamic. \
668                Use a static delimiter string to avoid surprising behavior.",
669            ),
670            Self::HeredocInSourceFilter => Some(
671                "Heredocs inside source-filtered code may be mangled by the filter. \
672                Avoid combining heredocs with source filters.",
673            ),
674            Self::HeredocInRegexCode => Some(
675                "Heredocs inside regex code blocks `(?{ ... })` can cause parsing failures. \
676                Move the heredoc content outside the regex.",
677            ),
678            Self::HeredocInEval => Some(
679                "Heredocs inside string `eval` are fragile and error-prone. \
680                Use a variable or block eval instead.",
681            ),
682            Self::HeredocTiedHandle => Some(
683                "Heredocs written to tied filehandles may not behave as expected. \
684                The tie interface may not handle multi-line heredoc output correctly.",
685            ),
686            Self::VersionIncompatFeature => Some(
687                "This Perl feature requires a newer Perl version than declared. \
688                Update 'use vN.NN' or 'use feature' to enable it.",
689            ),
690            // Perl::Critic codes carry per-policy descriptions; no generic hint needed.
691            Self::CriticSeverity1
692            | Self::CriticSeverity2
693            | Self::CriticSeverity3
694            | Self::CriticSeverity4
695            | Self::CriticSeverity5 => None,
696        }
697    }
698
699    /// Try to infer a diagnostic code from a message.
700    pub fn from_message(msg: &str) -> Option<Self> {
701        let msg_lower = msg.to_lowercase();
702        if msg_lower.contains("inside a begin block does not enable strict")
703            || msg_lower.contains("inside a phase block does not enable strict")
704        {
705            Some(Self::PhaseScopedStrictPragma)
706        } else if msg_lower.contains("inside a begin block does not enable warnings")
707            || msg_lower.contains("inside a phase block does not enable warnings")
708        {
709            Some(Self::PhaseScopedWarningsPragma)
710        } else if msg_lower.contains("use strict") {
711            Some(Self::MissingStrict)
712        } else if msg_lower.contains("use warnings") {
713            Some(Self::MissingWarnings)
714        } else if msg_lower.contains("unused variable") || msg_lower.contains("never used") {
715            Some(Self::UnusedVariable)
716        } else if msg_lower.contains("undefined") || msg_lower.contains("not declared") {
717            Some(Self::UndefinedVariable)
718        } else if msg_lower.contains("bareword filehandle") {
719            Some(Self::BarewordFilehandle)
720        } else if msg_lower.contains("two-argument") || msg_lower.contains("2-arg") {
721            Some(Self::TwoArgOpen)
722        } else if msg_lower.contains("invalid prototype character")
723            || msg_lower.contains("illegal character in prototype")
724        {
725            Some(Self::InvalidPrototype)
726        } else if msg_lower.contains("parse error") || msg_lower.contains("syntax error") {
727            Some(Self::ParseError)
728        } else {
729            None
730        }
731    }
732
733    /// Try to parse a code string into a DiagnosticCode.
734    pub fn parse_code(code: &str) -> Option<Self> {
735        match code {
736            "PL001" => Some(Self::ParseError),
737            "PL002" => Some(Self::SyntaxError),
738            "PL003" => Some(Self::UnexpectedEof),
739            "PL100" => Some(Self::MissingStrict),
740            "PL101" => Some(Self::MissingWarnings),
741            "PL102" => Some(Self::UnusedVariable),
742            "PL103" => Some(Self::UndefinedVariable),
743            "PL104" => Some(Self::VariableShadowing),
744            "PL105" => Some(Self::VariableRedeclaration),
745            "PL106" => Some(Self::DuplicateParameter),
746            "PL107" => Some(Self::ParameterShadowsGlobal),
747            "PL108" => Some(Self::UnusedParameter),
748            "PL109" => Some(Self::UnquotedBareword),
749            "PL110" => Some(Self::UninitializedVariable),
750            "PL111" => Some(Self::MisspelledPragma),
751            "PL112" => Some(Self::CaptureVarWithoutRegexMatch),
752            "PL200" => Some(Self::MissingPackageDeclaration),
753            "PL201" => Some(Self::DuplicatePackage),
754            "PL300" => Some(Self::DuplicateSubroutine),
755            "PL301" => Some(Self::MissingReturn),
756            "PL302" => Some(Self::InvalidPrototype),
757            "PL303" => Some(Self::RoleConflict),
758            "PL304" => Some(Self::MissingPodCoverage),
759            "PL400" => Some(Self::BarewordFilehandle),
760            "PL401" => Some(Self::TwoArgOpen),
761            "PL402" => Some(Self::ImplicitReturn),
762            "PL403" => Some(Self::AssignmentInCondition),
763            "PL404" => Some(Self::NumericComparisonWithUndef),
764            "PL405" => Some(Self::PrintfFormatMismatch),
765            "PL406" => Some(Self::UnreachableCode),
766            "PL407" => Some(Self::EvalErrorFlow),
767            "PL408" => Some(Self::DuplicateHashKey),
768            "PL409" => Some(Self::GotoUndefinedLabel),
769            "PL410" => Some(Self::LoopControlUndefinedLabel),
770            "PL500" => Some(Self::DeprecatedDefined),
771            "PL501" => Some(Self::DeprecatedArrayBase),
772            "PL502" => Some(Self::PhaseScopedStrictPragma),
773            "PL503" => Some(Self::PhaseScopedWarningsPragma),
774            "PL600" => Some(Self::SecurityStringEval),
775            "PL601" => Some(Self::SecurityBacktickExec),
776            "PL602" => Some(Self::SecuritySignalHandler),
777            "PL603" => Some(Self::SecuritySystemCall),
778            "PL604" => Some(Self::SecurityExecCall),
779            "PL605" => Some(Self::SecurityPipeOpen),
780            "PL606" => Some(Self::SecurityReadpipe),
781            "PL700" => Some(Self::UnusedImport),
782            "PL701" => Some(Self::ModuleNotFound),
783            "PL800" => Some(Self::HeredocInFormat),
784            "PL801" => Some(Self::HeredocInBegin),
785            "PL802" => Some(Self::HeredocDynamicDelimiter),
786            "PL803" => Some(Self::HeredocInSourceFilter),
787            "PL804" => Some(Self::HeredocInRegexCode),
788            "PL805" => Some(Self::HeredocInEval),
789            "PL806" => Some(Self::HeredocTiedHandle),
790            "PL900" => Some(Self::VersionIncompatFeature),
791            "PC001" => Some(Self::CriticSeverity1),
792            "PC002" => Some(Self::CriticSeverity2),
793            "PC003" => Some(Self::CriticSeverity3),
794            "PC004" => Some(Self::CriticSeverity4),
795            "PC005" => Some(Self::CriticSeverity5),
796            _ => None,
797        }
798    }
799
800    /// Get the category of this diagnostic code.
801    pub fn category(&self) -> DiagnosticCategory {
802        match self {
803            Self::ParseError | Self::SyntaxError | Self::UnexpectedEof => {
804                DiagnosticCategory::Parser
805            }
806
807            Self::MissingStrict
808            | Self::MissingWarnings
809            | Self::UnusedVariable
810            | Self::UndefinedVariable
811            | Self::VariableShadowing
812            | Self::VariableRedeclaration
813            | Self::DuplicateParameter
814            | Self::ParameterShadowsGlobal
815            | Self::UnusedParameter
816            | Self::UnquotedBareword
817            | Self::UninitializedVariable
818            | Self::MisspelledPragma
819            | Self::CaptureVarWithoutRegexMatch
820            | Self::PhaseScopedStrictPragma
821            | Self::PhaseScopedWarningsPragma => DiagnosticCategory::StrictWarnings,
822
823            Self::MissingPackageDeclaration | Self::DuplicatePackage => {
824                DiagnosticCategory::PackageModule
825            }
826
827            Self::DuplicateSubroutine
828            | Self::MissingReturn
829            | Self::InvalidPrototype
830            | Self::RoleConflict
831            | Self::MissingPodCoverage => DiagnosticCategory::Subroutine,
832
833            Self::BarewordFilehandle
834            | Self::TwoArgOpen
835            | Self::ImplicitReturn
836            | Self::AssignmentInCondition
837            | Self::NumericComparisonWithUndef
838            | Self::PrintfFormatMismatch
839            | Self::UnreachableCode
840            | Self::EvalErrorFlow
841            | Self::DuplicateHashKey
842            | Self::GotoUndefinedLabel
843            | Self::LoopControlUndefinedLabel
844            | Self::VersionIncompatFeature => DiagnosticCategory::BestPractices,
845
846            Self::DeprecatedDefined | Self::DeprecatedArrayBase => DiagnosticCategory::Deprecated,
847
848            Self::SecurityStringEval
849            | Self::SecurityBacktickExec
850            | Self::SecuritySignalHandler
851            | Self::SecuritySystemCall
852            | Self::SecurityExecCall
853            | Self::SecurityPipeOpen
854            | Self::SecurityReadpipe => DiagnosticCategory::Security,
855
856            Self::UnusedImport | Self::ModuleNotFound => DiagnosticCategory::Import,
857
858            Self::HeredocInFormat
859            | Self::HeredocInBegin
860            | Self::HeredocDynamicDelimiter
861            | Self::HeredocInSourceFilter
862            | Self::HeredocInRegexCode
863            | Self::HeredocInEval
864            | Self::HeredocTiedHandle => DiagnosticCategory::Heredoc,
865
866            Self::CriticSeverity1
867            | Self::CriticSeverity2
868            | Self::CriticSeverity3
869            | Self::CriticSeverity4
870            | Self::CriticSeverity5 => DiagnosticCategory::PerlCritic,
871        }
872    }
873}
874
875impl fmt::Display for DiagnosticCode {
876    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
877        write!(f, "{}", self.as_str())
878    }
879}
880
881/// Category of diagnostic codes.
882#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
883#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
884pub enum DiagnosticCategory {
885    /// Parser-related diagnostics (PL001-PL099)
886    Parser,
887    /// Strict/warnings pragmas and scope analysis (PL100-PL199)
888    StrictWarnings,
889    /// Package/module issues (PL200-PL299)
890    PackageModule,
891    /// Subroutine issues (PL300-PL399)
892    Subroutine,
893    /// Best practices and common mistakes (PL400-PL499)
894    BestPractices,
895    /// Deprecated syntax (PL500-PL599)
896    Deprecated,
897    /// Security anti-patterns (PL600-PL699)
898    Security,
899    /// Import/use diagnostics (PL700-PL799)
900    Import,
901    /// Heredoc anti-patterns (PL800-PL899)
902    Heredoc,
903    /// Perl::Critic violations (PC001-PC005)
904    PerlCritic,
905}
906
907impl fmt::Display for DiagnosticCategory {
908    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
909        match self {
910            Self::Parser => write!(f, "Parser"),
911            Self::StrictWarnings => write!(f, "Strict/Warnings"),
912            Self::PackageModule => write!(f, "Package/Module"),
913            Self::Subroutine => write!(f, "Subroutine"),
914            Self::BestPractices => write!(f, "Best Practices"),
915            Self::Deprecated => write!(f, "Deprecated"),
916            Self::Security => write!(f, "Security"),
917            Self::Import => write!(f, "Import"),
918            Self::Heredoc => write!(f, "Heredoc"),
919            Self::PerlCritic => write!(f, "Perl::Critic"),
920        }
921    }
922}