—use
strict;
use
warnings;
# ABSTRACT: Check for uncleaned imports
# KEYWORDS: testing namespaces clean dirty imports exports subroutines methods
our
$VERSION
=
'0.24'
;
use
Module::Runtime ();
use
Sub::Identify ();
use
Package::Stash 0.14;
use
Test::Builder;
use
File::Find ();
use
File::Spec;
our
@EXPORT
=
qw(namespaces_clean all_namespaces_clean)
;
#pod =head1 SYNOPSIS
#pod
#pod use strict;
#pod use warnings;
#pod use Test::CleanNamespaces;
#pod
#pod all_namespaces_clean;
#pod
#pod =head1 DESCRIPTION
#pod
#pod This module lets you check your module's namespaces for imported functions you
#pod might have forgotten to remove with L<namespace::autoclean> or
#pod L<namespace::clean> and are therefore available to be called as methods, which
#pod usually isn't want you want.
#pod
#pod =head1 FUNCTIONS
#pod
#pod All functions are exported by default.
#pod
#pod =head2 namespaces_clean
#pod
#pod namespaces_clean('YourModule', 'AnotherModule');
#pod
#pod Tests every specified namespace for uncleaned imports. If the module couldn't
#pod be loaded it will be skipped.
#pod
#pod =head2 all_namespaces_clean
#pod
#pod all_namespaces_clean;
#pod
#pod Runs L</namespaces_clean> for all modules in your distribution.
#pod
#pod =cut
sub
namespaces_clean {
my
(
@namespaces
) =
@_
;
local
$@;
my
$builder
= builder();
my
$result
= 1;
for
my
$ns
(
@namespaces
) {
unless
(
eval
{ Module::Runtime::require_module(
$ns
); 1 }) {
$builder
->skip(
"failed to load ${ns}: $@"
);
next
;
}
my
$imports
= _remaining_imports(
$ns
);
my
$ok
=
$builder
->ok(!
keys
(
%$imports
),
"${ns} contains no imported functions"
);
$ok
or
$builder
->diag(
$builder
->explain(
'remaining imports: '
=>
$imports
));
$result
&&=
$ok
;
}
return
$result
;
}
sub
all_namespaces_clean {
my
@modules
= find_modules(
@_
);
builder()->plan(
tests
=>
scalar
@modules
);
namespaces_clean(
@modules
);
}
# given a package name, returns a hashref of all remaining imports
sub
_remaining_imports {
my
$ns
=
shift
;
my
$symbols
= Package::Stash->new(
$ns
)->get_all_symbols(
'CODE'
);
my
@imports
;
my
$meta
;
if
(
$INC
{ Module::Runtime::module_notional_filename(
'Class::MOP'
) }
and
$meta
= Class::MOP::class_of(
$ns
)
and
$meta
->can(
'get_method_list'
))
{
my
%subs
=
%$symbols
;
delete
@subs
{
$meta
->get_method_list };
@imports
=
keys
%subs
;
}
elsif
(
$INC
{ Module::Runtime::module_notional_filename(
'Mouse::Util'
) }
and Mouse::Util->can(
'class_of'
) and
$meta
= Mouse::Util::class_of(
$ns
))
{
warn
'Mouse class detected - chance of false negatives is high!'
;
my
%subs
=
%$symbols
;
# ugh, this returns far more than the true list of methods
delete
@subs
{
$meta
->get_method_list };
@imports
=
keys
%subs
;
}
else
{
@imports
=
grep
{
my
$stash
= Sub::Identify::stash_name(
$symbols
->{
$_
});
$stash
ne
$ns
and
$stash
ne
'Role::Tiny'
}
keys
%$symbols
;
}
my
%imports
;
@imports
{
@imports
} =
map
{ Sub::Identify::sub_fullname(
$symbols
->{
$_
}) }
@imports
;
# these subs are special-cased - they are often provided by other
# modules, but cannot be wrapped with Sub::Name as the call stack
# is important
delete
@imports
{
qw(import unimport)
};
my
@overloads
=
grep
{
$imports
{
$_
} eq
'overload::nil'
||
$imports
{
$_
} eq
'overload::_nil'
}
keys
%imports
;
delete
@imports
{
@overloads
}
if
@overloads
;
if
(
"$]"
< 5.020)
{
# < 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.
# < haarg> but that only works if the symbol table entry is empty
# < haarg> if it exists, it has to use the *$const = sub () { $val } method, so the name is __ANON__
# < haarg> newer versions don't use that method
# < haarg> rather, newer versions of constant.pm don't use that method
# < haarg> and then the name ends up being YourPackage::__ANON__
my
@constants
=
grep
{
$imports
{
$_
} eq
'constant::__ANON__'
}
keys
%imports
;
delete
@imports
{
@constants
}
if
@constants
;
}
return
\
%imports
;
}
#pod =head2 find_modules
#pod
#pod my @modules = Test::CleanNamespaces->find_modules;
#pod
#pod Returns a list of modules in the current distribution. It'll search in
#pod C<blib/>, if it exists. C<lib/> will be searched otherwise.
#pod
#pod =cut
sub
find_modules {
my
@modules
;
for
my
$top
(-e
'blib'
? (
'blib/lib'
,
'blib/arch'
) :
'lib'
) {
File::Find::find({
no_chdir
=> 1,
wanted
=>
sub
{
my
$file
=
$_
;
return
unless
$file
=~ s/\.pm$//;
push
@modules
,
join
'::'
=> File::Spec->splitdir(
File::Spec->abs2rel(File::Spec->rel2abs(
$file
,
'.'
),
$top
)
);
},
},
$top
);
}
return
@modules
;
}
#pod =head2 builder
#pod
#pod my $builder = Test::CleanNamespaces->builder;
#pod
#pod Returns the C<Test::Builder> used by the test functions.
#pod
#pod =cut
{
my
$Test
= Test::Builder->new;
sub
builder {
$Test
}
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Test::CleanNamespaces - Check for uncleaned imports
=head1 VERSION
version 0.24
=head1 SYNOPSIS
use strict;
use warnings;
use Test::CleanNamespaces;
all_namespaces_clean;
=head1 DESCRIPTION
This module lets you check your module's namespaces for imported functions you
might have forgotten to remove with L<namespace::autoclean> or
L<namespace::clean> and are therefore available to be called as methods, which
usually isn't want you want.
=head1 FUNCTIONS
All functions are exported by default.
=head2 namespaces_clean
namespaces_clean('YourModule', 'AnotherModule');
Tests every specified namespace for uncleaned imports. If the module couldn't
be loaded it will be skipped.
=head2 all_namespaces_clean
all_namespaces_clean;
Runs L</namespaces_clean> for all modules in your distribution.
=head2 find_modules
my @modules = Test::CleanNamespaces->find_modules;
Returns a list of modules in the current distribution. It'll search in
C<blib/>, if it exists. C<lib/> will be searched otherwise.
=head2 builder
my $builder = Test::CleanNamespaces->builder;
Returns the C<Test::Builder> used by the test functions.
=head1 KNOWN ISSUES
Uncleaned imports from L<Mouse> classes are incompletely detected, due to its
lack of ability to return the correct method list -- it assumes that all subs
are meant to be callable as methods unless they originated from (were imported
by) one of: L<Mouse>, L<Mouse::Role>, L<Mouse::Util>,
L<Mouse::Util::TypeConstraints>, L<Carp>, L<Scalar::Util>, or L<List::Util>.
=head1 SEE ALSO
=over 4
=item *
L<namespace::clean>
=item *
L<namespace::autoclean>
=item *
L<namespace::sweep>
=item *
L<Sub::Exporter::ForMethods>
=item *
L<Test::API>
=item *
L<Sub::Name>
=item *
L<Sub::Install>
=item *
L<MooseX::MarkAsMethods>
=item *
L<Dist::Zilla::Plugin::Test::CleanNamespaces>
=back
=head1 SUPPORT
Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Test-CleanNamespaces>
(or L<bug-Test-CleanNamespaces@rt.cpan.org|mailto:bug-Test-CleanNamespaces@rt.cpan.org>).
There is also a mailing list available for users of this distribution, at
There is also an irc channel available for users of this distribution, at
L<C<#perl> on C<irc.perl.org>|irc://irc.perl.org/#perl-qa>.
=head1 AUTHOR
Florian Ragwitz <rafl@debian.org>
=head1 CONTRIBUTORS
=for stopwords Karen Etheridge Graham Knop
=over 4
=item *
Karen Etheridge <ether@cpan.org>
=item *
Graham Knop <haarg@haarg.org>
=back
=head1 COPYRIGHT AND LICENCE
This software is copyright (c) 2009 by Florian Ragwitz.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut