File Coverage

blib/lib/PPIx/XPath.pm
Criterion Covered Total %
statement 196 241 81.3
branch 22 38 57.8
condition 5 27 18.5
subroutine 64 97 65.9
pod 3 38 7.8
total 290 441 65.7


line stmt bran cond sub pod time code
1             package PPIx::XPath;
2 3     3   341378 use strict;
  3         3  
  3         79  
3 3     3   11 use warnings;
  3         3  
  3         78  
4 3     3   493 use PPI;
  3         87827  
  3         52  
5 3     3   12 use Carp;
  3         3  
  3         137  
6 3     3   11 use Scalar::Util qw(reftype blessed);
  3         8  
  3         108  
7 3     3   1484 use Tree::XPathEngine;
  3         45014  
  3         60  
8 3     3   57 use 5.006;
  3         6  
9             our $VERSION = '2.02'; # VERSION
10              
11             # ABSTRACT: an XPath implementation for the PDOM
12              
13              
14             sub new {
15 1     1 1 87 my ($class,$source) = @_;
16              
17 1 50       4 croak "PPIx::XPath->new needs a source document" unless defined($source);
18              
19 1         1 my $doc;
20 1 50 33     9 if (blessed($source) && $source->isa('PPI::Node')) {
    50 0        
      33        
21 0         0 $doc = $source;
22             }
23             elsif (reftype($source) eq 'SCALAR'
24             or (!ref($source) && -f $source)) {
25 1         8 $doc = PPI::Document->new($source);
26             }
27             else {
28 0   0     0 croak "PPIx::XPath expects either a PPI::Node or a file" .
29             " got a: [" .( ref($source) || $source ). ']';
30             }
31              
32 1         2256 return bless {doc=>$doc},$class;
33             }
34              
35              
36             {
37             my $legacy_names_rx;my %new_name_for;
38             sub clean_xpath_expr {
39 3     3 1 4 my (undef,$expr)=@_;
40              
41 3         29 $expr =~ s{$legacy_names_rx}{$new_name_for{$1}}ge;
  5         18  
42              
43 3         6 return $expr;
44             }
45              
46             my @PPI_Packs;
47             # taken from Devel::Symdump
48             # breadth-first search of packages under C
49             my @packages=('PPI');
50             while (my $pack=shift(@packages)) {
51             my %pack_symbols = do {
52 3     3   12 no strict 'refs'; ## no critic(ProhibitNoStrict)
  3         4  
  3         981  
53             %{*{"$pack\::"}}
54             };
55             while (my ($key,$val)=each(%pack_symbols)) {
56             # that {HASH} lookup is special for typeglobs, so we have
57             # to alias a local typeglob to make it work
58             local *ENTRY=$val;
59             # does this symbol table entry look like a sub-package?
60             if (defined $val && defined *ENTRY{HASH} && $key=~/::$/
61             && $key !~ /^::/
62             && $key ne 'main::' && $key ne '::') {
63              
64             # add it to the search list
65             my $p = "$pack\::$key";$p =~ s{::$}{};
66             push @packages,$p;
67              
68             # and add it to the map of names
69             $p =~ s{^PPI::}{};
70             next unless $p=~/::/;
71              
72             my $newname=$p;
73             $newname =~ s{::}{-}g;
74             push @PPI_Packs,$p;
75             $new_name_for{$p}=$newname;
76             }
77             }
78             }
79              
80             # @PPI_Packs now contains all the old-style names, build a regex
81             # to match them (the sort is important, we want to match longer
82             # names first!)
83             $legacy_names_rx='\b('.join(q{|},
84             sort {length($b) <=> length($a)} @PPI_Packs
85             ).')\b';
86             $legacy_names_rx=qr{$legacy_names_rx};
87             }
88              
89              
90             sub match {
91 3     3 1 1051 my ($self,$expr) = @_;
92              
93 3         7 $expr=$self->clean_xpath_expr($expr);
94              
95 3         11 Tree::XPathEngine->new()->findnodes($expr,$self->{doc});
96             }
97              
98              
99             package PPI::Element; ## no critic(ProhibitMultiplePackages)
100 3     3   13 use strict;
  3         3  
  3         121  
101 3     3   24 use warnings;
  3         4  
  3         2198  
