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
109    // Package/module (PL200-PL299)
110    /// Missing package declaration
111    MissingPackageDeclaration,
112    /// Duplicate package declaration
113    DuplicatePackage,
114
115    // Subroutine (PL300-PL399)
116    /// Duplicate subroutine definition
117    DuplicateSubroutine,
118    /// Missing explicit return statement
119    MissingReturn,
120
121    // Best practices (PL400-PL499)
122    /// Bareword filehandle usage
123    BarewordFilehandle,
124    /// Two-argument open() call
125    TwoArgOpen,
126    /// Implicit return value
127    ImplicitReturn,
128
129    // Perl::Critic violations (PC001-PC005)
130    /// Perl::Critic brutal (severity 1) violation
131    CriticSeverity1,
132    /// Perl::Critic cruel (severity 2) violation
133    CriticSeverity2,
134    /// Perl::Critic harsh (severity 3) violation
135    CriticSeverity3,
136    /// Perl::Critic stern (severity 4) violation
137    CriticSeverity4,
138    /// Perl::Critic gentle (severity 5) violation
139    CriticSeverity5,
140}
141
142impl DiagnosticCode {
143    /// Get the string representation of this code.
144    pub fn as_str(&self) -> &'static str {
145        match self {
146            DiagnosticCode::ParseError => "PL001",
147            DiagnosticCode::SyntaxError => "PL002",
148            DiagnosticCode::UnexpectedEof => "PL003",
149            DiagnosticCode::MissingStrict => "PL100",
150            DiagnosticCode::MissingWarnings => "PL101",
151            DiagnosticCode::UnusedVariable => "PL102",
152            DiagnosticCode::UndefinedVariable => "PL103",
153            DiagnosticCode::MissingPackageDeclaration => "PL200",
154            DiagnosticCode::DuplicatePackage => "PL201",
155            DiagnosticCode::DuplicateSubroutine => "PL300",
156            DiagnosticCode::MissingReturn => "PL301",
157            DiagnosticCode::BarewordFilehandle => "PL400",
158            DiagnosticCode::TwoArgOpen => "PL401",
159            DiagnosticCode::ImplicitReturn => "PL402",
160            DiagnosticCode::CriticSeverity1 => "PC001",
161            DiagnosticCode::CriticSeverity2 => "PC002",
162            DiagnosticCode::CriticSeverity3 => "PC003",
163            DiagnosticCode::CriticSeverity4 => "PC004",
164            DiagnosticCode::CriticSeverity5 => "PC005",
165        }
166    }
167
168    /// Get the documentation URL for this code, if available.
169    pub fn documentation_url(&self) -> Option<&'static str> {
170        match self {
171            DiagnosticCode::ParseError => Some("https://docs.perl-lsp.org/errors/PL001"),
172            DiagnosticCode::SyntaxError => Some("https://docs.perl-lsp.org/errors/PL002"),
173            DiagnosticCode::UnexpectedEof => Some("https://docs.perl-lsp.org/errors/PL003"),
174            DiagnosticCode::MissingStrict => Some("https://docs.perl-lsp.org/errors/PL100"),
175            DiagnosticCode::MissingWarnings => Some("https://docs.perl-lsp.org/errors/PL101"),
176            DiagnosticCode::UnusedVariable => Some("https://docs.perl-lsp.org/errors/PL102"),
177            DiagnosticCode::UndefinedVariable => Some("https://docs.perl-lsp.org/errors/PL103"),
178            DiagnosticCode::MissingPackageDeclaration => {
179                Some("https://docs.perl-lsp.org/errors/PL200")
180            }
181            DiagnosticCode::DuplicatePackage => Some("https://docs.perl-lsp.org/errors/PL201"),
182            DiagnosticCode::DuplicateSubroutine => Some("https://docs.perl-lsp.org/errors/PL300"),
183            DiagnosticCode::MissingReturn => Some("https://docs.perl-lsp.org/errors/PL301"),
184            DiagnosticCode::BarewordFilehandle => Some("https://docs.perl-lsp.org/errors/PL400"),
185            DiagnosticCode::TwoArgOpen => Some("https://docs.perl-lsp.org/errors/PL401"),
186            DiagnosticCode::ImplicitReturn => Some("https://docs.perl-lsp.org/errors/PL402"),
187            // Perl::Critic codes don't have centralized documentation
188            DiagnosticCode::CriticSeverity1
189            | DiagnosticCode::CriticSeverity2
190            | DiagnosticCode::CriticSeverity3
191            | DiagnosticCode::CriticSeverity4
192            | DiagnosticCode::CriticSeverity5 => None,
193        }
194    }
195
196    /// Get the default severity for this diagnostic code.
197    pub fn severity(&self) -> DiagnosticSeverity {
198        match self {
199            // Errors
200            DiagnosticCode::ParseError
201            | DiagnosticCode::SyntaxError
202            | DiagnosticCode::UnexpectedEof
203            | DiagnosticCode::UndefinedVariable => DiagnosticSeverity::Error,
204
205            // Warnings
206            DiagnosticCode::MissingStrict
207            | DiagnosticCode::MissingWarnings
208            | DiagnosticCode::UnusedVariable
209            | DiagnosticCode::MissingPackageDeclaration
210            | DiagnosticCode::DuplicatePackage
211            | DiagnosticCode::DuplicateSubroutine
212            | DiagnosticCode::MissingReturn
213            | DiagnosticCode::BarewordFilehandle
214            | DiagnosticCode::TwoArgOpen
215            | DiagnosticCode::ImplicitReturn
216            | DiagnosticCode::CriticSeverity1
217            | DiagnosticCode::CriticSeverity2 => DiagnosticSeverity::Warning,
218
219            // Information/Hints
220            DiagnosticCode::CriticSeverity3
221            | DiagnosticCode::CriticSeverity4
222            | DiagnosticCode::CriticSeverity5 => DiagnosticSeverity::Hint,
223        }
224    }
225
226    /// Get any diagnostic tags associated with this code.
227    pub fn tags(&self) -> &'static [DiagnosticTag] {
228        match self {
229            DiagnosticCode::UnusedVariable => &[DiagnosticTag::Unnecessary],
230            _ => &[],
231        }
232    }
233
234    /// Try to infer a diagnostic code from a message.
235    pub fn from_message(msg: &str) -> Option<DiagnosticCode> {
236        let msg_lower = msg.to_lowercase();
237        if msg_lower.contains("use strict") {
238            Some(DiagnosticCode::MissingStrict)
239        } else if msg_lower.contains("use warnings") {
240            Some(DiagnosticCode::MissingWarnings)
241        } else if msg_lower.contains("unused variable") || msg_lower.contains("never used") {
242            Some(DiagnosticCode::UnusedVariable)
243        } else if msg_lower.contains("undefined") || msg_lower.contains("not declared") {
244            Some(DiagnosticCode::UndefinedVariable)
245        } else if msg_lower.contains("bareword filehandle") {
246            Some(DiagnosticCode::BarewordFilehandle)
247        } else if msg_lower.contains("two-argument") || msg_lower.contains("2-arg") {
248            Some(DiagnosticCode::TwoArgOpen)
249        } else if msg_lower.contains("parse error") || msg_lower.contains("syntax error") {
250            Some(DiagnosticCode::ParseError)
251        } else {
252            None
253        }
254    }
255
256    /// Try to parse a code string into a DiagnosticCode.
257    pub fn parse_code(code: &str) -> Option<DiagnosticCode> {
258        match code {
259            "PL001" => Some(DiagnosticCode::ParseError),
260            "PL002" => Some(DiagnosticCode::SyntaxError),
261            "PL003" => Some(DiagnosticCode::UnexpectedEof),
262            "PL100" => Some(DiagnosticCode::MissingStrict),
263            "PL101" => Some(DiagnosticCode::MissingWarnings),
264            "PL102" => Some(DiagnosticCode::UnusedVariable),
265            "PL103" => Some(DiagnosticCode::UndefinedVariable),
266            "PL200" => Some(DiagnosticCode::MissingPackageDeclaration),
267            "PL201" => Some(DiagnosticCode::DuplicatePackage),
268            "PL300" => Some(DiagnosticCode::DuplicateSubroutine),
269            "PL301" => Some(DiagnosticCode::MissingReturn),
270            "PL400" => Some(DiagnosticCode::BarewordFilehandle),
271            "PL401" => Some(DiagnosticCode::TwoArgOpen),
272            "PL402" => Some(DiagnosticCode::ImplicitReturn),
273            "PC001" => Some(DiagnosticCode::CriticSeverity1),
274            "PC002" => Some(DiagnosticCode::CriticSeverity2),
275            "PC003" => Some(DiagnosticCode::CriticSeverity3),
276            "PC004" => Some(DiagnosticCode::CriticSeverity4),
277            "PC005" => Some(DiagnosticCode::CriticSeverity5),
278            _ => None,
279        }
280    }
281}
282
283impl fmt::Display for DiagnosticCode {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        write!(f, "{}", self.as_str())
286    }
287}
288
289/// Category of diagnostic codes.
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
291#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
292pub enum DiagnosticCategory {
293    /// Parser-related diagnostics (PL001-PL099)
294    Parser,
295    /// Strict/warnings pragmas (PL100-PL199)
296    StrictWarnings,
297    /// Package/module issues (PL200-PL299)
298    PackageModule,
299    /// Subroutine issues (PL300-PL399)
300    Subroutine,
301    /// Best practices (PL400-PL499)
302    BestPractices,
303    /// Perl::Critic violations (PC001-PC005)
304    PerlCritic,
305}
306
307impl DiagnosticCode {
308    /// Get the category of this diagnostic code.
309    pub fn category(&self) -> DiagnosticCategory {
310        match self {
311            DiagnosticCode::ParseError
312            | DiagnosticCode::SyntaxError
313            | DiagnosticCode::UnexpectedEof => DiagnosticCategory::Parser,
314
315            DiagnosticCode::MissingStrict
316            | DiagnosticCode::MissingWarnings
317            | DiagnosticCode::UnusedVariable
318            | DiagnosticCode::UndefinedVariable => DiagnosticCategory::StrictWarnings,
319
320            DiagnosticCode::MissingPackageDeclaration | DiagnosticCode::DuplicatePackage => {
321                DiagnosticCategory::PackageModule
322            }
323
324            DiagnosticCode::DuplicateSubroutine | DiagnosticCode::MissingReturn => {
325                DiagnosticCategory::Subroutine
326            }
327
328            DiagnosticCode::BarewordFilehandle
329            | DiagnosticCode::TwoArgOpen
330            | DiagnosticCode::ImplicitReturn => DiagnosticCategory::BestPractices,
331
332            DiagnosticCode::CriticSeverity1
333            | DiagnosticCode::CriticSeverity2
334            | DiagnosticCode::CriticSeverity3
335            | DiagnosticCode::CriticSeverity4
336            | DiagnosticCode::CriticSeverity5 => DiagnosticCategory::PerlCritic,
337        }
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn test_code_strings() {
347        assert_eq!(DiagnosticCode::ParseError.as_str(), "PL001");
348        assert_eq!(DiagnosticCode::MissingStrict.as_str(), "PL100");
349        assert_eq!(DiagnosticCode::CriticSeverity1.as_str(), "PC001");
350    }
351
352    #[test]
353    fn test_severity() {
354        assert_eq!(DiagnosticCode::ParseError.severity(), DiagnosticSeverity::Error);
355        assert_eq!(DiagnosticCode::UnusedVariable.severity(), DiagnosticSeverity::Warning);
356        assert_eq!(DiagnosticCode::CriticSeverity5.severity(), DiagnosticSeverity::Hint);
357    }
358
359    #[test]
360    fn test_from_message() {
361        assert_eq!(
362            DiagnosticCode::from_message("Missing 'use strict' pragma"),
363            Some(DiagnosticCode::MissingStrict)
364        );
365        assert_eq!(
366            DiagnosticCode::from_message("Unused variable $foo"),
367            Some(DiagnosticCode::UnusedVariable)
368        );
369    }
370
371    #[test]
372    fn test_from_str() {
373        assert_eq!(DiagnosticCode::parse_code("PL001"), Some(DiagnosticCode::ParseError));
374        assert_eq!(DiagnosticCode::parse_code("INVALID"), None);
375    }
376
377    #[test]
378    fn test_category() {
379        assert_eq!(DiagnosticCode::ParseError.category(), DiagnosticCategory::Parser);
380        assert_eq!(DiagnosticCode::MissingStrict.category(), DiagnosticCategory::StrictWarnings);
381        assert_eq!(DiagnosticCode::CriticSeverity1.category(), DiagnosticCategory::PerlCritic);
382    }
383
384    #[test]
385    fn test_tags() {
386        assert!(DiagnosticCode::UnusedVariable.tags().contains(&DiagnosticTag::Unnecessary));
387        assert!(DiagnosticCode::ParseError.tags().is_empty());
388    }
389}