File Coverage

blib/lib/Test/CleanNamespaces.pm
Criterion Covered Total %
statement 71 81 87.6
branch 17 22 77.2
condition 8 18 44.4
subroutine 15 15 100.0
pod 4 4 100.0
total 115 140 82.1


line stmt bran cond sub pod time code
1 7     7   157433 use strict;
  7         9  
  7         162  
2 7     7   23 use warnings;
  7         6  
  7         258  
3             package Test::CleanNamespaces; # git description: v0.21-2-ga6ae109
4             # ABSTRACT: Check for uncleaned imports
5             # KEYWORDS: testing namespaces clean dirty imports exports subroutines methods
6              
7             our $VERSION = '0.22';
8              
9 7     7   1669 use Module::Runtime ();
  7         4649  
  7         101  
10 7     7   2518 use Sub::Identify ();
  7         5068  
  7         128  
11 7     7   2552 use Package::Stash 0.14;
  7         25294  
  7         155  
12 7     7   578 use Test::Builder;
  7         6919  
  7         105  
13 7     7   19 use File::Find ();
  7         9  
  7         77  
14 7     7   19 use File::Spec;
  7         7  
  7         134  
15              
16 7     7   19 use Exporter 5.57 'import';
  7         66  
  7         4006  
17             our @EXPORT = qw(namespaces_clean all_namespaces_clean);
18              
19             #pod =head1 SYNOPSIS
20             #pod
21             #pod use strict;
22             #pod use warnings;
23             #pod use Test::CleanNamespaces;
24             #pod
25             #pod all_namespaces_clean;
26             #pod
27             #pod =head1 DESCRIPTION
28             #pod
29             #pod This module lets you check your module's namespaces for imported functions you
30             #pod might have forgotten to remove with L or
31             #pod L and are therefore available to be called as methods, which
32             #pod usually isn't want you want.
33             #pod
34             #pod =head1 FUNCTIONS
35             #pod
36             #pod All functions are exported by default.
37             #pod
38             #pod =head2 namespaces_clean
39             #pod
40             #pod namespaces_clean('YourModule', 'AnotherModule');
41             #pod
42             #pod Tests every specified namespace for uncleaned imports. If the module couldn't
43             #pod be loaded it will be skipped.
44             #pod
45             #pod =head2 all_namespaces_clean
46             #pod
47             #pod all_namespaces_clean;
48             #pod
49             #pod Runs L for all modules in your distribution.
50             #pod
51             #pod =cut
52              
53             sub namespaces_clean {
54 10     10 1 24212 my (@namespaces) = @_;
55 10         14 local $@;
56 10         27 my $builder = builder();
57              
58 10         13 my $result = 1;
59 10         18 for my $ns (@namespaces) {
60 10 100       15 unless (eval { Module::Runtime::require_module($ns); 1 }) {
  10         40  
  9         27272  
61 1         404 $builder->skip("failed to load ${ns}: $@");
62 1         94 next;
63             }
64              
65 9         25 my $imports = _remaining_imports($ns);
66              
67 9         74 my $ok = $builder->ok(!keys(%$imports), "${ns} contains no imported functions");
68 9 100       1414 $ok or $builder->diag($builder->explain('remaining imports: ' => $imports));
69              
70 9   66     8244 $result &&= $ok;
71             }
72              
73 10         56 return $result;
74             }
75              
76             sub all_namespaces_clean {
77 2     2 1 798 my @modules = find_modules(@_);
78 2         6 builder()->plan(tests => scalar @modules);
79 2         256 namespaces_clean(@modules);
80             }
81              
82             # given a package name, returns a hashref of all remaining imports
83             sub _remaining_imports {
84 17     17   53333 my $ns = shift;
85              
86 17         401 my $symbols = Package::Stash->new($ns)->get_all_symbols('CODE');
87 17         55 my @imports;
88              
89             my $meta;
90 17 50 33     62 if ($INC{ Module::Runtime::module_notional_filename('Class::MOP') }
    50 33        
      33        
      33        
91             and $meta = Class::MOP::class_of($ns)
92             and $meta->can('get_method_list'))
93             {
94 0         0 my %subs = %$symbols;
95 0         0 delete @subs{ $meta->get_method_list };
96 0         0 @imports = keys %subs;
97             }
98             elsif ($INC{ Module::Runtime::module_notional_filename('Mouse::Util') }
99             and Mouse::Util->can('class_of') and $meta = Mouse::Util::class_of($ns))
100             {
101 0         0 warn 'Mouse class detected - chance of false negatives is high!';
102              
103 0         0 my %subs = %$symbols;
104             # ugh, this returns far more than the true list of methods
105 0         0 delete @subs{ $meta->get_method_list };
106 0         0 @imports = keys %subs;
107             }
108             else
109             {
110             @imports = grep {
111 17         574 my $stash = Sub::Identify::stash_name($symbols->{$_});
  86         263  
112             $stash ne $ns
113             and $stash ne 'Role::Tiny'
114 86 100 66     483 and not eval { require Role::Tiny; Role::Tiny->is_role($stash) }
  18         2830  
  18         13919  
115             } keys %$symbols;
116             }
117              
118 17         43 my %imports; @imports{@imports} = map { Sub::Identify::sub_fullname($symbols->{$_}) } @imports;
  17         32  
  16         69  
119              
120             # these subs are special-cased - they are often provided by other
121             # modules, but cannot be wrapped with Sub::Name as the call stack
122             # is important
123 17         94 delete @imports{qw(import unimport)};
124              
125 17 100       44 my @overloads = grep { $imports{$_} eq 'overload::nil' || $imports{$_} eq 'overload::_nil' } keys %imports;
  11         51  
126 17 100       42 delete @imports{@overloads} if @overloads;
127              
128 17 50       73 if ("$]" < 5.010)
129             {
130 0         0 my @constants = grep { $imports{$_} eq 'constant::__ANON__' } keys %imports;
  0         0  
131 0 0       0 delete @imports{@constants} if @constants;
132             }
133              
134 17         100 return \%imports;
135             }
136              
137             #pod =head2 find_modules
138             #pod
139             #pod my @modules = Test::CleanNamespaces->find_modules;
140             #pod
141             #pod Returns a list of modules in the current distribution. It'll search in
142             #pod C, if it exists. C will be searched otherwise.
143             #pod
144             #pod =cut
145              
146             sub find_modules {
147 4     4 1 3193 my @modules;
148 4 100       83 for my $top (-e 'blib' ? ('blib/lib', 'blib/arch') : 'lib') {
149             File::Find::find({
150             no_chdir => 1,
151             wanted => sub {
152 36     36   33 my $file = $_;
153             return
154 36 100       1245 unless $file =~ s/\.pm$//;
155 4         513 push @modules, join '::' => File::Spec->splitdir(
156             File::Spec->abs2rel(File::Spec->rel2abs($file, '.'), $top)
157             );
158             },
159 7         442 }, $top);
160             }
161 4         18 return @modules;
162             }
163              
164             #pod =head2 builder
165             #pod
166             #pod my $builder = Test::CleanNamespaces->builder;
167             #pod
168             #pod Returns the C used by the test functions.
169             #pod
170             #pod =cut
171              
172             {
173             my $Test = Test::Builder->new;
174 12     12 1 27 sub builder { $Test }
175             }
176              
177             1;
178              
179             __END__