File Coverage

blib/lib/App/perlimports/ExportInspector.pm
Criterion Covered Total %
statement 199 209 95.2
branch 39 48 81.2
condition 14 15 93.3
subroutine 54 57 94.7
pod 3 3 100.0
total 309 332 93.0


line stmt bran cond sub pod time code
1             package App::perlimports::ExportInspector;
2              
3 75     75   4989 use Moo;
  75         17044  
  75         509  
4              
5             ## no critic (Modules::RequireExplicitInclusion, Subroutines::ProhibitCallsToUnexportedSubs, TestingAndDebugging::ProhibitNoStrict)
6              
7             our $VERSION = '0.000050';
8              
9 75     75   59724 use App::perlimports::Sandbox ();
  75         258  
  75         2006  
10 75     75   35089 use Class::Inspector ();
  75         225362  
  75         2039  
11 75     75   645 use List::Util qw( any );
  75         223  
  75         8397  
12 75     75   38553 use Module::Runtime qw( require_module );
  75         132533  
  75         539  
13 75     75   34417 use Sub::HandlesVia;
  75         144088  
  75         967  
14 75     75   5852874 use Try::Tiny qw( catch try );
  75         376  
  75         5280  
15 75     75   583 use Types::Standard qw(ArrayRef Bool HashRef Int InstanceOf Str);
  75         229  
  75         624  