102              
103 67     67 0 214 sub xpath_get_name { my $pack_name=substr($_[0]->class,5);
104 67         222 $pack_name =~ s/::/-/g;
105 67         108 $pack_name }
106 0     0 0 0 sub xpath_get_next_sibling { $_[0]->snext_sibling }
107 0     0 0 0 sub xpath_get_previous_sibling { $_[0]->sprevious_sibling }
108 0     0 0 0 sub xpath_get_root_node { $_[0]->top }
109 0     0 0 0 sub xpath_get_parent_node { $_[0]->parent }
110 67     67 0 772 sub xpath_is_element_node { 1 }
111 0     0 0 0 sub xpath_is_attribute_node { 0 }
112 0     0 0 0 sub xpath_is_document_node { 0 }
113             sub xpath_get_attributes {
114             return
115 4     4 0 9 PPIx::XPath::Attr->new($_[0],'significant'),
116             PPIx::XPath::Attr->new($_[0],'content'),
117             }
118 0     0 0 0 sub xpath_to_literal { "$_[0]" }
119 0     0 0 0 sub xpath_get_value { "$_[0]" }
120 1     1 0 55 sub xpath_string_value { "$_[0]" }
121              
122             sub xpath_cmp {
123 63     63 0 565 my( $a, $b)= @_;
124 63 50       213 if ( UNIVERSAL::isa( $b, 'PPIx::XPath::Attr')) {
    50          
125             # elt <=> att, compare the elt to the att->{elt}
126             # if the elt is the att->{elt} (cmp return 0) then -1, elt is before att
127 0   0     0 return ($a->_xpath_elt_cmp( $b->{parent}) ) || -1 ;
128             }
129             elsif ( UNIVERSAL::isa( $b, 'PPI::Document')) {
130             # elt <=> document, elt is after document
131 0         0 return 1;
132             } else {
133             # 2 elts, compare them
134 63         83 return $a->_xpath_elt_cmp( $b);
135             }
136             }
137              
138             sub _xpath_elt_cmp {
139 63     63   40 my ($a,$b)=@_;
140              
141             # easy cases
142 63 50       75 return 0 if( $a == $b);
143 63 100       228 return 1 if( $a->_xpath_in($b)); # a starts after b
144 48 100       309 return -1 if( $b->_xpath_in($a)); # a starts before b
145              
146             # ancestors does not include the element itself
147 42         262 my @a_pile= ($a, $a->_xpath_ancestors);
148 42         57 my @b_pile= ($b, $b->_xpath_ancestors);
149              
150             # the 2 elements are not in the same twig
151 42 50       47 return undef unless( $a_pile[-1] == $b_pile[-1]);
152              
153             # find the first non common ancestors (they are siblings)
154 42         144 my $a_anc= pop @a_pile;
155 42         28 my $b_anc= pop @b_pile;
156              
157 42         47 while( $a_anc == $b_anc) {
158 83         218 $a_anc= pop @a_pile;
159 83         112 $b_anc= pop @b_pile;
160             }
161              
162             # from there move left and right and figure out the order
163 42         124 my( $a_prev, $a_next, $b_prev, $b_next)= ($a_anc, $a_anc, $b_anc, $b_anc);
164 42         27 while () {
165 42   100     64 $a_prev= $a_prev->sprevious_sibling || return( -1);
166 19 100       260 return 1 if( $a_prev == $b_next);
167 3   50     17 $a_next= $a_next->snext_sibling || return( 1);
168 3 50       40 return -1 if( $a_next == $b_prev);
169 0   0     0 $b_prev= $b_prev->sprevious_sibling || return( 1);
170 0 0       0 return -1 if( $b_prev == $a_next);
171 0   0     0 $b_next= $b_next->snext_sibling || return( -1);
172 0 0       0 return 1 if( $b_next == $a_prev);
173             }
174             }
175              
176             sub _xpath_in {
177 111     111   68 my ($self, $ancestor)= @_;
178 111         146 while ( $self= $self->parent) {
179 250 100       1160 return $self if ( $self == $ancestor);
180             }
181             }
182              
183             sub _xpath_ancestors {
184 84     84   57 my( $self)= @_;
185 84         48 my @ancestors;
186 84         100 while ( $self= $self->parent) {
187 213         640 push @ancestors, $self;
188             }
189 84         233 return @ancestors;
190             }
191              
192             package PPI::Token; ## no critic(ProhibitMultiplePackages)
193 3     3   14 use strict;
  3         3  
  3         89  
194 3     3   12 use warnings;
  3         8  
  3         161  
195              
196 28     28 0 518 sub xpath_get_child_nodes { return }
197              
198             package PPI::Token::Quote::Double; ## no critic(ProhibitMultiplePackages)
199 3     3   11 use strict;
  3         3  
  3         42  
200 3     3   8 use warnings;
  3         2  
  3         155  
201              
202             sub xpath_get_attributes {
203 0     0 0 0 return $_[0]->SUPER::xpath_get_attributes,
204             PPIx::XPath::Attr->new($_[0],'interpolations'),
205             }
206              
207             package PPI::Token::Number; ## no critic(ProhibitMultiplePackages)
208 3     3   9 use strict;
  3         3  
  3         46  
209 3     3   7 use warnings;
  3         2  
  3         148  
