Skip Menu |

This queue is for tickets about the base CPAN distribution.

Report information
The Basics
Id: 28799
Status: resolved
Priority: 0/
Queue: base

People
Owner: Nobody in particular
Requestors: marvin [...] rectangular.com
Cc:
AdminCc:

Bug Information
Severity: Normal
Broken in: 2.12
Fixed in: (no value)



Subject: action-at-a-distance failure mode
Because base.pm will not require a module if a stash entry for the module's $VERSION variable already exists, code which generates such a stash entry can cause seemingly bizarre failure modes in far-away, unrelated places. Consider the following program, which we will call 'basebug.pl'. It's sole purpose is to print "Hello world.\n". use Baz; Baz->hello; Here's Baz.pm... package Baz; use Foo::MoreFoo; my $more_foo = Foo::MoreFoo->new; sub hello { print "Hello, world.\n" } 1; ... here's Foo/MoreFoo.pm, used by Baz... package Foo::MoreFoo; use base qw( Foo ); 1; ... and here's Foo::MoreFoo's parent, Foo.pm: package Foo; sub new { bless {}, shift } 1; Nothing out of the ordinary and so far, everything works. However, if we modify basebug.pl to use the Bar module... use Bar; use Baz; Baz->hello; ... which looks like this... package Bar; sub print_foo_version { require Foo; print $Foo::VERSION; } 1; ... we get compile-time failure in Baz.pm: /home/creamyg/perltest/ $ /usr/local/bleadperl/bin/perl5.9.5 basebug.pl Can't locate object method "new" via package "Foo::MoreFoo" at Baz.pm line 4. Compilation failed in require at basebug.pl line 2. BEGIN failed--compilation aborted at basebug.pl line 2. /home/creamyg/perltest/ $ The ordinary debugging path suggested by the error message -- examining first Baz.pm, then Foo/MoreFoo.pm, and finally Foo.pm -- will not reveal any obvious culprit. That's because the problem ultimately lies in base::has_version: sub has_version { my($base) = shift; my $vglob = ${$base.'::'}{VERSION}; return( ($vglob && *$vglob{SCALAR}) ? 1 : 0 ); } Once Bar.pm is loaded, has_version() returns true for 'Foo' -- even though $Foo::VERSION has neither been assigned to nor even accessed. As a result base.pm decides that it is not necessary to require Foo.pm at the request of Foo::MoreFoo. While this error does not occur frequently in the wild, when it does, the cost to the user is high because the debug path is obscure. I personally encountered it after failing to wrap a "use_ok" test in a BEGIN block; isolating it took me... longer than I would have liked. ;) Unfortunately, no fix seems to be possible without breaking backwards compatibility, so the only potential remedy is deprecation and replacement. Tested with blead perl v5.9.5 DEVEL31701.
From: MSCHWERN [...] cpan.org
On Sat Aug 11 21:34:48 2007, CREAMYG wrote: Show quoted text
> Unfortunately, no fix seems to be possible without breaking backwards > compatibility
Why do you say that?
From: JV [...] cpan.org
Very good report! So the problem is that base.pm's very well defined behaviour leads to mysterious error messages under exceptional cases. A potential solution would be base.pm using a different (and better) method to find out if a class has been / is being loaded, instead of testing its $VERSION. Would such a solution break existing code? I mean, is there really any code in the wild that actually prevents base.pm from loading a module by faking its $VERSION?
On Sun Aug 12 07:45:03 2007, JV wrote: Show quoted text
> A potential solution would be base.pm using a different (and better) > method to find out if a class has been / is being loaded, instead of > testing its $VERSION. Would such a solution break existing code? I mean, > is there really any code in the wild that actually prevents base.pm from > loading a module by faking its $VERSION?
If there is, they can change. has_version and others are written as they are to avoid accidentally populating the symbol table by checking if $VERSION is defined, thus avoiding base.pm itself causing a problem like this very one. A simple solution is to check if $VERSION is defined, rather than just having an entry in the symbol table. An even more robust solution would seem to be to simply check if there's anything defined in the package's symbol table. This also solves 28579.
Show quoted text
> Why do you say that?
An obvious improvement would be to check if $VERSION is defined. This would still leave open the possibility of action-at-a-distance if you define $VERSION somewhere else, so the design flaw would be mitigated rather than solved. However, you'll also get bogus results if you set some other package's @ISA. So maybe if $VERSION gets promoted to that level, we can consider the problem solved for practical purposes. Show quoted text
> A simple solution is to check if $VERSION is defined, rather than just > having an entry in the symbol table. > > An even more robust solution would seem to be to simply check if there's > anything defined in the package's symbol table. This also solves 28579.
Given the choice, I'd prefer the first option. The second provides more opportunites for action-at-a-distance. We can still stop setting $VERSION, though -- won't require() not bother reading a file if it finds an entry in %INC?
I just had a go at fixing this, and I've discovered that there are actually multiple problems. Switching to a defined-ness test for $MyModule::VERSION allows the require attempt to proceed where it would have stopped earlier. However, if 'require' fails, the presence of ANY symbol in the base package's stash will suppress the resulting exception, because of this line in base::import(): unless (%{"$base\::"}) { In the example test case I laid out, the presence of the $Foo::VERSION variable in Bar.pm creates such a stash entry. Thus if eval 'require Foo;' fails, you won't find out about it until later, e.g. when your subclass tries to invoke an inherited method and gives you a hard-to-grok error message.
Fixed in 2.18