File Coverage

blib/lib/Module/Extract/Use.pm
Criterion Covered Total %
statement 91 92 98.9
branch 24 26 92.3
condition 5 6 83.3
subroutine 26 27 96.3
pod 5 5 100.0
total 151 156 96.7


line stmt bran cond sub pod time code
1 5     5   1393663 use v5.10;
  5         15  
2 5     5   1959 use utf8;
  5         1271  
  5         44  
3              
4             package Module::Extract::Use;
5 5     5   182 use strict;
  5         8  
  5         118  
6              
7 5     5   18 use warnings;
  5         7  
  5         217  
8 5     5   19 no warnings;
  5     0   8  
  5         5959  
9              
10             our $VERSION = '1.055';
11              
12             =encoding utf8
13              
14             =head1 NAME
15              
16             Module::Extract::Use - Discover the modules a module explicitly uses
17              
18             =head1 SYNOPSIS
19              
20             use Module::Extract::Use;
21              
22             my $extor = Module::Extract::Use->new;
23              
24             my @modules = $extor->get_modules( $file );
25             if( $extor->error ) { ... }
26              
27             my $details = $extor->get_modules_with_details( $file );
28             foreach my $detail ( @$details ) {
29             printf "%s %s imports %s\n",
30             $detail->module, $detail->version,
31             join ' ', @{ $detail->imports }
32             }
33              
34             =head1 DESCRIPTION
35              
36             Extract the names of the modules used in a file using a static
37             analysis. Since this module does not run code, it cannot find dynamic
38             uses of modules, such as C. It only reports modules
39             that the file loads directly or are in the import lists for L
40             or L.
41              
42             The module can handle the conventional inclusion of modules with either
43             C or C as the statement:
44              
45             use Foo;
46             require Foo;
47              
48             use Foo 1.23;
49             use Foo qw(this that);
50              
51             It now finds C as an expression, which is useful to lazily
52             load a module once (and may be faster):
53              
54             sub do_something {
55             state $rc = require Foo;
56             ...
57             }
58              
59             Additionally, it finds module names used with C and C,
60             either of which establishes an inheritance relationship:
61              
62             use parent qw(Foo);
63             use base qw(Foo);
64              
65             In the case of namespaces found in C or C, the value of
66             the C method is false. In all other cases, it is true. You
67             can then skip those namespaces:
68              
69             my $details = $extor->get_modules_with_details( $file );
70             foreach my $detail ( @$details ) {
71             next unless $detail->direct;
72              
73             ...
74             }
75              
76             This module does not discover runtime machinations to load something,
77             such as string evals:
78              
79             eval "use Foo";
80              
81             my $bar = 'Bar';
82             eval "use $bar";
83              
84             If you want that, you might consider L (a confusingly
85             similar name).
86              
87             =cut
88              
89             =over 4
90              
91             =item new
92              
93             Makes an object. The object doesn't do anything just yet, but you need
94             it to call the methods.
95              
96             =cut
97              
98             sub new {
99 3     3 1 1662 my $class = shift;
100              
101 3         7 my $self = bless {}, $class;
102              
103 3         10 $self->init;
104              
105 3         6 $self;
106             }
107              
108             =item init
109              
110             Set up the object. You shouldn't need to call this yourself.
111              
112             =cut
113              
114             sub init {
115 3     3 1 10 $_[0]->_clear_error;
116             }
117              
118             =item get_modules( FILE )
119              
120             Returns a list of namespaces explicity use-d in FILE. Returns the
121             empty list if the file does not exist or if it can't parse the file.
122              
123             Each used namespace is only in the list even if it is used multiple
124             times in the file. The order of the list does not correspond to
125             anything so don't use the order to infer anything.
126              
127             =cut
128              
129             sub get_modules {
130 7     7 1 28723 my( $self, $file ) = @_;
131              
132 7         26 $self->_clear_error;
133              
134 7         17 my $details = $self->get_modules_with_details( $file );
135              
136 7         24 my @modules = map { $_->module } @$details;
  12         46  
137              
138 7         68 @modules;
139             }
140              
141             =item get_modules_with_details( FILE )
142              
143             Returns a list of hash references, one reference for each namespace
144             explicitly use-d in FILE. Each reference has keys for:
145              
146             namespace - the namespace, always defined
147             version - defined if a module version was specified
148             imports - an array reference to the import list
149             pragma - true if the module thinks this namespace is a pragma
150             direct - false if the module name came from parent or base
151              
152             Each used namespace is only in the list even if it is used multiple
153             times in the file. The order of the list does not correspond to
154             anything so don't use the order to infer anything.
155              
156             =cut
157              
158             sub get_modules_with_details {
159 9     9 1 1615 my( $self, $file ) = @_;
160              
161 9         20 $self->_clear_error;
162              
163 9         19 my $modules = $self->_get_ppi_for_file( $file );
164 9 100       10140 return [] unless defined $modules;
165              
166 6         23 $modules;
167             }
168              
169             sub _get_ppi_for_file {
170 9     9   15 my( $self, $file ) = @_;
171              
172 9 100       144 unless( -e $file ) {
173 1         9 $self->_set_error( ref( $self ) . ": File [$file] does not exist!" );
174 1         3 return;
175             }
176              
177 8         1505 require PPI;
178              
179 8         540614 my $Document = eval { PPI::Document->new( $file ) };
  8         75  
180 8 100       328992 unless( $Document ) {
181 2         17 $self->_set_error( ref( $self ) . ": Could not parse file [$file]" );
182 2         3 return;
183             }
184              
185             # this handles the
186             # use Foo;
187             # use Bar;
188 6         40 my $regular_modules = $self->_regular_load( $Document );
189              
190             # this handles
191             # use parent qw(...)
192 6         28 my $isa_modules = $self->_isa_load( $regular_modules );
193              
194             # this handles
195             # my $rc = require Foo;
196 6         23 my $expression_loads = $self->_expression_load( $Document );
197              
198 6         18 my @modules = map { @$_ }
  18         41  
199             $regular_modules,
200             $isa_modules,
201             $expression_loads
202             ;
203              
204 6         47 return \@modules;
205             }
206              
207             sub _regular_load {
208 6     6   16 my( $self, $Document ) = @_;
209              
210             my $modules = $Document->find(
211             sub {
212 1229     1229   15104 $_[1]->isa( 'PPI::Statement::Include' )
213             }
214 6         57 );
215              
216 6 100       97 return [] unless $modules;
217              
218 5         13 my %Seen;
219             my @modules =
220 29 100       390 grep { ! $Seen{ $_->{module} }++ && $_->{module} }
221             map {
222 5         16 my $hash = bless {
223             direct => 1,
224             content => $_->content,
225             pragma => $_->pragma,
226             module => $_->module,
227             imports => [ $self->_list_contents( $_->arguments ) ],
228 29 50       1333 version => eval{ $_->module_version->literal || ( undef ) },
  29         540  
229             }, 'Module::Extract::Use::Item';
230             } @$modules;
231              
232 5         34 \@modules;
233             }
234              
235             sub _isa_load {
236 6     6   24 my( $self, $modules ) = @_;
237             my @isa_modules =
238             map {
239 1         3 my $m = $_;
240             map {
241 1         4 bless {
242             content => $m->content,
243             pragma => '',
244             direct => 0,
245             module => $_,
246             imports => [],
247             version => undef,
248             }, 'Module::Extract::Use::Item';
249 1         2 } @{ $m->imports };
  1         3  
250             }
251 6 100       16 grep { $_->module eq 'parent' or $_->module eq 'base' }
  22         41  
252             @$modules;
253              
254 6         14 \@isa_modules;
255             }
256              
257             sub _expression_load {
258 6     6   15 my( $self, $Document ) = @_;
259              
260             my $in_statements = $Document->find(
261             sub {
262 1229     1229   13927 my $sib;
263 1229 100 100     4546 $_[1]->isa( 'PPI::Token::Word' ) &&
      66        
264             $_[1]->content eq 'require' &&
265             ( $sib = $_[1]->snext_sibling() ) &&
266             $sib->isa( 'PPI::Token::Word' )
267             }
268 6         42 );
269              
270 6 100       105 return [] unless $in_statements;
271              
272             my @modules =
273             map {
274 1         16 bless {
  2         64  
275             content => $_->parent->content,
276             pragma => undef,
277             direct => 1,
278             module => $_->snext_sibling->content,
279             imports => [],
280             version => undef,
281             }, 'Module::Extract::Use::Item';
282             }
283             @$in_statements;
284              
285 1         45 \@modules;
286             }
287              
288 0         0 BEGIN {
289             package Module::Extract::Use::Item;
290              
291 1     1   3427 sub direct { $_[0]->{direct} }
292 2     2   43 sub content { $_[0]->{content} }
293 1     1   8 sub pragma { $_[0]->{pragma} }
294 56     56   167 sub module { $_[0]->{module} }
295 1     1   2 sub imports { $_[0]->{imports} }
296 1     1   6 sub version { $_[0]->{version} }
297             }
298              
299             sub _list_contents {
300 29     29   2653 my( $self, $node ) = @_;
301              
302 29         39 eval {
303 29 100       130 if( ! defined $node ) {
    100          
    100          
    50          
304 15         37 return;
305             }
306             elsif( $node->isa( 'PPI::Token::QuoteLike::Words' ) ) {
307 6         55 ( $node->literal )
308             }
309             elsif( $node->isa( 'PPI::Structure::List' ) ) {
310 1     5   14 my $nodes = $node->find( sub{ $_[1]->isa( 'PPI::Token::Quote' ) } );
  5         47  
311 1         11 map { $_->string } @$nodes;
  2         17  
312             }
313             elsif( $node->isa( 'PPI::Token::Quote' ) ) {
314 7         26 ( $node->string );
315             }
316             };
317              
318             }
319              
320             =item error
321              
322             Return the error from the last call to C.
323              
324             =cut
325              
326 3     3   9 sub _set_error { $_[0]->{error} = $_[1]; }
327              
328 19     19   45 sub _clear_error { $_[0]->{error} = '' }
329              
330 2     2 1 42 sub error { $_[0]->{error} }
331              
332             =back
333              
334             =head1 TO DO
335              
336             =head1 SEE ALSO
337              
338             L, L
339              
340             =head1 SOURCE AVAILABILITY
341              
342             The source code is on GitHub:
343              
344             https://github.com/briandfoy/module-extract-use
345              
346             =head1 AUTHOR
347              
348             brian d foy, C<< >>
349              
350             =head1 COPYRIGHT AND LICENSE
351              
352             Copyright © 2008-2026, brian d foy C<< >>. All rights reserved.
353              
354             This project is under the Artistic License 2.0.
355              
356             =cut
357              
358             1;