File Coverage

blib/lib/Mojolicious/Command/listdeps.pm
Criterion Covered Total %
statement 35 143 24.4
branch 1 62 1.6
condition 0 26 0.0
subroutine 10 21 47.6
pod 1 1 100.0
total 47 253 18.5


line stmt bran cond sub pod time code
1             package Mojolicious::Command::listdeps;
2             ##----------------------------------------------------------------------------
3             ## :mode=perl:indentSize=2:tabSize=2:noTabs=true:
4             ##****************************************************************************
5             ## NOTES:
6             ## * Intent is to have perl critic to complete with no errors when run
7             ## at the HARSH (3) level
8             ##****************************************************************************
9            
10             =head1 NAME
11            
12             Mojolicious::Command::listdeps - Command to list dependencies for a
13             Mojolicious project
14            
15             =head1 VERSION
16            
17             Version 0.07
18            
19             =head1 DESCRIPTION
20            
21             L lists all module dependencies, and
22             is typically invoked from the command line in the root of your
23             L project
24            
25             =head1 SYNOPSIS
26            
27             use Mojolicious::Command::listdeps;
28            
29             my $command = Mojolicious::Command::listdeps->new;
30             $command->run(@ARGV);
31            
32             =head1 COMMANDLINE OPTIONS
33            
34             The listdeps command supports the following command line options:
35            
36             =over 2
37            
38             =item --include-tests
39            
40             Include dependencies required for tests
41            
42             =item --missing
43            
44             Only list missing modules
45            
46             =item --skip-lib
47            
48             Do not list modules found in ./lib as a dependency
49            
50             =item --verbose
51            
52             List additional information
53            
54             =item --core
55            
56             Include core modules in list
57            
58             =item --cpanfile
59            
60             Create or append module information to cpanfile
61            
62             =back
63            
64             =cut
65            
66             ##****************************************************************************
67             ##****************************************************************************
68 1     1   830 use strict;
  1         2  
  1         27  
69 1     1   5 use warnings;
  1         1  
  1         43  
70 1     1   5 use Mojo::Base 'Mojolicious::Command';
  1         2  
  1         12  
71 1     1   792045 use File::Find;
  1         2  
  1         56  
72 1     1   5 use File::Spec;
  1         2  
  1         44  
73 1     1   4185 use Module::CoreList;
  1         54044  
  1         11  
74 1     1   579 use Cwd qw(abs_path);
  1         2  
  1         87  
75 1     1   1321 use Getopt::Long qw(GetOptions :config pass_through);
  1         11902  
  1         4  
