perl-diagnostics 0.16.0

Unified diagnostic codes, types, and catalog for Perl LSP.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
use super::{DiagnosticCode, DiagnosticSeverity, DiagnosticTag};

fn is_undefined_variable_message(msg_lower: &str) -> bool {
    contains_diagnostic_phrase(msg_lower, "not declared")
        || contains_diagnostic_phrase(msg_lower, "undefined variable")
        || (contains_diagnostic_phrase(msg_lower, "undefined")
            && (contains_diagnostic_phrase(msg_lower, "variable")
                || contains_diagnostic_phrase(msg_lower, "global symbol")))
        // Real Perl: "Global symbol "$x" requires explicit package name at file.pl line N."
        || contains_diagnostic_phrase(msg_lower, "requires explicit package name")
}

fn contains_diagnostic_phrase(haystack: &str, needle: &str) -> bool {
    haystack.match_indices(needle).any(|(start, matched)| {
        let end = start + matched.len();
        has_phrase_boundary(haystack, start, end)
    })
}

fn has_phrase_boundary(haystack: &str, start: usize, end: usize) -> bool {
    let bytes = haystack.as_bytes();
    let before_is_word = start
        .checked_sub(1)
        .and_then(|index| bytes.get(index))
        .is_some_and(|byte| is_phrase_word_byte(*byte));
    let after_is_word = bytes.get(end).is_some_and(|byte| is_phrase_word_byte(*byte));

    !before_is_word && !after_is_word
}

fn is_phrase_word_byte(byte: u8) -> bool {
    byte.is_ascii_alphanumeric() || byte == b'_'
}