210              
211             sub xpath_get_attributes {
212 0     0 0 0 return $_[0]->SUPER::xpath_get_attributes,
213             PPIx::XPath::Attr->new($_[0],'base'),
214             }
215              
216             package PPI::Token::Word; ## no critic(ProhibitMultiplePackages)
217 3     3   8 use strict;
  3         4  
  3         41  
218 3     3   7 use warnings;
  3         2  
  3         140  
219              
220             sub xpath_get_attributes {
221 0     0 0 0 return $_[0]->SUPER::xpath_get_attributes,
222             PPIx::XPath::Attr->new($_[0],'method-call'),
223             }
224              
225             package PPI::Token::Comment; ## no critic(ProhibitMultiplePackages)
226 3     3   10 use strict;
  3         2  
  3         42  
227 3     3   7 use warnings;
  3         3  
  3         124  
228              
229             sub xpath_get_attributes {
230 0     0 0 0 return $_[0]->SUPER::xpath_get_attributes,
231             PPIx::XPath::Attr->new($_[0],'line'),
232             }
233              
234             package PPI::Token::HereDoc; ## no critic(ProhibitMultiplePackages)
235 3     3   9 use strict;
  3         2  
  3         40  
236 3     3   7 use warnings;
  3         3  
  3         128  
237              
238             # TODO: add access to the contents of the heredoc (->heredoc method)
239              
240             sub xpath_get_attributes {
241 0     0 0 0 return $_[0]->SUPER::xpath_get_attributes,
242             PPIx::XPath::Attr->new($_[0],'terminator'),
243             }
244              
245             package PPI::Token::Prototype; ## no critic(ProhibitMultiplePackages)
246 3     3   9 use strict;
  3         2  
  3         38  
247 3     3   9 use warnings;
  3         13  
  3         250  
248              
249 0     0 0 0 sub xpath_to_literal { $_[0]->prototype }
250 0     0 0 0 sub xpath_get_value { $_[0]->prototype }
251 0     0 0 0 sub xpath_string_value { $_[0]->prototype }
252              
253             package PPI::Node; ## no critic(ProhibitMultiplePackages)
254 3     3   13 use strict;
  3         8  
  3         71  
255 3     3   9 use warnings;
  3         2  
  3         220  
256              
257 42     42 0 1704 sub xpath_get_child_nodes { $_[0]->schildren }
258             sub xpath_get_attributes {
259 4     4 0 8 return $_[0]->SUPER::xpath_get_attributes,
260             PPIx::XPath::Attr->new($_[0],'scope'),
261             }
262              
263             package PPI::Token::Attribute; ## no critic(ProhibitMultiplePackages)
264 3     3   10 use strict;
  3         2  
  3         44  
265 3     3   10 use warnings;
  3         2  
  3         159  
266              
267             sub xpath_get_attributes {
268 0     0 0 0 return $_[0]->SUPER::xpath_get_attributes,
269             PPIx::XPath::Attr->new($_[0],'identifier'),
270             PPIx::XPath::Attr->new($_[0],'parameters'),
271             }
272              
273             package PPI::Token::Symbol; ## no critic(ProhibitMultiplePackages)
274 3     3   8 use strict;
  3         3  
  3         41  
275 3     3   7 use warnings;
  3         4  
  3         197  
276              
277             sub xpath_get_attributes {
278 0     0 0 0 return $_[0]->SUPER::xpath_get_attributes,
279             PPIx::XPath::Attr->new($_[0],'symbol'),
280             PPIx::XPath::Attr->new($_[0],'canonical'),
281             PPIx::XPath::Attr->new($_[0],'raw_type'),
282             PPIx::XPath::Attr->new($_[0],'symbol_typel'),
283             }
284              
285             package PPI::Statement; ## no critic(ProhibitMultiplePackages)
286 3     3   9 use strict;
  3         22  
  3         83  
287 3     3   9 use warnings;
  3         3  
  3         248  
288              
289             sub xpath_get_attributes {
290 4     4 0 6 return $_[0]->SUPER::xpath_get_attributes,
291             PPIx::XPath::Attr->new($_[0],'label'),
292             PPIx::XPath::Attr->new($_[0],'stable'),
293             PPIx::XPath::Attr->new($_[0],'type'),
294             }
295              
296             package PPI::Statement::Sub; ## no critic(ProhibitMultiplePackages)
297 3     3   10 use strict;
  3         3  
  3         48  
298 3     3   6 use warnings;
  3         4  
  3         211  
