File Coverage

blib/lib/App/perlimports/Document.pm
Criterion Covered Total %
statement 307 369 83.2
branch 103 140 73.5
condition 69 87 79.3
subroutine 55 61 90.1
pod 3 3 100.0
total 537 660 81.3


line stmt bran cond sub pod time code
1             package App::perlimports::Document;
2              
3 73     73   1230046 use Moo;
  73         572548  
  73         382  
4 73     73   139377 use utf8;
  73         995  
  73         393  
5              
6             our $VERSION = '0.000051';
7              
8 73     73   32897 use App::perlimports::Annotations ();
  73         371  
  73         2425  
9 73     73   38547 use App::perlimports::ExportInspector ();
  73         400  
  73         2745  
10 73     73   40051 use App::perlimports::Include ();
  73         429  
  73         2740  
11 73     73   622 use App::perlimports::Sandbox ();
  73         168  
  73         1628  
12 73     73   471 use File::Basename qw( fileparse );
  73         203  
  73         8188  
13 73     73   617 use List::Util qw( any uniq );
  73         203  
  73         5373  
14 73     73   536 use Module::Runtime qw( module_notional_filename );
  73         210  
  73         906  
15 73     73   3900 use MooX::StrictConstructor;
  73         220  
  73         917  
16 73     73   141969 use Path::Tiny qw( path );
  73         861757  
  73         4309  
17 73     73   647 use PPI::Document ();
  73         205  
  73         1913  
18 73         4544 use PPIx::Utils::Classification qw(
19             is_function_call
20             is_hash_key
21             is_method_call
22 73     73   531 );
  73         199  
23 73     73   506 use Ref::Util qw( is_plain_arrayref is_plain_hashref );
  73         251  
  73         3384  
24 73     73   526 use Sub::HandlesVia;
  73         195  
  73         730  
25 73     73   56470 use Text::Diff ();
  73         503238  
  73         2188  
26 73     73   579 use Try::Tiny qw( catch try );
  73         1387  
  73         4541  
27 73     73   537 use Types::Standard qw( ArrayRef Bool HashRef InstanceOf Maybe Object Str );
  73         166  
  73         697  
