Skip to main content

perl_diagnostics_codes/
lib.rs

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