File Coverage

blib/lib/Filter/Syntactic.pm
Criterion Covered Total %
statement 61 71 85.9
branch 6 12 50.0
condition 5 10 50.0
subroutine 10 11 90.9
pod n/a
total 82 104 78.8


line stmt bran cond sub pod time code
1             package Filter::Syntactic;
2              
3 5     5   784865 use 5.022;
  5         22  
4 5     5   46 use warnings;
  5         9  
  5         554  
5              
6             our $VERSION = '0.000002';
7              
8 5     5   3120 use Filter::Simple;
  5         104305  
  5         33  
9 5     5   5653 use PPR::X;
  5         424321  
  5         340  
10 5     5   3190 use experimental 'signatures';
  5         18812  
  5         32  
11              
12 15     15   30 sub _expected ($what = q{}) {
  15         78  
  15         30  
13 15         20 state ($expected, $unexpected);
14 15 100       41 if ($what) {
15 12         27 $expected = $what;
16 12 50       19 $unexpected = do { substr($_,pos()) =~ m{ \A \s* (\S \N*) } ? $1 : substr($_,pos(),20) };
  12         1336  
17             }
18             else {
19 3         11 my $expectation = qq{Expected $expected but found "$unexpected"};
20 3         10 $expected = $unexpected = q{};
21 3         9 return $expectation;
22             }
23             }
24              
25             # Extract context information...
26 13     13   28 sub _line_comment ($str, $pos, $line_offset) {
  13         26  
  13         25  
  13         20  
  13         24  
27 13   100     50 $pos //= 0;
28 13         53 return "\n#" . _line_loc($str, $pos, $line_offset) . "\n";
29             }
30              
31 23     23   249000 sub _line_loc ($str, $pos, $line_offset) {
  23         48  
  23         39  
  23         35  
  23         33  
32 23   50     57 $pos //= 0;
33 23         198 my $line_num = $line_offset + (substr($str,0,$pos) =~ tr/\n//);
34 23         268 return "line $line_num";
35             }
36              
37             sub import {
38             # Generated filters use subroutine signatures (which were experimental until 5.36)...
39             if ($] < 5.36) {
40             experimental->import('signatures');
41             }
42             }
43              
44             FILTER {
45             return if m{ \A __(DATA|END)__ \n }xms;
46              
47             # Remember where we parked...
48             my ($filename, $start_line) = (caller 1)[1,2];
49              
50             # What filter blocks look like...
51             my @filters;
52             my $PERL_WITH_FILTER_BLOCKS = qr{
53             \A (?&PerlEntireDocument) \z
54              
55             (?(DEFINE)
56             (? (?>
57             filter \b
58             (?>
59             (?<_> (?>(?&PerlNWS))
60             (?{ _expected('PPR rule name') })
61             (?(? [A-Za-z_]++ )) (?>(?&PerlOWS))
62             (?{ _expected('mode, rule, or code block') })
63             (? :extend | ) (?>(?&PerlOWS))
64             (?{ _expected('rule or code block') })
65             (? \( (?>(?&PPR_X_balanced_parens)) \) )?+ (?>(?&PerlOWS))
66             (?{ _expected('code block') })
67             (? (?>(?&PerlBlock)) )
68             )
69             (?{ _expected();
70             my $len = length($+{_}) + 6;
71             push @filters, { POS => pos() - $len, LEN => $len, END => pos(),
72             BLOCKPOS => pos() - length($+{BLOCK}), %+ };
73             })
74             |
75             (?{ push @filters, { POS => pos() - 6, EXPECTED => _expected(), INVALID => 1 } })
76             )
77             |
78             (?>(?&PerlStdControlBlock))
79             ))
80             )
81              
82             $PPR::X::GRAMMAR
83             }xms;
84              
85             # Did we find any???
86             if (/$PERL_WITH_FILTER_BLOCKS/) {
87              
88             # Delete all the filters, reporting bad filters, but leaving the line numbers unchanged...
89             my $invalid;
90             for my $filter (reverse @filters) {
91             if ($filter->{INVALID}) {
92             substr($_, $filter->{POS}, 0)
93             = qq{BEGIN { die "Invalid filter specification. \Q$filter->{EXPECTED}\E" } };
94             $invalid = 1;
95             }
96             else {
97             substr($_, $filter->{POS}, $filter->{LEN}) =~ tr/\n/ /c;
98             }
99             }
100             return if $invalid;
101              
102             # Normalize filters...
103             for my $filter (@filters) {
104              
105             $filter->{RULENAME} =~ s{ \A (?:Perl)?+ (.*) \z }{Perl$1}xms;
106             $filter->{STDNAME} = $filter->{RULENAME} =~ s{ \A Perl }{PerlStd}xmsr;
107             $filter->{REGEX} //= "(?&$filter->{STDNAME})";
108             my $active_regex = $filter->{REGEX}
109             =~ s{ \(\?\(DEFINE\) (?>(?&PPR_X_balanced_parens)) \)
110             $PPR::X::GRAMMAR }{}gxmsr;
111             my @captures = _uniq($active_regex =~ m{ \(\?< \K [^>]++ }gxms);
112             $filter->{CAPTURES} = \@captures;
113             $filter->{UNPACK} = @captures > 1 ? '[@+{'. join(',', map { "'$_'" } @captures) .'}]'
114             : @captures == 1 ? qq{[\$+{'$captures[0]'}]}
115             : q{[]};
116             my $PARAMS = join ',', map { '$'.$_ } @captures;
117             $filter->{HANDLER} = qq{sub ($PARAMS)}
118             . _line_comment($_,$filter->{BLOCKPOS},$start_line)
119             . $filter->{BLOCK};
120             }
121              
122             # Build progressive regexes for each filter...
123             my ($PATTERN, $SELFPATTERN);
124             for my $f (keys @filters) {
125             my $filter = $filters[$f];
126              
127             # The pattern for this filter needs to capture match information...
128             $SELFPATTERN = $filter->{REGEX};
129             $PATTERN = qq{
130             (?<_> $filter->{REGEX} )
131             (?{ my \$len = length(\$+{_});
132             push \@Filter::Syntactic::captures, { RULENAME => '$filter->{RULENAME}',
133             CAPTURES => $filter->{UNPACK},
134             MATCH => \$+{_},
135             POS => pos() - \$len,
136             LEN => \$len,
137             END => pos(),
138             }
139             })
140             };
141              
142             # If this filter extends a current rule, it needs to include the standard syntax...
143             if ($filter->{MODE} eq ':extend') {
144             $PATTERN .= qq{ | (?>(?&$filter->{STDNAME})) };
145             }
146              
147             # The reparsing rule ALWAYS includes the standard syntax...
148             # (because it's reparsing partially transformed source code, which may be standard Perl)
149             $SELFPATTERN .= qq{ | (?>(?&$filter->{STDNAME})) };
150              
151             # Then we wrap it in the appropriately named subrule...
152             $PATTERN = qq{ (?<$filter->{RULENAME}> $PATTERN ) };
153             $SELFPATTERN = qq{ (?<$filter->{RULENAME}> $SELFPATTERN ) };
154              
155             # The filter also needs to recognize any new syntax for any later filters...
156             my $SELFEXTRAS = q{};
157             for my $next_filter (@filters[$f+1..$#filters]) {
158             my $NEXT_PAT = $next_filter->{REGEX};
159             $NEXT_PAT = $next_filter->{MODE} eq ':extend'
160             ? qq{ (?<$next_filter->{RULENAME}> $NEXT_PAT | (?>(?&$next_filter->{STDNAME}))) }
161             : qq{ (?<$next_filter->{RULENAME}> $NEXT_PAT ) };
162              
163             $PATTERN .= $NEXT_PAT;
164             $SELFEXTRAS .= $NEXT_PAT;
165             }
166              
167             # And the filter's version of the full document-parsing regex gets saved in the filter...
168             $filter->{FULLREGEX}
169             = qq{ \\A (?&PerlEntireDocument) \\z (?(DEFINE) $PATTERN ) \$PPR::X::GRAMMAR };
170             $filter->{SELFREGEX}
171             = qq{ \\A $SELFPATTERN \\z (?(DEFINE) $SELFEXTRAS ) \$PPR::X::GRAMMAR };
172             }
173              
174             # Build handlers...
175             for my $filter (@filters) {
176             my $PARAMS = join ',', map { '$'.$_ } @{$filter->{CAPTURES}};
177             my $__LINE__ = _line_loc($_,$filter->{POS},$start_line);
178             $filter->{HANDLER} = qq{sub ($PARAMS)}
179             . _line_comment($_,$filter->{BLOCKPOS},$start_line)
180             . qq{ { # Check for nested replacements...
181             if (\$_ ne \$_{MATCH}) {
182             if (m{$filter->{SELFREGEX}}xms) {
183             ($PARAMS) = \@{$filter->{UNPACK}};
184             }
185             else {
186             warn 'filter $filter->{NAME} from ', __PACKAGE__,
187             ' (', __FILE__, ' $__LINE__)',
188             ' is not recursively self-consistent at ',
189             "\$_{LOC}\n";
190             }
191             }
192              
193             # Execute the transformation...
194             $filter->{BLOCK};
195             }
196             }
197             . _line_comment($_,$filter->{END},$start_line);
198             }
199              
200             # Build the lookup table of transformation handlers for each filter...
201             my $LUT = q{my %_HANDLER = (}
202             . join(',', map { qq{ '$_->{RULENAME}' => $_->{HANDLER} } } @filters)
203             . q{);};
204              
205             # Build replacement processing loops...
206             my $FIRST_FILTER = 1;
207             my $PROC_LOOPS = q{ my ($filename, $start_line); };
208             for my $filter (@filters) {
209             $PROC_LOOPS .= q{ local @Filter::Syntactic::captures;
210             ($filename, $start_line) = (caller 1)[1,2];
211             }
212             . qq{ if (m{$filter->{FULLREGEX}}xms) }
213             . (q{ {
214             # Index captures and generate error message context info...
215             my $index = 1;
216             for my $capture (sort {$a->{POS} <=> $b->{POS}} @Filter::Syntactic::captures) {
217             $capture->{ORD} = $index++;
218             $capture->{LOC} = qq{$filename }
219             . Filter::Syntactic::_line_loc(
220             $_, $capture->{POS}, $start_line
221             );
222             }
223              
224             # Identify and record any nested captures...
225             for my $c (reverse keys @Filter::Syntactic::captures) {
226             my $capture = $Filter::Syntactic::captures[$c];
227              
228             POSSIBLE_OUTER:
229             for my $prev (@Filter::Syntactic::captures[reverse 0..$c-1]) {
230             last POSSIBLE_OUTER if $prev->{END} < $capture->{POS};
231             if ($capture->{END} > $prev->{END}) {
232             push @{$prev->{OUTERS}}, $capture;
233             use Scalar::Util 'weaken';
234             weaken($prev->{OUTERS}[-1]);
235             }
236             }
237             }
238              
239             # Install replacement code and any adjust outer captures...
240             for my $capture
241             (sort {$b->{POS} <=> $a->{POS}} @Filter::Syntactic::captures) {
242             # Generate replacement code...
243             my $replacement = do {
244             local $_ = substr($_, $capture->{POS}, $capture->{LEN});
245             local *_ = $capture;
246             $_HANDLER{ $capture->{RULENAME} }(@{$capture->{CAPTURES}});
247             };
248              
249             # Replace capture...
250             substr($_, $capture->{POS}, $capture->{LEN}) = $replacement;
251              
252             # Adjust length of surrounding captures...
253             my $delta = length($replacement) - $capture->{LEN};
254             for my $outer (@{$capture->{OUTERS}}) {
255             $outer->{LEN} += $delta;
256             }
257             }
258             if ($_debugging) {
259             Filter::Syntactic::_debug(
260             'Before filter ' => $_prev_under,
261             ' After filter ' => $_,
262             );
263             $_prev_under = $_;
264             }
265             }
266             } =~ s{}{$filter->{NAME}}gr
267             )
268             . ( $FIRST_FILTER
269             ? q{ else {
270             # Failure to parse the initial source code is an external issue...
271             my $error = $PPR::X::ERROR->origin($start_line, $filename);
272             my $diagnostic = "syntax error at $filename line " . $error->line;
273             $diagnostic .= qq{\nnear: }
274             . ($error->source =~ s{ \A (\s* \S \N* ) .* }{$1}xmsr
275             =~ tr/\n/ /r)
276             if $diagnostic !~ /, near/;
277             die "$diagnostic\n";
278             }
279             }
280             : q{ else {
281             # Report the (presumably) filter-induced syntax error...
282             my $error = $PPR::X::ERROR->origin($start_line, $filename);
283             my $diagnostic = "syntax error at $filename line " . $error->line;
284             $diagnostic .= qq{\nnear: }
285             . ($error->source =~ s{ \A (\s* \S \N* ) .* }{$1}xmsr
286             =~ tr/\n/ /r)
287             if $diagnostic !~ /, near/;
288             die "Possible problem with source filter at ",
289             (caller 1)[1] . " line ", ($start_line-1) . "\n",
290             "\n$diagnostic\n",
291             "(possibly the result of source filtering by ",
292             __PACKAGE__ . " at line " . ($start_line-1) . ")\n";
293             }
294             }
295             );
296             $FIRST_FILTER = 0;
297             }
298              
299             # Create a final syntax check after all the filters have been applied...
300             my $FINAL_CHECK = q{
301             if ($_ !~ m{ \A (?>(?&PerlEntireDocument)) \z $PPR::X::GRAMMAR }xms) {
302             # Report that the final transformation isn't valid Perl...
303             my ($file, $line) = (caller 1)[1,2]; $line--;
304             my $error = $PPR::X::ERROR->origin($start_line, $filename);
305             my $diagnostic = "syntax error at $filename line " . $error->line;
306             $diagnostic .= qq{\nnear: }
307             . ($error->source =~ s{ \A (\s* \S \N* ) .* }{$1}xmsr
308             =~ tr/\n/ /r)
309             if $diagnostic !~ /, near/;
310             die "Possible problem with source filter at $file line $line\n",
311             "\n$diagnostic\n",
312             "(possibly the result of source filtering by " . __PACKAGE__ . " at line $line)\n";
313             }
314             } =~ s{}{$start_line - 1}gre;
315              
316             # If there was more than one filter, debug the final state...
317             if (@filters > 1) {
318             $FINAL_CHECK .= q{
319             Filter::Syntactic::_debug(
320             'Initial source' => $_initial_under, ' Final source' => $_, "final"
321             ) if $_debugging;
322             }
323             }
324              
325             # Put the entire source filter together...
326             my $FILTER = qq{
327             use Filter::Simple;
328              
329             FILTER {
330             # Handle options...
331             my \$_debugging = \@_ && \$_[1] && \$_[1] eq '-debug';
332             if (!\$_debugging && \$_[1]) {
333             warn "Unknown option: \$_[1] at " . join(' line ', (caller 1)[1,2]) . "\n";
334             }
335              
336             # Prep for debugging...
337             my \$_prev_under = \$_;
338             my \$_initial_under = \$_;
339              
340             # Build filter...
341             $LUT;
342             $PROC_LOOPS
343             $FINAL_CHECK
344             } { terminator => "" };
345             };
346              
347             # Install new filter, adjusting line reporting...
348             substr ($_, $filters[0]{POS}//0, 0)
349             = $FILTER . _line_comment($_, $filters[0]{POS}, $start_line);
350             }
351             else {
352             # Report syntax error...
353             my $error = $PPR::X::ERROR->origin($start_line, $filename);
354             my $diagnostic = $error->diagnostic || "syntax error at $filename line " . $error->line;
355             $diagnostic .= qq{\nnear: } . ($error->source =~ tr/\n/ /r) if $diagnostic !~ /, near/;
356             die "$diagnostic\n";
357             }
358             } {terminator => ""};
359              
360 3     3   12 sub _uniq (@list) {
  3         13  
  3         7  
361 3         7 my %seen;
362 3         16 return grep {!$seen{$_}++} @list;
  4         32  
363             }
364              
365 1     1   110 sub _debug ($pre_label, $pre, $post_label, $post, $is_final = 0) {
  1         3  
  1         5  
  1         2  
  1         2  
  1         3  
  1         2  
366             # Set up the (possibly paged) output stream for debugging info...
367 1         3 state $DBOUT = do {
368 1         3 my $fh;
369 1 50 33     15 if ($ENV{DIFFPAGER} && open $fh, "|$ENV{DIFFPAGER}") { $fh }
  0 50 33     0  
370 0         0 elsif ($ENV{PAGER} && open $fh, "|$ENV{PAGER}" ) { $fh }
371 1         4 else { \*STDERR }
372             };
373              
374             # If we can diff, then diff...
375 1 50       5 if (eval { require Text::Diff }) {
  1 0       853  
376 1         10944 print {$DBOUT} "--- $pre_label\n+++ $post_label\n",
  1         8  
377             Text::Diff::diff(\$pre, \$post) . "\n";
378             }
379              
380             # Otherwise, just print out each post-transformation source (except the last)...
381             elsif (!$is_final) {
382 0           print {$DBOUT} '=====[ '. _trim($post_label) . " ]========================\n\n$post\n";
  0            
383             }
384              
385             # For the last, just rule a line under the previous output (which will be identical)...
386             else {
387 0           print {$DBOUT} ('=' x 50), "\n\n";
  0            
388             }
389             }
390              
391 0     0     sub _trim ($str) {
  0            
  0            
392 0           return $str =~ s{^\s+}{}r =~ s{\s*$}{}r;
393             }
394              
395             1; # Magic true value required at end of module
396             __END__