299              
300             sub xpath_get_attributes {
301 4     4 0 371 return $_[0]->SUPER::xpath_get_attributes,
302             PPIx::XPath::Attr->new($_[0],'name'),
303             PPIx::XPath::Attr->new($_[0],'prototype'),
304             PPIx::XPath::Attr->new($_[0],'forward'),
305             PPIx::XPath::Attr->new($_[0],'reserved'),
306             }
307              
308             package PPI::Statement::Package; ## no critic(ProhibitMultiplePackages)
309 3     3   11 use strict;
  3         4  
  3         51  
310 3     3   7 use warnings;
  3         5  
  3         163  
311              
312             sub xpath_get_attributes {
313 0     0 0 0 return $_[0]->SUPER::xpath_get_attributes,
314             PPIx::XPath::Attr->new($_[0],'namespace'),
315             PPIx::XPath::Attr->new($_[0],'file-scoped'),
316             }
317              
318             package PPI::Statement::Include; ## no critic(ProhibitMultiplePackages)
319 3     3   12 use strict;
  3         3  
  3         44  
320 3     3   7 use warnings;
  3         3  
  3         225  
321              
322             sub xpath_get_attributes {
323 0     0 0 0 return $_[0]->SUPER::xpath_get_attributes,
324             PPIx::XPath::Attr->new($_[0],'module'),
325             PPIx::XPath::Attr->new($_[0],'module-version'),
326             PPIx::XPath::Attr->new($_[0],'version'),
327             PPIx::XPath::Attr->new($_[0],'version-literal'),
328             PPIx::XPath::Attr->new($_[0],'pragma'),
329             }
330              
331             package PPI::Structure; ## no critic(ProhibitMultiplePackages)
332 3     3   10 use strict;
  3         3  
  3         42  
333 3     3   6 use warnings;
  3         3  
  3         172  
334              
335             sub xpath_get_attributes {
336 0     0 0 0 return $_[0]->SUPER::xpath_get_attributes,
337             PPIx::XPath::Attr->new($_[0],'start'),
338             PPIx::XPath::Attr->new($_[0],'finish'),
339             PPIx::XPath::Attr->new($_[0],'braces'),
340             }
341              
342             package PPI::Document; ## no critic(ProhibitMultiplePackages)
343 3     3   9 use strict;
  3         2  
  3         40  
344 3     3   7 use warnings;
  3         3  
  3         181  
345              
346 7     7 0 12743 sub xpath_get_root_node { $_[0] }
347 0     0 0 0 sub xpath_get_parent_node { return }
348 0     0 0 0 sub xpath_is_attribute_node { 0 }
349 0     0 0 0 sub xpath_is_document_node { 1 }
350              
351             package PPIx::XPath::Attr; ## no critic(ProhibitMultiplePackages)
352 3     3   9 use strict;
  3         3  
  3         34  
353 3     3   10 use warnings;
  3         6  
  3         839  
354              
355             sub new {
356 40     40   69 my ($class,$parent,$name)=@_;
357              
358 40         65 my $meth=$parent->can($name);
359 40 100       57 return unless $meth;
360              
361 36         26 my $value;
362 36 100       24 eval {$value=$meth->($parent);1} or return;
  36         53  
  32         548  
363              
364 32 100       42 return unless defined $value;
365              
366 28         92 return bless {parent=>$parent,name=>$name,value=>$value},$class;
367             }
368              
369 28     28   173 sub xpath_get_name { $_[0]->{name} }
370 0     0   0 sub xpath_get_root_node { $_[0]->{parent}->top }
371 0     0   0 sub xpath_get_parent_node { $_[0]->{parent} }
372 0     0   0 sub xpath_is_element_node { 0 }
373 0     0   0 sub xpath_is_attribute_node { 1 }
374 0     0   0 sub xpath_is_document_node { 0 }
375 0     0   0 sub xpath_to_literal { $_[0]->{value} }
376 0     0   0 sub xpath_get_value { $_[0]->{value} }
377 4     4   220 sub xpath_string_value { $_[0]->{value} }
378 0     0     sub xpath_to_number { Tree::XPathEngine::Number->new($_[0]->{value}) }
379              
380             sub xpath_cmp {
381 0     0     my( $a, $b)= @_;
382 0 0         if ( UNIVERSAL::isa( $b, 'PPIx::XPath::Attr')) {
    0          
383             # 2 attributes, compare their elements, then their name
384             return ($a->{parent}->_xpath_elt_cmp( $b->{parent}) )
385 0   0       || ($a->{name} cmp $b->{name});
386             }
387             elsif ( UNIVERSAL::isa( $b, 'PPI::Document')) {
388             # att <=> document, att is after document
389 0           return 1;
390             }
391             else {
392             # att <=> elt : compare the att->elt and the elt
393             # if att->elt is the elt (cmp returns 0) then 1 (elt is before att)
394 0   0       return ($a->{parent}->_xpath_elt_cmp( $b) ) || 1 ;
395             }
396             }
397              
398             1;
399              
400             __END__