File Coverage

blib/lib/Devel/Decouple.pm
Criterion Covered Total %
statement 135 135 100.0
branch 25 28 89.2
condition 15 19 78.9
subroutine 34 34 100.0
pod 15 15 100.0
total 224 231 96.9


line stmt bran cond sub pod time code
1             # $Id$
2             package Devel::Decouple;
3            
4 4     4   175930 use strict;
  4         8  
  4         129  
5 4     4   19 use warnings;
  4         8  
  4         96  
6 4     4   20 use Carp;
  4         10  
  4         261  
7 4     4   3170 use version; our $VERSION = qv(0.0.3);
  4         16833  
  4         24  
8            
9 4     4   420 use base 'Exporter';
  4         7  
  4         506  
10             our @EXPORT = qw{ from function functions as default_sub preserved };
11            
12 4     4   4129 use Class::Inspector;
  4         16710  
  4         129  
13 4     4   3544 use PPI::Document;
  4         596118  
  4         165  
14 4     4   3789 use PPI::Find;
  4         4328  
  4         134  
15 4     4   9430 use Monkey::Patch qw{ patch_package };
  4         36281  
  4         220  
16 4     4   93 use List::MoreUtils qw{ uniq };
  4         9  
  4         3989  
17            
18             ### PUBLIC METHODS: ########################
19            
20             sub new {
21 17     17 1 20728 my $class = shift;
22 17         74 return bless {}, $class;
23             }
24            
25             sub decouple {
26 15     15 1 35 my $self = shift;
27 15   33     57 my $module = shift || croak q{You must supply a module name to the 'decouple' method};
28 15         46 my @args = @_;
29 15 100       68 my $modules = shift @args if ref $args[0] eq 'ARRAY';
30            
31             # build the params hash
32 15         25 my %params;
33 15         37 $params{_MODULE_} = $module;
34 15 50       176 $params{_DOCUMENT_} = Class::Inspector->resolved_filename( $module )
35             or croak "could not resolve the canonical name of $module";
36 15   100     1336 $params{_MODULES_} = $modules || '_ALL_';
37 15         77 $params{_CODE_} = { @args };
38 15   100 3   101 $params{_CODE_}{_DEFAULT_} ||= sub { return };
  3         875  
39            
40 15         130 $self->{$_} = $params{$_} for keys %params;
41            
42 15         61 $self->_build;
43            
44 15         169 return $self;
45             }
46            
47             sub module {
48 34     34 1 49 my $self = shift;
49 34         251 return $self->{_MODULE_};
50             }
51            
52             sub document {
53 18     18 1 33 my $self = shift;
54 18         169 return $self->{_DOCUMENT_};
55             }
56            
57             sub modules {
58 18     18 1 746 my $self = shift;
59 17         66 return exists $self->{_MODULES_}
60 18 100       86 ? @{ $self->{_MODULES_} }
61             : undef;
62             }
63            
64             sub called_imports{
65 17     17 1 34 my $self = shift;
66 16         60 return exists $self->{_CALLED_IMPORTS_}
67 17 100       64 ? @{ $self->{_CALLED_IMPORTS_} }
68             : undef;
69             }
70            
71             sub all_functions {
72 2     2 1 6 my $self = shift;
73            
74 1         4 return $self->module
75 2 100       8 ? @{Class::Inspector->functions( $self->module )}
76             : undef;
77             }
78            
79             sub revert{
80 2     2 1 925 my $self = shift;
81 2         5 my @functions = @_;
82            
83 2         4 map { $self->{_PATCHES_}{$_} = undef } @functions;
  1         5  
84            
85 2         35 return $self;
86             }
87            
88             sub report{
89 2     2 1 89 my $self = shift;
90            
91 2 100       9 return $self->document
92             ? $self->_build_report
93             : croak qq{ 'report' called on uninitialized object };
94             }
95            
96             ### EXPORTS FOR SYNTACTIC SUGAR: ###########
97            
98             sub from(\@;%) {
99 10     10 1 90 return @_;
100             }
101            
102             sub function($) {
103 18     18 1 1056 return shift;
104             }
105            
106             sub functions(\@$;%) {
107 5     5 1 39 my $functions = shift;
108 5         10 my $code = shift;
109 5         9 my %args = @_;
110            
111 5         9 my %map = map { $_ => $code } @{$functions};
  10         31  
  5         11  
112            
113 5         30 return %map, %args;
114             }
115            
116             sub as(&) {
117 27     27 1 150 return shift;
118             }
119            
120             sub default_sub() {
121 10     10 1 70 return '_DEFAULT_';
122             }
123            
124             sub preserved() {
125 6     6 1 25 return '_PRESERVED_';
126             }
127            
128             ### PRIVATE METHODS: #######################
129            
130             sub _build {
131 15     15   27 my $self = shift;
132            
133 15         45 $self->_build_imports();
134 15         7558 $self->_set_code_substitutions();
135             }
136            
137             sub _build_report {
138 1     1   2 my $self = shift;
139 1         3 my $spacing = 20;
140 1         2 my $indent = 4;
141            
142             # format a simplistic report...
143 1   33     5 my $report = "\nFunction-import usage statistics for ".($self->module||$self->document).":\n";
144            
145 1         15 for my $module ( $self->modules ){
146 2         9 $report .= " "x($indent)."$module\n";
147 2         10 map { $report .= " "x(2*$indent)."$_"." "x($spacing-length($_))."calls: ".
148 2         14 $self->{_CALLED_IMPORT_STATS_}{$module}{$_}{ _NUMBER_OF_CALLS_ }."\tlines: ".
149 2         15 (join ',', @{$self->{_CALLED_IMPORT_STATS_}{$module}{$_}{_LINE_NUMBERS_}}).".\n" }
150 2         4 sort keys %{$self->{_CALLED_IMPORT_STATS_}{$module}};
151             }
152            
153 1         10 return $report;
154             }
155            
156             sub _build_imports {
157 15     15   25 my $self = shift;
158            
159 15         63 my ($CallFinder,$Document) = $self->_create_call_finder;
160 15         89 my @found = $CallFinder->in( $Document );
161            
162 15         339 for my $module ( $self->_get_modules ){
163 4     4   58 no strict 'refs';
  4         7  
  4         2441  
164 1752         6533 for my $function ( @{$module.'::EXPORT'}, @{$module.'::EXPORT_OK'} ){
  1752         10508  
  1752         7541  
165 8190         108224 for my $token ( @found ){
166 106470 100       1344797 if ( $token eq $function ){
167 30         528 $self->{_CALLED_IMPORT_STATS_}{$module}{$function}{_NUMBER_OF_CALLS_}++;
168 30         63 push @{$self->{_CALLED_IMPORT_STATS_}{$module}{$function}{_LINE_NUMBERS_}}, $token->line_number;
  30         214  
169             }
170             }
171             }
172             }
173            
174 15         935 $self->{ _MODULES_ } = [ keys %{ $self->{_CALLED_IMPORT_STATS_} }];
  15         126  
175 15         98 for my $module ( $self->modules ){
176 30         49 push @{$self->{_CALLED_IMPORTS_}}, $_ for keys %{$self->{_CALLED_IMPORT_STATS_}{$module}};
  30         120  
  30         137  
177             }
178            
179 15         172 return $self;
180             }
181            
182             sub _get_modules {
183 15     15   31 my $self = shift;
184            
185 1740         1631 $self->{_MODULES_} eq '_ALL_'
186 1740         3085 ? return map { my $module = $_; $module =~ s{\.pm}{}; $module }
  1740         2450  
  1740         1811  
187 1740         4923 map { my $module = $_; $module =~ s{[\/\\]}{::}g; $module }
  1740         2646  
  1740         3756  
188 6         31 grep { m{\.pm$} } keys %INC
189 15 100       929 : return @{ $self->{_MODULES_} };
190             }
191            
192             sub _create_call_finder {
193 15     15   22 my $self = shift;
194            
195 15         50 my $Document = PPI::Document->new( $self->document );
196             my $Finder = PPI::Find->new(
197             sub {
198 990 100   990   19979 $_[1]->isa( 'PPI::Element' ) | $_[1]->isa( 'PPI::Token' )
    50          
199             ? $_[0]->isa( 'PPI::Token::Word' )
200             ? return 1
201             : return 0
202             : return;
203 15         102001 });
204            
205 15         185 return $Finder, $Document;
206             }
207            
208             sub _set_code_substitutions{
209 15     15   30 my $self = shift;
210            
211 15         80 for my $function ( uniq $self->called_imports, keys %{$self->{_CODE_}} ){
  15         205  
212 46 100 100     1928 next if ( (defined $self->{_CODE_}{$function} && ref $self->{_CODE_}{$function} ne 'CODE')
      100        
213             || $function eq '_DEFAULT_');
214            
215 30 100 100     190 if ( ref $self->{_CODE_}{$function} eq 'CODE' || ref $self->{_CODE_}{_DEFAULT_} eq 'CODE' ){
216 28 100       91 $self->{_PATCHES_}{$function} = patch_package(
    50          
217             $self->module, # module
218             $function, # function-name
219             ref $self->{_CODE_}{$function} eq 'CODE' # code
220             ? $self->{_CODE_}{$function}
221             : $self->{_CODE_}{_DEFAULT_}
222             ) or croak "couldn't install the code stub for '$function'";
223             }
224             }
225             }
226            
227             1; # return true
228             __END__