16              
17             with 'App::perlimports::Role::Logger';
18              
19             has at_export => (
20             is => 'ro',
21             isa => ArrayRef [Str],
22             lazy => 1,
23             handles_via => 'Array',
24             handles => {
25             has_at_export => 'count',
26             },
27             default => sub { shift->_implicit->{export} },
28             );
29              
30             has at_export_ok => (
31             is => 'ro',
32             isa => ArrayRef [Str],
33             lazy => 1,
34             handles_via => 'Array',
35             handles => {
36             all_at_export_ok => 'elements',
37             has_at_export_ok => 'count',
38             },
39             default => sub { shift->_implicit->{export_ok} },
40             );
41              
42             has at_export_fail => (
43             is => 'ro',
44             isa => ArrayRef [Str],
45             lazy => 1,
46             default => sub { shift->_implicit->{export_fail} },
47             );
48              
49             has at_export_tags => (
50             is => 'ro',
51             isa => ArrayRef [Str],
52             lazy => 1,
53             default => sub { shift->_implicit->{export_tags} },
54             );
55              
56             has class_isa => (
57             is => 'ro',
58             isa => ArrayRef [Str],
59             lazy => 1,
60             default => sub { shift->_implicit->{class_isa} },
61             );
62              
63             has has_fatal_error => (
64             is => 'ro',
65             isa => Bool,
66             lazy => 1,
67             default => sub {
68             my $self = shift;
69             ( $self->_implicit->{fatal_error}
70             || $self->explicit_exports->{fatal_error} )
71             ? 1
72             : 0;
73             },
74             );
75              
76             has _implicit => (
77             is => 'ro',
78             isa => HashRef,
79             lazy => 1,
80             builder => '_build_implicit',
81             );
82              
83             has import_flags => (
84             is => 'ro',
85             isa => ArrayRef,
86             lazy => 1,
87             handles_via => 'Array',
88             handles => {
89             has_import_flags => 'count',
90             },
91             builder => '_build_import_flags',
92             );
93              
94             has is_exporter => (
95             is => 'ro',
96             isa => Bool,
97             lazy => 1,
98             builder => '_build_is_exporter',
99             );
100              
101             has isa_test_builder => (
102             is => 'ro',
103             isa => Bool,
104             lazy => 1,
105             builder => '_build_isa_test_builder',
106             );
107              
108             has explicit_exports => (
109             is => 'ro',
110             isa => HashRef,
111             lazy => 1,
112             handles_via => 'Hash',
113             handles => {
114             has_explicit_exports => 'count',
115             explicit_export_names => 'keys',
116             explicit_export_values => 'values',
117             },
118             builder => '_build_explicit_exports',
119             );
120              
121             has implicit_exports => (
122             is => 'ro',
123             isa => HashRef,
124             lazy => 1,
125             handles_via => 'Hash',
126             handles => {
127             has_implicit_exports => 'count',
128             implicit_export_names => 'keys',
129             implicit_export_values => 'values',
130             },
131             builder => '_build_implicit_exports',
132             );
133              
134             sub _build_implicit_exports {
135 71     71   13112 my $self = shift;
136 71         1297 my $pkg = $self->_pkg_for_implicit;
137             return $self->is_exporter
138             ? $self->_list_to_hash( $pkg, $self->at_export )
139 71 100       2713 : $self->_list_to_hash( $pkg, $self->_implicit->{_maybe_exports} );
140             }
141              
142             has is_moose_class => (
143             is => 'ro',
144             isa => Bool,
145             lazy => 1,
146             builder => '_build_is_moose_class',
147             );
148              
149             has is_moo_class => (
150             is => 'ro',
151             isa => Bool,
152             lazy => 1,
153             builder => '_build_is_moo_class',
154             );
155              
156             has is_moose_type_class => (
157             is => 'ro',
158             isa => Bool,
159             lazy => 1,
160             builder => '_build_is_moose_type_class',
161             );
162              
163             has is_oo_class => (
164             is => 'ro',
165             isa => Bool,
166             lazy => 1,
167             builder => '_build_is_oo_class',
168             );
169              
170             has _module_name => (
171             is => 'ro',
172             isa => Str,
173             init_arg => 'module_name',
174             required => 1,
175             );
176              
177             has pkg_isa => (
178             is => 'ro',
179             isa => ArrayRef [Str],
180             lazy => 1,
181             default => sub {
182 75     75   205967 no strict 'refs';
  75         294  
  75         80244  
183             return [ @{ shift->_pkg_for_implicit . '::ISA' } ];
184             },
185             );
186              
187             has _pkg_for_implicit => (
188             is => 'ro',
189             isa => Str,
190             lazy => 1,
191             default => sub { return shift()->_random_pkg_name },
192             );
193              
194             has success_counter => (
195             traits => ['Counter'],
196             is => 'ro',
197             isa => Int,
198             default => 0,
199             handles => {
200             _increment_success_counter => 'inc',
201             },
202             );
203              
204             has uses_moose => (
205             is => 'ro',
206             isa => Bool,
207             lazy => 1,
208             builder => '_build_uses_moose',
209             );
210              
211             sub evals_ok {
212 62     62 1 14596 my $self = shift;
213              
214 62         1661 $self->explicit_exports;
215 62         3301 $self->implicit_exports;
216 62         2306 return $self->success_counter;
217             }
218              
219             sub _build_explicit_exports {
220 65     65   18883 my $self = shift;
221              
222             # If this is Exporter, then the exportable symbols will be listed in either
223             # @EXPORT or @EXPORT_OK. Maybe in both?
224 65 100 100     449 if ( $self->has_at_export_ok || $self->has_at_export ) {
225             return $self->_list_to_hash(
226             $self->_pkg_for_implicit, # reuse package name
227 50         4421 [ @{ $self->at_export }, @{ $self->at_export_ok } ]
  49         1490  
  49         2996  
228             );
229             }
230              
231             # If this is Sub::Exporter, we can cheat and see what's in the :all tag
232 15         639 my $pkg = $self->_random_pkg_name;
233 15         88 my $use_statement = sprintf( 'use %s qw(:all);', $self->_module_name );
234 15         58 my ( $exports, $fatal_error )
235             = $self->_exports_for_include( $pkg, $use_statement );
236 15 100       103 if ($fatal_error) {
237 4         105 return { fatal_error => $fatal_error };
238             }
239              
240 11         60 return $self->_list_to_hash( $pkg, $exports );
241              
242             # If this module uses something other than Exporter or Sub::Exporter, we
243             # probably returned an empty hash above. We could guess and say it's the
244             # default exports + possibly something else. It's probably less confusing
245             # to leave it up to the code which uses this object to decide how to handle
246             # it.
247             }
248              
249             sub _build_import_flags {
250 46     47   1251 my $self = shift;
251              
252 46         307 my %modules = (
253             Carp => ['verbose'],
254             English => ['-no_match_vars'],
255             );
256              
257             return
258             exists $modules{ $self->_module_name }
259 46 100       499 ? $modules{ $self->_module_name }
260             : [];
261             }
262              
263             sub _build_is_exporter {
264 71     72   1353 my $self = shift;
265              
266 71 100   49   398 return 1 if any { $_ eq 'Exporter' } @{ $self->class_isa };
  48         1677  
  71         1343  
267 40 100 100     1016 return $self->has_at_export || $self->has_at_export_ok ? 1 : 0;
268             }
269              
270             sub _build_is_oo_class {
271 52     53   27251 my $self = shift;
272              
273 52 100 100     437 return 0 if $self->has_implicit_exports || $self->has_explicit_exports;
274              
275 4         159 my $methods
276             = Class::Inspector->methods( $self->_module_name, 'full', 'public' );
277              
278             return any {
279 245 100   246   654 $_ eq 'Moose::Object::BUILDALL' || $_ eq 'Moo::Object::BUILDALL'
280 4         5260 } @{$methods};
  4         66  
281             }
282              
283             sub _build_isa_test_builder {
284 32     32   1410 my $self = shift;
285 32 100   28   199 if ( any { $_ eq 'Test::Builder::Module' }
  28         486  
286 32         641 @{ $self->_implicit->{class_isa} } ) {
287 4         72 return 1;
288             }
289              
290 28 100       805 return 0 if $self->_module_name !~ m{\ATest};
291              
292 1         13 my $err = App::perlimports::Sandbox::eval_pkg(
293             $self->_module_name,
294             sprintf( 'use %s qw( some_function );', $self->_module_name )
295             );
296              
297             # Catch cases like Test::HTML::Lint, where, which doesn't subclass
298             # Test::Builder, but essentially calls Tester::Builder->new->plan(@_); in
299             # its import(). The error will be something like "plan() doesn't understand
300             # some_function at"
301 1 50       7 if ( $err =~ m{plan} ) {
302 0         0 return 1;
303             }
304 1         25 return 0;
305             }
306              
307             sub _list_to_hash {
308 130     130   7022 my $self = shift;
309 130         317 my $pkg = shift;
310 130         235 my $list = shift;
311              
312 130         255 my %hash;
313 130         248 for my $item ( @{$list} ) {
  130         380  
314 6501         8938 my $value = $item;
315 6501         10553 $value =~ s{^&}{};
316 6501         13508 $hash{$item} = $value;
317             }
318              
319             # Specifically for File::chdir, which exports a typeglob, but doesn't
320             # implement every possibility.
321 130         1477 for my $key ( keys %hash ) {
322 6473 100       16391 if ( substr( $key, 0, 1 ) eq '*' ) {
323 110         216 my $thing = substr( $key, 1 );
324 110         177 for my $sigil ( '&', '$', '@', '%' ) {
325 440         9588 my $symbol_name = $sigil . $pkg . '::' . $thing;
326 440 100       839 if ( Symbol::Get::get($symbol_name) ) {
327 120         5089 $hash{ $sigil . $thing } = $key;
328             }
329             }
330             }
331             }
332              
333             # Treat Moose type libraries a bit differently. Importing ArrayRef, for
334             # instance, also imports is_ArrayRef and to_ArrayRef (if a coercion)
335             # exists. So, let's deal with that here.
336 130 50       3178 if ( $self->is_moose_type_class ) {
337 0         0 for my $key ( keys %hash ) {
338 0 0       0 if ( $key =~ m{^(is_|to_)} ) {
339 0         0 $hash{$key} = substr( $key, 3 );
340             }
341             }
342             }
343              
344 130         6379 return \%hash;
345             }
346              
347             sub _build_implicit {
348 72     72   993 my $self = shift;
349              
350 72         363 my $module_name = $self->_module_name;
351 72         1340 my $pkg = $self->_pkg_for_implicit;
352 72         2261 my $use_statement = "use $module_name;";
353 72         331 my ( $maybe_exports, $fatal_error )
354             = $self->_exports_for_include( $pkg, $use_statement );
355              
356 75     75   3512 no strict 'refs';
  74         262  
  74         27406  
357             my $aggregated = {
358 72         815 class_isa => [ @{ $self->_module_name . '::ISA' } ],
359 72         793 export => [ @{ $self->_module_name . '::EXPORT' } ],
360 72         502 export_fail => [ @{ $self->_module_name . '::EXPORT_FAIL' } ],
361 72         765 export_ok => [ @{ $self->_module_name . '::EXPORT_OK' } ],
362 72         232 export_tags => [ @{ $self->_module_name . '::EXPORT_TAGS' } ],
  72         816  
363             fatal_error => $fatal_error,
364             _maybe_exports => $maybe_exports,
365             };
366              
367 72         1805 return $aggregated;
368             }
369              
370             sub _exports_for_include {
371 87     87   201 my $self = shift;
372 87         196 my $pkg = shift;
373 87         175 my $use_statement = shift;
374              
375 87         296 my $logger = $self->logger;
376              
377             # If you're importing Moose into a namespace and following that with an
378             # import of namespace::autoclean, you may find that symbols like "after"
379             # and "around" are no longer found.
380             #
381             # We log available symbols inside the BEGIN block in order to defeat
382             # namespace::autoclean, which removes symbols from the stash after
383             # compilation but before runtime. Thanks to Florian Ragwitz for the tip and
384             # the preceding explanation.
385              
386 87         380 my $to_eval = <<"EOF";
387             package $pkg;
388              
389             use Symbol::Get;
390             $use_statement
391             our \@__EXPORTABLES;
392              
393             BEGIN {
394             \@__EXPORTABLES = Symbol::Get::get_names();
395             }
396             1;
397             EOF
398              
399 87         582 $self->logger->debug($to_eval);
400              
401             my $logger_cb = sub {
402 10     10   26 my $msg = shift;
403 10         25 my $level = 'info';
404              
405             # Mojo classes tend to throw "Can't locate :all.pm in @INC". This is
406             # expected and shouldn't be raised to the warning level.
407              
408 10 100 66     149 if ( $msg =~ qr{Can't locate} && $msg !~ m{\:all\.pm in \@INC} ) {
409 7         21 $level = 'warning';
410             }
411              
412             $logger->log(
413 10         111 level => $level,
414             message => sprintf(
415             "Problem trying to eval %s\n%s",
416             $pkg,
417             $msg,
418             ),
419             );
420 87         9833 };
421              
422 87         529 local $SIG{__WARN__} = $logger_cb;
423              
424 87         268 local $@ = undef;
425             ## no critic (BuiltinFunctions::ProhibitStringyEval)
426             ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
427 87     37   7890 eval $to_eval;
  37     37   19933  
  37     37   70059  
  37     19   1270  
  37     15   8531  
  33     14   100156  
  33     7   2097  
  37     7   50671  
  23     7   711  
  19     5   273  
  15     5   529  
  15     5   1624  
  14     4   11819  
  14     4   642  
  14     4   16784  
  7     4   84  
  7     4   20  
  7     4   195  
  7         549  
  7         24  
  7         1120  
  7         21760  
  5         80  
  5         16  
  5         162  
  5         630  
  5         717  
  5         346  
  5         893  
  4         30  
  4         13  
  4         130  
  4         679  
  4         14  
  4         233  
  4         1671  
  4         36  
  4         13  
  4         119  
  4         1382  
  4         15720  
  4         768  
  4         26  
428              
429 87 100       491 if ($@) {
430 10         46 $logger_cb->($@);
431 10         1726 return undef, $@;
432             }
433             else {
434 77         513 $self->_increment_success_counter;
435             }
436              
437             ## no critic (TestingAndDebugging::ProhibitNoStrict)
438 74     75   700 no strict 'refs';
  74         1454  
  74         11420  
439             my @export
440 3525   100     12352 = grep { $_ !~ m{(?:BEGIN|ISA|__EXPORTABLES)} && $_ !~ m{^__ANON__} }
441 77         110650 @{ $pkg . '::__EXPORTABLES' };
  77         519  
442 74     75   617 use strict;
  74         736  
  74         47956  
443             ## use critic
444              
445 77         924 return \@export, undef;
446             }
447              
448             sub _random_pkg_name {
449 87     87   203 my $self = shift;
450 87         565 return App::perlimports::Sandbox::pkg_for( $self->_module_name );
451             }
452              
453             sub _build_is_moose_class {
454 54     54   18149 my $self = shift;
455              
456             return
457 3 100   3   193 any { $_ eq 'Moose::Object' || $_ eq 'Test::Class::Moose' }
458 54         313 @{ $self->pkg_isa };
  54         1127  
459             }
460              
461             sub _build_uses_moose {
462 48     48   2755 my $self = shift;
463 48 50       256 if ( $self->_maybe_require_module('Moose::Util') ) {
464 48 100       633 return Moose::Util::find_meta( $self->_module_name ) ? 1 : 0;
465             }
466 0         0 return 0;
467             }
468              
469             sub _build_is_moo_class {
470 47     47   1876 my $self = shift;
471 47 50       237 if ( $self->_maybe_require_module('Class::Inspector') ) {
472             return 1
473 47 50   4061   354 if any { $_ eq 'Moo::is_class' } @{ Class::Inspector->methods(
  4061 50       77425  
  47         691  
474             $self->_module_name, 'full', 'public'
475             )
476             || []
477             };
478             }
479 47         1567 return 0;
480             }
481              
482             sub _build_is_moose_type_class {
483 70     70   1077 my $self = shift;
484              
485             return
486 48 50   48   2989 any { $_ eq 'MooseX::Types::Base' || $_ eq 'MooseX::Types::Combine' }
487 70         409 @{ $self->class_isa };
  70         1389  
488             }
489              
490             sub explicit_export_names_match_values {
491 0     0 1 0 my $self = shift;
492             return
493 0         0 join( q{}, sort $self->explicit_export_names ) eq
494             join( q{}, sort $self->explicit_export_values );
495             }
496              
497             sub implicit_export_names_match_values {
498 0     0 1 0 my $self = shift;
499             return
500 0         0 join( q{}, sort $self->implicit_export_names ) eq
501             join( q{}, sort $self->implicit_export_values );
502             }
503              
504             sub _maybe_require_module {
505 95     95   233 my $self = shift;
506 95         321 my $module_to_require = shift;
507              
508 95         927 $self->logger->info("going to require $module_to_require");
509              
510 95         8717 my $success;
511             try {
512 95     95   5662 require_module($module_to_require);
513 95         2910193 $success = 1;
514             }
515             catch {
516 0     0   0 $self->logger->info("$module_to_require error. $_");
517 95         1141 };
518              
519 95         2084 return $success;
520             }
521              
522             1;
523              
524             # ABSTRACT: Inspect code for exportable symbols
525              
526             __END__
527              
528             =pod
529              
530             =encoding UTF-8
531              
532             =head1 NAME
533              
534             App::perlimports::ExportInspector - Inspect code for exportable symbols
535              
536             =head1 VERSION
537              
538             version 0.000050
539              
540             =head1 SYNOPSIS
541              
542             use strict;
543             use warnings;
544              
545             use App::perlimport::ExportInspector ();
546              
547             my $ei = App::perlimport::ExportInspector->new(
548             module_name => 'Carp',
549             );
550              
551             my $exports = $ei->explicit_exports;
552              
553             =head1 DESCRIPTION
554              
555             Inspect modules to see what they might export.
556              
557             =head1 MOTIVATION
558              
559             Since we're (maybe) importing symbols as part of this process, we've sandboxed
560             it a little bit by not doing it in L<App::perlimports> directly.
561              
562             =head1 METHODS
563              
564             The following methods are available.
565              
566             =head2 implicit_exports
567              
568             A HashRef with keys representing symbols which a module implicitly exports
569             (i.e. via C<use Module::Name;>. The values represent the import value which
570             you would need in order to explicitly import the symbol. Often these will be
571             the same, but there are exceptions. For example, a type library may export
572             C<is_ArrayRef>, but you import it via C<use My::Type::Library qw( ArrayRef );>.
573              
574             =head2 explicit_exports
575              
576             A HashRef with keys representing symbols which a module explicitly exports
577             (i.e. via C<use Module::Name qw( foo bar );>. The values represent the import
578             value which you would need in order to explicitly import the symbol. Often
579             these will be the same, but there are exceptions. For example, a type library
580             may export C<is_ArrayRef>, but you import it via C<use My::Type::Library qw(
581             ArrayRef );>.
582              
583             In cases where we cannot be certain about the explicit exports, you can try to
584             fall back to the implicit exports to get an idea of what this module can
585             export.
586              
587             =head2 evals_ok
588              
589             Returns true if either implicit or explicit exports can be built without
590             setting C<$@>.
591              
592             =head2 implicit_export_names_match_values
593              
594             Returns true if the keys and values in C<implicit_exports> match.
595              
596             =head2 explicit_export_names_match_values
597              
598             Returns true if the keys and values in C<explicit_exports> match.
599              
600             =head2 success_counter
601              
602             Returns an integer representing the number of times we were able to execute
603             eval statements for this package which did not pollute C<$@>.
604              
605             =head1 CAVEATS
606              
607             This may not work with modules using some creative way of managing symbol
608             exports.
609              
610             =head1 AUTHOR
611              
612             Olaf Alders <olaf@wundercounter.com>
613              
614             =head1 COPYRIGHT AND LICENSE
615              
616             This software is copyright (c) 2020 by Olaf Alders.
617              
618             This is free software; you can redistribute it and/or modify it under
619             the same terms as the Perl 5 programming language system itself.
620              
621             =cut