File Coverage

blib/lib/Perl/PrereqScanner/Scanner/DistZilla/PluginBundle.pm
Criterion Covered Total %
statement 39 50 78.0
branch 8 22 36.3
condition 10 18 55.5
subroutine 9 10 90.0
pod 0 1 0.0
total 66 101 65.3


line stmt bran cond sub pod time code
1             # ABSTRACT: scan for required plugins in Dist::Zilla plugin bundles
2              
3             ######################################################################
4             # Copyright (C) 2021 Asher Gordon <AsDaGo@posteo.net> #
5             # #
6             # This program is free software: you can redistribute it and/or #
7             # modify it under the terms of the GNU General Public License as #
8             # published by the Free Software Foundation, either version 3 of #
9             # the License, or (at your option) any later version. #
10             # #
11             # This program is distributed in the hope that it will be useful, #
12             # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
14             # General Public License for more details. #
15             # #
16             # You should have received a copy of the GNU General Public License #
17             # along with this program. If not, see #
18             # <http://www.gnu.org/licenses/>. #
19             ######################################################################
20              
21             package Perl::PrereqScanner::Scanner::DistZilla::PluginBundle;
22             $Perl::PrereqScanner::Scanner::DistZilla::PluginBundle::VERSION = '0.001';
23 1     1   1683 use v5.18.0;
  1         4  
24 1     1   6 use strict;
  1         2  
  1         21  
25 1     1   6 use warnings;
  1         2  
  1         27  
26 1     1   5 use feature 'lexical_subs';
  1         3  
  1         125  
27 1     1   7 no warnings 'experimental::lexical_subs';
  1         2  
  1         41  
28 1     1   6 use namespace::autoclean;
  1         2  
  1         7  
29 1     1   97 use Moose;
  1         3  
  1         7  
