File Coverage

blib/lib/Mojolicious/Command/listdeps.pm
Criterion Covered Total %
statement 96 143 67.1
branch 24 62 38.7
condition 7 26 26.9
subroutine 15 21 71.4
pod 1 1 100.0
total 143 253 56.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.08
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   737 use strict;
  1         2  
  1         25  
69 1     1   5 use warnings;
  1         1  
  1         39  
70 1     1   4 use Mojo::Base 'Mojolicious::Command';
  1         2  
  1         8  
71 1     1   120418 use File::Find;
  1         2  
  1         52  
72 1     1   5 use File::Spec;
  1         2  
  1         20  
73 1     1   3830 use Module::CoreList;
  1         53001  
  1         11  
74 1     1   575 use Cwd qw(abs_path);
  1         2  
  1         80  
75 1     1   1286 use Getopt::Long qw(GetOptions :config pass_through);
  1         11219  
  1         5  
76            
77             our $VERSION = "0.08";
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 434 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         12 );
181            
182             ## See if we can load the required modules
183 1         348 foreach my $module (qq{PPI}, qq{Module::Info},)
184             {
185 2 50       6 unless (_load_module($module))
186             {
187 0         0 print STDERR (qq{ERROR: Could not load $module!\n});
188 0         0 return -1;
189             }
190             }
191            
192             ## Convert perl version to something find_version can use
193 1         4 my $numeric_v = _numify_version($^V);
194            
195             ## Determine hash of core modules
196 1         8 my $core_modules = Module::CoreList->find_version($numeric_v);
197 1 50       26 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 1         2 my @files = ();
208            
209             ## Find files to be scanned
210             File::Find::find(
211             {
212             wanted => sub {
213             ## Always look for modules (*.pm)
214 64 100   64   194 push(@files, File::Spec->canonpath($File::Find::name))
215             if ($_ =~ /\.pm$/x);
216             ## Also check test scripts (*.t) if enabled
217 64 50 33     1945 push(@files, File::Spec->canonpath($File::Find::name))
218             if ($include_tests && ($_ =~ /\.t$/x));
219             },
220             },
221 1         97 qq{.}, ## Starting directory
222             );
223            
224             ## Set additional library paths
225 1 50       15 if (-d qq{lib})
226             {
227             ## Use canonpath to conver file separators
228 1         25 $lib_dir = File::Spec->canonpath(abs_path(qq{./lib}));
229             }
230            
231             ## Display extra information
232 1 50       5 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 1         4 my $dependencies = _scan_for_dependencies(@files);
247            
248             ## Process the list
249 1         5 _process_results($dependencies, $core_modules);
250 1         15 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 1     1   3 my $modules_ref = shift;
264 1         2 my $core_modules = shift;
265 1         2 my $cpanfh;
266            
267             ## Set the include path for Module::Info
268 1         8 my @new_inc = @INC;
269 1 50       6 push(@new_inc, $lib_dir) if ($lib_dir);
270            
271             ## Open file if needed
272 1 50       4 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 1         2 foreach my $key (sort(keys(%{$modules_ref})))
  1         12  
285             {
286             ## Convert Module/Name.pm (if needed)
287 8         180 my $module = $key;
288 8         14 $module =~ s{/}{::}gx;
289            
290             ## Skip core modules
291 8 100 66     34 next if (exists($core_modules->{$module}) && $skip_core);
292            
293             ## Get the module info
294 1         563 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 1 50 33     361 next if ($missing_only && $module_info);
298            
299             ## Skip modules that are not located in $lib_dir
300             next
301 1 0 33     5 if ($skip_lib
      33        
      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 1 50       3 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 1         339 print($module);
324 1 50       5 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 1         13 print(qq{\n});
339             }
340            
341             ## Close the cpanfile (if it was open)
342 1 50       18 close($cpanfh) if ($cpanfh);
343 1         4 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 1     1   2 my @files = @_;
358 1         3 my $result = {};
359            
360             ## Iterate through the list of files
361 1         3 foreach my $file (@files)
362             {
363             ## Use PPI to parse the perl source
364 2         15034 my $ppi_doc = PPI::Document->new($file);
365            
366             ## See if PPI encountered problems
367 2 50       461412 if (defined($ppi_doc))
368             {
369             ## Find regular use and require
370 2   50     13 my $includes = $ppi_doc->find('Statement::Include') || [];
371 2         142065 for my $node (@{$includes})
  2         6  
372             {
373             ## Ignore perl version require/use statments (i.e. "use 5.8;"
374 16 50       50 next if ($node->version);
375            
376             ## lib.pm is not a "real" dependency, so ignore it
377 16 50       351 next if grep { $_ eq $node->module } qw{ lib };
  16         44  
378            
379             ## Check for inheritance ("base 'Foo::Bar';"
380 16 50       364 if (grep { $_ eq $node->module } qw{ base parent })
  32         389  
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 16 50       354 next unless $node->module;
409            
410             ## Add the dependency
411 16         347 _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 1         14879 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 16     16   317 my $hash_ref = shift;
436 16         23 my $module_name = shift;
437 16         20 my $used_by = shift;
438            
439             ## See if entry exists
440 16 100       38 unless (exists($hash_ref->{$module_name}))
441             {
442             ## Entry does not exist, so create a new entry
443 8         30 $hash_ref->{$module_name} = {used_by => [],};
444             }
445            
446             ## Add to the used_by key
447 16         21 push(@{$hash_ref->{$module_name}->{used_by}}, $used_by);
  16         50  
448            
449 16         48 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 3     3   6 my $module = shift;
462            
463             ## For ease of reading
464 3         8 my $eval_stmt = qq{require $module; import $module; 1;};
465            
466             ## Attempt to load module
467 3         258 my $loaded = eval $eval_stmt; ## no critic (ProhibitStringyEval)
468            
469 3         30 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 1     1   3 my $ver = shift;
482            
483             ## See if version has multiple dots
484 1 50       11 if ($ver =~ /\..+\./x)
485             {
486             ## We need the version module to convert
487 1 50       3 unless (_load_module(qq{version}))
488             {
489 0         0 print STDERR (qq{ERROR: Cannot determine version from "$ver"\n});
490 0         0 return -1;
491             }
492             ## Convert version into number
493 1         14 $ver = version->new($ver)->numify;
494             }
495             ## Added 0 ensures perl treats variable as numeric
496 1         6 $ver += 0;
497            
498 1         3 return $ver;
499             }
500            
501             1;
502             __END__