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   5165 use Moo;
  75         17506  
  75         474  
4              
5             ## no critic (Modules::RequireExplicitInclusion, Subroutines::ProhibitCallsToUnexportedSubs, TestingAndDebugging::ProhibitNoStrict)
6              
7             our $VERSION = '0.000051';
8              
9 75     75   58540 use App::perlimports::Sandbox ();
  75         269  
  75         2114  
10 75     75   36092 use Class::Inspector ();
  75         228692  
  75         2177  
11 75     75   615 use List::Util qw( any );
  75         217  
  75         8227  
12 75     75   39211 use Module::Runtime qw( require_module );
  75         130983  
  75         1794  
13 75     75   33595 use Sub::HandlesVia;
  75         143214  
  75         967  
14 75     75   5624034 use Try::Tiny qw( catch try );
  75         340  
  75         4730  
15 75     75   578 use Types::Standard qw(ArrayRef Bool HashRef Int InstanceOf Str);
  75         232  
  75         598  
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   11980 my $self = shift;
136 71         1246 my $pkg = $self->_pkg_for_implicit;
137             return $self->is_exporter
138             ? $self->_list_to_hash( $pkg, $self->at_export )
139 71 100       2743 : $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   205688 no strict 'refs';
  75         235  
  75         78680  
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 14323 my $self = shift;
213              
214 62         1643 $self->explicit_exports;
215 62         3331 $self->implicit_exports;
216 62         2065 return $self->success_counter;
217             }
218              
219             sub _build_explicit_exports {
220 65     65   17880 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     378 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         3860 [ @{ $self->at_export }, @{ $self->at_export_ok } ]
  49         1419  
  49         2956  
228             );
229             }
230              
231             # If this is Sub::Exporter, we can cheat and see what's in the :all tag
232 15         581 my $pkg = $self->_random_pkg_name;
233 15         116 my $use_statement = sprintf( 'use %s qw(:all);', $self->_module_name );
234 15         86 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         66 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   1128 my $self = shift;
251              
252 46         294 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   1260 my $self = shift;
265              
266 71 100   49   337 return 1 if any { $_ eq 'Exporter' } @{ $self->class_isa };
  48         1531  
  71         1215  
