The @ISA variable is a package variable that is used by all classes to indicate the class's parent (or parents). While this variable can be safely read to learn a class's inheritance hierarchy, it must not be modified at runtime [Conway 2005].

Noncompliant Code Example (@ISA)

This noncompliant code example defines a base class and an object class with simple methods:

{
  package Base;

  sub new {
    my $class = shift;
    my $self = {}; # no parent
    bless $self, $class;
    print "new Base\n";
    return $self;
  };

  sub base_value {return 1;}
}


{
  package Derived;
  our @ISA = qw(Base);  # establishes inheritance

  sub new {
    my $class = shift;
    my $self = $class->SUPER::new(@_);  # relies on established inheritance
    print "new Derived\n";
    return $self;
  };

  sub derived_value {return 2;}
}


BEGIN {
  my $derived = Derived->new();
  my $b = $derived->base_value();
  my $d = $derived->derived_value();
  print "base_value = $b\n";
  print "derived_value = $d\n";
}

When the code is run, we get a program error:

Can't locate object method "new" via package "Derived::SUPER" at ...

This error occurs because the BEGIN block is evaluated at the beginning of runtime, before the @ISA statement can be evaluated. Consequently, when the Derived::new() constructor is invoked, the Derived class has an empty parents list and therefore fails to invoke Base::new().

Compliant Solution (parent)

This compliant solution uses the parent module rather than directly modifying the @ISA variable.

# ... package Base is unchanged

{
  package Derived;
  use parent qw(Base);

  sub new {
    my $class = shift;
    my $self = $class->SUPER::new(@_);  # relies on established inheritance
    print "new Derived\n";
    return $self;
  };

  sub derived_value {return 2;}
}

# ... The rest of the code is unchanged

The parent module establishes the inheritance hierarchy at parse time, before any runtime code, including the BEGIN block, is evaluated. When the Derived::new() constructor is invoked, Perl knows that Derived is an instance of Base, and the program produces the correct output:

new Base
new Derived
base_value = 1
derived_value = 2

Risk Assessment

Modifying class inheritance at runtime can introduce subtle bugs and is usually a sign of poor design.

Recommendation

Severity

Likelihood

Remediation Cost

Priority

Level

OOP00-PL

Low

Unlikely

Low

P3

L3

Automated Detection

Tool

Diagnostic

Perl::Critic

ClassHierarchies::ProhibitExplicitISA

Bibliography

 


3 Comments

  1. Via email, Michael Greb says:
    > OOP00-PL
    >
    > Use of base is discouraged in favor of parent, even base's own docs suggest this:
    >> Unless you are using the fields pragma, consider this module
    >> discouraged
    > in
    > favor of the lighter-weight parent.
    > https://metacpan.org/module/base
    >
    > I believe use of fields, the only time someone would want base over parent, is also strongly discouraged by the Perl Community now a days.

    You're right. I've updated the compliant solution to use parent instead of base. And I've added base to EXP31-PL as a deprecated module.

  2. Anonymous

    This doesn't say (nor do I think it should) that you can't modify @ISA directly at BEGIN time, which would also avoid the problem.

    {
      package Derived;
      BEGIN{ our @ISA = qw'Base' }
      ...
    }

    Also there is a bug in the provided compliant solution.

    If the packages are in the same file (as it appears in the non-compliant code) then you have to use:

    use parent -norequire, qw'Base'

     

    There is also another bug 

    print "base_value = $d\n";

    should be

    print "base_value = $b\n";

    1. This doesn't say (nor do I think it should) that you can't modify @ISA directly at BEGIN time, which would also avoid the problem.

      Agreed.

      If the packages are in the same file (as it appears in the non-compliant code) then you have to use:

      use parent -norequire, qw'Base'

      Well, the CS works fine on my OSX box (running perl 5.16). So "-norequire," is er...not required :)

      There is also another bug 

      print"base_value = $d\n";

      should be

      print"base_value = $b\n";

      Fixed, thanks!