30              
31             with 'Perl::PrereqScanner::Scanner';
32              
33             #pod =head1 DESCRIPTION
34             #pod
35             #pod This scanner will look for the following indicators:
36             #pod
37             #pod =begin :list
38             #pod
39             #pod * calls to the C<add_bundle> method
40             #pod
41             #pod * calls to the C<add_plugins> method
42             #pod
43             #pod =end :list
44             #pod
45             #pod Currently this only works for plugin bundles using the
46             #pod C<Dist::Zilla::Role::PluginBundle::Easy> role.
47             #pod
48             #pod =cut
49              
50             my $is_quote = sub {
51             my ($self) = @_;
52             $self->isa('PPI::Token::Quote') ||
53             $self->isa('PPI::Token::QuoteLike::Words');
54             };
55              
56             my $quote_contents = sub {
57             my ($self) = @_;
58             return $self->string if $self->isa('PPI::Token::Quote');
59             return $self->literal
60             if $self->isa('PPI::Token::QuoteLike::Words');
61             return;
62             };
63              
64             # Warn at the position of a certain element.
65             my sub warn_at {
66 0     0   0 my ($element, $msg) = @_;
67 0 0       0 unless ($msg =~ /\n$/) {
68 0         0 my ($file, $line) = map $element->$_,
69             qw(logical_filename logical_line_number);
70 0 0 0     0 $msg .= ' at' if defined ($file // $line);
71 0 0       0 $msg .= " $file" if defined $file;
72 0 0       0 $msg .= " line $line" if defined $line;
73 0         0 $msg .= "\n";
74             }
75 0         0 warn $msg;
76             }
77              
78             # Handle expressions and remove commas and fat arrows.
79             my sub process_list {
80 6 50 33 6   108 @_ = $_[0]->schildren if @_ == 1 && $_[0]->isa('PPI::Statement');
81 6   66     104 grep { ! ($_->isa('PPI::Token::Operator') && /^(?:,|=>)$/) } @_;
  27         170  
82             }
83              
84             # Return an actual hash from a parsed hash in PPI form.
85             my sub process_hash {
86             my %hash;
87             @_ = $_[0]->schildren if @_ == 1 && $_[0]->isa('PPI::Statement');
88              
89             my (@name, $value);
90             my $processing = 'name';
91             foreach my $element (@_) {
92             if ($element->isa('PPI::Token::Operator') &&
93             $element =~ /^(?:,|=>)$/) {
94             if ($processing eq 'name') {
95             $processing = 'value';
96             my @this_name = @name;
97             @name = ();
98              
99             my $allow_bareword = $element eq '=>';
100             my $msg = "Hash key (@this_name) is not a string";
101             $msg .= ' or bareword' if $allow_bareword;
102             unless (@this_name == 1) {
103             warn_at $element, $msg;
104             next;
105             }
106              
107             my ($name) = @this_name;
108             if ($name->isa('PPI::Token::Quote')) {
109             $name = $name->string;
110             }
111             elsif ($name->isa('PPI::Token::Word')) {
112             unless ($allow_bareword) {
113             warn_at $element, $msg;
114             next;
115             }
116             $name = $name->literal;
117             }
118             else {
119             warn_at $element, $msg;
120             next;
121             }
122              
123             $value = $hash{$name} = [];
124             }
125             else { # processing value
126             $processing = 'name';
127             undef $value;
128             }
129             }
130             else { # not an operator
131             if ($processing eq 'name') {
132             push @name, $element;
133             }
134             else { # processing value
135             push @$value, $element if defined $value;
136             }
137             }
138             }
139              
140             return %hash;
141             }
142              
143             my sub get_bundle_pkg {
144             my ($bundle) = @_;
145             $bundle =~ s/^\@?/Dist::Zilla::PluginBundle::/r;
146             }
147              
148             my %prefixes = (
149             '=' => '',
150             '%' => 'Dist::Zilla::Stash::',
151             '' => 'Dist::Zilla::Plugin::',
152             );
153             my $prefixes = join '|', map quotemeta, sort keys %prefixes;
154              
155             my sub get_plugin_pkg {
156             my ($plugin) = @_;
157             $plugin =~ s/^($prefixes)/$prefixes{$1}/r;
158             }
159              
160             # Plugin bundles that use other plugin bundles specified by
161             # options. TODO: Maybe add more of these.
162             my %parent_bundles = (
163             Filter => sub {
164             my ($opts_element, %opts) = @_;
165             my ($bundle, $version) = @opts{qw(-bundle -version)};
166             return unless defined $bundle;
167              
168             unless (@$bundle == 1) {
169             warn_at $bundle->[0] // $opts_element,
170             'No bundle given for -bundle key';
171             return;
172             }
173             ($bundle) = @$bundle;
174             unless ($bundle->isa('PPI::Token::Quote')) {
175             warn_at $bundle, 'Bundle is not a quoted string';
176             return;
177             }
178             $bundle = $bundle->string;
179              
180             if (defined $version) {
181             unless (@$version == 1) {
182             warn_at $version->[0] // $opts_element,
183             'No version given for -version key';
184             return;
185             }
186             ($version) = @$version;
187             if ($version->isa('PPI::Token::Quote')) {
188             $version = $version->string;
189             }
190             elsif ($version->isa('PPI::Token::Number')) {
191             $version = $version->literal;
192             }
193             else {
194             warn_at $version, 'Version is not a quoted string or number';
195             return;
196             }
197             }
198              
199             return [$bundle => $version];
200             }
201             );
202             $parent_bundles{"Dist::Zilla::PluginBundle::$_"} = delete $parent_bundles{$_}
203             foreach keys %parent_bundles;
204              
205             # Get plugins from an argument to add_plugins().
206             my sub get_plugins {
207             my ($arg) = @_;
208             return [$arg->$quote_contents] if $arg->$is_quote;
209             if ($arg->isa('PPI::Structure::Constructor')) {
210             return 'not an array reference'
211             unless ($arg->braces // '') eq '[]';
212             return 'array reference is empty'
213             unless my ($plugin, $opts) = process_list $arg->schildren;
214             return [($plugin->$quote_contents)[0]] if $plugin->$is_quote;
215             return [$plugin->literal] if $plugin->isa('PPI::Token::Word');
216             return 'first element of array reference ' .
217             'is not word or quoted string';
218             }
219             return 'not a quoted string or anonymous array reference';
220             }
221              
222             # Valid tokens and the subroutines to process their arguments.
223             my %tokens = (
224             add_bundle => sub {
225             my ($req, $bundle, $opts) = @_;
226              
227             return 'no arguments' unless defined $bundle;
228              
229             my $name;
230             if ($bundle->isa('PPI::Token::Word')) {
231             $name = $bundle->literal;
232             }
233             elsif ($bundle->isa('PPI::Token::Quote')) {
234             $name = $bundle->string;
235             }
236             else {
237             return [$bundle, "first argument ($bundle) not a " .
238             'bareword or quoted string'];
239             }
240             $name = get_bundle_pkg $name;
241              
242             $req->add_minimum($name => 0);
243              
244             # Get the plugin bundles used by this one, if any.
245             return unless my $get_children = $parent_bundles{$name};
246              
247             my %opts;
248             if ($opts->isa('PPI::Structure::Constructor') &&
249             ($opts->braces // '') eq '{}') {
250             %opts = process_hash $opts->schildren;
251             }
252             else {
253             warn_at $opts, 'Not an anonymous hash reference';
254             }
255              
256             foreach ($get_children->($opts, %opts)) {
257             my ($name, $version) = ref eq 'ARRAY' ? @$_ : $_;
258             $name = get_bundle_pkg $name;
259             $version //= 0;
260             $req->add_minimum($name => $version);
261             }
262              
263             return;
264             },
265              
266             add_plugins => sub {
267             my $req = shift;
268             $req->add_minimum((get_plugin_pkg $_) => 0) foreach map {
269             my $arg = $_[$_];
270             my $ret = get_plugins $arg;
271             return [$arg, "argument $_ ($arg): $ret"]
272             unless ref $ret eq 'ARRAY';
273             @$ret;
274             } 0 .. $#_;
275             return;
276             },
277             );
278              
279             sub scan_for_prereqs {
280 1     1 0 36454 my ($self, $ppi_doc, $req) = @_;
281              
282 1         3 foreach my $node (@{$ppi_doc->find('Statement')}) {
  1         9  
283 28         7323 my @children = $node->schildren;
284 28         406 my $found_arrow;
285 28   100     110 until ($found_arrow || ! @children) {
286 107         515 my $op = shift @children;
287 107   100     472 $found_arrow = $op->isa('PPI::Token::Operator') &&
288             $op eq '->';
289             }
290 28 100       249 next unless $found_arrow;
291              
292 5 50       20 next unless my $name = shift @children;
293 5 100       15 next unless my $add = $tokens{$name};
294              
295 4         24 my ($args) = @children;
296 4         6 my $err;
297 4 50 33     24 if (defined $args && $args->isa('PPI::Structure::List')) {
298 4         42 $err = $add->($req, process_list $args->schildren);
299             }
300             else {
301 0         0 $err = 'no arguments';
302             }
303              
304 4 50       27 if (defined $err) {
305 0 0         ($node, $err) = @$err if ref $err eq 'ARRAY';
306 0           warn_at $node, "Cannot parse call to $name: $err";
307             }
308             }
309             }
310              
311             1;
312              
313             __END__
314              
315             =pod
316              
317             =encoding UTF-8
318              
319             =head1 NAME
320              
321             Perl::PrereqScanner::Scanner::DistZilla::PluginBundle - scan for required plugins in Dist::Zilla plugin bundles
322              
323             =head1 VERSION
324              
325             version 0.001
326              
327             =head1 DESCRIPTION
328              
329             This scanner will look for the following indicators:
330              
331             =over 4
332              
333             =item *
334              
335             calls to the C<add_bundle> method
336              
337             =item *
338              
339             calls to the C<add_plugins> method
340              
341             =back
342              
343             Currently this only works for plugin bundles using the
344             C<Dist::Zilla::Role::PluginBundle::Easy> role.
345              
346             =head1 BUGS
347              
348             Please report any bugs or feature requests on the bugtracker website
349             L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perl-PrereqScanner-Scanner-DistZilla-PluginBundle>
350             or by email to
351             L<bug-Perl-PrereqScanner-Scanner-DistZilla-PluginBundle@rt.cpan.org|mailto:bug-Perl-PrereqScanner-Scanner-DistZilla-PluginBundle@rt.cpan.org>.
352              
353             When submitting a bug or request, please include a test-file or a
354             patch to an existing test-file that illustrates the bug or desired
355             feature.
356              
357             =head1 AUTHOR
358              
359             Asher Gordon <AsDaGo@posteo.net>
360              
361             =head1 COPYRIGHT AND LICENSE
362              
363             Copyright (C) 2021 Asher Gordon <AsDaGo@posteo.net>
364              
365             This program is free software: you can redistribute it and/or modify
366             it under the terms of the GNU General Public License as published by
367             the Free Software Foundation, either version 3 of the License, or (at
368             your option) any later version.
369              
370             This program is distributed in the hope that it will be useful, but
371             WITHOUT ANY WARRANTY; without even the implied warranty of
372             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
373             General Public License for more details.
374              
375             You should have received a copy of the GNU General Public License
376             along with this program. If not, see <http://www.gnu.org/licenses/>.
377              
378             =cut