The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

use strict;
use warnings;

use Test::Most tests => 28;
use Class::Sniff;

{

    package Abstract;

    sub new {
        my ( $class, $arg_for ) = @_;
        #
        # Forcing this to be a long method to force a 'long method' report
        #
        #
        #
        #
        #
        #
        my $self = bless {} => $class;
        return $self;
    }

    sub foo { }
    sub bar { }
    sub baz { }

    package Child1;
    our @ISA = 'Abstract';
    use Carp 'croak';
    sub foo { 1 }

    package Child2;
    our @ISA = 'Abstract';
    sub foo { 1 }
    sub bar { }

    package Grandchild;
    our @ISA = qw<Child1 Child2>;
    sub foo  { }    # diamond inheritance
    sub bar  { }    # Not a problem because it's inherited through 1 path
    sub quux { }    # no inheritance
}

can_ok 'Class::Sniff', 'new';
my $sniff = Class::Sniff->new( { class => 'Grandchild', method_length => 10 } );
can_ok $sniff, 'report';
ok my $report = $sniff->report, '... and it should return a report of potential issues';

like $report, qr/Report for class: Grandchild/,
    'The report should have a title';

my $bar = qr/[^|]*\|[^|]*/;
my $bar_newline = qr/$bar \| \s* $bar/x;
like $report, qr/Overridden Methods/,
    'The report should identify overridden methods';
like $report, qr/bar $bar Grandchild $bar_newline Abstract $bar_newline Child2 /,
    '... identifying the method and the class(es) it is overridden in';

like $report, qr/Unreachable Methods/,
    'The report should identify unreachable methods';
like $report, qr/bar $bar Child2/,
    '... identifying the method and the class(es) it is unreachable in';

like $report, qr/Multiple Inheritance/,
    'The report should identify multiple inheritance';
like $report, qr/Grandchild $bar Child1 $bar_newline Child2 /x,
    '... identifying all parent classes';

like $report, qr/Exported Subroutines/,
    'The report should identify exported subroutines';
like $report,
  qr/Child1 $bar croak $bar Carp $bar_newline/x,
  '... and not miss any';

like $report, qr/Duplicate Methods/,
    'The report should identify duplicate methods';
like $report,
    qr/Abstract::foo     $bar Grandchild::bar 
                 $bar_newline Grandchild::quux
                 $bar_newline Grandchild::foo
                 $bar_newline Child2::bar
                 $bar_newline Abstract::bar
                 $bar_newline Abstract::baz $bar \n
    \| \s* Child2::foo   $bar Child1::foo/x,
  '... and not miss any';
like $report, qr/Long Methods/,
    'The report should identify long methods';
like $report,
    qr/Abstract::new $bar \d+/x,
  '... and not miss any';

# Multiple search paths through a hierarchy are a smell because it implies MI
# and possibly unreachable methods.

can_ok $sniff, 'paths';
my $expected_paths = [
    [ 'Grandchild', 'Child1', 'Abstract' ],
    [ 'Grandchild', 'Child2', 'Abstract' ]
];
eq_or_diff [$sniff->paths], $expected_paths,
    '... and it should report inheritance paths';

{
    package One;
    our @ISA = qw/Two Three/;
    package Two;
    package Three;
    our @ISA = qw/Four Six/;
    package Four;
    our @ISA = 'Five';
    package Five;
    package Six;
}
#    5
#    |
#    4  6
#    | /
# 2  3
#  \ |
#    1
# 1 -> 2
# 1 -> 3 -> 4 -> 5
# 1 -> 3 -> 6
my $complex_sniff = Class::Sniff->new({class => 'One'});
$expected_paths = [
    [ 'One', 'Two' ],
    [ 'One', 'Three', 'Four', 'Five' ],
    [ 'One', 'Three', 'Six' ]
];
eq_or_diff [$complex_sniff->paths], $expected_paths,
    '... even for convoluted hierarchies';

# overridden methods aren't really a smell, but a programmer can compare the
# classes they're overridden in (listed in search order) against the paths to
# understand why something can't be reached.

can_ok $sniff, 'overridden';
my $expected_overridden = {
    'bar' => [ 'Grandchild', 'Abstract', 'Child2' ],
    'foo' => [ 'Grandchild', 'Child1',   'Abstract', 'Child2' ]
};
eq_or_diff $sniff->overridden, $expected_overridden,
  '... and it should return an HoA with overridden methods and the classes';

# 'unreachable' methods can be called directly, but this lists the classes
# overridden methods are listed in which won't allow them to be called.
# We don't account for AUTOLOAD.

can_ok $sniff, 'unreachable';
my $expected_unreachable = [
    'Child2::bar',
    'Child2::foo',
];

eq_or_diff [sort $sniff->unreachable], $expected_unreachable,
  '... and it should return an HoA with unreachable methods and the classes';

can_ok $sniff, 'multiple_inheritance';
eq_or_diff [$sniff->multiple_inheritance], ['Grandchild'],
    '... and it should return classes with more than one parent';
eq_or_diff [$complex_sniff->multiple_inheritance],
    [qw/One Three/],
    '... in the order their found in the hierarchy';

{
    package Platypus;
    our @ISA = qw<SpareParts Duck>;
    package Duck;
    our @ISA = 'SpareParts';
    sub quack {}
    package SpareParts;
    our @ISA = 'Animal';
    sub quack {}
    package Animal;
}

#         Animal
#           |
#      SpareParts
#       |    |   
#       |   Duck 
#       |    |   
#      Platypus

my $platypus = Class::Sniff->new({
    class     => 'Platypus',
    universal => 1,
});
eq_or_diff [$platypus->unreachable], ['Duck::quack'],
    'Unbalanced inheritance graphs are parsed properly';

# Circular inheritance really breaks things!
if ( $] < 5.010000 ) {
    eval <<'    END';
    package Un;
    our @ISA = 'Deux';
    package Deux;
    our @ISA = 'Trois';
    package Trois;
    our @ISA = 'Un';
    END
}

SKIP: {
    skip 'Circular inheritance is now fatal in 5.10 and up', 1
        if $] >= 5.010000;
    throws_ok { Class::Sniff->new({class => 'Un'}) }
        qr/^Circular path found/,
    'Circular paths should throw a fatal error';
}