File Coverage

blib/lib/Grep/Query/Parser.pm
Criterion Covered Total %
statement 92 93 98.9
branch 36 42 85.7
condition 9 11 81.8
subroutine 12 12 100.0
pod 0 1 0.0
total 149 159 93.7


line stmt bran cond sub pod time code
1             package Grep::Query::Parser;
2              
3 9     9   57 use strict;
  9         17  
  9         241  
4 9     9   43 use warnings;
  9         16  
  9         412  
5              
6             our $VERSION = '1.009';
7             $VERSION = eval $VERSION;
8              
9 9     9   46 use Carp;
  9         18  
  9         815  
10             our @CARP_NOT = qw(Grep::Query);
11              
12 9     9   3747 use Grep::Query::Parser::QOPS;
  9         23  
  9         282  
13 9     9   10567 use Parse::RecDescent;
  9         396818  
  9         75  
14 9     9   5446 use IO::Scalar;
  9         34157  
  9         565  
15 9     9   71 use Scalar::Util qw(blessed reftype looks_like_number);
  9         18  
  9         9583  
16              
17             my $PARSER;
18              
19             sub parsequery
20             {
21 437     437 0 838 my $query = shift;
22              
23             # we keep the actual parser as a singleton in case it's used multiple times
24             #
25 437 100       1146 if (!$PARSER)
26             {
27 8         36 local $/ = undef;
28 8         186 my $grammar = ;
29 8         70 local $Parse::RecDescent::skip = qr#(?ms:\s+|/\*.*?\*/)*#;
30 8         85 $PARSER = Parse::RecDescent->new($grammar);
31 8 50       410926 die("Failed to parse query grammar") unless defined($PARSER);
32             }
33              
34             # since diagnostics/errors during actual parsing go to STDERR, we use an in memory trap for that
35             # so we can present it the way we want it
36             #
37 437         766 my $parsedQuery;
38 437         905 my $fieldRefs = [];
39 437         2998 my $errholder = IO::Scalar->new();
40             {
41 437         19551 local *STDERR = $errholder;
  437         1399  
42 437         3493 $parsedQuery = $PARSER->parsequery($query);
43             }
44            
45             # if we didn't get a parse tree, the query syntax is probably wrong, so report that
46             #
47 437 100       2451035 if (!$parsedQuery)
48             {
49             # make sure we have some form of string, and make sure it doesn't end in a newline, since that
50             # causes croak to drop the file/line information
51             #
52 14   50     52 my $capturedError = "$errholder" || 'UNKNOWN ERROR';
53 14         198 do {} while (chomp($capturedError));
54 14 50       166 croak($capturedError) unless $parsedQuery;
55             }
56              
57             # we need the parse tree in a somewhat simplified and predigested format for the actual ops
58             #
59 423         1581 __preprocessParsedQuery($parsedQuery, $fieldRefs);
60              
61             # ensure that the query either uses field names for every test, or not at all
62             #
63 421         838 my $oldFieldRefCount = scalar(@$fieldRefs);
64 421 50       989 if ($oldFieldRefCount)
65             {
66 421         765 @$fieldRefs = grep { defined($_) } @$fieldRefs;
  900         2193  
67 421         766 my $newFieldRefCount = scalar(@$fieldRefs);
68 421 100 100     1566 croak("Query must use field names for all matches or none") if ($newFieldRefCount && $newFieldRefCount != $oldFieldRefCount)
69             }
70 420         1219 $fieldRefs = [ __uniq(@$fieldRefs) ];
71            
72 420         2176 return ($parsedQuery, $fieldRefs);
73             }
74              
75             # recursively dig down in the parse tree and simplify by removing items we don't really need,
76             # rename some to simpler forms, and predigest the tests
77             #
78             sub __preprocessParsedQuery
79             {
80 7082     7082   10496 my $parsedQuery = shift;
81 7082         8986 my $fieldRefs = shift;
82            
83 7082         13919 my $r = reftype($parsedQuery);
84 7082 100       12486 if ($r)
85             {
86 5265 100       11960 if ($r eq 'ARRAY')
    100          
87             {
88 308         643 foreach my $i (@$parsedQuery)
89             {
90 479         909 __preprocessParsedQuery($i, $fieldRefs);
91             }
92             }
93             elsif ($r eq 'HASH')
94             {
95 3541         5734 delete($parsedQuery->{__RULE__});
96 3541         4686 delete($parsedQuery->{lparen});
97            
98 3541         14230 foreach my $altk (grep(/^_alternation_/, keys(%$parsedQuery)))
99             {
100 1260         2429 my $alt = $parsedQuery->{$altk};
101 1260         1957 delete($parsedQuery->{$altk});
102 1260         1907 my $keep = 1;
103 1260 100 100     5515 $keep = 0 if (ref($alt) eq 'ARRAY' && scalar(@$alt) == 0);
104 1260 50 66     3518 $keep = 0 if (blessed($alt) && $alt->{rparen});
105 1260 100       3127 if ($keep)
106             {
107 308 50       823 die("PRE-EXISTING '__ALT'?") if exists($parsedQuery->{__ALT});
108 308         771 $parsedQuery->{__ALT} = $alt;
109             }
110             }
111            
112 3541         7801 foreach my $k (keys(%$parsedQuery))
113             {
114             # the actual tests needs to be predigested a bit - ensure the regexps are compiled, insert
115             # subs that will do the actual comparisons etc.
116             #
117 6182 100       11331 if ($k eq 'field_op_value_test')
118             {
119 902         2297 push(@$fieldRefs, $parsedQuery->{$k}->{field});
120 902         1687 my $op = $parsedQuery->{$k}->{op};
121 902 100       5448 if ($op eq 'true')
    100          
    100          
    100          
    50          
122             {
123 17         1399 $parsedQuery->{$k}->{op} = eval "sub { 1 }";
124             }
125             elsif ($op eq 'false')
126             {
127 13         924 $parsedQuery->{$k}->{op} = eval "sub { 0 }";
128             }
129             elsif ($op =~ /^(?:regexp|=~)$/)
130             {
131 517         1511 $parsedQuery->{$k}->{value} = __compileRx($parsedQuery->{$k}->{value});
132 516         1207 $parsedQuery->{$k}->{op} = __getAnonWithOp('=~');
133             }
134             elsif ($op =~ /^(?:eq|ne|[lg][te])$/)
135             {
136 124         251 $parsedQuery->{$k}->{op} = __getAnonWithOp($op);
137             }
138             elsif ($op =~ /^(?:[=!<>]=|<|>)$/)
139             {
140 231         540 my $possibleNumber = $parsedQuery->{$k}->{value};
141 231 100       971 croak("Not a number for '$op': '$possibleNumber'") unless looks_like_number($possibleNumber);
142 230         512 $parsedQuery->{$k}->{op} = __getAnonWithOp($op);
143             }
144             else
145             {
146 0         0 die("Unexpected op: '$op'");
147             }
148             }
149 6180         12373 __preprocessParsedQuery($parsedQuery->{$k}, $fieldRefs);
150             }
151             }
152             }
153              
154 7076         11689 return $parsedQuery;
155             }
156              
157             # helpers
158             #
159             sub __uniq
160             {
161 420     420   714 my %seen;
162 420         1178 grep( { !$seen{$_}++ } @_ );
  541         2034  
163             }
164              
165             sub __compileRx
166             {
167 517     517   1008 my $re = shift;
168            
169 517         796 my $cre;
170 517 100       885 if (! eval { $cre = qr/$re/ })
  517         7404  
171             {
172 1         10 $@ =~ /^(.+)\sat\s/;
173 1         17 croak("Bad regular expression:\n $re\n $1");
174             }
175            
176 516         1497 return($cre);
177             }
178              
179             sub __getAnonWithOp
180             {
181 870     870   1408 my $op = shift;
182              
183 870         76722 return eval "sub { defined(\$_[0]) ? \$_[0] $op \$_[1] : 0 }";
184             }
185              
186             1;
187              
188             __DATA__