File Coverage

blib/lib/Grep/Query/Parser.pm
Criterion Covered Total %
statement 114 120 95.0
branch 53 66 80.3
condition 11 16 68.7
subroutine 19 20 95.0
pod 0 1 0.0
total 197 223 88.3


line stmt bran cond sub pod time code
1             package Grep::Query::Parser;
2              
3 10     10   69 use strict;
  10         17  
  10         279  
4 10     10   47 use warnings;
  10         15  
  10         511  
5              
6             our $VERSION = '1.011';
7             $VERSION = eval $VERSION;
8              
9 10     10   56 use Carp;
  10         17  
  10         921  
10             our @CARP_NOT = qw(Grep::Query);
11              
12 10     10   4322 use Grep::Query::Parser::QOPS;
  10         24  
  10         305  
13 10     10   12195 use Parse::RecDescent;
  10         457771  
  10         70  
14 10     10   5701 use IO::Scalar;
  10         38790  
  10         488  
15 10     10   79 use Scalar::Util qw(blessed reftype looks_like_number);
  10         21  
  10         1038  
16 10     10   5472 use Data::DPath qw(dpath);
  10         986803  
  10         87  
17              
18             my $PARSER;
19              
20             sub parsequery
21             {
22 440     440 0 719 my $query = shift;
23              
24             # we keep the actual parser as a singleton in case it's used multiple times
25             #
26 440 100       1020 if (!$PARSER)
27             {
28 9         38 local $/ = undef;
29 9         293 my $grammar = ;
30 9         80 local $Parse::RecDescent::skip = qr#(?ms:\s+|/\*.*?\*/)*#;
31 9         78 $PARSER = Parse::RecDescent->new($grammar);
32 9 50       474317 die("Failed to parse query grammar") unless defined($PARSER);
33             }
34              
35             # since diagnostics/errors during actual parsing go to STDERR, we use an in memory trap for that
36             # so we can present it the way we want it
37             #
38 440         679 my $parsedQuery;
39 440         840 my $fieldRefs = [];
40 440         2546 my $errholder = IO::Scalar->new();
41             {
42 440         18022 local *STDERR = $errholder;
  440         1141  
43 440         3036 $parsedQuery = $PARSER->parsequery($query);
44             }
45            
46             # if we didn't get a parse tree, the query syntax is probably wrong, so report that
47             #
48 440 100       2322038 if (!$parsedQuery)
49             {
50             # make sure we have some form of string, and make sure it doesn't end in a newline, since that
51             # causes croak to drop the file/line information
52             #
53 14   50     63 my $capturedError = "$errholder" || 'UNKNOWN ERROR';
54 14         149 do {} while (chomp($capturedError));
55 14 50       159 croak($capturedError) unless $parsedQuery;
56             }
57              
58             # we need the parse tree in a somewhat simplified and predigested format for the actual ops
59             #
60 426         1540 __preprocessParsedQuery($parsedQuery, $fieldRefs);
61              
62             # ensure that the query either uses field names for every test, or not at all
63             #
64 424         736 my $oldFieldRefCount = scalar(@$fieldRefs);
65 424 50       878 if ($oldFieldRefCount)
66             {
67 424         761 @$fieldRefs = grep { defined($_) } @$fieldRefs;
  867         2082  
68 424         688 my $newFieldRefCount = scalar(@$fieldRefs);
69 424 100 100     1380 croak("Query must use field names for all matches or none") if ($newFieldRefCount && $newFieldRefCount != $oldFieldRefCount)
70             }
71 423         912 $fieldRefs = [ __uniq(@$fieldRefs) ];
72            
73 423         2005 return ($parsedQuery, $fieldRefs);
74             }
75              
76             # recursively dig down in the parse tree and simplify by removing items we don't really need,
77             # rename some to simpler forms, and predigest the tests
78             #
79             sub __preprocessParsedQuery
80             {
81 6816     6816   9856 my $parsedQuery = shift;
82 6816         8596 my $fieldRefs = shift;
83            
84 6816         12779 my $r = reftype($parsedQuery);
85 6816 100       12235 if ($r)
86             {
87 5059 100       11335 if ($r eq 'ARRAY')
    100          
88             {
89 296         561 foreach my $i (@$parsedQuery)
90             {
91 443         823 __preprocessParsedQuery($i, $fieldRefs);
92             }
93             }
94             elsif ($r eq 'HASH')
95             {
96 3419         5454 delete($parsedQuery->{__RULE__});
97 3419         5071 delete($parsedQuery->{lparen});
98            
99 3419         13543 foreach my $altk (grep(/^_alternation_/, keys(%$parsedQuery)))
100             {
101 1240         2231 my $alt = $parsedQuery->{$altk};
102 1240         1923 delete($parsedQuery->{$altk});
103 1240         1769 my $keep = 1;
104 1240 100 100     5041 $keep = 0 if (ref($alt) eq 'ARRAY' && scalar(@$alt) == 0);
105 1240 50 66     3236 $keep = 0 if (blessed($alt) && $alt->{rparen});
106 1240 100       2968 if ($keep)
107             {
108 296 50       685 die("PRE-EXISTING '__ALT'?") if exists($parsedQuery->{__ALT});
109 296         738 $parsedQuery->{__ALT} = $alt;
110             }
111             }
112            
113 3419         7743 foreach my $k (keys(%$parsedQuery))
114             {
115             # the actual tests needs to be predigested a bit - ensure the regexps are compiled, insert
116             # subs that will do the actual comparisons etc.
117             #
118 5949 100       10669 if ($k eq 'field_op_value_test')
119             {
120 869         2086 push(@$fieldRefs, $parsedQuery->{$k}->{field});
121 869         1582 my $op = $parsedQuery->{$k}->{op};
122 869 100       6800 if ($op eq 'true')
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    50          
123             {
124 17     190   96 $parsedQuery->{$k}->{op} = sub { 1 };
  190         439  
125             }
126             elsif ($op eq 'false')
127             {
128 13     190   75 $parsedQuery->{$k}->{op} = sub { 0 };
  190         361  
129             }
130             elsif ($op eq 'defined')
131             {
132 1 100   4   15 $parsedQuery->{$k}->{op} = sub { defined($_[0]) ? 1 : 0 };
  4         17  
133             }
134             elsif ($op eq 'path')
135             {
136 18         32 my $key = $parsedQuery->{$k}->{value};
137 18     54   78 $parsedQuery->{$k}->{op} = sub { scalar(dpath($key)->match($_[2]) ) };
  54         172  
138             }
139             elsif ($op eq 'exists')
140             {
141 0         0 my $key = $parsedQuery->{$k}->{value};
142 0 0 0 0   0 $parsedQuery->{$k}->{op} = sub { ( defined($_[0]) && ref($_[0]) eq 'HASH' ) ? exists($_[0]->{$key}) : 0 };
  0         0  
143             }
144             elsif ($op eq 'type')
145             {
146 2         4 my $v = $parsedQuery->{$k}->{value};
147 2 50       14 croak("Bad value for '$op' => '$v', must be one of: 'scalar', 'array' or 'hash'") unless $v =~ /^(?:scalar|array|hash)$/i;
148 2 100 100 6   10 $parsedQuery->{$k}->{op} = sub { defined($_[0]) ? (lc($v) eq (lc(ref($_[0])) || 'scalar') ) : 0 };
  6         35  
149             }
150             elsif ($op =~ /^size(.+)/)
151             {
152 1         5 my $compop = $1;
153 1         2 my $possibleNumber = $parsedQuery->{$k}->{value};
154 1 50       7 croak("Not a number for '$op': '$possibleNumber'") unless looks_like_number($possibleNumber);
155 1         7 my $comparator = __getAnonWithOp($compop);
156             $parsedQuery->{$k}->{op} =
157             sub
158             {
159 5     5   25 my $reftype = ref($_[0]);
160             my $sz = ($reftype eq 'ARRAY')
161 0         0 ? scalar(@{$_[0]})
162             : ($reftype eq 'HASH')
163 5 50       16 ? scalar(keys(%{$_[0]}))
  0 50       0  
164             : length($_[0]);
165 5         98 $comparator->($sz, $possibleNumber);
166 1         13 };
167             }
168             elsif ($op =~ /^(?:regexp|=~)$/)
169             {
170 478         1190 $parsedQuery->{$k}->{value} = __compileRx($parsedQuery->{$k}->{value});
171 477         968 $parsedQuery->{$k}->{op} = __getAnonWithOp('=~');
172             }
173             elsif ($op =~ /^(?:eq|ne|[lg][te])$/)
174             {
175 126         281 $parsedQuery->{$k}->{op} = __getAnonWithOp($op);
176             }
177             elsif ($op =~ /^(?:[=!<>]=|<|>)$/)
178             {
179 213         467 my $possibleNumber = $parsedQuery->{$k}->{value};
180 213 100       754 croak("Not a number for '$op': '$possibleNumber'") unless looks_like_number($possibleNumber);
181 212         454 $parsedQuery->{$k}->{op} = __getAnonWithOp($op);
182             }
183             else
184             {
185 0         0 die("Unexpected op: '$op'");
186             }
187             }
188 5947         11747 __preprocessParsedQuery($parsedQuery->{$k}, $fieldRefs);
189             }
190             }
191             }
192              
193 6810         11093 return $parsedQuery;
194             }
195              
196             # helpers
197             #
198             sub __uniq
199             {
200 423     423   602 my %seen;
201 423         993 grep( { !$seen{$_}++ } @_ );
  506         1745  
202             }
203              
204             sub __compileRx
205             {
206 478     478   818 my $re = shift;
207            
208 478         664 my $cre;
209 478 100       759 if (! eval { $cre = qr/$re/ })
  478         5845  
210             {
211 1         8 $@ =~ /^(.+)\sat\s/;
212 1         17 croak("Bad regular expression:\n $re\n $1");
213             }
214            
215 477         1346 return($cre);
216             }
217              
218             sub __getAnonWithOp
219             {
220 816     816   1368 my $op = shift;
221              
222 816         66192 return eval "sub { defined(\$_[0]) ? \$_[0] $op \$_[1] : 0 }";
223             }
224              
225             1;
226              
227             __DATA__