impl DiagnosticCode {
    /// Get the string representation of this code.
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::ParseError => "PL001",
            Self::SyntaxError => "PL002",
            Self::UnexpectedEof => "PL003",
            Self::MissingStrict => "PL100",
            Self::MissingWarnings => "PL101",
            Self::UnusedVariable => "PL102",
            Self::UndefinedVariable => "PL103",
            Self::VariableShadowing => "PL104",
            Self::VariableRedeclaration => "PL105",
            Self::DuplicateParameter => "PL106",
            Self::ParameterShadowsGlobal => "PL107",
            Self::UnusedParameter => "PL108",
            Self::UnquotedBareword => "PL109",
            Self::UninitializedVariable => "PL110",
            Self::MisspelledPragma => "PL111",
            Self::CaptureVarWithoutRegexMatch => "PL112",
            Self::MissingPackageDeclaration => "PL200",
            Self::DuplicatePackage => "PL201",
            Self::DuplicateSubroutine => "PL300",
            Self::MissingReturn => "PL301",
            Self::InvalidPrototype => "PL302",
            Self::RoleConflict => "PL303",
            Self::MissingPodCoverage => "PL304",
            Self::BarewordFilehandle => "PL400",
            Self::TwoArgOpen => "PL401",
            Self::ImplicitReturn => "PL402",
            Self::AssignmentInCondition => "PL403",
            Self::NumericComparisonWithUndef => "PL404",
            Self::PrintfFormatMismatch => "PL405",
            Self::UnreachableCode => "PL406",
            Self::EvalErrorFlow => "PL407",
            Self::DuplicateHashKey => "PL408",
            Self::GotoUndefinedLabel => "PL409",
            Self::LoopControlUndefinedLabel => "PL410",
            Self::DeprecatedDefined => "PL500",
            Self::DeprecatedArrayBase => "PL501",
            Self::PhaseScopedStrictPragma => "PL502",
            Self::PhaseScopedWarningsPragma => "PL503",
            Self::SecurityStringEval => "PL600",
            Self::SecurityBacktickExec => "PL601",
            Self::SecuritySignalHandler => "PL602",
            Self::SecuritySystemCall => "PL603",
            Self::SecurityExecCall => "PL604",
            Self::SecurityPipeOpen => "PL605",
            Self::SecurityReadpipe => "PL606",
            Self::UnusedImport => "PL700",
            Self::ModuleNotFound => "PL701",
            Self::HeredocInFormat => "PL800",
            Self::HeredocInBegin => "PL801",
            Self::HeredocDynamicDelimiter => "PL802",
            Self::HeredocInSourceFilter => "PL803",
            Self::HeredocInRegexCode => "PL804",
            Self::HeredocInEval => "PL805",
            Self::HeredocTiedHandle => "PL806",
            Self::VersionIncompatFeature => "PL900",
            Self::CriticSeverity1 => "PC001",
            Self::CriticSeverity2 => "PC002",
            Self::CriticSeverity3 => "PC003",
            Self::CriticSeverity4 => "PC004",
            Self::CriticSeverity5 => "PC005",
        }
    }

    /// Get the documentation URL for this code, if available.
    pub fn documentation_url(&self) -> Option<&'static str> {
        let code = self.as_str();
        // Perl::Critic codes don't have centralized documentation
        if code.starts_with("PC") {
            return None;
        }
        // Build URL from stable code string for all PL codes
        Some(match code {
            "PL001" => "https://docs.perl-lsp.org/errors/PL001",
            "PL002" => "https://docs.perl-lsp.org/errors/PL002",
            "PL003" => "https://docs.perl-lsp.org/errors/PL003",
            "PL100" => "https://docs.perl-lsp.org/errors/PL100",
            "PL101" => "https://docs.perl-lsp.org/errors/PL101",
            "PL102" => "https://docs.perl-lsp.org/errors/PL102",
            "PL103" => "https://docs.perl-lsp.org/errors/PL103",
            "PL104" => "https://docs.perl-lsp.org/errors/PL104",
            "PL105" => "https://docs.perl-lsp.org/errors/PL105",
            "PL106" => "https://docs.perl-lsp.org/errors/PL106",
            "PL107" => "https://docs.perl-lsp.org/errors/PL107",
            "PL108" => "https://docs.perl-lsp.org/errors/PL108",
            "PL109" => "https://docs.perl-lsp.org/errors/PL109",
            "PL110" => "https://docs.perl-lsp.org/errors/PL110",
            "PL111" => "https://docs.perl-lsp.org/errors/PL111",
            "PL112" => "https://docs.perl-lsp.org/errors/PL112",
            "PL200" => "https://docs.perl-lsp.org/errors/PL200",
            "PL201" => "https://docs.perl-lsp.org/errors/PL201",
            "PL300" => "https://docs.perl-lsp.org/errors/PL300",
            "PL301" => "https://docs.perl-lsp.org/errors/PL301",
            "PL302" => "https://docs.perl-lsp.org/errors/PL302",
            "PL303" => "https://docs.perl-lsp.org/errors/PL303",
            "PL304" => "https://docs.perl-lsp.org/errors/PL304",
            "PL400" => "https://docs.perl-lsp.org/errors/PL400",
            "PL401" => "https://docs.perl-lsp.org/errors/PL401",
            "PL402" => "https://docs.perl-lsp.org/errors/PL402",
            "PL403" => "https://docs.perl-lsp.org/errors/PL403",
            "PL404" => "https://docs.perl-lsp.org/errors/PL404",
            "PL405" => "https://docs.perl-lsp.org/errors/PL405",
            "PL406" => "https://docs.perl-lsp.org/errors/PL406",
            "PL407" => "https://docs.perl-lsp.org/errors/PL407",
            "PL408" => "https://docs.perl-lsp.org/errors/PL408",
            "PL409" => "https://docs.perl-lsp.org/errors/PL409",
            "PL410" => "https://docs.perl-lsp.org/errors/PL410",
            "PL500" => "https://docs.perl-lsp.org/errors/PL500",
            "PL501" => "https://docs.perl-lsp.org/errors/PL501",
            "PL502" => "https://docs.perl-lsp.org/errors/PL502",
            "PL503" => "https://docs.perl-lsp.org/errors/PL503",
            "PL600" => "https://docs.perl-lsp.org/errors/PL600",
            "PL601" => "https://docs.perl-lsp.org/errors/PL601",
            "PL602" => "https://docs.perl-lsp.org/errors/PL602",
            "PL603" => "https://docs.perl-lsp.org/errors/PL603",
            "PL604" => "https://docs.perl-lsp.org/errors/PL604",
            "PL605" => "https://docs.perl-lsp.org/errors/PL605",
            "PL606" => "https://docs.perl-lsp.org/errors/PL606",
            "PL700" => "https://docs.perl-lsp.org/errors/PL700",
            "PL701" => "https://docs.perl-lsp.org/errors/PL701",
            "PL800" => "https://docs.perl-lsp.org/errors/PL800",
            "PL801" => "https://docs.perl-lsp.org/errors/PL801",
            "PL802" => "https://docs.perl-lsp.org/errors/PL802",
            "PL803" => "https://docs.perl-lsp.org/errors/PL803",
            "PL804" => "https://docs.perl-lsp.org/errors/PL804",
            "PL805" => "https://docs.perl-lsp.org/errors/PL805",
            "PL806" => "https://docs.perl-lsp.org/errors/PL806",
            "PL900" => "https://docs.perl-lsp.org/errors/PL900",
            _ => return None,
        })
    }

    /// Get the default severity for this diagnostic code.
    pub fn severity(&self) -> DiagnosticSeverity {
        match self {
            // Errors
            Self::ParseError
            | Self::SyntaxError
            | Self::UnexpectedEof
            | Self::UndefinedVariable
            | Self::VariableRedeclaration
            | Self::DuplicateParameter
            | Self::UnquotedBareword => DiagnosticSeverity::Error,

            // Warnings
            Self::MissingStrict
            | Self::MissingWarnings
            | Self::UnusedVariable
            | Self::VariableShadowing
            | Self::ParameterShadowsGlobal
            | Self::UnusedParameter
            | Self::UninitializedVariable
            | Self::MisspelledPragma
            | Self::MissingPackageDeclaration
            | Self::DuplicatePackage
            | Self::DuplicateSubroutine
            | Self::MissingReturn
            | Self::InvalidPrototype
            | Self::RoleConflict
            | Self::BarewordFilehandle
            | Self::TwoArgOpen
            | Self::ImplicitReturn
            | Self::AssignmentInCondition
            | Self::NumericComparisonWithUndef
            | Self::PrintfFormatMismatch
            | Self::DuplicateHashKey
            | Self::GotoUndefinedLabel
            | Self::LoopControlUndefinedLabel
            | Self::DeprecatedDefined
            | Self::DeprecatedArrayBase
            | Self::PhaseScopedStrictPragma
            | Self::PhaseScopedWarningsPragma
            | Self::SecurityStringEval
            | Self::SecurityBacktickExec
            | Self::SecuritySignalHandler
            | Self::SecuritySystemCall
            | Self::SecurityExecCall
            | Self::SecurityPipeOpen
            | Self::SecurityReadpipe
            | Self::ModuleNotFound
            | Self::VersionIncompatFeature
            | Self::EvalErrorFlow
            | Self::CriticSeverity1
            | Self::CriticSeverity2 => DiagnosticSeverity::Warning,

            // Information
            Self::CaptureVarWithoutRegexMatch
            | Self::HeredocInFormat
            | Self::HeredocInBegin
            | Self::HeredocDynamicDelimiter
            | Self::HeredocInSourceFilter
            | Self::HeredocInRegexCode
            | Self::HeredocInEval
            | Self::HeredocTiedHandle => DiagnosticSeverity::Information,

            // Hints
            Self::MissingPodCoverage
            | Self::UnusedImport
            | Self::UnreachableCode
            | Self::CriticSeverity3
            | Self::CriticSeverity4
            | Self::CriticSeverity5 => DiagnosticSeverity::Hint,
        }
    }

    /// Get any diagnostic tags associated with this code.
    pub fn tags(&self) -> &'static [DiagnosticTag] {
        match self {
            Self::UnusedVariable
            | Self::UnusedParameter
            | Self::UnusedImport
            | Self::UnreachableCode => &[DiagnosticTag::Unnecessary],
            Self::DeprecatedDefined | Self::DeprecatedArrayBase => &[DiagnosticTag::Deprecated],
            _ => &[],
        }
    }

    /// Return a human-readable context hint for this diagnostic code.
    ///
    /// Hints are short, actionable explanations that help users understand
    /// what the diagnostic means and how to resolve it.  Perl::Critic codes
    /// return `None` because their per-policy descriptions already serve this
    /// purpose.
    pub fn context_hint(&self) -> Option<&'static str> {
        match self {
            Self::ParseError => Some(
                "The parser could not understand this code. \
                Check for missing semicolons, unmatched brackets, or incorrect syntax.",
            ),
            Self::SyntaxError => Some(
                "Perl syntax error. Check for typos, missing operators, \
                or unbalanced parentheses near this location.",
            ),
            Self::UnexpectedEof => Some(
                "The file ended unexpectedly. Check for unclosed blocks `{}`, \
                heredocs, or multi-line strings.",
            ),
            Self::MissingStrict => Some(
                "Add `use strict;` at the top of your file. \
                Strict mode catches common variable mistakes at compile time.",
            ),
            Self::MissingWarnings => Some(
                "Add `use warnings;` at the top of your file. \
                Warnings highlight many common programming mistakes.",
            ),
            Self::UnusedVariable => Some(
                "This variable is declared but never used. \
                Remove it, or prefix with `_` (e.g., `$_unused`) to suppress.",
            ),
            Self::UndefinedVariable => Some(
                "This variable was not declared with `my`, `our`, or `local`. \
                Add `use strict;` and declare all variables before use.",
            ),
            Self::MissingPackageDeclaration => Some(
                "This file has no `package` declaration. \
                Add `package MyModule;` at the top for module files.",
            ),
            Self::DuplicatePackage => Some(
                "This package name is declared more than once in the same file. \
                Each package should appear once, or split into separate files.",
            ),
            Self::DuplicateSubroutine => Some(
                "A subroutine with this name is defined more than once. \
                The later definition silently replaces the earlier one.",
            ),
            Self::MissingReturn => Some(
                "This subroutine has no explicit `return` statement. \
                Add `return $value;` to make the return value clear.",
            ),
            Self::RoleConflict => Some(
                "Two or more consumed Moo/Moose roles provide the same method. \
                Define the method in the class or remove one of the conflicting roles.",
            ),
            Self::MissingPodCoverage => Some(
                "This exported subroutine has no corresponding `=head2` or `=item` POD section. \
                Add documentation so users of your module can discover its API.",
            ),
            Self::InvalidPrototype => Some(
                "The prototype contains a character that Perl does not recognise. \
                Valid prototype characters are: $, @, %, &, *, \\, ;, +, _ and spaces. \
                See perlsub for the full prototype syntax.",
            ),
            Self::BarewordFilehandle => Some(
                "Bareword filehandles (e.g., `open FH, ...`) are global and unsafe. \
                Use a lexical filehandle instead: `open my $fh, '<', $file or die $!;`",
            ),
            Self::TwoArgOpen => Some(
                "Two-argument `open()` is vulnerable to injection. \
                Use three-argument form: `open my $fh, '<', $filename or die $!;`",
            ),
            Self::ImplicitReturn => Some(
                "The return value of this expression is used implicitly. \
                Make it explicit with `return` or assign it to a variable.",
            ),
            Self::AssignmentInCondition => Some(
                "This looks like an assignment `=` inside a condition where \
                a comparison `==` or `eq` was likely intended.",
            ),
            Self::NumericComparisonWithUndef => Some(
                "Comparing a potentially undefined value with a numeric operator \
                produces a warning at runtime. Check for definedness first with `defined()`.",
            ),
            Self::EvalErrorFlow => Some(
                "Read `$@` or `$EVAL_ERROR` immediately after an `eval` or `try` \
                block; intervening statements can clobber the exception state.",
            ),
            Self::UnreachableCode => Some(
                "This statement cannot be executed because a preceding statement \
                unconditionally exits (return, die, exit, croak). Remove or relocate it.",
            ),
            Self::DuplicateHashKey => Some(
                "This hash key appears more than once in the same literal. \
                Only the last value will be used; the earlier assignment is silently discarded.",
            ),
            Self::GotoUndefinedLabel => Some(
                "This goto target label is not defined in the current file. \
                Define the label or use a dynamic goto form only when the target is known at runtime.",
            ),
            Self::LoopControlUndefinedLabel => Some(
                "This `next`, `last`, or `redo` references a label that is not defined in the current file. \
                Add a matching `LABEL:` on an enclosing loop, or remove the label to target the innermost loop.",
            ),
            Self::PrintfFormatMismatch => Some(
                "The number of format specifiers does not match the number of arguments. \
                Each %s/%d/%f/etc. consumes one argument (except %% which consumes none).",
            ),
            Self::VariableShadowing => Some(
                "This variable shadows an outer variable with the same name. \
                Rename it to avoid confusion, or use the outer variable directly.",
            ),
            Self::VariableRedeclaration => Some(
                "This variable is declared again in the same scope. \
                Remove the duplicate `my` declaration and reuse the existing variable.",
            ),
            Self::DuplicateParameter => Some(
                "This subroutine signature has a duplicate parameter name. \
                Each parameter must have a unique name.",
            ),
            Self::ParameterShadowsGlobal => Some(
                "This subroutine parameter shadows a global (`our`) variable. \
                Rename the parameter to avoid confusion with the global.",
            ),
            Self::UnusedParameter => Some(
                "This subroutine parameter is declared but never used. \
                Remove it or prefix with `_` (e.g., `$_unused`) to suppress.",
            ),
            Self::UnquotedBareword => Some(
                "This bareword is used where a quoted string is expected. \
                Under `use strict`, barewords are not allowed. Quote it: `'word'`.",
            ),
            Self::UninitializedVariable => Some(
                "This variable is used before being assigned a value. \
                Initialize it before use to avoid `Use of uninitialized value` warnings.",
            ),
            Self::MisspelledPragma => Some(
                "This pragma name appears to be misspelled. \
                Check the spelling and ensure the module is installed.",
            ),
            Self::CaptureVarWithoutRegexMatch => Some(
                "Capture variables ($1, $2, etc.) are only meaningful after a successful regex match. \
                Perform a regex match with =~ /.../ before using $1 or $2.",
            ),
            Self::DeprecatedDefined => Some(
                "`defined(@array)` and `defined(%hash)` are deprecated since Perl 5.6. \
                Use `@array` or `%hash` directly in boolean context instead.",
            ),
            Self::DeprecatedArrayBase => Some(
                "The `$[` variable is deprecated. Array indices always start at 0 \
                in modern Perl. Remove any assignment to `$[`.",
            ),
            Self::PhaseScopedStrictPragma => Some(
                "`use strict` inside a phase block only applies inside that block. \
                Move `use strict;` to file scope for file-wide strict enforcement.",
            ),
            Self::PhaseScopedWarningsPragma => Some(
                "`use warnings` inside a phase block only applies inside that block. \
                Move `use warnings;` to file scope for file-wide warnings coverage.",
            ),
            Self::SecurityStringEval => Some(
                "String `eval` executes arbitrary code and is a security risk. \
                Use block eval `eval { ... }` or safer alternatives.",
            ),
            Self::SecurityBacktickExec => Some(
                "Backticks/`qx()` execute shell commands and can be exploited. \
                Use `system()` with a list form or IPC::Run for safer execution.",
            ),
            Self::SecuritySignalHandler => Some(
                "Assigning to $SIG{__DIE__} or $SIG{__WARN__} globally changes exception \
                and warning handling for the whole process. Use `local` to scope the handler.",
            ),
            Self::SecuritySystemCall => Some(
                "`system()` executes a shell command. If the arguments include user input, \
                use the list form `system($cmd, @args)` to avoid shell injection.",
            ),
            Self::SecurityExecCall => Some(
                "`exec()` replaces the current process with a shell command. If arguments \
                include user input, use the list form `exec($cmd, @args)` to avoid shell injection.",
            ),
            Self::SecurityPipeOpen => Some(
                "Pipe-open executes a shell command. Pass a list to `open` for safe argument \
                handling: `open(my $fh, '-|', $cmd, @args)` instead of `open(my $fh, \"|$cmd\")`.",
            ),
            Self::SecurityReadpipe => Some(
                "`readpipe()` executes a shell command (equivalent to backticks/qx//). \
                Use `open(my $fh, '-|', $cmd, @args)` or IPC::Run for safer command execution.",
            ),
            Self::UnusedImport => Some(
                "This module is imported but none of its exports appear to be used. \
                Remove the `use` statement to reduce unnecessary dependencies.",
            ),
            Self::ModuleNotFound => Some(
                "This module was not found in the workspace or configured include paths. \
                Install it with cpanm or add it to cpanfile.",
            ),
            Self::HeredocInFormat => Some(
                "Heredocs inside `format` blocks can cause subtle parsing issues. \
                Extract the heredoc content into a variable before the format.",
            ),
            Self::HeredocInBegin => Some(
                "Heredocs inside `BEGIN` blocks may behave unexpectedly due to \
                compile-time execution. Move the heredoc outside the BEGIN block.",
            ),
            Self::HeredocDynamicDelimiter => Some(
                "The heredoc delimiter contains a variable, making it dynamic. \
                Use a static delimiter string to avoid surprising behavior.",
            ),
            Self::HeredocInSourceFilter => Some(
                "Heredocs inside source-filtered code may be mangled by the filter. \
                Avoid combining heredocs with source filters.",
            ),
            Self::HeredocInRegexCode => Some(
                "Heredocs inside regex code blocks `(?{ ... })` can cause parsing failures. \
                Move the heredoc content outside the regex.",
            ),
            Self::HeredocInEval => Some(
                "Heredocs inside string `eval` are fragile and error-prone. \
                Use a variable or block eval instead.",
            ),
            Self::HeredocTiedHandle => Some(
                "Heredocs written to tied filehandles may not behave as expected. \
                The tie interface may not handle multi-line heredoc output correctly.",
            ),
            Self::VersionIncompatFeature => Some(
                "This Perl feature requires a newer Perl version than declared. \
                Update 'use vN.NN' or 'use feature' to enable it.",
            ),
            // Perl::Critic codes carry per-policy descriptions; no generic hint needed.
            Self::CriticSeverity1
            | Self::CriticSeverity2
            | Self::CriticSeverity3
            | Self::CriticSeverity4
            | Self::CriticSeverity5 => None,
        }
    }

    /// Try to infer a diagnostic code from a message.
    pub fn from_message(msg: &str) -> Option<Self> {
        let msg_lower = msg.to_lowercase();
        if contains_diagnostic_phrase(&msg_lower, "inside a begin block does not enable strict")
            || contains_diagnostic_phrase(&msg_lower, "inside a phase block does not enable strict")
        {
            Some(Self::PhaseScopedStrictPragma)
        } else if contains_diagnostic_phrase(
            &msg_lower,
            "inside a begin block does not enable warnings",
        ) || contains_diagnostic_phrase(
            &msg_lower,
            "inside a phase block does not enable warnings",
        ) {
            Some(Self::PhaseScopedWarningsPragma)
        } else if contains_diagnostic_phrase(&msg_lower, "use strict") {
            Some(Self::MissingStrict)
        } else if contains_diagnostic_phrase(&msg_lower, "use warnings") {
            Some(Self::MissingWarnings)
        } else if contains_diagnostic_phrase(&msg_lower, "unused variable")
            || contains_diagnostic_phrase(&msg_lower, "never used")
        {
            Some(Self::UnusedVariable)
        } else if is_undefined_variable_message(&msg_lower) {
            Some(Self::UndefinedVariable)
        } else if contains_diagnostic_phrase(&msg_lower, "bareword filehandle") {
            Some(Self::BarewordFilehandle)
        } else if contains_diagnostic_phrase(&msg_lower, "two-argument")
            || contains_diagnostic_phrase(&msg_lower, "2-argument")
            || contains_diagnostic_phrase(&msg_lower, "2-arg")
        {
            Some(Self::TwoArgOpen)
        // Real Perl: "Subroutine foo redefined at file.pl line N."
        } else if contains_diagnostic_phrase(&msg_lower, "subroutine")
            && contains_diagnostic_phrase(&msg_lower, "redefined")
        {
            Some(Self::DuplicateSubroutine)
        } else if contains_diagnostic_phrase(&msg_lower, "invalid prototype character")
            || contains_diagnostic_phrase(&msg_lower, "illegal character in prototype")
            // Real Perl: "Prototype mismatch: sub foo ($$) vs sub foo ($) at file.pl line N."
            || contains_diagnostic_phrase(&msg_lower, "prototype mismatch")
        {
            Some(Self::InvalidPrototype)
        } else if contains_diagnostic_phrase(&msg_lower, "parse error")
            || contains_diagnostic_phrase(&msg_lower, "syntax error")
        {
            Some(Self::ParseError)
        // Real Perl: "Use of uninitialized value $x in string at file.pl line N."
        // Also: "Use of uninitialized value in concatenation (.) or string at file.pl line N."
        } else if contains_diagnostic_phrase(&msg_lower, "uninitialized value") {
            Some(Self::UninitializedVariable)
        // Real Perl: "Can't locate Foo/Bar.pm in @INC (...) at file.pl line N."
        } else if contains_diagnostic_phrase(&msg_lower, "can't locate")
            && msg_lower.contains(".pm")
        {
            Some(Self::ModuleNotFound)
        // Real Perl: `Bareword "foo" not allowed while 'strict subs' in use at file.pl line N.`
        // Real Perl: "Unquoted string "foo" may clash with future reserved word at file.pl line N."
        // Note: the actual bareword name is interpolated between "Bareword" and "not allowed",
        // so we match on "strict subs" (unique to this warning) or "unquoted string".
        } else if contains_diagnostic_phrase(&msg_lower, "strict subs")
            || contains_diagnostic_phrase(&msg_lower, "unquoted string")
            || contains_diagnostic_phrase(&msg_lower, "bareword not allowed")
        {
            Some(Self::UnquotedBareword)
        // Real Perl: "defined(@array) is deprecated (it's always defined) at file.pl line N."
        // Real Perl: "defined(%hash) is deprecated (it's always defined) at file.pl line N."
        } else if msg_lower.contains("defined(")
            && contains_diagnostic_phrase(&msg_lower, "deprecated")
        {
            Some(Self::DeprecatedDefined)
        } else {
            None
        }
    }

    /// Try to parse a code string into a DiagnosticCode.
    pub fn parse_code(code: &str) -> Option<Self> {
        match code {
            "PL001" => Some(Self::ParseError),
            "PL002" => Some(Self::SyntaxError),
            "PL003" => Some(Self::UnexpectedEof),
            "PL100" => Some(Self::MissingStrict),
            "PL101" => Some(Self::MissingWarnings),
            "PL102" => Some(Self::UnusedVariable),
            "PL103" => Some(Self::UndefinedVariable),
            "PL104" => Some(Self::VariableShadowing),
            "PL105" => Some(Self::VariableRedeclaration),
            "PL106" => Some(Self::DuplicateParameter),
            "PL107" => Some(Self::ParameterShadowsGlobal),
            "PL108" => Some(Self::UnusedParameter),
            "PL109" => Some(Self::UnquotedBareword),
            "PL110" => Some(Self::UninitializedVariable),
            "PL111" => Some(Self::MisspelledPragma),
            "PL112" => Some(Self::CaptureVarWithoutRegexMatch),
            "PL200" => Some(Self::MissingPackageDeclaration),
            "PL201" => Some(Self::DuplicatePackage),
            "PL300" => Some(Self::DuplicateSubroutine),
            "PL301" => Some(Self::MissingReturn),
            "PL302" => Some(Self::InvalidPrototype),
            "PL303" => Some(Self::RoleConflict),
            "PL304" => Some(Self::MissingPodCoverage),
            "PL400" => Some(Self::BarewordFilehandle),
            "PL401" => Some(Self::TwoArgOpen),
            "PL402" => Some(Self::ImplicitReturn),
            "PL403" => Some(Self::AssignmentInCondition),
            "PL404" => Some(Self::NumericComparisonWithUndef),
            "PL405" => Some(Self::PrintfFormatMismatch),
            "PL406" => Some(Self::UnreachableCode),
            "PL407" => Some(Self::EvalErrorFlow),
            "PL408" => Some(Self::DuplicateHashKey),
            "PL409" => Some(Self::GotoUndefinedLabel),
            "PL410" => Some(Self::LoopControlUndefinedLabel),
            "PL500" => Some(Self::DeprecatedDefined),
            "PL501" => Some(Self::DeprecatedArrayBase),
            "PL502" => Some(Self::PhaseScopedStrictPragma),
            "PL503" => Some(Self::PhaseScopedWarningsPragma),
            "PL600" => Some(Self::SecurityStringEval),
            "PL601" => Some(Self::SecurityBacktickExec),
            "PL602" => Some(Self::SecuritySignalHandler),
            "PL603" => Some(Self::SecuritySystemCall),
            "PL604" => Some(Self::SecurityExecCall),
            "PL605" => Some(Self::SecurityPipeOpen),
            "PL606" => Some(Self::SecurityReadpipe),
            "PL700" => Some(Self::UnusedImport),
            "PL701" => Some(Self::ModuleNotFound),
            "PL800" => Some(Self::HeredocInFormat),
            "PL801" => Some(Self::HeredocInBegin),
            "PL802" => Some(Self::HeredocDynamicDelimiter),
            "PL803" => Some(Self::HeredocInSourceFilter),
            "PL804" => Some(Self::HeredocInRegexCode),
            "PL805" => Some(Self::HeredocInEval),
            "PL806" => Some(Self::HeredocTiedHandle),
            "PL900" => Some(Self::VersionIncompatFeature),
            "PC001" => Some(Self::CriticSeverity1),
            "PC002" => Some(Self::CriticSeverity2),
            "PC003" => Some(Self::CriticSeverity3),
            "PC004" => Some(Self::CriticSeverity4),
            "PC005" => Some(Self::CriticSeverity5),
            _ => None,
        }
    }
}