File Coverage

blib/lib/CPANfile/Parse/PPI.pm
Criterion Covered Total %
statement 99 100 99.0
branch 52 58 89.6
condition 11 12 91.6
subroutine 12 12 100.0
pod 1 1 100.0
total 175 183 95.6


line stmt bran cond sub pod time code
1             package CPANfile::Parse::PPI;
2              
3             # ABSTRACT: Parse Is with PPI
4              
5 12     12   4921 use strict;
  12         80  
  12         302  
6 12     12   51 use warnings;
  12         19  
  12         428  
7              
8             our $VERSION = '0.06'; # VERSION
9              
10 12     12   54 use Carp qw(carp croak);
  12         17  
  12         1116  
11 12     12   70 use List::Util qw(first any);
  12         22  
  12         1497  
12 12     12   6727 use Moo;
  12         133333  
  12         52  
13 12     12   36340 use PPI;
  12         1228202  
  12         10405  
14              
15             my $strict;
16              
17             has meta => (
18             is => 'ro',
19             default => sub { +{} },
20             isa => sub {
21             die if 'HASH' ne ref $_[0];
22             }
23             );
24              
25             has modules => (
26             is => 'ro',
27             isa => sub {
28             die if 'ARRAY' ne ref $_[0];
29             }
30             );
31              
32              
33             sub BUILDARGS {
34 31     31 1 261801 my ($class, $file_or_code) = @_;
35              
36 31         119 my ($meta, @modules) = _parse( $file_or_code );
37              
38             return {
39 28         20009 modules => \@modules,
40             meta => $meta,
41             };
42             }
43              
44             sub import {
45 12 100   12   92 $strict = 1 if grep{ $_ eq '-strict' }@_;
  15         390  
46             }
47              
48             sub _parse {
49 31     31   66 my ($file_or_code) = @_;
50              
51 31         240 my $doc = PPI::Document->new( $file_or_code );
52              
53             # 'feature' and 'on' are handled separately
54 31         360860 my @bindings = qw(
55             mirror osname
56             requires recommends conflicts suggests
57             test_requires author_requires configure_requires build_requires
58             );
59              
60             my $requires = $doc->find(
61             sub {
62 3777 100   3777   33825 $_[1]->isa('PPI::Token::Word') and do {
63 432         770 my $content = $_[1]->content;
64 432         2022 first { $content eq $_ } @bindings;
  2221         3110  
65             }
66             }
67 31         383 );
68              
69 31 50       432 return if !$requires;
70              
71 31         54 my @modules;
72 31         202 my $meta = {};
73              
74             REQUIRED:
75 31 50       78 for my $required ( @{ $requires || [] } ) {
  31         190  
76              
77             # 'mirror' can be an attribute for "requires" as well as a keyword
78             # _scan_attrs should have removed all 'mirrors' that are used as
79             # an attribute for 'requires'. So skip those PPI nodes...
80 304 50       735 next REQUIRED if !$required;
81              
82              
83 304         683 my $value = $required->snext_sibling;
84              
85 304         5672 my $type = $required->content;
86 304         1227 my %on_feature = (
87             on => '',
88             feature => '',
89             );
90              
91 304 100 66     971 if ( $type eq 'mirror' or $type eq 'osname' ) {
92 2 50       7 push @{ $meta->{$type} }, $value->content if $value;
  0         0  
93             }
94              
95 304 100       698 if ( -1 != index $type, '_' ) {
96 2         7 (my $stage, $type) = split /_/, $type, 2;
97 2 100       5 $stage = 'develop' if $stage eq 'author';
98 2         4 $on_feature{on} = $stage;
99             }
100              
101 304         504 my %attr = _scan_attrs( $required, $type );
102              
103 304 100       720 next REQUIRED if !$value;
104              
105 302 100       856 my $can_string = $value->can('string') ? 1 : 0;
106 302 100       798 my $prereq = $can_string ?
107             $value->string :
108             $value->content;
109              
110             #next REQUIRED if $prereq eq 'perl';
111              
112 302 100 100     2805 if (
113             $value->isa('PPI::Token::Symbol') ||
114             $prereq =~ m{\A[^A-Za-z]}
115             ) {
116 6 100       71 carp 'Cannot handle dynamic code' if !$strict;
117 6 100       2013 croak 'Cannot handle dynamic code' if $strict;
118              
119 3         12 next REQUIRED;
120             }
121              
122 296         403 my $parent_node = $value;
123              
124             PARENT:
125 296         314 while ( 1 ) {
126 593         1163 $parent_node = $parent_node->parent;
127 593 50       2639 last PARENT if !$parent_node;
128 593 100       1502 last PARENT if $parent_node->isa('PPI::Document');
129              
130 435 100       917 if ( $parent_node->isa('PPI::Structure::Block') ) {
131 139         280 $parent_node = $parent_node->parent;
132             my ($on_feature) = $parent_node->find_first(
133             sub {
134             # need to create token var because 'any' messes up $_
135 161     161   2555 my $token = $_[1];
136             $token->isa('PPI::Token::Word')
137             && (
138 146         337 any { $token->content eq $_ }
139 161 100       815 (qw{on feature})
140             );
141             }
142 139         879 );
143 139 100       2022 if ($on_feature) {
144 138         275 my $word = $on_feature->snext_sibling;
145 138 100       2515 my $condition
146             = $word->can('string')
147             ? $word->string
148             : $word->content;
149 138         697 $on_feature{ $on_feature->content } = $condition;
150 138         439 last PARENT;
151              
152             }
153             else {
154 1         3 next PARENT;
155             }
156              
157             }
158             }
159              
160 296         371 my $version = '';
161 296         492 my $sibling = $value->snext_sibling;
162             SIBLING:
163 296         4769 while ( 1 ) {
164 365 100       727 last SIBLING if !$sibling;
165              
166 296 100       700 do { $sibling = $sibling->snext_sibling; next SIBLING } if !$sibling->isa('PPI::Token::Operator');
  69         150  
  69         1231  
167              
168 227         364 my $value = $sibling->snext_sibling;
169 227 100       4110 last SIBLING if !$value;
170              
171 222 100       730 $version = $value->can('string') ? $value->string : $value->content;
172              
173 222         1009 last SIBLING;
174             }
175              
176             push @modules, {
177             name => $prereq,
178             version => $version,
179             type => $type,
180             stage => $on_feature{on},
181             feature => $on_feature{feature},
182 296         1709 %attr,
183             };
184             }
185              
186 28         256 return $meta, @modules;
187             }
188              
189             sub _scan_attrs {
190 304     304   456 my ($required, $type) = @_;
191              
192 304 100 100     603 return if $type ne 'requires' && $type ne 'recommends';
193              
194 299         501 my $sibling = $required->snext_sibling;
195              
196 299         5074 my %attr;
197             my @to_delete;
198 299         0 my $delete;
199              
200 299         647 while ( $sibling ) {
201 1108         16790 my $content = $sibling->content;
202 1108 100 100     4982 if ( $content eq 'mirror' or $content eq 'dist' ) {
203 8         12 $delete = 1;
204 8         17 my $value_node = $sibling->snext_sibling->snext_sibling;
205 8 50       392 $attr{$content} = $value_node->can('string') ?
206             $value_node->string :
207             $value_node->content;
208             }
209              
210 1108 100       1585 push @to_delete, $sibling if $delete;
211 1108         2005 $sibling = $sibling->snext_sibling;
212             }
213              
214 299         6355 $_->remove for @to_delete;
215              
216 299         1633 return %attr;
217             }
218              
219             1;
220              
221             __END__