File Coverage

lib/Perl500503Syntax/OrDie.pm
Criterion Covered Total %
statement 389 436 89.2
branch 148 192 77.0
condition 56 93 60.2
subroutine 23 24 95.8
pod 2 2 100.0
total 618 747 82.7


line stmt bran cond sub pod time code
1             package Perl500503Syntax::OrDie;
2              
3             ######################################################################
4             #
5             # Perl500503Syntax::OrDie - Validate Perl 5.005_03 source compatibility
6             #
7             # Copyright (c) 2026 INABA Hitoshi
8             #
9             ######################################################################
10              
11 24     17   1418364 use strict;
  24         48  
  24         1282  
12 24 100 33 17   579 BEGIN { if ($] < 5.006 && !defined(&warnings::import)) {
13 7         833 $INC{'warnings.pm'} = 'stub'; eval 'package warnings; sub import {}' } }
  7         20  
14 24     17   151 use warnings; local $^W = 1;
  24         52  
  24         1383  
15              
16 24     17   108 use vars qw($VERSION @BLACKLIST @REGEX_BLACKLIST @RAW_BLACKLIST $_OPEN_GUARDED $_MKDIR_GUARDED);
  24         232  
  24         122162  
17              
18             $VERSION = '0.02';
19              
20             # ======================================================================
21             # BLACKLIST
22             # Each entry: [ qr/pattern/, 'human-readable description' ]
23             # Matched against source after masking comments and string literals.
24             # ======================================================================
25             @BLACKLIST = (
26              
27             # ------------------------------------------------------------------
28             # Perl 5.6 features
29             # ------------------------------------------------------------------
30              
31             # our VARIABLE
32             [ qr/\bour\s*[\$\@\%\*]/,
33             "'our' declaration (introduced in Perl 5.6; use 'use vars' instead)" ],
34              
35             # 3-argument open: after masking the mode string becomes "X", so we
36             # detect by counting commas inside open(...)
37             [ do { my $kw = 'op' . 'en'; qr/\b$kw\s*\(\s*[^,\)]+\s*,\s*[^,\)]+\s*,/ },
38             "3-argument open() (introduced in Perl 5.6; use 2-argument form)" ],
39              
40             # use utf8
41             [ qr/\buse\s+utf8\b/,
42             "'use utf8' (introduced in Perl 5.6)" ],
43              
44             # use VERSION >= 5.6 (numeric forms: 5.6, 5.06, 5.006, 5.010, 5.100 ...)
45             # Pattern: 5. followed by (0*[6-9]) OR (0[1-9]\d+) OR ([1-9]\d+)
46             # This catches 5.6, 5.06, 5.006, 5.010, 5.012, 5.100 etc.
47             # but not 5.5, 5.005, 5.004
48             [ qr/\buse\s+5\s*\.\s*(?:0*[6-9]|0[1-9]\d+|[1-9]\d+)\b/,
49             "use VERSION >= 5.6 (target is Perl 5.005_03)" ],
50              
51             # use v5.6, use v5.10, ...
52             [ qr/\buse\s+v5\s*\.\s*[6-9]/,
53             "use vVERSION >= v5.6 (target is Perl 5.005_03)" ],
54             [ qr/\buse\s+v5\s*\.1/,
55             "use vVERSION >= v5.10 (target is Perl 5.005_03)" ],
56              
57             # \x{HHHH} Unicode escape
58             [ qr/\\x\{[0-9A-Fa-f]+\}/,
59             "\\x{} Unicode escape (introduced in Perl 5.6)" ],
60              
61             # \N{name} named character
62             [ qr/\\N\{[^}]+\}/,
63             "\\N{} named character escape (introduced in Perl 5.6)" ],
64              
65             # @+ / @- match-position arrays: $+[N] $-[N] @+ @-
66             [ qr/(?:\$[+\-]\[|\@[+\-](?!\w))/,
67             'match-position arrays @+/@- or $+[N]/$-[N] (introduced in Perl 5.6)' ],
68              
69             # CHECK { } / INIT { } named phase blocks
70             [ qr/\b(?:CHECK|INIT)\s*\{/,
71             "CHECK/INIT phase blocks (introduced in Perl 5.6)" ],
72              
73             # v-strings: v1.2.3 or v5.6.0
74             [ qr/(?
75             "v-string notation (introduced in Perl 5.6; use a plain number instead)" ],
76              
77             # $^V (version object; use $] instead)
78             [ qr/\$\^V\b/,
79             '$^V version object (introduced in Perl 5.6; use $] instead)' ],
80              
81             # :lvalue subroutine attribute
82             [ qr/\bsub\s+\w[\w:]*\s*(?:\([^)]*\)\s*)?:[^{]*\blvalue\b/,
83             "':lvalue' subroutine attribute (introduced in Perl 5.6)" ],
84              
85             # typeglob component slice *FH{IO} *foo{SCALAR} *foo{CODE} etc.
86             # Perl 5.005_03 does not support typeglob-element access via *name{SLOT}.
87             # Guard: require leading sigil or word boundary to avoid false positives.
88             [ qr/\*\w[\w:]*\s*\{\s*(?:IO|SCALAR|ARRAY|HASH|CODE|GLOB|FORMAT|NAME|PACKAGE)\s*\}/,
89             "typeglob component access *name{SLOT} (introduced in Perl 5.6)" ],
90              
91             # ------------------------------------------------------------------
92             # Perl 5.8 features
93             # ------------------------------------------------------------------
94              
95             # use encoding
96             [ qr/\buse\s+encoding\b/,
97             "'use encoding' (introduced in Perl 5.8)" ],
98              
99             # use constant with a hashref (hash of constants) -- Perl 5.8
100             # Single-value form use constant NAME => VALUE is 5.004+, always OK.
101             # Hash-ref form use constant { A => 1, B => 2 } was added in Perl 5.8.
102             [ qr/\buse\s+constant\s*\{/,
103             "'use constant { HASH }' multi-constant form (introduced in Perl 5.8; use separate 'use constant' statements)" ],
104              
105             # ------------------------------------------------------------------
106             # Perl 5.10 features
107             # ------------------------------------------------------------------
108              
109             # defined-or-assignment operator
110             [ do { my $p = '/' . '/='; qr/$p/ },
111             "defined-or assignment '" . '/' . "/=' (introduced in Perl 5.10)" ],
112              
113             # say HANDLE or say LIST
114             # Exclude ->say(...) method calls and say => hash-key usage
115             [ do { my $kw = 'sa' . 'y'; qr/(?)\b$kw\b(?!\s*=>)/ },
116             "'say' (introduced in Perl 5.10)" ],
117              
118             # sta-te $var
119             [ do { my $kw = 'sta' . 'te'; qr/\b$kw\s+[\$\@\%]/ },
120             "'sta" . "te' variable (introduced in Perl 5.10)" ],
121              
122             # given(...)
123             [ qr/\bgiven\s*\(/,
124             "'given' (introduced in Perl 5.10)" ],
125              
126             # when(...)
127             [ qr/\bwhen\s*\(/,
128             "'when' (introduced in Perl 5.10)" ],
129              
130             # smart-match ~~
131             [ qr/~~/,
132             "smart-match operator '~~' (introduced in Perl 5.10)" ],
133              
134             # use feature
135             [ qr/\buse\s+feature\b/,
136             "'use feature' (introduced in Perl 5.10)" ],
137              
138             # defined-or operator (two slashes, not part of s///, m//, =~, !~)
139             # and not the defined-or-assign variant
140             [ qr/(?
141             "defined-or operator (introduced in Perl 5.10)" ],
142              
143             # UNITCHECK phase block
144             [ qr/\bUNITCHECK\s*\{/,
145             "UNITCHECK phase block (introduced in Perl 5.10)" ],
146              
147             # ${^MATCH} ${^PREMATCH} ${^POSTMATCH} (used with /p flag -- 5.10)
148             [ qr/\$\{\^(?:MATCH|PREMATCH|POSTMATCH)\}/,
149             "\${^MATCH}/\${^PREMATCH}/\${^POSTMATCH} (introduced in Perl 5.10; require /p flag)" ],
150              
151             # ------------------------------------------------------------------
152             # Perl 5.12 features
153             # ------------------------------------------------------------------
154              
155             # package NAME VERSION
156             [ qr/\bpackage\s+\w[\w:]*\s+v?\d/,
157             "'package NAME VERSION' (introduced in Perl 5.12)" ],
158              
159             # yada-yada operator ... (not to be confused with .. range)
160             [ do { my $p = '\\.' x 3; qr/(?
161             "yada-yada operator '...' (introduced in Perl 5.12)" ],
162              
163             # ------------------------------------------------------------------
164             # Perl 5.14 features
165             # ------------------------------------------------------------------
166              
167             # /r (non-destructive) flag on s/// or tr///
168             # _mask_source emits __SR__ or __TRR__ marker when r flag is present.
169             [ qr/__SR__|__TRR__/,
170             "s///r or tr///r non-destructive flag (introduced in Perl 5.14)" ],
171              
172             # ------------------------------------------------------------------
173             # Perl 5.16 features
174             # ------------------------------------------------------------------
175              
176             # __SUB__ token (reference to the current subroutine)
177             [ qr/__SUB__/,
178             "__SUB__ (introduced in Perl 5.16; use explicit sub name or \$_[0] recursion)" ],
179              
180             # ------------------------------------------------------------------
181             # Perl 5.18 features
182             # ------------------------------------------------------------------
183              
184             # my sub NAME / state sub NAME -- lexical subroutines (Perl 5.18)
185             [ do { my $kw = 'su' . 'b'; qr/\b(?:my|state)\s+$kw\s+\w/ },
186             "'my sub'/'state sub' lexical subroutine (introduced in Perl 5.18)" ],
187              
188             # ------------------------------------------------------------------
189             # Perl 5.20 features
190             # ------------------------------------------------------------------
191              
192             # subroutine signatures: sub foo ($x, $y) { (Perl 5.20)
193             [ qr/\bsub\s+\w+\s*\([^\)]*(?
194             "subroutine signature (introduced in Perl 5.20)" ],
195              
196             # postfix dereference $ref->@* $ref->%* $ref->&*
197             [ qr/->\s*[\@\%\&\$]\s*\*/,
198             "postfix dereference (introduced in Perl 5.20)" ],
199              
200             # %hash{LIST} and %array[LIST] key/value (index/value) slices (Perl 5.20)
201             # Exclude plain %hash alone (no subscript) and %hash = (...) assignment.
202             # Require preceding context that cannot be an assignment target start.
203             [ qr/(?:[\(=,;!&|?:]|\breturn\b)\s*\%\w[\w:]*\s*[\{\[]/,
204             "key/value hash/array slice %hash{} or %array[] (introduced in Perl 5.20)" ],
205              
206             # ------------------------------------------------------------------
207             # Perl 5.22 features
208             # ------------------------------------------------------------------
209              
210             # &. |. ^. ~. string bitwise operators (use feature 'bitwise')
211             [ qr/(?:[&|^]\.=?|~\.)/,
212             "string bitwise operator '&.' '|.' '^.' '~.' (introduced in Perl 5.22)" ],
213              
214             # \$scalar in foreach -- reference aliasing (use feature 'refaliasing')
215             [ qr/\bforeach\s+\\(?:my\s+)?[\$\@\%\*]/,
216             "reference aliasing in foreach (introduced in Perl 5.22; use index-based loop instead)" ],
217              
218             # <<>> double-diamond operator
219             [ qr/<
220             "<<>> double-diamond operator (introduced in Perl 5.22)" ],
221              
222             # /n flag (non-capturing groups make all captures non-capturing)
223             [ qr{(?:=~|!~|(?
224             "/n non-capturing regex flag (introduced in Perl 5.22)" ],
225              
226             # ------------------------------------------------------------------
227             # Perl 5.26 features
228             # ------------------------------------------------------------------
229              
230             # <<~ indented heredoc
231             [ qr/<<~/,
232             "<<~ indented heredoc (introduced in Perl 5.26)" ],
233              
234             # ------------------------------------------------------------------
235             # Perl 5.32 features
236             # ------------------------------------------------------------------
237              
238             # isa infix operator: $obj isa ClassName
239             # Exclude ->isa() method calls (> before) and UNIVERSAL::isa() (:: before)
240             # Exclude isa( call form (followed by open paren = sub call, not infix)
241             [ qr/(?:])(?])\bisa\b(?!::)(?!\s*\()/,
242             "'isa' infix operator (introduced in Perl 5.32; use UNIVERSAL::isa() instead)" ],
243              
244             # ------------------------------------------------------------------
245             # Perl 5.34+ features
246             # ------------------------------------------------------------------
247              
248             # try { } catch ($e) { }
249             [ qr/\btry\s*\{/,
250             "'try' block (introduced in Perl 5.34)" ],
251              
252             # ------------------------------------------------------------------
253             # Perl 5.36 features
254             # ------------------------------------------------------------------
255              
256             # use builtin
257             [ qr/\buse\s+builtin\b/,
258             "'use builtin' (introduced in Perl 5.36)" ],
259              
260             # for my ($a, $b) (@list) -- iterable-value variables in for loop
261             [ qr/\bfor\s+my\s*\(/,
262             "'for my (\$a,\$b)' paired iteration (introduced in Perl 5.36)" ],
263              
264             # ------------------------------------------------------------------
265             # Perl 5.38+ features
266             # ------------------------------------------------------------------
267              
268             # class Foo { }
269             [ qr/\bclass\s+\w/,
270             "'class' keyword (introduced in Perl 5.38)" ],
271              
272             # ------------------------------------------------------------------
273             # Perl 5.40 features
274             # ------------------------------------------------------------------
275              
276             # ^^ high-precedence logical XOR operator ($x ^^ $y)
277             [ qr/\^\^[^>]/,
278             "'^^ / ^^=' high-precedence logical XOR (introduced in Perl 5.40)" ],
279              
280             # __CLASS__ keyword
281             [ qr/__CLASS__/,
282             "'__CLASS__' keyword (introduced in Perl 5.40; use __PACKAGE__ or \$class instead)" ],
283              
284             # ------------------------------------------------------------------
285             # Perl 5.42 features
286             # ------------------------------------------------------------------
287              
288             # any { } LIST and all { } LIST -- experimental keyword operators
289             # Exclude files that have "use List::Util" with any/all in the import list,
290             # since those are sub calls, not keywords. Detection deferred to
291             # _check_source() which performs the import pre-scan.
292             # (Pattern is applied conditionally; see _check_source().)
293              
294             # my method NAME -- lexical method declaration inside class block
295             [ do { my $kw = 'meth' . 'od'; qr/\bmy\s+$kw\b/ },
296             "'my method' lexical method declaration (introduced in Perl 5.42)" ],
297              
298             # ->& invocation of lexical method (Perl 5.42)
299             [ qr/->&\s*\w/,
300             "'->&name' lexical method call operator (introduced in Perl 5.42)" ],
301              
302             );
303              
304             # ======================================================================
305             # BLACKLIST_CONDITIONAL
306             # Entries that are only applied when certain file-level conditions hold.
307             # Each entry: [ $condition_key, qr/pattern/, 'description' ]
308             # $condition_key is checked against %cond hash in _check_source().
309             # ======================================================================
310             # (Currently used for any/all keyword detection.)
311              
312             # ======================================================================
313             # REGEX_BLACKLIST
314             # Patterns checked against the CONTENT of regex literals only.
315             # Each entry: [ qr/pattern/, 'human-readable description' ]
316             # ======================================================================
317             @REGEX_BLACKLIST = (
318              
319             # \x{HHHH} Unicode escape in regex -- Perl 5.6
320             [ qr/\\x\{[0-9A-Fa-f]+\}/,
321             "\\x{} Unicode escape (introduced in Perl 5.6)" ],
322              
323             # \N{name} named character in regex -- Perl 5.6
324             [ qr/\\N\{[^}]+\}/,
325             "\\N{} named character escape (introduced in Perl 5.6)" ],
326              
327             # \p{} \P{} Unicode property escapes -- Perl 5.6
328             [ qr/\\[pP]\{/,
329             "\\p{}\\P{} Unicode property in regex (introduced in Perl 5.6)" ],
330              
331             # \K (keep) -- Perl 5.10
332             [ qr/\\K/,
333             "\\K (keep) in regex (introduced in Perl 5.10)" ],
334              
335             # Named capture (?...) or \k backreference -- Perl 5.10
336             [ qr/(?:\(\?<[A-Za-z_]|\\k<)/,
337             "named capture (?...) or \\k (introduced in Perl 5.10)" ],
338              
339             # Branch reset group (?|...) -- Perl 5.10
340             [ qr/\(\?[|]/,
341             "branch reset (?|...) in regex (introduced in Perl 5.10)" ],
342              
343             # Backtrack control verbs (*FAIL) (*ACCEPT) (*PRUNE) (*SKIP) etc -- Perl 5.10
344             [ qr/\(\*[A-Z]/,
345             "backtrack control verb (*VERB) in regex (introduced in Perl 5.10)" ],
346              
347             # \h \H \v \V \R -- horizontal/vertical whitespace -- Perl 5.10
348             [ qr/\\[hHvVR](?!\w)/,
349             "\\h/\\H/\\v/\\V/\\R regex escape (introduced in Perl 5.10)" ],
350              
351             # Variable-length lookbehind -- experimental Perl 5.30, stable Perl 5.38
352             [ qr/\(\?<[=!][^)]*(?:\{|\*|\+)[^)]*\)/,
353             "variable-length lookbehind in regex (experimental from Perl 5.30, stable in Perl 5.38; use fixed-length)" ],
354              
355             # Possessive quantifiers in regex: a++ a*+ a?+ a{n,m}+ -- Perl 5.10
356             [ qr/(?:[A-Za-z0-9_)\].])(?:[+*?]|\{\d+(?:,\d*)?\})\+/,
357             "possessive quantifier (++/*+/?+/{n,m}+) in regex (introduced in Perl 5.10)" ],
358              
359             # Recursive patterns (?PARNO) (?&name) (?R) -- Perl 5.10
360             [ qr/\(\?(?:[0-9]+|[+-][0-9]+|R|&\w+)\)/,
361             "recursive pattern (?PARNO) / (?&name) / (?R) (introduced in Perl 5.10)" ],
362              
363             # \g{N} relative or absolute backreference -- Perl 5.10
364             [ qr/\\g\{/,
365             "\\g{N} relative/absolute backreference (introduced in Perl 5.10)" ],
366              
367             );
368              
369             # ======================================================================
370             # RAW_BLACKLIST
371             # Abolished: string and regex contents must be freely writable in
372             # Perl 5.005_03 code. Checking runtime string values (PerlIO layer
373             # names, sprintf format flags, etc.) is not the role of a static
374             # syntax checker.
375             #
376             # Former entries and why they were removed:
377             # PerlIO layer (:utf8 etc) in open()/binmode()
378             # -> open() with a PerlIO layer requires 3-argument form, already
379             # caught by the 3-argument open() entry in @BLACKLIST.
380             # binmode() layer string is a runtime value, not syntax.
381             # sprintf/printf "%v" format flag
382             # -> The format string is a runtime value, not a syntax construct.
383             # A string literal containing "%v" is valid Perl 5.005_03 syntax.
384             # ======================================================================
385             @RAW_BLACKLIST = ();
386              
387             # ======================================================================
388             # import() -- called when the caller writes: use Perl500503Syntax::OrDie;
389             # ======================================================================
390             sub import {
391 0     0   0 my $class = shift;
392              
393 0         0 my ($pkg, $file, $line) = caller(0);
394              
395 0 0 0     0 if (defined $file && $file ne '' && $file ne '-e' && $file ne '-') {
      0        
      0        
396 0 0       0 if (-f $file) {
397 0         0 my @violations = _check_file($file);
398 0 0       0 if (@violations) {
399 0         0 die join('', @violations);
400             }
401             }
402             }
403              
404 0         0 _install_runtime_guards();
405 0         0 return;
406             }
407              
408             # ======================================================================
409             # _check_file($path)
410             # Returns a list of violation strings (empty list = no violations).
411             # ======================================================================
412             sub _check_file {
413 10     10   23 my ($file) = @_;
414              
415 10         26 local *_ORDIE_FH;
416 10         913 eval 'CORE::' . 'open(_ORDIE_FH, $file) or die $!';
417 10 50       51 if ($@) {
418 0         0 warn "Perl500503Syntax::OrDie: cannot open '$file': $!\n";
419 0         0 return ();
420             }
421 10         16 my $source = do { local $/; <_ORDIE_FH> };
  10         40  
  10         425  
422 10         105 close _ORDIE_FH;
423              
424 10         66 return _check_source($source, $file);
425             }
426              
427             # ======================================================================
428             # _check_source($source, $filename)
429             # Returns a list of violation strings (empty list = no violations).
430             # ======================================================================
431             sub _check_source {
432 340     340   12192 my ($source, $file) = @_;
433 340 50 33     1272 $file = '(unknown)' unless defined $file && $file ne '';
434              
435 340         410 my @violations;
436              
437 340         568 my ($masked, $regex_bodies_ref) = _mask_source($source);
438 340         1358 my @lines = split(/\n/, $masked, -1);
439 340         1229 my @rawlines = split(/\n/, $source, -1);
440              
441             # ------------------------------------------------------------------
442             # Pre-scan: detect use List::Util qw(... any ... all ...)
443             # If found, the any/all BLOCK keyword check is suppressed because
444             # those names are imported subs, not the new keyword form.
445             # ------------------------------------------------------------------
446 340         459 my $listutil_any_imported = 0;
447 340         420 my $listutil_all_imported = 0;
448             {
449 340         428 my $cm = _mask_comments($source);
  340         580  
450 340 100       902 if ($cm =~ /\buse\s+List::Util\b([^;]+);/s) {
451 5         18 my $args = $1;
452 5 100       16 $listutil_any_imported = 1 if $args =~ /\bany\b/;
453 5 100       14 $listutil_all_imported = 1 if $args =~ /\ball\b/;
454             }
455             }
456              
457             # Stage 1: BLACKLIST -- scan masked source (comments + strings masked)
458 340         591 for my $entry (@BLACKLIST) {
459 16320         18210 my ($pattern, $desc) = @{$entry};
  16320         22874  
460 16320         18014 my $lineno = 0;
461 16320         19260 for my $mline (@lines) {
462 178800         137149 $lineno++;
463 178800 100       316796 if ($mline =~ $pattern) {
464 105         372 push @violations,
465             "Perl500503Syntax::OrDie: VIOLATION at $file line $lineno:\n"
466             . " $desc\n";
467             }
468             }
469             }
470              
471             # Stage 1b: any/all BLOCK keyword -- conditional on import pre-scan
472             {
473 340 100       2908 my $any_pattern = $listutil_any_imported ? qr/(?!x)x/ : qr/\bany\s*\{/;
474 340 100       742 my $all_pattern = $listutil_all_imported ? qr/(?!x)x/ : qr/\ball\s*\{/;
475 340         433 my $desc_any = "'any BLOCK LIST' keyword operator (introduced in Perl 5.42; use List::Util instead)";
476 340         448 my $desc_all = "'all BLOCK LIST' keyword operator (introduced in Perl 5.42; use List::Util instead)";
477 340         402 my $lineno = 0;
478 340         440 for my $mline (@lines) {
479 3725         2972 $lineno++;
480 3725 100       6128 if ($mline =~ $any_pattern) {
481 3         11 push @violations,
482             "Perl500503Syntax::OrDie: VIOLATION at $file line $lineno:\n"
483             . " $desc_any\n";
484             }
485 3725 100       6564 if ($mline =~ $all_pattern) {
486 2         8 push @violations,
487             "Perl500503Syntax::OrDie: VIOLATION at $file line $lineno:\n"
488             . " $desc_all\n";
489             }
490             }
491             }
492              
493             # Possessive quantifiers in code: a++ a*+ a?+ a{n,m}+
494             {
495 340         457 my $lineno2 = 0;
  340         406  
  340         421  
496 340         469 for my $mline (@lines) {
497 3725         2864 $lineno2++;
498 3725         4937 while ($mline =~ /(?<=[a-zA-Z0-9_.)\]])((?:[+*?]|\{\d+(?:,\d*)?\})\+)/g) {
499 65         91 my $qpos = pos($mline) - length($1) - 1;
500 65         69 my $before = substr($mline, 0, $qpos + 1);
501 65 50       184 next if $before =~ /\$\w+$/;
502 0 0       0 next if $before =~ /\@\w+$/;
503 0         0 push @violations,
504             "Perl500503Syntax::OrDie: VIOLATION at $file line $lineno2:\n"
505             . " possessive quantifier (++/*+/?+/{n,m}+) (introduced in Perl 5.10)\n";
506             }
507             }
508             }
509              
510             # Stage 2: RAW_BLACKLIST -- comment-stripped raw source
511 340         632 my $comment_masked = _mask_comments($source);
512 340         1497 my @cmlines = split(/\n/, $comment_masked, -1);
513 340         560 for my $entry (@RAW_BLACKLIST) {
514 0         0 my ($pattern, $desc) = @{$entry};
  0         0  
515 0         0 my $lineno = 0;
516 0         0 for my $cmline (@cmlines) {
517 0         0 $lineno++;
518 0 0       0 if ($cmline =~ $pattern) {
519 0         0 push @violations,
520             "Perl500503Syntax::OrDie: VIOLATION at $file line $lineno:\n"
521             . " $desc\n";
522             }
523             }
524             }
525              
526             # Stage 3: REGEX_BLACKLIST -- content of regex literals only
527             # Each element of @$regex_bodies_ref is [$body_text, $line_number].
528 340         488 for my $entry (@REGEX_BLACKLIST) {
529 4080         4422 my ($pattern, $desc) = @{$entry};
  4080         5715  
530 4080         4407 for my $rbody (@{$regex_bodies_ref}) {
  4080         5673  
531 3228         2615 my ($body_text, $body_line) = @{$rbody};
  3228         3362  
532 3228 100       7034 if ($body_text =~ $pattern) {
533 41         170 push @violations,
534             "Perl500503Syntax::OrDie: VIOLATION at $file line $body_line:\n"
535             . " $desc\n";
536             }
537             }
538             }
539              
540 340         1902 return @violations;
541             }
542              
543             # ======================================================================
544             # _mask_comments($source)
545             # ======================================================================
546             sub _mask_comments {
547 680     680   1066 my ($src) = @_;
548 680         808 my $out = '';
549 680         774 my $pos = 0;
550 680         818 my $len = length($src);
551 680         1152 while ($pos < $len) {
552 65912         56506 my $ch = substr($src, $pos, 1);
553 65912 100       66189 if ($ch eq '#') {
554 1038         1881 my $end = index($src, "\n", $pos);
555 1038 50       1073 $end = $len if $end == -1;
556 1038         1078 $out .= '#' . ('X' x ($end - $pos - 1));
557 1038         817 $pos = $end;
558 1038         1151 next;
559             }
560 64874 100       65401 if ($ch eq '"') {
561 502         621 my ($rep, $len2) = _mask_dquote($src, $pos);
562 502         703 $out .= substr($src, $pos, $len2);
563 502         436 $pos += $len2;
564 502         619 next;
565             }
566 64372 100       63696 if ($ch eq "'") {
567 484         525 my ($rep, $len2) = _mask_squote($src, $pos);
568 484         623 $out .= substr($src, $pos, $len2);
569 484         367 $pos += $len2;
570 484         568 next;
571             }
572 63888         50551 $out .= $ch;
573 63888         66188 $pos++;
574             }
575 680         1622 return $out;
576             }
577              
578             # ======================================================================
579             # _mask_source($source)
580             #
581             # Returns ($masked_source, \@regex_bodies).
582             # Each element of @regex_bodies is [$body_text, $start_line_number].
583             # ======================================================================
584             sub _mask_source {
585 398     398   13420 my ($src) = @_;
586 398         522 my $out = '';
587 398         530 my $pos = 0;
588 398         531 my $len = length($src);
589 398         505 my @regex_bodies;
590 398         463 my $in_pod = 0;
591              
592 398         748 while ($pos < $len) {
593 118861         117047 my $ch = substr($src, $pos, 1);
594              
595             # -- __END__ / __DATA__ : mask everything after this line ------
596 118861 100 100     141018 if ($ch eq '_' && ($pos == 0 || substr($src, $pos - 1, 1) eq "\n")) {
      100        
597 12 100 66     82 if (substr($src, $pos, 7) eq '__END__' || substr($src, $pos, 8) eq '__DATA__') {
598 3         26 my $rest = substr($src, $pos);
599 3         5010 $rest =~ s/[^\n]/X/g;
600 3         11 $out .= $rest;
601 3         972 last;
602             }
603             }
604              
605             # -- POD block -------------------------------------------------
606 118858 50 66     214991 if (!$in_pod && $ch eq '=' && ($pos == 0 || substr($src, $pos - 1, 1) eq "\n")) {
      33        
      66        
607 0         0 my $nxt = substr($src, $pos, 12);
608 0 0       0 if ($nxt =~ /^=(head|over|item|back|pod|begin|end|for|encoding)\b/) {
609 0         0 $in_pod = 1;
610             }
611             }
612 118858 50       137040 if ($in_pod) {
613 0 0 0     0 if ($ch eq '=' && ($pos == 0 || substr($src, $pos - 1, 1) eq "\n")) {
      0        
614 0 0       0 if (substr($src, $pos, 4) eq '=cut') {
615 0         0 my $end2 = index($src, "\n", $pos);
616 0 0       0 $end2 = $len if $end2 == -1;
617 0         0 $out .= 'X' x ($end2 - $pos);
618 0         0 $pos = $end2;
619 0         0 $in_pod = 0;
620 0         0 next;
621             }
622             }
623 0 0       0 $out .= ($ch eq "\n") ? "\n" : 'X';
624 0         0 $pos++;
625 0         0 next;
626             }
627              
628             # -- single-line comment # ... --------------------------------
629 118858 100       138256 if ($ch eq '#') {
630 1587         2041 my $end = index($src, "\n", $pos);
631 1587 100       2159 $end = $len if $end == -1;
632 1587         2738 $out .= '#' . ('X' x ($end - $pos - 1));
633 1587         1528 $pos = $end;
634 1587         2129 next;
635             }
636              
637             # -- double-quoted string "..." --------------------------------
638 117271 100       132081 if ($ch eq '"') {
639 1086         1808 my ($rep, $len2) = _mask_dquote($src, $pos);
640 1086         1443 $out .= $rep;
641 1086         1166 $pos += $len2;
642 1086         1529 next;
643             }
644              
645             # -- single-quoted string '...' --------------------------------
646 116185 100       130082 if ($ch eq "'") {
647 1320         2071 my ($rep, $len2) = _mask_squote($src, $pos);
648 1320         1762 $out .= $rep;
649 1320         1472 $pos += $len2;
650 1320         1835 next;
651             }
652              
653             # -- backtick string `...` -------------------------------------
654 114865 100       128419 if ($ch eq '`') {
655 2         7 my ($rep, $len2) = _mask_delimited($src, $pos, '`', '`');
656 2         5 $out .= $rep;
657 2         3 $pos += $len2;
658 2         5 next;
659             }
660              
661             # -- q{} qq{} qw{} qx{} (but NOT qr) ---------------------------
662 114863 100 66     142237 if ($ch eq 'q' && $pos + 1 < $len) {
663 476         603 my $nxt = substr($src, $pos + 1, 1);
664 476 100 66     1390 if ($nxt =~ /^[qwx]$/ && $pos + 2 < $len) {
    100          
665 35         73 my $d = substr($src, $pos + 2, 1);
666 35 50       152 if ($d =~ /\S/) {
667 35         88 my $cl = _matching_delim($d);
668 35         134 my ($rep, $len2) = _mask_delimited($src, $pos + 2, $d, $cl);
669 35         78 $out .= substr($src, $pos, 2) . $rep;
670 35         68 $pos += 2 + $len2;
671 35         120 next;
672             }
673             }
674             elsif ($nxt =~ /^[\{\(\[\/\|!<]$/) {
675 1         4 my $cl = _matching_delim($nxt);
676 1         4 my ($rep, $len2) = _mask_delimited($src, $pos + 1, $nxt, $cl);
677 1         3 $out .= 'q' . $rep;
678 1         2 $pos += 1 + $len2;
679 1         2 next;
680             }
681             }
682              
683             # -- heredoc <
684 114827 100 66     137110 if ($ch eq '<' && $pos + 1 < $len && substr($src, $pos + 1, 1) eq '<') {
      100        
685 9         34 my $rest = substr($src, $pos);
686 9 100       49 if ($rest =~ /^(<<\s*([\"']?)(\w+)\2[^\r\n]*\r?\n)/) {
687 7         34 my ($header, $quote, $sentinel) = ($1, $2, $3);
688 7         12 my $hlen = length($header);
689 7         13 my $body = substr($src, $pos + $hlen);
690 7         88 my $re = qr/^\Q$sentinel\E\r?\n/m;
691 7 50       82 if ($body =~ $re) {
692 7         16 my $used = length($&);
693 7         12 my $total = $hlen + $used;
694 7         13 my $raw = substr($src, $pos, $total);
695 7         54 (my $mraw = $raw) =~ s/[^\n]/X/g;
696 7         12 $out .= $mraw;
697 7         9 $pos += $total;
698 7         25 next;
699             }
700             }
701             }
702              
703             # -- Regex/subst/transliteration operators ----------------------
704 114820 100       214038 if ($out !~ /[\w\$\@\%]\z/) {
705 68596         150825 my $rest = substr($src, $pos);
706 68596 100       99954 if ($rest =~ /\A(qr|m(?!y(?:\b|\s*=>))|s(?!ub(?:\b|\s*\{))|tr|y)\s*([^\w\s#])/s) {
707 236         450 my $op = $1;
708 236         377 my $delim = $2;
709 236         486 my $close = _matching_delim($delim);
710 236         337 my $ws = '';
711 236 50       1565 if (substr($rest, length($op)) =~ /\A(\s*)([^\w\s#])/s) {
712 236         309 $ws = $1;
713 236         322 $delim = $2;
714 236         310 $close = _matching_delim($delim);
715             }
716 236         322 my $op_len = length($op) + length($ws) + 1;
717              
718             # Compute line number of the start of this regex
719 236         390 my $cur_line = 1 + _count_newlines($src, 0, $pos);
720              
721 236         314 $pos += $op_len;
722              
723 236         537 my ($body1, $blen1) = _mask_delimited_raw($src, $pos, $delim, $close);
724 236         350 $pos += $blen1;
725 236         472 my $pat1 = substr($body1, 0, length($body1) - 1);
726 236 100 66     1040 push @regex_bodies, [$pat1, $cur_line]
727             unless $op eq 'tr' || $op eq 'y';
728              
729 236         323 my $repl_len = 0;
730 236 100 100     884 if ($op eq 's' || $op eq 'tr' || $op eq 'y') {
      66        
731 33         73 my $delim2 = $delim;
732 33         74 my $close2 = $close;
733 33 100       80 if ($delim ne $close) {
734 9         27 my $gap = substr($src, $pos);
735 9 50       48 if ($gap =~ /\A(\s*)([^\w\s])/s) {
736 9         22 $pos += length($1) + 1;
737 9         25 $repl_len += length($1) + 1;
738 9         24 $delim2 = $2;
739 9         21 $close2 = _matching_delim($delim2);
740             }
741             }
742 33         1033 my ($body2, $blen2) = _mask_delimited_raw($src, $pos, $delim2, $close2);
743 33         74 $pos += $blen2;
744 33         47 $repl_len += $blen2;
745             }
746              
747 236         299 my $flags = '';
748 236 50       2755 if (substr($src, $pos) =~ /\A([msixpgeodualncrs]*)/s) {
749 236         527 $flags = $1;
750 236         290 $pos += length($flags);
751             }
752              
753 236         278 my $marker = '';
754 236 100       428 if ($flags =~ /r/) {
755 3 100 33     12 if ($op eq 's') { $marker = '__SR__'; }
  2 50       4  
756 1         2 elsif ($op eq 'tr' || $op eq 'y') { $marker = '__TRR__'; }
757             }
758              
759 236         479 my $raw_body = substr($src,
760             $pos - length($flags) - $repl_len - $blen1,
761             $blen1 + $repl_len);
762 236         2150 (my $mbody = $ws . $raw_body) =~ s/[^\n]/X/g;
763 236         363 $out .= $op . $mbody . $flags . $marker;
764 236         641 next;
765             }
766             }
767              
768             # -- bare /regex/ -- only in regex context ----------------------
769 114584 100 100     464010 if ($ch eq '/' &&
770             $out =~ /(?:=~|!~|[=(,\{\[!&|;]|\b(?:if|while|unless|until|not|and|or|return))\s*\z/s)
771             {
772 324         882 my $cur_line = 1 + _count_newlines($src, 0, $pos);
773 324         496 $pos++;
774 324         882 my ($body, $blen) = _mask_delimited_raw($src, $pos, '/', '/');
775 324         497 $pos += $blen;
776 324         615 my $pat = substr($body, 0, length($body) - 1);
777 324         960 push @regex_bodies, [$pat, $cur_line];
778 324         487 my $flags = '';
779 324 50       2292 if (substr($src, $pos) =~ /\A([msixpgeodualn]*)/s) {
780 324         1003 $flags = $1;
781 324         437 $pos += length($flags);
782             }
783 324         2749 (my $mtext = '/' . $body) =~ s/[^\n]/X/g;
784 324         2120 $out .= $mtext . $flags;
785 324         838 next;
786             }
787              
788 114260         177518 $out .= $ch;
789 114260         133717 $pos++;
790             }
791              
792 398         1412 return ($out, \@regex_bodies);
793             }
794              
795             # ======================================================================
796             # _count_newlines($str, $from, $to)
797             # Count newline characters in $str between positions $from and $to-1.
798             # Used to compute line numbers from byte offsets.
799             # ======================================================================
800             sub _count_newlines {
801 560     560   1031 my ($str, $from, $to) = @_;
802 560         633 my $count = 0;
803 560         598 my $p = $from;
804 560         783 while ($p < $to) {
805 4534903 100       4848883 $count++ if substr($str, $p, 1) eq "\n";
806 4534903         4564840 $p++;
807             }
808 560         1720 return $count;
809             }
810              
811             # _mask_delimited_raw($src, $start, $open, $close)
812             sub _mask_delimited_raw {
813 593     593   1348 my ($src, $start, $open, $close) = @_;
814 593         678 my $pos = $start;
815 593         738 my $slen = length($src);
816 593         643 my $depth = 1;
817 593         951 my $paired = ($open ne $close);
818 593         753 my $out = '';
819              
820 593         1070 while ($pos < $slen) {
821 8066         7746 my $c = substr($src, $pos, 1);
822 8066         7257 $out .= $c;
823 8066         6719 $pos++;
824 8066 100 66     11181 if ($c eq '\\' && $pos < $slen) {
825 1287         1451 $out .= substr($src, $pos, 1);
826 1287         1066 $pos++;
827 1287         1667 next;
828             }
829 6779 100       7056 if ($paired) {
830 390 50       438 $depth++ if $c eq $open;
831 390 100       426 $depth-- if $c eq $close;
832 390 100       536 last if $depth == 0;
833             }
834             else {
835 6389 100       9217 last if $c eq $close;
836             }
837             }
838 593         1692 return ($out, $pos - $start);
839             }
840              
841             # ----------------------------------------------------------------------
842             sub _mask_dquote {
843 1588     1588   2106 my ($src, $start) = @_;
844 1588         1564 my $pos = $start + 1;
845 1588         1586 my $len = length($src);
846 1588         1647 my $out = '"';
847 1588         2126 while ($pos < $len) {
848 67214         56437 my $ch = substr($src, $pos, 1);
849 67214 100       80837 if ($ch eq '\\') {
    100          
    100          
850 1247 50       1749 my $nx = ($pos + 1 < $len) ? substr($src, $pos + 1, 1) : '';
851 1247 50       1511 $out .= ($nx eq "\n") ? "\\\n" : 'XX';
852 1247         1478 $pos += 2;
853             }
854             elsif ($ch eq '"') {
855 1584         1513 $out .= '"'; $pos++; last;
  1584         1357  
  1584         1647  
856             }
857             elsif ($ch eq "\n") {
858 753         575 $out .= "\n"; $pos++;
  753         702  
859             }
860             else {
861 63630         47646 $out .= 'X'; $pos++;
  63630         62591  
862             }
863             }
864 1588         2909 return ($out, $pos - $start);
865             }
866              
867             sub _mask_squote {
868 1804     1804   2422 my ($src, $start) = @_;
869 1804         1801 my $pos = $start + 1;
870 1804         1677 my $len = length($src);
871 1804         1660 my $out = "'";
872 1804         2360 while ($pos < $len) {
873 83191         68677 my $ch = substr($src, $pos, 1);
874 83191 100       100189 if ($ch eq '\\') {
    100          
    100          
875 311 50       402 my $nx = ($pos + 1 < $len) ? substr($src, $pos + 1, 1) : '';
876 311 100 100     689 $out .= ($nx eq "'" || $nx eq '\\') ? 'XX' : ('\\' . $nx);
877 311         372 $pos += 2;
878             }
879             elsif ($ch eq "'") {
880 1800         1601 $out .= "'"; $pos++; last;
  1800         1598  
  1800         1866  
881             }
882             elsif ($ch eq "\n") {
883 2109         1487 $out .= "\n"; $pos++;
  2109         1971  
884             }
885             else {
886 78971         59618 $out .= 'X'; $pos++;
  78971         77886  
887             }
888             }
889 1804         3351 return ($out, $pos - $start);
890             }
891              
892             sub _mask_delimited {
893 38     38   81 my ($src, $start, $open, $close) = @_;
894 38         60 my $pos = $start + 1;
895 38         74 my $len = length($src);
896 38         49 my $depth = 1;
897 38         56 my $out = $open;
898 38   100     123 while ($pos < $len && $depth > 0) {
899 987         1050 my $ch = substr($src, $pos, 1);
900 987 50 100     2291 if ($ch eq '\\') {
    100          
    100          
    100          
901 0         0 $out .= 'XX'; $pos += 2;
  0         0  
902             }
903             elsif ($open ne $close && $ch eq $open) {
904 1         2 $depth++; $out .= $open; $pos++;
  1         2  
  1         2  
905             }
906             elsif ($ch eq $close) {
907 39         54 $depth--; $out .= $close; $pos++;
  39         48  
  39         87  
908             }
909             elsif ($ch eq "\n") {
910 20         24 $out .= "\n"; $pos++;
  20         28  
911             }
912             else {
913 927         872 $out .= 'X'; $pos++;
  927         1687  
914             }
915             }
916 38         104 return ($out, $pos - $start);
917             }
918              
919             sub _matching_delim {
920 517     517   715 my ($o) = @_;
921 517 50       1418 return '{' eq $o ? '}' : '(' eq $o ? ')' :
    50          
    100          
    100          
922             '[' eq $o ? ']' : '<' eq $o ? '>' : $o;
923             }
924              
925             # ======================================================================
926             # _install_runtime_guards()
927             # ======================================================================
928             sub _install_runtime_guards {
929 9 100   9   144 unless ($_OPEN_GUARDED) {
930 7         24 $_OPEN_GUARDED = 1;
931 24     17   314 no strict 'refs';
  24         45  
  24         2266  
932 7         44 *{'CORE::GLOBAL::open'} = \&_guarded_open;
  7         96  
933             }
934 9 100       35 unless ($_MKDIR_GUARDED) {
935 7         11 $_MKDIR_GUARDED = 1;
936 24     17   105 no strict 'refs';
  24         159  
  24         3338  
937 7         12 *{'CORE::GLOBAL::mkdir'} = \&_guarded_mkdir;
  7         90  
938             }
939 9         3055 return;
940             }
941              
942             sub _guarded_open {
943 19 100   19   772139 if (@_ >= 3) {
944 13         76 my ($p, $f, $l) = caller(0);
945 13         177 die "Perl500503Syntax::OrDie: RUNTIME VIOLATION at $f line $l:\n"
946             . " 3-argument open() is not supported in Perl 5.005_03\n";
947             }
948 19 100 66     25 if (@_ >= 2 && ref $_[1]) {
949 2         12 my ($p, $f, $l) = caller(0);
950 2         19 die "Perl500503Syntax::OrDie: RUNTIME VIOLATION at $f line $l:\n"
951             . " open() with a reference as mode is not supported"
952             . " in Perl 5.005_03\n";
953             }
954 24     17   107 no strict 'refs';
  24         79  
  17         20292  
955 19 50       9 if (@_ == 1) {
956 0         0 my $ofn = 'CORE::' . 'open';
957 0         0 return &{$ofn}($_[0]);
  0         0  
958             }
959 19         5331 return CORE::open($_[0], $_[1]);
960             }
961              
962             sub _guarded_mkdir {
963 6 100   6   436 if (@_ < 2) {
964 3         13 my ($p, $f, $l) = caller(0);
965 3         18 die "Perl500503Syntax::OrDie: RUNTIME VIOLATION at $f line $l:\n"
966             . " mkdir() requires an explicit mode argument in Perl 5.005_03\n";
967             }
968 6         378 return CORE::mkdir($_[0], $_[1]);
969             }
970              
971             # ======================================================================
972             # Public API
973             # ======================================================================
974             sub check_file {
975 3     3 1 1083 my $first = shift;
976 3 100 66     31 my $path = ($first eq 'Perl500503Syntax::OrDie' || ref $first) ? shift : $first;
977 3         11 my @v = _check_file($path);
978 3 100       8 if (@v) {
979 1         10 die join('', @v);
980             }
981 2         7 return;
982             }
983              
984             sub check_source {
985 10     10 1 406 my $first = shift;
986 10         15 my ($src, $label);
987 10 100 66     40 if ($first eq 'Perl500503Syntax::OrDie' || ref $first) {
988 2         5 ($src, $label) = @_;
989             }
990             else {
991 8         14 ($src, $label) = ($first, shift);
992             }
993 10         21 my @violations = _check_source($src, $label);
994 10         36 return @violations;
995             }
996              
997             # ======================================================================
998             # Command-line interface
999             # ======================================================================
1000             sub _run_as_command {
1001 7 50 66 7   55 if (!@ARGV || ($ARGV[0] eq '--help') || ($ARGV[0] eq '-h')) {
      66        
1002 1         3 print "Usage: perl Perl500503Syntax/OrDie.pm [ ...]\n";
1003 1         2 print " perl Perl500503Syntax/OrDie.pm -\n";
1004 1         4 print "\n";
1005 1         2 print " Check each for constructs not available in Perl 5.005_03.\n";
1006 1         1 print " Use '-' to read from standard input.\n";
1007 1         1 print " Violations are reported with file name and line number.\n";
1008 1         2 print "\n";
1009 1         1 print "Example:\n";
1010 1         5 print " perl lib/Perl500503Syntax/OrDie.pm myscript.pl\n";
1011 1         0 exit 0;
1012             }
1013              
1014 6         8 my $ok = 0;
1015 6         7 my $fail = 0;
1016 6         23 my $sep = '-' x 60;
1017 6         17 for my $path (@ARGV) {
1018 7         29 print "$sep\n";
1019 7         7 my @violations;
1020 7 50       190 if ($path eq '-') {
    50          
1021 0         0 print "Checking: (standard input)\n";
1022 0         0 my $source = do { local $/; };
  0         0  
  0         0  
1023 0         0 @violations = _check_source($source, '(stdin)');
1024             }
1025             elsif (!-f $path) {
1026 0         0 print "ERROR: file not found: $path\n";
1027 0         0 $fail++;
1028 0         0 next;
1029             }
1030             else {
1031 7         16 print "Checking: $path\n";
1032 7         42 @violations = _check_file($path);
1033             }
1034 7 100       20 if (@violations) {
1035 4         19 print join('', @violations);
1036 4         9 $fail++;
1037             }
1038             else {
1039 3         12 print " -> No violations found.\n";
1040 3         23 $ok++;
1041             }
1042             }
1043 6         18 print "$sep\n";
1044 6         14 my $total = $ok + $fail;
1045 6         18 print "Results: $ok/$total passed";
1046 6 100       18 if ($fail) {
1047 4         7 print ", $fail failed";
1048             }
1049 6         10 print "\n";
1050 6 100         exit($fail ? 1 : 0);
1051             }
1052              
1053              
1054             _run_as_command() if $0 eq __FILE__;
1055              
1056             1;
1057              
1058             __END__