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