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   517026 use strict;
  7         69  
  7         206  
2 7     7   36 use warnings;
  7         18  
  7         347  
3             package Test::CleanNamespaces; # git description: v0.23-5-gf8e88b1
4             # ABSTRACT: Check for uncleaned imports
5             # KEYWORDS: testing namespaces clean dirty imports exports subroutines methods
6              
7             our $VERSION = '0.24';
8              
9 7     7   2121 use Module::Runtime ();
  7         7048  
  7         156  
10 7     7   3341 use Sub::Identify ();
  7         7427  
  7         184  
11 7     7   3243 use Package::Stash 0.14;
  7         40213  
  7         223  
12 7     7   691 use Test::Builder;
  7         59607  
  7         160  
13 7     7   41 use File::Find ();
  7         15  
  7         99  
14 7     7   32 use File::Spec;
  7         16  
  7         196  
15              
16 7     7   32 use Exporter 5.57 'import';
  7         109  
  7         6058  
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 27666 my (@namespaces) = @_;
55 10         19 local $@;
56 10         28 my $builder = builder();
57              
58 10         19 my $result = 1;
59 10         37 for my $ns (@namespaces) {
60 10 100       18 unless (eval { Module::Runtime::require_module($ns); 1 }) {
  10         36  
  9         27396  
61 1         471 $builder->skip("failed to load ${ns}: $@");
62 1         193 next;
63             }
64              
65 9         30 my $imports = _remaining_imports($ns);
66              
67 9         73 my $ok = $builder->ok(!keys(%$imports), "${ns} contains no imported functions");
68 9 100       3850 $ok or $builder->diag($builder->explain('remaining imports: ' => $imports));
69              
70 9   66     8368 $result &&= $ok;
71             }
72              
73 10         180 return $result;
74             }
75              
76             sub all_namespaces_clean {
77 2     2 1 1244 my @modules = find_modules(@_);
78 2         8 builder()->plan(tests => scalar @modules);
79 2         1379 namespaces_clean(@modules);
80             }
81              
82             # given a package name, returns a hashref of all remaining imports
83             sub _remaining_imports {
84 17     17   78982 my $ns = shift;
85              
86 17         410 my $symbols = Package::Stash->new($ns)->get_all_symbols('CODE');
87 17         92 my @imports;
88              
89             my $meta;
90 17 50 33     63 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         823 my $stash = Sub::Identify::stash_name($symbols->{$_});
  86         335  
112             $stash ne $ns
113             and $stash ne 'Role::Tiny'
114 86 100 66     591 and not eval { require Role::Tiny; Role::Tiny->is_role($stash) }
  18         2792  
  18         21049  
115             } keys %$symbols;
116             }
117              
118 17         59 my %imports; @imports{@imports} = map { Sub::Identify::sub_fullname($symbols->{$_}) } @imports;
  17         45  
  16         75  
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         110 delete @imports{qw(import unimport)};
124              
125 17 100       51 my @overloads = grep { $imports{$_} eq 'overload::nil' || $imports{$_} eq 'overload::_nil' } keys %imports;
  11         69  
126 17 100       91 delete @imports{@overloads} if @overloads;
127              
128 17 50       87 if ("$]" < 5.020)
129             {
130             # < haarg> 5.10+ allows sticking a readonly scalar ref directly in the symbol table, rather than a glob. when auto-promoted to a sub, it will have the correct name.
131             # < haarg> but that only works if the symbol table entry is empty
132             # < haarg> if it exists, it has to use the *$const = sub () { $val } method, so the name is __ANON__
133             # < haarg> newer versions don't use that method
134             # < haarg> rather, newer versions of constant.pm don't use that method
135             # < haarg> and then the name ends up being YourPackage::__ANON__
136 0         0 my @constants = grep { $imports{$_} eq 'constant::__ANON__' } keys %imports;
  0         0  
137 0 0       0 delete @imports{@constants} if @constants;
138             }
139              
140 17         107 return \%imports;
141             }
142              
143             #pod =head2 find_modules
144             #pod
145             #pod my @modules = Test::CleanNamespaces->find_modules;
146             #pod
147             #pod Returns a list of modules in the current distribution. It'll search in
148             #pod C, if it exists. C will be searched otherwise.
149             #pod
150             #pod =cut
151              
152             sub find_modules {
153 4     4 1 3781 my @modules;
154 4 100       91 for my $top (-e 'blib' ? ('blib/lib', 'blib/arch') : 'lib') {
155             File::Find::find({
156             no_chdir => 1,
157             wanted => sub {
158 36     36   106 my $file = $_;
159             return
160 36 100       2033 unless $file =~ s/\.pm$//;
161 4         702 push @modules, join '::' => File::Spec->splitdir(
162             File::Spec->abs2rel(File::Spec->rel2abs($file, '.'), $top)
163             );
164             },
165 7         639 }, $top);
166             }
167 4         26 return @modules;
168             }
169              
170             #pod =head2 builder
171             #pod
172             #pod my $builder = Test::CleanNamespaces->builder;
173             #pod
174             #pod Returns the C used by the test functions.
175             #pod
176             #pod =cut
177              
178             {
179             my $Test = Test::Builder->new;
180 12     12 1 30 sub builder { $Test }
181             }
182              
183             1;
184              
185             __END__