File Coverage

blib/lib/Grep/Query/Parser.pm
Criterion Covered Total %
statement 108 114 94.7
branch 51 64 79.6
condition 11 16 68.7
subroutine 17 18 94.4
pod 0 1 0.0
total 187 213 87.7


line stmt bran cond sub pod time code
1             package Grep::Query::Parser;
2              
3 10     10   52 use strict;
  10         17  
  10         244  
4 10     10   38 use warnings;
  10         14  
  10         353  
5              
6             our $VERSION = '1.010';
7             $VERSION = eval $VERSION;
8              
9 10     10   42 use Carp;
  10         23  
  10         785  
10             our @CARP_NOT = qw(Grep::Query);
11              
12 10     10   3469 use Grep::Query::Parser::QOPS;
  10         21  
  10         278  
13 10     10   9618 use Parse::RecDescent;
  10         372081  
  10         71  
14 10     10   5069 use IO::Scalar;
  10         31413  
  10         410  
15 10     10   64 use Scalar::Util qw(blessed reftype looks_like_number);
  10         20  
  10         12628  
16              
17             my $PARSER;
18              
19             sub parsequery
20             {
21 452     452 0 696 my $query = shift;
22              
23             # we keep the actual parser as a singleton in case it's used multiple times
24             #
25 452 100       989 if (!$PARSER)
26             {
27 9         39 local $/ = undef;
28 9         172 my $grammar = ;
29 9         65 local $Parse::RecDescent::skip = qr#(?ms:\s+|/\*.*?\*/)*#;
30 9         96 $PARSER = Parse::RecDescent->new($grammar);
31 9 50       391087 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 452         636 my $parsedQuery;
38 452         783 my $fieldRefs = [];
39 452         2698 my $errholder = IO::Scalar->new();
40             {
41 452         17746 local *STDERR = $errholder;
  452         1031  
42 452         3157 $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 452 100       2112797 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     37 my $capturedError = "$errholder" || 'UNKNOWN ERROR';
53 14         132 do {} while (chomp($capturedError));
54 14 50       134 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 438         1510 __preprocessParsedQuery($parsedQuery, $fieldRefs);
60              
61             # ensure that the query either uses field names for every test, or not at all
62             #
63 436         716 my $oldFieldRefCount = scalar(@$fieldRefs);
64 436 50       839 if ($oldFieldRefCount)
65             {
66 436         710 @$fieldRefs = grep { defined($_) } @$fieldRefs;
  921         1889  
67 436         690 my $newFieldRefCount = scalar(@$fieldRefs);
68 436 100 100     1380 croak("Query must use field names for all matches or none") if ($newFieldRefCount && $newFieldRefCount != $oldFieldRefCount)
69             }
70 435         945 $fieldRefs = [ __uniq(@$fieldRefs) ];
71            
72 435         1987 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 7240     7240   9312 my $parsedQuery = shift;
81 7240         8414 my $fieldRefs = shift;
82            
83 7240         11974 my $r = reftype($parsedQuery);
84 7240 100       10818 if ($r)
85             {
86 5378 100       10262 if ($r eq 'ARRAY')
    100          
87             {
88 314         583 foreach my $i (@$parsedQuery)
89             {
90 485         816 __preprocessParsedQuery($i, $fieldRefs);
91             }
92             }
93             elsif ($r eq 'HASH')
94             {
95 3621         5030 delete($parsedQuery->{__RULE__});
96 3621         4115 delete($parsedQuery->{lparen});
97            
98 3621         12601 foreach my $altk (grep(/^_alternation_/, keys(%$parsedQuery)))
99             {
100 1292         2101 my $alt = $parsedQuery->{$altk};
101 1292         1695 delete($parsedQuery->{$altk});
102 1292         1627 my $keep = 1;
103 1292 100 100     4869 $keep = 0 if (ref($alt) eq 'ARRAY' && scalar(@$alt) == 0);
104 1292 50 66     3173 $keep = 0 if (blessed($alt) && $alt->{rparen});
105 1292 100       2830 if ($keep)
106             {
107 314 50       710 die("PRE-EXISTING '__ALT'?") if exists($parsedQuery->{__ALT});
108 314         665 $parsedQuery->{__ALT} = $alt;
109             }
110             }
111            
112 3621         6953 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 6319 100       9679 if ($k eq 'field_op_value_test')
118             {
119 923         2020 push(@$fieldRefs, $parsedQuery->{$k}->{field});
120 923         1488 my $op = $parsedQuery->{$k}->{op};
121 923 100       6420 if ($op eq 'true')
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    50          
122             {
123 17     190   63 $parsedQuery->{$k}->{op} = sub { 1 };
  190         355  
124             }
125             elsif ($op eq 'false')
126             {
127 13     190   52 $parsedQuery->{$k}->{op} = sub { 0 };
  190         297  
128             }
129             elsif ($op eq 'defined')
130             {
131 1 100   4   5 $parsedQuery->{$k}->{op} = sub { defined($_[0]) ? 1 : 0 };
  4         12  
132             }
133             elsif ($op eq 'exists')
134             {
135 0         0 my $key = $parsedQuery->{$k}->{value};
136 0 0 0 0   0 $parsedQuery->{$k}->{op} = sub { ( defined($_[0]) && ref($_[0]) eq 'HASH' ) ? exists($_[0]->{$key}) : 0 };
  0         0  
137             }
138             elsif ($op eq 'type')
139             {
140 2         3 my $v = $parsedQuery->{$k}->{value};
141 2 50       11 croak("Bad value for '$op' => '$v', must be one of: 'scalar', 'array' or 'hash'") unless $v =~ /^(?:scalar|array|hash)$/i;
142 2 100 100 6   8 $parsedQuery->{$k}->{op} = sub { defined($_[0]) ? (lc($v) eq (lc(ref($_[0])) || 'scalar') ) : 0 };
  6         26  
143             }
144             elsif ($op =~ /^size(.+)/)
145             {
146 1         3 my $compop = $1;
147 1         3 my $possibleNumber = $parsedQuery->{$k}->{value};
148 1 50       6 croak("Not a number for '$op': '$possibleNumber'") unless looks_like_number($possibleNumber);
149 1         4 my $comparator = __getAnonWithOp($compop);
150             $parsedQuery->{$k}->{op} =
151             sub
152             {
153 5     5   8 my $reftype = ref($_[0]);
154             my $sz = ($reftype eq 'ARRAY')
155 0         0 ? scalar(@{$_[0]})
156             : ($reftype eq 'HASH')
157 5 50       28 ? scalar(keys(%{$_[0]}))
  0 50       0  
158             : length($_[0]);
159 5         89 $comparator->($sz, $possibleNumber);
160 1         6 };
161             }
162             elsif ($op =~ /^(?:regexp|=~)$/)
163             {
164 523         1266 $parsedQuery->{$k}->{value} = __compileRx($parsedQuery->{$k}->{value});
165 522         995 $parsedQuery->{$k}->{op} = __getAnonWithOp('=~');
166             }
167             elsif ($op =~ /^(?:eq|ne|[lg][te])$/)
168             {
169 126         245 $parsedQuery->{$k}->{op} = __getAnonWithOp($op);
170             }
171             elsif ($op =~ /^(?:[=!<>]=|<|>)$/)
172             {
173 240         486 my $possibleNumber = $parsedQuery->{$k}->{value};
174 240 100       932 croak("Not a number for '$op': '$possibleNumber'") unless looks_like_number($possibleNumber);
175 239         498 $parsedQuery->{$k}->{op} = __getAnonWithOp($op);
176             }
177             else
178             {
179 0         0 die("Unexpected op: '$op'");
180             }
181             }
182 6317         10591 __preprocessParsedQuery($parsedQuery->{$k}, $fieldRefs);
183             }
184             }
185             }
186              
187 7234         9998 return $parsedQuery;
188             }
189              
190             # helpers
191             #
192             sub __uniq
193             {
194 435     435   616 my %seen;
195 435         995 grep( { !$seen{$_}++ } @_ );
  560         1746  
196             }
197              
198             sub __compileRx
199             {
200 523     523   920 my $re = shift;
201            
202 523         680 my $cre;
203 523 100       807 if (! eval { $cre = qr/$re/ })
  523         6200  
204             {
205 1         8 $@ =~ /^(.+)\sat\s/;
206 1         15 croak("Bad regular expression:\n $re\n $1");
207             }
208            
209 522         1259 return($cre);
210             }
211              
212             sub __getAnonWithOp
213             {
214 888     888   1303 my $op = shift;
215              
216 888         67748 return eval "sub { defined(\$_[0]) ? \$_[0] $op \$_[1] : 0 }";
217             }
218              
219             1;
220              
221             __DATA__