Search code examples
perloopintrospectionmoose

Determine where Moose attributes and methods were inherited from?


I often work on a huge, not-very-well-documented, object-oriented Perl repo at my place of employment. While maintaining the code, I frequently need to trace things that are inherited from other classes so that I can understand what they're doing. For example, I need to figure out what $self->mystery is and what it's doing:

package Foo::Bar;
use Moose;
use Method::Signatures;
use Foo::Bar::Element;
use Foo::Bar::Function;
use base qw (Baz::Foo::Bar);

method do_stuff ($some_arg) {
    # mystery is not defined in Foo::Bar
    my $mystery = $self->mystery;
    $mystery->another_mystery($some_arg);
}

I usually find myself spending way too much time tracing through parent classes. So my question is, is there an easy way for me to figure out where $self->mystery comes from? Or in other words, I need to find where mystery is declared.

And by "easy way", I don't mean using ack or grep to string search through files. I'm hoping there's some sort of debugging module I can install and use which could help give me some insight.

Thank you.


Solution

  • Thanks to Standard Perl . . . the comes_from Method!

    You don’t need to download any special tool or module this, let alone some giant IDE because your undocumented class structure has gotten too complicated for mere humans ever to understand without a hulking IDE.

    Why not? Simple: Standard Perl contains everything you need to get the answer you’re looking for. The easy way to find out where something comes from is to use the very useful comes_from method:

    $origin        = $self->comes_from("mystery");
    $secret_origin = $self->comes_from("another_mystery");
    $birthplace    = Some::Class->comes_from("method_name");
    

    That will return the original name of the subroutine which that method would resolve to. As you see, comes_from works as both an object method and a class method, just like can and isa.

    Note that when I say the name of the subroutine it resolves to, I mean where that subroutine was originally created, back before any importing or inheritance. For example, this code:

    use v5.10.1;
    use Path::Router;
    my($what, $method) = qw(Path::Router dump);
    say "$what->$method is really ", $what->comes_from($method);
    

    prints out:

    Path::Router->dump is really Moose::Object::dump
    

    Similar calls would also reveal things like:

    Net::SMTP->mail     is really Net::SMTP::mail
    Net::SMTP->status   is really Net::Cmd::status
    Net::SMTP->error    is really IO::Handle::error
    

    It works just fine on plain ole subroutines, too:

    SQL::Translator::Parser::Storable->normalize_name 
     is really SQL::Translator::Utils::normalize_name
    

    The lovely comes_from method isn’t quite built in though it requires nothing outside of Standard Perl. To make it accessible to you and all your classes and objects and more, just add this bit of code somewhere — anywhere you please really :)

    sub UNIVERSAL::comes_from($$) {
        require B;
        my($invocant, $invoke) = @_;
        my $coderef  = $invocant->can($invoke) || return;
        my $cv       = B::svref_2object($coderef);
        return unless $cv->isa("B::CV");            
        my $gv       = $cv->GV;
        return if $gv->isa("B::SPECIAL");
        my $subname  = $gv->NAME;
        my $packname = $gv->STASH->NAME;
        return $packname . "::" . $subname;
    }
    

    By declaring that as a UNIVERSAL sub, now everybody who’s anybody gets to play with it, just like they do with can and isa. Enjoy!