76            
77             our $VERSION = "0.07";
78            
79             ##****************************************************************************
80             ## Object attributes
81             ##****************************************************************************
82            
83             =head1 ATTRIBUTES
84            
85             L inherits the following attributes
86             from L
87            
88             =cut
89            
90             ##------------------------------------------------------------
91            
92             =head2 C
93            
94             Short description displayed in the "mojo" command list
95            
96             =cut
97            
98             ##------------------------------------------------------------
99             has description => qq{List module dependencies.\n};
100            
101             ##------------------------------------------------------------
102            
103             =head2 C
104            
105             Displayed in response to mojo help listdeps
106            
107             =cut
108            
109             ##------------------------------------------------------------
110             has usage => << "EOF";
111             usage: $0 listdeps [OPTIONS]
112            
113             Parses all files found in the current directory and below and
114             prints the names of perl modules used in those files.
115            
116             These options are available:
117             --include-tests Include dependencies required for tests
118             --missing Only list missing modules
119             --skip-lib Do not list modules found in ./lib as a dependency
120             --verbose List additional information
121             --core Include core modules in list
122             --cpanfile Create or append module information to cpanfile
123             EOF
124            
125             ##-----------------------------------------
126             ## Module variables
127             ##-----------------------------------------
128             my $include_tests = 0; ## Scan test modules also
129             my $missing_only = 0; ## Display only missing modules
130             my $verbose = 0; ## Extra verbage
131             my $skip_core = 1; ## Skip core modules
132             my $skip_lib = 0; ## Skip modules found in ./lib
133             my $lib_dir = qq{}; ## Local ./lib if found
134             my $cpanfile = qq{}; ## Name of cpanfile
135            
136             ##****************************************************************************
137             ## Object methods
138             ##****************************************************************************
139            
140             =head1 METHODS
141            
142             L inherits its methods from
143             from L
144            
145             =cut
146            
147             ##****************************************************************************
148             ##****************************************************************************
149            
150             =head2 C
151            
152             $command->run;
153             $command->run(@ARGV);
154            
155             Used to invoke the command.
156            
157             =cut
158            
159             ##----------------------------------------------------------------------------
160             sub run ## no critic (RequireArgUnpacking)
161             {
162 1     1 1 767 my $self = shift;
163 1         3 my @args = @_;
164            
165             ## Parse the options
166             GetOptions(
167 0     0   0 'include-tests' => sub { $include_tests = 1; },
168 0     0   0 'core' => sub { $skip_core = 0; },
169 0     0   0 'missing' => sub { $missing_only = 1; },
170 0     0   0 'skip-lib' => sub { $skip_lib = 1; },
171 0     0   0 'verbose' => sub { $verbose = 1; },
172             'cpanfile:s' =>
173             sub
174             {
175 0     0   0 my $opt_name = shift;
176 0         0 $cpanfile = shift;
177             ## If no value is provided, use the default "cpanfile"
178 0 0       0 $cpanfile = qq{cpanfile} unless ($cpanfile);
179             },
180 1         15 );
181            
182             ## See if we can load the required modules
183 1         369 foreach my $module (qq{PPI}, qq{Module::Info},)
184             {
185 1 50       5 unless (_load_module($module))
186             {
187 1         102 print STDERR (qq{ERROR: Could not load $module!\n});
188 1         9 return -1;
189             }
190             }
191            
192             ## Convert perl version to something find_version can use
193 0         0 my $numeric_v = _numify_version($^V);
194            
195             ## Determine hash of core modules
196 0         0 my $core_modules = Module::CoreList->find_version($numeric_v);
197 0 0       0 unless ($core_modules)
198             {
199 0         0 print STDERR (
200             qq{ERROR: Could not determine list of core modules },
201             qq{for this version of perl!\n}
202             );
203 0         0 return -1;
204             }
205            
206             ## List of files to scan
207 0         0 my @files = ();
208            
209             ## Find files to be scanned
210             File::Find::find(
211             {
212             wanted => sub {
213             ## Always look for modules (*.pm)
214 0 0   0   0 push(@files, File::Spec->canonpath($File::Find::name))
215             if ($_ =~ /\.pm$/x);
216             ## Also check test scripts (*.t) if enabled
217 0 0 0     0 push(@files, File::Spec->canonpath($File::Find::name))
218             if ($include_tests && ($_ =~ /\.t$/x));
219             },
220             },
221 0         0 qq{.}, ## Starting directory
222             );
223            
224             ## Set additional library paths
225 0 0       0 if (-d qq{lib})
226             {
227             ## Use canonpath to conver file separators
228 0         0 $lib_dir = File::Spec->canonpath(abs_path(qq{./lib}));
229             }
230            
231             ## Display extra information
232 0 0       0 if ($verbose)
233             {
234 0 0       0 print(
235             qq{Checking for module dependencies (},
236             ($include_tests ? qq{including} : qq{ignoring}),
237             qq{ test scripts)\n}
238             );
239 0 0       0 print(qq{Adding "./lib/" to include path\n}) if ($lib_dir);
240 0 0       0 print(qq{Skipping modules loaded from "$lib_dir"\n}) if ($lib_dir);
241 0         0 print(qq{Scanning the following:},
242             qq{\n "}, join(qq{",\n "}, @files), qq{"\n});
243             }
244            
245             ## Now scan files for dependencies
246 0         0 my $dependencies = _scan_for_dependencies(@files);
247            
248             ## Process the list
249 0         0 _process_results($dependencies, $core_modules);
250 0         0 return (0);
251             }
252            
253             ##----------------------------------------------------------------------------
254             ## @fn _process_results($modules_ref, $core_modules)
255             ## @brief Process the hash reference containing the moudle dependencies
256             ## @param $modules_ref - HASH reference whose keys are dependencies
257             ## @param $core_modules - HASH reference whose keys are core perl modules
258             ## @return
259             ## @note
260             ##----------------------------------------------------------------------------
261             sub _process_results
262             {
263 0     0   0 my $modules_ref = shift;
264 0         0 my $core_modules = shift;
265 0         0 my $cpanfh;
266            
267             ## Set the include path for Module::Info
268 0         0 my @new_inc = @INC;
269 0 0       0 push(@new_inc, $lib_dir) if ($lib_dir);
270            
271             ## Open file if needed
272 0 0       0 if ($cpanfile)
273             {
274 0         0 open($cpanfh, qq{>>}, $cpanfile);
275 0         0 print {$cpanfh} (qq{##}, qq{-} x 60, qq{\n});
  0         0  
276 0         0 print {$cpanfh} (qq{## Auto generated },
  0         0  
277             scalar(localtime),
278             qq{\n## using $0 listdeps },
279             qq{--cpanfile "$cpanfile"\n});
280 0         0 print {$cpanfh} (qq{##}, qq{-} x 60, qq{\n});
  0         0  
281             }
282            
283             ## Process the list
284 0         0 foreach my $key (sort(keys(%{$modules_ref})))
  0         0  
285             {
286             ## Convert Module/Name.pm (if needed)
287 0         0 my $module = $key;
288 0         0 $module =~ s{/}{::}gx;
289            
290             ## Skip core modules
291 0 0 0     0 next if (exists($core_modules->{$module}) && $skip_core);
292            
293             ## Get the module info
294 0         0 my $module_info = Module::Info->new_from_module($module, @new_inc);
295            
296             ## Skip modules that can be found (i.e. have $module_info
297 0 0 0     0 next if ($missing_only && $module_info);
298            
299             ## Skip modules that are not located in $lib_dir
300             next
301 0 0 0     0 if ($skip_lib
      0        
      0        
302             && $module_info
303             && $lib_dir
304             && ($lib_dir eq substr($module_info->file, 0, length($lib_dir))));
305            
306             ## See if we are creating a cpanfile
307 0 0       0 if ($cpanfh)
308             {
309             ## Write module name to the file
310 0         0 print {$cpanfh}(qq{require "$module"});
  0         0  
311 0         0 my $version;
312 0         0 eval { $version = $module_info->version; };
  0         0  
313 0 0 0     0 $version = qq{} if ($version and ($version eq qq{undef}));
314 0 0       0 if ($version)
315             {
316             ## Write version information to the cpanfile
317 0         0 print {$cpanfh}(qq{, "$version"});
  0         0  
318             }
319 0         0 print {$cpanfh}(qq{;\n});
  0         0  
320             }
321            
322             ## If we get here, then we need to list the file
323 0         0 print($module);
324 0 0       0 if ($verbose)
325             {
326 0 0       0 if ($module_info)
327             {
328             ## Found the module, so display the filename
329 0         0 print(qq{ loaded from "}, $module_info->file, qq{"});
330             }
331             else
332             {
333             ## Module is missing, so display name of files using the module
334             print(qq{ MISSING used by "},
335 0         0 join(qq{", "}, @{$modules_ref->{$module}->{used_by}}), qq{"});
  0         0  
336             }
337             }
338 0         0 print(qq{\n});
339             }
340            
341             ## Close the cpanfile (if it was open)
342 0 0       0 close($cpanfh) if ($cpanfh);
343 0         0 return;
344            
345             }
346            
347             ##----------------------------------------------------------------------------
348             ## @fn _scan_for_dependencies(@file_list)
349             ## @brief Use PPI to scan the list of files, returning a hash whose keys
350             ## are module names
351             ## @param @file_list - List of files to scan
352             ## @return HASH REFERENCE - Hash reference whose keys are module names
353             ## @note Based on code in Perl::PrereqScanner
354             ##----------------------------------------------------------------------------
355             sub _scan_for_dependencies
356             {
357 0     0   0 my @files = @_;
358 0         0 my $result = {};
359            
360             ## Iterate through the list of files
361 0         0 foreach my $file (@files)
362             {
363             ## Use PPI to parse the perl source
364 0         0 my $ppi_doc = PPI::Document->new($file);
365            
366             ## See if PPI encountered problems
367 0 0       0 if (defined($ppi_doc))
368             {
369             ## Find regular use and require
370 0   0     0 my $includes = $ppi_doc->find('Statement::Include') || [];
371 0         0 for my $node (@{$includes})
  0         0  
372             {
373             ## Ignore perl version require/use statments (i.e. "use 5.8;"
374 0 0       0 next if ($node->version);
375            
376             ## lib.pm is not a "real" dependency, so ignore it
377 0 0       0 next if grep { $_ eq $node->module } qw{ lib };
  0         0  
378            
379             ## Check for inheritance ("base 'Foo::Bar';"
380 0 0       0 if (grep { $_ eq $node->module } qw{ base parent })
  0         0  
381             {
382             ## Ignore the arguments, just look for the name of the parent
383             my @important = grep {
384 0 0       0 $_->isa('PPI::Token::QuoteLike::Words')
  0         0  
385             || $_->isa('PPI::Token::Quote')
386             } $node->arguments;
387            
388             ## Based on code from Perl::PrereqScanner
389             my @base_modules = map {
390 0         0 (
391             (
392 0 0 0     0 $_->isa('PPI::Token::QuoteLike::Words')
393             || $_->isa('PPI::Token::Number')
394             ) ? $_->literal : $_->string
395             )
396             } @important;
397            
398             ## Add the modules
399 0         0 foreach my $module (@base_modules)
400             {
401             ## Add the dependency of the parent
402 0         0 _add_used_by($result, $module, $file);
403             }
404             }
405             else
406             {
407             ## Skip statements like "require $foo"
408 0 0       0 next unless $node->module;
409            
410             ## Add the dependency
411 0         0 _add_used_by($result, $node->module, $file);
412             }
413             }
414             }
415             else
416             {
417 0         0 print STDERR (qq{Could not scan file "$file"\n});
418             }
419             }
420            
421 0         0 return ($result);
422             }
423            
424             ##----------------------------------------------------------------------------
425             ## @fn _add_used_by($hash_ref, $module_name, $used_by)
426             ## @brief Add an entry to the given hash (or create the entry if needed)
427             ## @param $hash_ref - HASH reference whose keys are module names
428             ## @param $module_name - Name of the required module
429             ## @param $used_by - Name of the script requiring the module
430             ## @return
431             ## @note
432             ##----------------------------------------------------------------------------
433             sub _add_used_by
434             {
435 0     0   0 my $hash_ref = shift;
436 0         0 my $module_name = shift;
437 0         0 my $used_by = shift;
438            
439             ## See if entry exists
440 0 0       0 unless (exists($hash_ref->{$module_name}))
441             {
442             ## Entry does not exist, so create a new entry
443 0         0 $hash_ref->{$module_name} = {used_by => [],};
444             }
445            
446             ## Add to the used_by key
447 0         0 push(@{$hash_ref->{$module_name}->{used_by}}, $used_by);
  0         0  
448            
449 0         0 return;
450             }
451            
452             ##----------------------------------------------------------------------------
453             ## @fn _load_module($module)
454             ## @brief Load the given module and return TRUE if module was loaded
455             ## @param $module - Name of the module
456             ## @return
457             ## @note
458             ##----------------------------------------------------------------------------
459             sub _load_module
460             {
461 1     1   2 my $module = shift;
462            
463             ## For ease of reading
464 1         3 my $eval_stmt = qq{require $module; import $module; 1;};
465            
466             ## Attempt to load module
467 1         100 my $loaded = eval $eval_stmt; ## no critic (ProhibitStringyEval)
468            
469 1         10 return $loaded;
470             }
471            
472             ##----------------------------------------------------------------------------
473             ## @fn numify_version($ver)
474             ## @brief Examine proivded version and return as version number
475             ## @param $ver - Version
476             ## @return SCALAR - Numeric representation of version
477             ## @note
478             ##----------------------------------------------------------------------------
479             sub _numify_version
480             {
481 0     0     my $ver = shift;
482            
483             ## See if version has multiple dots
484 0 0         if ($ver =~ /\..+\./x)
485             {
486             ## We need the version module to convert
487 0 0         unless (_load_module(qq{version}))
488             {
489 0           print STDERR (qq{ERROR: Cannot determine version from "$ver"\n});
490 0           return -1;
491             }
492             ## Convert version into number
493 0           $ver = version->new($ver)->numify;
494             }
495             ## Added 0 ensures perl treats variable as numeric
496 0           $ver += 0;
497            
498 0           return $ver;
499             }
500            
501             1;
502             __END__