File Coverage

blib/lib/Test/CircularDependencies.pm
Criterion Covered Total %
statement 99 110 90.0
branch 31 52 59.6
condition 2 6 33.3
subroutine 14 14 100.0
pod 0 5 0.0
total 146 187 78.0


line stmt bran cond sub pod time code
1             package Test::CircularDependencies;
2 2     2   38571 use strict;
  2         5  
  2         56  
3 2     2   10 use warnings;
  2         4  
  2         111  
4              
5             our $VERSION = '0.02';
6              
7             =head1 NAME
8              
9             Test::CircularDependencies - make sure non of the modules depend on themselves
10              
11             =head1 SYNOPSIS
12              
13              
14             perl -Ilib script/find-circular-dependencies.pl t/circular_dependency/my_exe.pl --dir t/circular_dependency/
15              
16             =head1 DESCRIPTION
17              
18             Given one or more scripts, modules, or directories containing those, create a data structure that represents the dependencies.
19             Allow the user to restrict the recursion to files found specific directories.
20              
21             So let's say we have several application in our company and I'd like to make sure there are no circular dependencies.
22              
23             projectA/
24             lib/A.pm
25             bin/exe.pl
26             projectB/
27             lib/
28             B.pm
29             Module/
30             C.pm
31             D.pm
32              
33             but for histoical reasons while C.pm holds 'package Module::C;' D.pm holds 'package D;' so
34             when we use this we need to
35              
36             use lib 'projectA/lib';
37             use lib 'projectB/lib';
38             use lib 'projectB/lib/Module';
39              
40             See als L<circular::require>
41              
42             =head1 AUTHOR
43              
44             L<Gabor Szabo|http://szabgab.com/>
45              
46             =head1 COPYRIGHT
47              
48             Copyright 2015 Gabor Szabo, All Rights Reserved.
49              
50             You may use, modify, and distribute this package under the
51             same terms as Perl itself.
52              
53             =cut
54              
55 2     2   11 use Carp qw(croak);
  2         8  
  2         138  
56 2     2   1926 use Data::Dumper qw(Dumper);
  2         21444  
  2         161  
57 2     2   15 use Exporter qw(import);
  2         3  
  2         61  
58 2     2   7230 use Module::CoreList ();
  2         105775  
  2         993  
59             #use Module::Path qw(module_path);
60 2     2   2344 use Perl::PrereqScanner;
  2         1254626  
  2         158  
61              
62             our @EXPORT_OK = qw(find_dependencies test_loops);
63              
64             my %depends;
65             my @loops;
66              
67             ### From here copy of functions from patched version of Module::Path
68             ### https://github.com/neilbowers/Module-Path/issues/17
69             ### remove these if that patch gets applied.
70 2     2   20 use Cwd qw/ abs_path /;
  2         4  
  2         356  