267 40 100 100     972 return $self->has_at_export || $self->has_at_export_ok ? 1 : 0;
268             }
269              
270             sub _build_is_oo_class {
271 52     53   26738 my $self = shift;
272              
273 52 100 100     380 return 0 if $self->has_implicit_exports || $self->has_explicit_exports;
274              
275 4         152 my $methods
276             = Class::Inspector->methods( $self->_module_name, 'full', 'public' );
277              
278             return any {
279 245 100   246   653 $_ eq 'Moose::Object::BUILDALL' || $_ eq 'Moo::Object::BUILDALL'
280 4         5253 } @{$methods};
  4         76  
281             }
282              
283             sub _build_isa_test_builder {
284 32     32   1250 my $self = shift;
285 32 100   28   195 if ( any { $_ eq 'Test::Builder::Module' }
  28         404  
286 32         588 @{ $self->_implicit->{class_isa} } ) {
287 4         68 return 1;
288             }
289              
290 28 100       735 return 0 if $self->_module_name !~ m{\ATest};
291              
292 1         11 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       8 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   6510 my $self = shift;
309 130         273 my $pkg = shift;
310 130         245 my $list = shift;
311              
312 130         235 my %hash;
313 130         262 for my $item ( @{$list} ) {
  130         372  
314 6510         8742 my $value = $item;
315 6510         8861 $value =~ s{^&}{};
316 6510         12930 $hash{$item} = $value;
317             }
318              
319             # Specifically for File::chdir, which exports a typeglob, but doesn't
320             # implement every possibility.
321 130         1356 for my $key ( keys %hash ) {
322 6482 100       15990 if ( substr( $key, 0, 1 ) eq '*' ) {
323 110         189 my $thing = substr( $key, 1 );
324 110         175 for my $sigil ( '&', '$', '@', '%' ) {
325 440         9912 my $symbol_name = $sigil . $pkg . '::' . $thing;
326 440 100       789 if ( Symbol::Get::get($symbol_name) ) {
327 120         5210 $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       3077 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         6303 return \%hash;
345             }
346              
347             sub _build_implicit {
348 72     72   1022 my $self = shift;
349              
350 72         313 my $module_name = $self->_module_name;
351 72         1295 my $pkg = $self->_pkg_for_implicit;
352 72         2440 my $use_statement = "use $module_name;";
353 72         318 my ( $maybe_exports, $fatal_error )
354             = $self->_exports_for_include( $pkg, $use_statement );
355              
356 75     75   3699 no strict 'refs';
  74         250  
  74         28014  
357             my $aggregated = {
358 72         747 class_isa => [ @{ $self->_module_name . '::ISA' } ],
359 72         663 export => [ @{ $self->_module_name . '::EXPORT' } ],
360 72         426 export_fail => [ @{ $self->_module_name . '::EXPORT_FAIL' } ],
361 72         653 export_ok => [ @{ $self->_module_name . '::EXPORT_OK' } ],
362 72         218 export_tags => [ @{ $self->_module_name . '::EXPORT_TAGS' } ],
  72         761  
363             fatal_error => $fatal_error,
364             _maybe_exports => $maybe_exports,
365             };
366              
367 72         1784 return $aggregated;
368             }
369              
370             sub _exports_for_include {
371 87     87   199 my $self = shift;
372 87         174 my $pkg = shift;
373 87         558 my $use_statement = shift;
374              
375 87         278 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         355 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         516 $self->logger->debug($to_eval);
400              
401             my $logger_cb = sub {
402 10     10   27 my $msg = shift;
403 10         26 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     169 if ( $msg =~ qr{Can't locate} && $msg !~ m{\:all\.pm in \@INC} ) {
409 7         25 $level = 'warning';
410             }
411              
412             $logger->log(
413 10         97 level => $level,
414             message => sprintf(
415             "Problem trying to eval %s\n%s",
416             $pkg,
417             $msg,
418             ),
419             );
420 87         9158 };
421              
422 87         455 local $SIG{__WARN__} = $logger_cb;
423              
424 87         267 local $@ = undef;
425             ## no critic (BuiltinFunctions::ProhibitStringyEval)
426             ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
427 87     37   7752 eval $to_eval;
  37     37   18834  
  37     37   70729  
  37     19   1286  
  37     15   8774  
  33     14   95029  
  33     7   2004  
  37     7   48870  
  23     7   673  
  19     5   256  
  15     5   542  
  15     5   1365  
  14     4   11215  
  14     4   600  
  14     4   16638  
  7     4   75  
  7     4   17  
  7     4   210  
  7         1150  
  7         1326  
  7         1056  
  7         22092  
  5         53  
  5         17  
  5         145  
  5         30  
  5         15  
  5         325  
  5         755  
  4         31  
  4         12  
  4         156  
  4         1141  
  4         690  
  4         166  
  4         1247  
  4         30  
  4         13  
  4         134  
  4         837  
  4         14112  
  4         846  
  4         37  
428              
429 87 100       465 if ($@) {
430 10         55 $logger_cb->($@);
431 10         1734 return undef, $@;
432             }
433             else {
434 77         510 $self->_increment_success_counter;
435             }
436              
437             ## no critic (TestingAndDebugging::ProhibitNoStrict)
438 74     75   691 no strict 'refs';
  74         1392  
  74         11304  
439             my @export
440 3531   100     12224 = grep { $_ !~ m{(?:BEGIN|ISA|__EXPORTABLES)} && $_ !~ m{^__ANON__} }
441 77         106085 @{ $pkg . '::__EXPORTABLES' };
  77         442  
442 74     75   630 use strict;
  74         646  
  74         47169  
443             ## use critic
444              
445 77         861 return \@export, undef;
446             }
447              
448             sub _random_pkg_name {
449 87     87   201 my $self = shift;
450 87         449 return App::perlimports::Sandbox::pkg_for( $self->_module_name );
451             }
452              
453             sub _build_is_moose_class {
454 54     54   17210 my $self = shift;
455              
456             return
457 3 100   3   185 any { $_ eq 'Moose::Object' || $_ eq 'Test::Class::Moose' }
458 54         303 @{ $self->pkg_isa };
  54         1033  
459             }
460              
461             sub _build_uses_moose {
462 48     48   2497 my $self = shift;
463 48 50       200 if ( $self->_maybe_require_module('Moose::Util') ) {
464 48 100       583 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   1781 my $self = shift;
471 47 50       200 if ( $self->_maybe_require_module('Class::Inspector') ) {
472             return 1
473 47 50   4064   325 if any { $_ eq 'Moo::is_class' } @{ Class::Inspector->methods(
  4064 50       75825  
  47         625  
474             $self->_module_name, 'full', 'public'
475             )
476             || []
477             };
478             }
479 47         1610 return 0;
480             }
481              
482             sub _build_is_moose_type_class {
483 70     70   993 my $self = shift;
484              
485             return
486 48 50   48   2888 any { $_ eq 'MooseX::Types::Base' || $_ eq 'MooseX::Types::Combine' }
487 70         362 @{ $self->class_isa };
  70         1336  
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   204 my $self = shift;
506 95         297 my $module_to_require = shift;
507              
508 95         812 $self->logger->info("going to require $module_to_require");
509              
510 95         7909 my $success;
511             try {
512 95     95   5011 require_module($module_to_require);
513 95         2867341 $success = 1;
514             }
515             catch {
516 0     0   0 $self->logger->info("$module_to_require error. $_");
517 95         964 };
518              
519 95         1850 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.000051
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