28              
29             with 'App::perlimports::Role::Logger';
30              
31             has _annotations => (
32             is => 'ro',
33             isa => InstanceOf ['App::perlimports::Annotations'],
34             lazy => 1,
35             default => sub {
36             return App::perlimports::Annotations->new(
37             ppi_document => shift->ppi_document );
38             },
39             );
40              
41             has _cache => (
42             is => 'ro',
43             isa => Bool,
44             init_arg => 'cache',
45             lazy => 1,
46             default => 0,
47             );
48              
49             has _cache_dir => (
50             is => 'ro',
51             isa => InstanceOf ['Path::Tiny'],
52             lazy => 1,
53             builder => '_build_cache_dir',
54             );
55              
56             has _filename => (
57             is => 'ro',
58             isa => Str,
59             init_arg => 'filename',
60             required => 1,
61             );
62              
63             has _ignore_modules => (
64             is => 'ro',
65             isa => HashRef,
66             init_arg => 'ignore_modules',
67             default => sub { +{} },
68             );
69              
70             has _ignore_modules_pattern => (
71             is => 'ro',
72             isa => ArrayRef [Str],
73             init_arg => 'ignore_modules_pattern',
74             default => sub { [] },
75             );
76              
77             has includes => (
78             is => 'ro',
79             isa => ArrayRef [Object],
80             handles_via => 'Array',
81             handles => {
82             all_includes => 'elements',
83             },
84             lazy => 1,
85             builder => '_build_includes',
86             );
87              
88             has _inspectors => (
89             is => 'ro',
90             isa => HashRef [ Maybe [Object] ],
91             handles_via => 'Hash',
92             handles => {
93             all_inspector_names => 'keys',
94             _get_inspector_for => 'get',
95             _has_inspector_for => 'exists',
96             _set_inspector_for => 'set',
97             },
98             lazy => 1,
99             default => sub { +{} },
100             );
101              
102             has interpolated_symbols => (
103             is => 'ro',
104             isa => HashRef,
105             lazy => 1,
106             builder => '_build_interpolated_symbols',
107             );
108              
109             has json => (
110             is => 'ro',
111             isa => Bool,
112             lazy => 1,
113             default => 0,
114             );
115              
116             has _json_encoder => (
117             is => 'ro',
118             isa => InstanceOf ['Cpanel::JSON::XS'],
119             lazy => 1,
120             default => sub {
121             require Cpanel::JSON::XS;
122             return Cpanel::JSON::XS->new;
123             },
124             );
125              
126             has lint => (
127             is => 'ro',
128             isa => Bool,
129             lazy => 1,
130             default => 0,
131             );
132              
133             has my_own_inspector => (
134             is => 'ro',
135             isa => Maybe [ InstanceOf ['App::perlimports::ExportInspector'] ],
136             lazy => 1,
137             builder => '_build_my_own_inspector',
138             );
139              
140             has never_exports => (
141             is => 'ro',
142             isa => HashRef,
143             lazy => 1,
144             builder => '_build_never_exports',
145             );
146              
147             has _never_export_modules => (
148             is => 'ro',
149             isa => ArrayRef [Str],
150             init_arg => 'never_export_modules',
151             predicate => '_has_never_export_modules',
152             );
153              
154             has original_imports => (
155             is => 'ro',
156             isa => HashRef,
157             handles_via => 'Hash',
158             handles => {
159             _reset_original_import => 'set',
160             },
161             lazy => 1,
162             builder => '_build_original_imports',
163             );
164              
165             has _padding => (
166             is => 'ro',
167             isa => Bool,
168             init_arg => 'padding',
169             default => 1,
170             );
171              
172             has ppi_document => (
173             is => 'ro',
174             isa => Object,
175             lazy => 1,
176             builder => '_build_ppi_document',
177             );
178              
179             has possible_imports => (
180             is => 'ro',
181             isa => ArrayRef [Object],
182             lazy => 1,
183             builder => '_build_possible_imports',
184             );
185              
186             has _ppi_selection => (
187             is => 'ro',
188             isa => Object,
189             init_arg => 'ppi_selection',
190             lazy => 1,
191             default => sub { $_[0]->ppi_document },
192             );
193              
194             has _preserve_duplicates => (
195             is => 'ro',
196             isa => Bool,
197             init_arg => 'preserve_duplicates',
198             default => 1,
199             );
200              
201             has _preserve_unused => (
202             is => 'ro',
203             isa => Bool,
204             init_arg => 'preserve_unused',
205             default => 1,
206             );
207              
208             has _sub_exporter_export_list => (
209             is => 'ro',
210             isa => ArrayRef,
211             handles_via => 'Array',
212             handles => {
213             sub_exporter_export_list => 'elements',
214             },
215             lazy => 1,
216             builder => '_build_sub_exporter_export_list',
217             );
218              
219             has _sub_names => (
220             is => 'ro',
221             isa => HashRef,
222             handles_via => 'Hash',
223             handles => {
224             is_sub_name => 'exists',
225             },
226             lazy => 1,
227             builder => '_build_sub_names',
228             );
229              
230             has _tidy_whitespace => (
231             is => 'ro',
232             isa => Bool,
233             init_arg => 'tidy_whitespace',
234             lazy => 1,
235             default => sub { 1 },
236             );
237              
238             has _verbose => (
239             is => 'ro',
240             isa => Bool,
241             init_arg => 'verbose',
242             default => sub { 0 },
243             );
244              
245             around BUILDARGS => sub {
246             my ( $orig, $class, @args ) = @_;
247              
248             my %args = @args;
249             if ( my $modules = delete $args{ignore_modules} ) {
250             my %modules = map { $_ => 1 } @{$modules};
251             $args{ignore_modules} = \%modules;
252             }
253              
254             if ( my $selection = delete $args{selection} ) {
255             $args{ppi_selection} = PPI::Document->new( \$selection );
256             }
257              
258             return $class->$orig(%args);
259             };
260              
261             my %default_ignore = (
262             'Carp::Always' => 1,
263             'Constant::Generate' => 1,
264             'Data::Printer' => 1,
265             'DDP' => 1,
266             'Devel::Confess' => 1,
267             'Encode::Guess' => 1,
268             'Env' => 1, # see t/env.t
269             'Exception::Class' => 1,
270             'Exporter' => 1,
271             'Exporter::Lite' => 1,
272             'Feature::Compat::Try' => 1,
273             'Filter::Simple' => 1,
274             'Git::Sub' => 1,
275             'HTTP::Message::PSGI' => 1, # HTTP::Request::(to|from)_psgi
276             'Import::Into' => 1,
277             'MLDBM' => 1,
278             'Modern::Perl' => 1,
279             'Mojo::Base' => 1,
280             'Mojo::Date' => 1,
281             'Mojolicious::Lite' => 1,
282             'Moo' => 1,
283             'Moo::Role' => 1,
284             'Moose' => 1,
285             'Moose::Exporter' => 1,
286             'Moose::Role' => 1,
287             'MooseX::NonMoose' => 1,
288             'MooseX::Role::Parameterized' => 1,
289             'MooseX::SemiAffordanceAccessor' => 1,
290             'MooseX::StrictConstructor' => 1,
291             'MooseX::TraitFor::Meta::Class::BetterAnonClassNames' => 1,
292             'MooseX::Types' => 1,
293             'MooX::StrictConstructor' => 1,
294             'namespace::autoclean' => 1,
295             'PerlIO::gzip' => 1,
296             'Regexp::Common' => 1,
297             'Sort::ByExample' => 1,
298             'Struct::Dumb' => 1,
299             'Sub::Exporter' => 1,
300             'Sub::Exporter::Progressive' => 1,
301             'Sub::HandlesVia' => 1,
302             'Syntax::Keyword::Try' => 1,
303             'Term::Size::Any' => 1,
304             'Test2::Util::HashBase' => 1,
305             'Test::Exception' => 1,
306             'Test::Needs' => 1,
307             'Test::Number::Delta' => 1,
308             'Test::Pod' => 1,
309             'Test::Pod::Coverage' => 1,
310             'Test::Requires::Git' => 1,
311             'Test::RequiresInternet' => 1,
312             'Test::Warnings' => 1,
313             'Test::Whitespaces' => 1,
314             'Test::XML' => 1,
315             'Types::Standard' => 1,
316             'URI::QueryParam' => 1,
317             );
318              
319             # Funky stuff could happen with inner packages.
320             sub _build_my_own_inspector {
321 32     32   1749 my $self = shift;
322             my $pkgs
323             = $self->ppi_document->find(
324 32 100   1763   549 sub { $_[1]->isa('PPI::Statement::Package') && $_[1]->file_scoped } );
  1763         20520  
325              
326 32 100 66     723 if ( !$pkgs || $pkgs->[0]->namespace eq 'main' ) {
327 31         682 return;
328             }
329              
330 1         31 my $pkg = $pkgs->[0];
331              
332             # file_scoped() doesn't seem to be very reliable, so let's just try a crude
333             # check to see if this is a package we might actually find on disk before
334             # we try to require it.
335 1         3 my $notional_file
336             = fileparse( module_notional_filename( $pkg->namespace ) );
337 1         98 my $provided_file = fileparse( $self->_filename );
338 1 50       6 return unless $notional_file eq $provided_file;
339              
340 1         7 return App::perlimports::ExportInspector->new(
341             logger => $self->logger,
342             module_name => $pkg->namespace,
343             );
344             }
345              
346             sub _build_includes {
347 52     52   1386 my $self = shift;
348              
349             # version() returns a value if this a dependency on a version of Perl, e.g
350             # use 5.006;
351             # require 5.006;
352             #
353             # We check for type so that we can filter out undef types or "no".
354              
355             ## no critic (Subroutines::ProhibitCallsToUnexportedSubs)
356             return $self->_ppi_selection->find(
357             sub {
358 2015     2015   254328 $_[1]->isa('PPI::Statement::Include')
359             && !$_[1]->pragma # no pragmas
360             && !$_[1]->version # Perl version requirement
361             && $_[1]->type
362             && ( $_[1]->type eq 'use'
363             || $_[1]->type eq 'require' )
364             && !$self->_is_ignored( $_[1] )
365             && !$self->_has_import_switches( $_[1]->module )
366             && !App::perlimports::Sandbox::eval_pkg(
367             $_[1]->module,
368             "$_[1]"
369             );
370             }
371 52   100     1117 ) || [];
372             ## use critic
373             }
374              
375             sub _build_possible_imports {
376 34     34   554 my $self = shift;
377             my $before = $self->ppi_document->find(
378             sub {
379 1883     1883   32566 $_[1]->isa('PPI::Token::Word')
380             || $_[1]->isa('PPI::Token::Symbol')
381             || $_[1]->isa('PPI::Token::Label')
382             || $_[1]->isa('PPI::Token::Prototype');
383             }
384 34   50     616 ) || [];
385              
386 34         692 my @after;
387 34         160 for my $word ( @{$before} ) {
  34         124  
388              
389             # Without the sub name check, we accidentally turn
390             # use List::Util ();
391             # sub any { }
392             #
393             # into
394             #
395             # use List::Util qw( any );
396             # sub any {}
397 421 100       1402 next if $self->is_sub_name("$word");
398              
399 408         9194 my $isa_symbol = $word->isa('PPI::Token::Symbol');
400              
401 408 100 100     1363 next if !$isa_symbol && is_method_call($word);
402              
403             # A hash key might, for example, be a variable.
404 404 100 100     13723 if (
      100        
405             !$isa_symbol
406             && !(
407             $word->statement
408             && $word->statement->isa('PPI::Statement::Variable')
409             )
410             && is_hash_key($word)
411             ) {
412 6         695 next;
413             }
414              
415 398         34000 push @after, $word;
416             }
417              
418 34         1160 return \@after;
419             }
420              
421             sub _build_ppi_document {
422 53     53   889 my $self = shift;
423 53         589 return PPI::Document->new( $self->_filename );
424             }
425              
426             # Create a key for every included module.
427             # use Carp;
428             # use Data::Dumper qw( Dumper );
429             # use POSIX ();
430             #
431             # becomes:
432             #
433             # {
434             # Carp => undef,
435             # 'Data::Dumper' => ['Dumper'],
436             # POSIX => [],
437             # }
438             #
439             # The name is a bit of a misnomer. It starts out as a list of original imports,
440             # but with each include that gets processed, this list also gets updated. We do
441             # this so that we can keep track of what previous modules are really importing.
442             # Might not be bad to rename this.
443              
444             sub _build_original_imports {
445 41     41   904 my $self = shift;
446              
447             # We're missing requires which could be followed by an import.
448             my $found = $self->ppi_document->find(
449             sub {
450 2195     2195   54478 $_[1]->isa('PPI::Statement::Include')
451             && !$_[1]->pragma # no pragmas
452             && !$_[1]->version # Perl version requirement
453             && $_[1]->type
454             && $_[1]->type eq 'use';
455             }
456 41   50     753 ) || [];
457              
458 41         869 my %imports;
459              
460 41         107 for my $include ( @{$found} ) {
  41         160  
461 64         224 my $pkg = $include->module;
462 64 100       1901 $imports{$pkg} = undef unless exists $imports{$pkg};
463              
464             # this is probably wrong
465             #next if $self->_is_ignored($pkg);
466              
467             # If a module has been included multiple times, we want to have a
468             # cumulative tally of what has been explicitly imported.
469 64         389 my $found_for_include = _imports_for_include($include);
470 64 100       288 if ($found_for_include) {
471 29 100       165 if ( $imports{$pkg} ) {
472 2         13 push @{ $imports{$pkg} }, @{$found_for_include};
  2         9  
  2         8  
473             }
474             else {
475 27         99 $imports{$pkg} = $found_for_include;
476             }
477             }
478             }
479              
480 41         1087 return \%imports;
481             }
482              
483             sub _build_sub_exporter_export_list {
484 32     32   444 my $self = shift;
485              
486             my $sub_ex = $self->ppi_document->find(
487             sub {
488 1763     1763   23977 $_[1]->isa('PPI::Statement::Include')
489             && $_[1]->module eq 'Sub::Exporter';
490             }
491 32   100     680 ) || [];
492 32 100       693 return [] unless @{$sub_ex};
  32         222  
493              
494 1         2 my @found;
495 1         2 for my $include ( @{$sub_ex} ) {
  1         13  
496 1         8 my @arguments = $include->arguments;
497 1         54 for my $arg (@arguments) {
498 3 100       13 if ( $arg->isa('PPI::Structure::Constructor') ) {
499             ## no critic (BuiltinFunctions::ProhibitStringyEval)
500 1         8 my $thing = eval $arg;
501 1 50       8 if ( is_plain_hashref($thing) ) {
502 1 50       5 if ( is_plain_arrayref( $thing->{exports} ) ) {
503 1         3 push @found, @{ $thing->{exports} };
  1         12  
504             }
505             }
506             }
507             }
508             }
509              
510 1         11 return [ uniq @found ];
511             }
512              
513             sub _imports_for_include {
514 100     100   1307 my $include = shift;
515              
516 100         236 my $imports = undef;
517              
518 100         622 for my $child ( $include->schildren ) {
519 402 100 66     6089 if ( $child->isa('PPI::Structure::List')
520             && !defined $imports ) {
521 14         159 $imports = [];
522             }
523 402 100 100     2351 if ( !$child->isa('PPI::Token::QuoteLike::Words')
524             && !$child->isa('PPI::Token::Quote::Single') ) {
525 351         815 next;
526             }
527 51 100       235 if ( defined $imports ) {
528 4         6 push( @{$imports}, $child->literal );
  4         18  
529             }
530             else {
531 47         342 $imports = [ $child->literal ];
532             }
533             }
534 100         511 return $imports;
535             }
536              
537             sub _extract_symbols_from_snippet {
538 15     15   142 my $snippet = shift;
539 15 50       65 return () unless defined $snippet;
540              
541             # Restore line breaks and tabs
542 15         54 $snippet =~ s{\\n}{\n}g;
543 15         48 $snippet =~ s{\\t}{\t}g;
544              
545 15         113 my $doc = PPI::Document->new( \$snippet );
546 15 100       21632 return () unless defined $doc;
547              
548             my @symbols
549 14 100       44 = map { $_ . q{} } @{ $doc->find('PPI::Token::Symbol') || [] };
  6         2467  
  14         54  
550              
551 14   100     2921 my $casts = $doc->find('PPI::Token::Cast') || [];
552 14         4721 for my $cast ( @{$casts} ) {
  14         47  
553              
554             # Optimistically avoid misinterpreting regex assertions as casts
555             # We don't want to match on "A" in the following example:
556             # if ( $thing =~ m{ \A b }x ) { ... }
557 5 100       296 next if $cast eq '\\';
558              
559 3         79 my $full_cast = $cast . $cast->snext_sibling;
560 3         332 my $cast_as_doc = PPI::Document->new( \$full_cast );
561             push @symbols,
562 1         671 map { $_ . q{} }
563 3 100       5647 @{ $cast_as_doc->find('PPI::Token::Symbol') || [] };
  3         13  
564              
565 3   50     1185 my $words = $cast_as_doc->find('PPI::Token::Word') || [];
566              
567             ## Turn ${FOO} into $FOO
568 3 100 66     1670 if ( $words
      100        
569             && scalar @$words == 1
570             && $full_cast =~ m/([\$\@\%])\{$words->[0]}/ ) {
571 1         33 push @symbols, $1 . $words->[0];
572 1         13 next;
573             }
574              
575             # This could likely be a source of false positives.
576 2         43 for my $word (@$words) {
577 3 100       334 push @symbols, "$word" if is_function_call($word);
578             }
579             }
580              
581 14         761 return @symbols;
582             }
583              
584             sub _unnest_quotes {
585 12     12   30 my $self = shift;
586 12         35 my $token = shift;
587 12         34 my @words = @_;
588              
589 12 100 100     127 if ( !$token->isa('PPI::Token::Quote')
590             || $token->isa('PPI::Token::Quote::Single') ) {
591 3         19 return @words;
592             }
593              
594 9         57 push @words, _extract_symbols_from_snippet( $token->string );
595              
596 9         726 my $doc = PPI::Document->new( \$token->string );
597 9 100       12377 return @words unless $doc;
598              
599 8         35 my $quotes = $doc->find('PPI::Token::Quote');
600 8 100       3350 return @words unless $quotes;
601              
602 3         17 for my $q (@$quotes) {
603 3         15 push @words, _extract_symbols_from_snippet("$q");
604 3         133 push @words, $self->_unnest_quotes($q);
605             }
606              
607 3         194 return @words;
608             }
609              
610             sub _build_interpolated_symbols {
611 33     33   864 my $self = shift;
612 33         102 my @symbols;
613              
614 33         77 for my $token (
615             @{
616             $self->ppi_document->find(
617             sub {
618 1799 50 100 1799   54870 ( $_[1]->isa('PPI::Token::Quote')
      66        
      66        
619             && !$_[1]->isa('PPI::Token::Quote::Single') )
620             || $_[1]->isa('PPI::Token::Quote::Interpolate')
621             || $_[1]->isa('PPI::Token::QuoteLike::Regexp')
622             || $_[1]->isa('PPI::Token::Regexp');
623             }
624             )
625 33 100       605 || []
626             }
627             ) {
628 9 100 66     292 if ( $token->isa('PPI::Token::Regexp')
629             || $token->isa('PPI::Token::QuoteLike::Regexp') ) {
630 1         51 for my $snippet (
631             $token->get_match_string,
632             $token->get_substitute_string,
633             ) {
634 1         50 push @symbols, _extract_symbols_from_snippet($snippet);
635             }
636             }
637              
638 9         170 push @symbols, $self->_unnest_quotes($token);
639             }
640              
641             # Crude hack to catch vars like ${FOO_BAR} in heredocs.
642 33         1124 for my $heredoc (
643             @{
644             $self->ppi_document->find(
645             sub {
646 1799     1799   20102 $_[1]->isa('PPI::Token::HereDoc');
647             }
648             )
649 33 100       832 || []
650             }
651             ) {
652 3         105 my $content = join "\n", $heredoc->heredoc;
653 3 100       27 next if $heredoc =~ m{'};
654 2         24 push @symbols, _extract_symbols_from_snippet($content);
655             }
656              
657             # Catch vars like ${FOO_BAR}. This is probably not good enough.
658 33         790 for my $cast (
659             @{
660             $self->ppi_document->find(
661 1799     1799   20227 sub { $_[1]->isa('PPI::Token::Cast'); }
662             )
663 33 100       742 || []
664             }
665             ) {
666 2 50 33     40 if ( !$cast->snext_sibling
667             || !$cast->snext_sibling->isa('PPI::Structure::Block') ) {
668 0         0 next;
669             }
670              
671 2         167 my $sigil = $cast . q{};
672 2         14 my $sibling = $cast->snext_sibling . q{};
673 2 50       130 if ( $sibling =~ m/{(\w+)}/ ) {
674 2         12 push @symbols, $sigil . $1;
675             }
676             }
677 33         803 my %symbols = map { $_ => 1 } @symbols;
  12         55  
678 33         776 return \%symbols;
679             }
680              
681             # Returns a HashRef of modules which will always be converted to avoid imports.
682             # This is mostly for speed and a matter of convenience so that we don't have to
683             # examine modules (like strictly Object Oriented modules) which we know will
684             # not have anything to export.
685              
686             sub _build_never_exports {
687 36     36   632 my $self = shift;
688              
689 36         378 my %modules = (
690             'App::perlimports::Include' => 1,
691             'File::Spec' => 1,
692             'HTTP::Daemon' => 1,
693             'HTTP::Headers' => 1,
694             'HTTP::Response' => 1,
695             'HTTP::Tiny' => 1,
696             'LWP::UserAgent' => 1,
697             'URI' => 1,
698             'WWW::Mechanize' => 1,
699             );
700              
701 36 50       356 if ( $self->_has_never_export_modules ) {
702 0         0 for my $module ( @{ $self->_never_export_modules } ) {
  0         0  
703 0         0 $modules{$module} = 1;
704             }
705             }
706              
707 36         649 return \%modules;
708             }
709              
710             sub _build_sub_names {
711 34     34   76525 my $self = shift;
712              
713 34         98 my %sub_names;
714 34         117 for my $sub (
715             @{
716             $self->ppi_document->find(
717 1883     1883   21295 sub { $_[1]->isa('PPI::Statement::Sub') }
718             )
719 34 100       952 || []
720             }
721             ) {
722 11         189 my @children = $sub->schildren;
723 11 50 33     238 if ( $children[0] eq 'sub'
724             && $children[1]->isa('PPI::Token::Word') ) {
725 11         362 $sub_names{"$children[1]"} = 1;
726             }
727             }
728              
729 34         795 return \%sub_names;
730             }
731              
732             sub _has_import_switches {
733 54     54   2003 my $self = shift;
734 54         180 my $module_name = shift;
735              
736             # If switches are being passed to import, we can't guess as what is correct
737             # here.
738             #
739             # Getopt::Long uses a leading colon rather than a dash. This overrides
740             # Exporter's defaults. You would normally assume that :config is an export
741             # tag, but instead it's something entirely different.
742             #
743             # use Getopt::Long qw(:config no_ignore_case bundling);
744             #
745             # We will leave this case as broken for the time being. I'm not sure how
746             # common that invocation is.
747              
748 54 100 66     1212 if ( exists $self->original_imports->{$module_name}
749 39     39   474 && any { $_ =~ m{^[\-]} }
750 54 100       2627 @{ $self->original_imports->{$module_name} || [] } ) {
751 1         12 return 1;
752             }
753 53         1092 return 0;
754             }
755              
756             sub _is_used_fully_qualified {
757 13     13   660 my $self = shift;
758 13         34 my $module_name = shift;
759              
760             # We could tighten this up and check that the word following "::" is a sub
761             # which exists in that package.
762             #
763             # Module::function
764             # Module::->new
765             # isa => ArrayRef[Module::]
766             return 1 if $self->ppi_document->find(
767             sub {
768             (
769 2433 100 100 2433   67185 $_[1]->isa('PPI::Token::Word')
      100        
      100        
770             && (
771             $_[1]->content =~ m{\A${module_name}::[a-zA-Z0-9_]*\z}
772             || ( $_[1]->content eq ${module_name}
773             && $_[1]->snext_sibling eq '->' )
774             )
775             )
776             || ( $_[1]->isa('PPI::Token::Symbol')
777             && $_[1] =~ m{\A[*\$\@\%]+${module_name}::[a-zA-Z0-9_]} );
778             }
779 13 100       314 );
780              
781             # We could combine the regexes, but this is easy to read.
782 4         89 for my $key ( keys %{ $self->interpolated_symbols } ) {
  4         105  
783              
784             # package level variable
785 1 50       55 return 1 if $key =~ m{\A[*\$\@\%]+${module_name}::[a-zA-Z0-9_]+\z};
786              
787             # function
788 0 0       0 return 1 if $key =~ m/\A${module_name}::[a-zA-Z0-9_]+\z/;
789             }
790              
791 3         73 return 0;
792             }
793              
794             sub _is_ignored {
795 75     75   17273 my $self = shift;
796 75         169 my $element = shift;
797              
798             my $res
799             = exists $default_ignore{ $element->module }
800             || exists $self->_ignore_modules->{ $element->module }
801             || $self->_annotations->is_ignored($element)
802             || (
803 0     0   0 any { $element->module =~ /$_/ }
804 75   100     270 grep { $_ } @{ $self->_ignore_modules_pattern || [] }
805             )
806             || ( $self->inspector_for( $element->module )
807             && !$self->inspector_for( $element->module )->evals_ok );
808 73         1073 return $res;
809             }
810              
811             sub inspector_for {
812 196     196 1 7228 my $self = shift;
813 196         383 my $module = shift;
814              
815             # This would produce a warning and no helpful information.
816 196 50       605 return undef if $module eq 'Exporter';
817              
818 196 100       844 if ( $self->_has_inspector_for($module) ) {
819 130         2977 return $self->_get_inspector_for($module);
820             }
821              
822 66 50       3524 if ( $self->_cache ) {
823 0         0 require Sereal::Decoder; ## no perlimports
824 0         0 my $decoder = Sereal::Decoder->new( {} );
825 0         0 my $file = $self->_cache_file_for_module($module);
826 0         0 my $inspector;
827 0 0       0 if ( -e $file ) {
828             try {
829 0     0   0 $inspector = $decoder->decode_from_file($file);
830 0         0 $self->_set_inspector_for( $module, $inspector );
831             }
832             catch {
833 0     0   0 $self->logger->error($_);
834 0         0 };
835 0 0       0 if ($inspector) {
836 0         0 $self->logger->info("Using cached version of $module");
837 0         0 $inspector->set_logger( $self->logger );
838 0         0 return $inspector;
839             }
840             }
841             }
842              
843             try {
844 66     66   4956 $self->_set_inspector_for(
845             $module,
846             App::perlimports::ExportInspector->new(
847             logger => $self->logger,
848             module_name => $module,
849             )
850             );
851             }
852             catch {
853 2     2   23478 $self->logger->info( 'inspector_for' . $_ );
854 0         0 $self->_set_inspector_for( $module, undef );
855 66         2887 };
856              
857 64         413762 return $self->_get_inspector_for($module);
858             }
859              
860             sub tidied_document {
861 41     41 1 6586 return shift->_lint_or_tidy_document;
862             }
863              
864             sub linter_success {
865 1     1 1 103 return shift->_lint_or_tidy_document;
866             }
867              
868             # Kind of on odd interface, but right now we return either a tidied document or
869             # the result of linting. Could probably clean this up at some point, but I'm
870             # not sure yet how much the linting will change.
871             sub _lint_or_tidy_document {
872 42     42   115 my $self = shift;
873              
874 42         135 my $linter_error = 0;
875 42         124 my %processed;
876              
877             INCLUDE:
878 42         243 foreach my $include ( $self->all_includes ) {
879              
880             # If a module is used more than once, that's usually a mistake.
881 42 100 100     3411 if ( !$self->_preserve_duplicates
882             && exists $processed{ $include->module } ) {
883              
884 1 50       53 if ( $self->lint ) {
885 0         0 $self->_warn_diff_for_linter(
886             'has already been used and should be removed',
887             $include,
888             $include->content,
889             q{}
890             );
891 0         0 $linter_error = 1;
892 0         0 next INCLUDE;
893             }
894              
895 1         24 $self->logger->info( $include->module
896             . ' has already been used. Removing at line '
897             . $include->line_number );
898 1         143 _remove_with_trailing_characters($include);
899 1         3 next INCLUDE;
900             }
901              
902 41         365 $self->logger->notice( '📦 ' . "Processing include: $include" );
903              
904             my $e = App::perlimports::Include->new(
905             document => $self,
906             include => $include,
907             logger => $self->logger,
908 41         5451 original_imports => $self->original_imports->{ $include->module },
909             pad_imports => $self->_padding,
910             tidy_whitespace => $self->_tidy_whitespace,
911             );
912 41         2317 my $elem;
913             try {
914 41     41   3048 $elem = $e->formatted_ppi_statement;
915             }
916             catch {
917 0     0   0 my $error = $_;
918 0         0 $self->logger->error( 'Error in ' . $self->_filename );
919 0         0 $self->logger->error( 'Trying to format: ' . $include );
920 0         0 $self->logger->error( 'Error is: ' . $error );
921 41         461 };
922              
923 41 50       7467 next INCLUDE unless $elem;
924              
925             # If this is a module with bare imports which is not used anywhere,
926             # maybe we can just remove it.
927 41 100       319 if ( !$self->_preserve_unused ) {
928 15         71 my @args = $elem->arguments;
929              
930 15 100 100     785 if ( $args[0]
      100        
931             && $args[0] eq '()'
932             && !$self->_is_used_fully_qualified( $include->module ) ) {
933              
934 2 50       47 if ( $self->lint ) {
935 0         0 $self->_warn_diff_for_linter(
936             'appears to be unused and should be removed',
937             $include, $include->content,
938             q{}
939             );
940 0         0 $linter_error = 1;
941 0         0 next INCLUDE;
942             }
943              
944 2         121 $self->logger->info( 'Removing '
945             . $include->module
946             . ' as it appears to be unused' );
947 2         296 _remove_with_trailing_characters($include);
948              
949 2         45 next INCLUDE;
950             }
951             }
952              
953             ## no critic (Subroutines::ProhibitCallsToUnexportedSubs)
954             # Let's see if the import itself might break something
955 39 100       378 if ( my $err
956             = App::perlimports::Sandbox::eval_pkg( $elem->module, "$elem" ) )
957             {
958 1         11 $self->logger->warning(
959             sprintf(
960             'New include (%s) triggers error (%s)', $elem, $err
961             )
962             );
963 1         163 next INCLUDE;
964             }
965             ## use critic
966              
967 38         391 my $inserted = $include->replace($elem);
968 38 50       3362 if ( !$inserted ) {
969 0         0 $self->logger->error( 'Could not insert ' . $elem );
970             }
971             else {
972 38         208 $processed{ $include->module } = 1;
973              
974 38 100       2112 if ( $self->lint ) {
975             my $before = join q{ },
976 2         27 map { $_->content } $include->arguments;
  1         37  
977 2         40 my $after = join q{ }, map { $_->content } $elem->arguments;
  2         89  
978              
979 2 50       50 if ( $before ne $after ) {
980 2         7 $self->_warn_diff_for_linter(
981             'import arguments need tidying',
982             $include,
983             $include->content,
984             $elem->content
985             );
986 2         120 $linter_error = 1;
987 2         13 next INCLUDE;
988             }
989             }
990              
991 36         1402 $self->logger->info("resetting imports for |$elem|");
992              
993             # Now reset original_imports so that we can account for any changes
994             # when processing includes further down the list.
995 36         4293 my $doc = PPI::Document->new( \"$elem" );
996              
997 36 50       46092 if ( !$doc ) {
998 0         0 $self->logger->error("PPI could not parse $elem");
999             }
1000             else {
1001             my $new_include
1002             = $doc->find(
1003 36     288   290 sub { $_[1]->isa('PPI::Statement::Include') } );
  288         3953  
1004              
1005 36         622 $self->_reset_original_import(
1006             $include->module,
1007             _imports_for_include( $new_include->[0] )
1008             );
1009             }
1010             }
1011             }
1012              
1013 42         4344 $self->_maybe_cache_inspectors;
1014              
1015             # We need to do serialize in order to preserve HEREDOCs.
1016             # See https://metacpan.org/pod/PPI::Document#serialize
1017 42 100       1484 return $self->lint ? !$linter_error : $self->_ppi_selection->serialize;
1018             }
1019              
1020             sub _warn_diff_for_linter {
1021 2     2   109 my $self = shift;
1022 2         7 my $reason = shift;
1023 2         3 my $include = shift;
1024 2         4 my $before = shift;
1025 2         5 my $after = shift;
1026 2         3 my $after_deleted = !$after;
1027              
1028 2         5 my $json;
1029             my $justification;
1030              
1031 2 50       43 if ( $self->json ) {
1032              
1033 0         0 my $loc = { start => { line => $include->line_number } };
1034 0         0 my $content = $include->content;
1035 0         0 my @lines = split( m{\n}, $content );
1036              
1037 0 0       0 if ( $lines[0] =~ m{[^\s]} ) {
1038 0         0 $loc->{start}->{column} = @-;
1039             }
1040 0         0 $loc->{end}->{line} = $include->line_number + @lines - 1;
1041 0         0 $loc->{end}->{column} = length( $lines[-1] );
1042              
1043 0         0 $json = {
1044             filename => $self->_filename,
1045             location => $loc,
1046             module => $include->module,
1047             reason => $reason,
1048             };
1049             }
1050             else {
1051 2         66 $justification = sprintf(
1052             '❌ %s (%s) at %s line %i',
1053             $include->module, $reason, $self->_filename, $include->line_number
1054             );
1055             }
1056              
1057 2         130 my $padding = $include->line_number - 1;
1058 2         50 $before = sprintf( "%s%s\n", "\n" x $padding, $before );
1059 2         7 $after = sprintf( "%s%s\n", "\n" x $padding, $after );
1060 2 50       6 chomp $after if $after_deleted;
1061              
1062 2         16 my $diff = Text::Diff::diff(
1063             \$before, \$after,
1064             {
1065             CONTEXT => 0,
1066             STYLE => 'Unified',
1067             }
1068             );
1069              
1070 2 50       955 if ( $self->json ) {
1071 0         0 $json->{diff} = $diff;
1072 0         0 $self->logger->error( $self->_json_encoder->encode($json) );
1073             }
1074             else {
1075 2         39 $self->logger->error($justification);
1076 2         162 $self->logger->error($diff);
1077             }
1078             }
1079              
1080             sub _remove_with_trailing_characters {
1081 3     3   10 my $include = shift;
1082              
1083 3         29 while ( my $next = $include->next_sibling ) {
1084 7 50 66     384 if ( !$next->isa('PPI::Token::Whitespace')
1085             && !$next->isa('PPI::Token::Comment') ) {
1086 0         0 last;
1087             }
1088 7         65 $next->remove;
1089 7 100       381 last if $next eq "\n";
1090             }
1091 3         58 $include->remove;
1092 3         189 return;
1093             }
1094              
1095             sub _build_cache_dir {
1096             my $base_path
1097             = defined $ENV{HOME} && -d path( $ENV{HOME}, '.cache' )
1098 0 0 0 0   0 ? path( $ENV{HOME}, '.cache' )
1099             : path('/tmp');
1100              
1101 0         0 my $cache_dir = $base_path->child( 'perlimports', $VERSION );
1102 0         0 $cache_dir->mkpath;
1103              
1104 0         0 return $cache_dir;
1105             }
1106              
1107             sub _cache_file_for_module {
1108 0     0   0 my $self = shift;
1109 0         0 my $module = shift;
1110              
1111 0         0 return $self->_cache_dir->child($module);
1112             }
1113              
1114             sub _maybe_cache_inspectors {
1115 42     42   114 my $self = shift;
1116 42 50       1093 return unless $self->_cache;
1117              
1118 0           my @names = sort $self->all_inspector_names;
1119 0           $self->logger->info('maybe cache');
1120 0 0         return unless @names;
1121              
1122 0           my $append = 0;
1123 0           require Sereal::Encoder; ## no perlimports
1124 0           my $encoder = Sereal::Encoder->new(
1125             { croak_on_bless => 0, undef_unknown => 1, } );
1126              
1127 0           for my $name ( $self->all_inspector_names ) {
1128 0           my $file = $self->_cache_file_for_module($name);
1129 0 0         next if -e $file;
1130              
1131 0           $self->logger->info("I would like to cache $name at $file");
1132 0           $encoder->encode_to_file(
1133             $file,
1134             $self->inspector_for($name),
1135             $append
1136             );
1137             }
1138 0           return;
1139             }
1140              
1141             1;
1142              
1143             # ABSTRACT: Make implicit imports explicit
1144              
1145             __END__
1146              
1147             =pod
1148              
1149             =encoding UTF-8
1150              
1151             =head1 NAME
1152              
1153             App::perlimports::Document - Make implicit imports explicit
1154              
1155             =head1 VERSION
1156              
1157             version 0.000051
1158              
1159             =head1 MOTIVATION
1160              
1161             This module is to be used internally by L<perlimports>. It shouldn't be relied
1162             upon by anything else.
1163              
1164             =head2 inspector_for( $module_name )
1165              
1166             Returns an L<App::perlimports::ExporterInspector> object for the given module.
1167              
1168             =head2 linter_success
1169              
1170             Returns true if document was linted without errors, otherwise false.
1171              
1172             =head2 tidied_document
1173              
1174             Returns a serialized PPI document with (hopefully) tidy import statements.
1175              
1176             =head1 AUTHOR
1177              
1178             Olaf Alders <olaf@wundercounter.com>
1179              
1180             =head1 COPYRIGHT AND LICENSE
1181              
1182             This software is copyright (c) 2020 by Olaf Alders.
1183              
1184             This is free software; you can redistribute it and/or modify it under
1185             the same terms as the Perl 5 programming language system itself.
1186              
1187             =cut