71             my $SEPARATOR;
72              
73             BEGIN {
74 2 50   2   22 if ($^O =~ /^(dos|os2)/i) {
    50          
75 0         0 $SEPARATOR = '\\';
76             } elsif ($^O =~ /^MacOS/i) {
77 0         0 $SEPARATOR = ':';
78             } else {
79 2         1777 $SEPARATOR = '/';
80             }
81             }
82              
83             sub module_path
84             {
85 6     6 0 19 my ($module, $args) = @_;
86 6         14 my $relpath;
87             my $fullpath;
88              
89 6         72 ($relpath = $module) =~ s/::/$SEPARATOR/g;
90 6 50       46 $relpath .= '.pm' unless $relpath =~ m!\.pm$!;
91              
92 6 50       25 my @inc = $args->{dirs} ? @{$args->{dirs}} : @INC;
  6         23  
93              
94             DIRECTORY:
95 6         23 foreach my $dir (@inc) {
96 6 50       22 next DIRECTORY if not defined($dir);
97              
98             # see 'perldoc -f require' on why you might find
99             # a reference in @INC
100 6 50       18 next DIRECTORY if ref($dir);
101              
102 6 50 33     267 next unless -d $dir && -x $dir;
103              
104             # The directory path might have a symlink somewhere in it,
105             # so we get an absolute path (ie resolve any symlinks).
106             # The previous attempt at this only dealt with the case
107             # where the final directory in the path was a symlink,
108             # now we're trying to deal with symlinks anywhere in the path.
109 6         16 my $abs_dir = $dir;
110 6         10 eval { $abs_dir = abs_path($abs_dir); };
  6         333  
111 6 50 33     47 next DIRECTORY if $@ || !defined($abs_dir);
112              
113 6         19 $fullpath = $abs_dir.$SEPARATOR.$relpath;
114 6 50       146 return $fullpath if -f $fullpath;
115             }
116              
117 0         0 return undef;
118             }
119             ### end of Module::Path code.
120              
121              
122             sub test_loops {
123 2     2 0 915 my ($input, $dirs, $text) = @_;
124 2         10 my @loops = find_dependencies($input, $dirs);
125              
126 2         1450 require Test::Builder;
127             # TODO check if there is a plan already and croak if there is none? or plan if there is none? $Test->plan(@_);
128 2         30 my $Test = Test::Builder->new;
129 2         23 $Test->ok( !scalar(@loops), $text );
130 2 50       1823 if (@loops) {
131 2         4 foreach my $loop (@loops) {
132 2         18 $Test->diag( "Loop found: @$loop" );
133             }
134             }
135 2         277 return not scalar @loops;
136             }
137              
138             {
139             my @tree;
140             my %in_tree;
141             sub find_loop {
142 22     22 0 37 my ($elem) = @_;
143            
144 22 100       52 if ($in_tree{$elem}) {
145 2         9 push @loops, [@tree, $elem];
146 2         7 return;
147             } else {
148 20         28 push @tree, $elem;
149 20         39 $in_tree{$elem} = 1;
150 20         23 foreach my $dep (sort keys %{$depends{$elem} }) {
  20         61  
151 14         37 find_loop($dep);
152             }
153 20         30 pop @tree;
154 20         42 delete $in_tree{$elem};
155             }
156             }
157             }
158              
159              
160             sub find_dependencies {
161 2     2 0 5 my ($inputs, $dirs, $verbose) = @_;
162              
163 2         6 @loops = ();
164 2         7 %depends = ();
165              
166 2         2 my @queue;
167              
168 2 50       9 croak "Requires at least one input.\n" if not @$inputs;
169 2         6 foreach my $inp (@$inputs) {
170 2 50       49 if (-f $inp) {
171 2         4 push @queue, $inp;
172             next
173 2         5 }
174 0 0       0 if (-d $inp) {
175 0         0 croak "Cannot handle directories yet '$inp'.\n";
176             }
177 0         0 croak "Invalid argument '$inp' (not file and not directory).\n";
178             }
179 2         44 my $scanner = Perl::PrereqScanner->new;
180 2         113604 while (@queue) {
181 10         31745 my $module = shift @queue;
182 10 100       47 next if $depends{$module};
183 8         24 $depends{$module} = {};
184 8 100       338 my $path = -f $module ? $module : module_path($module, { dirs => $dirs });
185 8 50       35 if (not $path) {
186 0         0 croak "Can't find $module\n";
187 0         0 next;
188             }
189              
190             # Huge files (eg currently Perl::Tidy) will cause PPI to barf
191             # So we need to catch those, keep calm, and carry on
192 8         12 my $prereqs = eval { $scanner->scan_file($path); };
  8         56  
193 8 50       115168 if ($@) {
194 0         0 warn $@;
195 0         0 next;
196             }
197 8         46 my $depsref = $prereqs->as_string_hash();
198 8         402 foreach my $dep (keys %{ $depsref }) {
  8         28  
199 32 100       59541 next if is_core($dep);
200 16 100       559585 next if $dep eq 'perl';
201 8 50       33 say $dep if $verbose;
202 8 50       36 die "Self dependency for '$module'?" if $module eq $dep;
203 8         36 $depends{$module}{$dep} = 1;
204 8         68 push(@queue, $dep);
205             }
206             }
207              
208             #print Dumper \%depends;
209 2         20 foreach my $root (sort keys %depends) {
210 8         36 find_loop($root);
211 8         21 delete $depends{$root}; # so we won't find the same loop multiple times.
212             }
213 2         64 return @loops;
214             }
215              
216             sub is_core
217             {
218 32     32 0 90 my $module = shift;
219 32 50       134 my $version = @_ > 0 ? shift : $^V;
220              
221 32 100       101 return 0 unless defined(my $first_release = Module::CoreList::first_release($module));
222 16 50       84590 return 0 unless $version >= $first_release;
223 16 50       81 return 1 if !defined(my $final_release = Module::CoreList::removed_from($module));
224 0           return $version <= $final_release;
225             }
226              
227             1;
228              
229