| Devel-FIXME documentation | Contained in the Devel-FIXME distribution. |
Devel::FIXME - Semi intelligent, pending issue reminder system.
this($code)->isa("broken"); # FIXME this line has a bug
Usually we're too busy to fix things like circular refs, edge cases and so forth when we're spewing code into the editor. This is because concentration is usually too valuable a resource to throw to waste over minor issues. But that doesn't mean the issues don't exist. So usually we remind ourselves they do:
... # FIXME I hope someone finds this comment
and then search through the source tree for occurrances of FIXME every now
and then, say with grep -ri fixme src/.
This pretty much works until your code base grows, and you have too many FIXMEs to prioritise them, or even visually tell them apart.
This package's purpose is to provide reminders to FIXMEs (without the user explicitly searching), and also controlling when, or which reminders will be displayed.
There are several ways to get your code fixed in the indeterminate future.
The first is a sort-of source filter like compile time fix, which does not affect shipped code.
$code; # FIXME broken
That's it. When Devel::FIXME is loaded, it will emit warnings for such
comments in any file that was already loaded, and subsequently loaded files as
they are required. The most reasonable way to get it to work is to set the
environment variable PERL5OPT, so that it contains -MDevel::FIXME. When
perl is then started without taint mode on, the module will be loaded
automatically.
The regex for finding FIXMEs in a line of source is returned by the regex
class method (thus it is overridable). It's quite crummy, really. It matches an
occurrance of a hash sign (#), followed by optional white space and then
FIXME or XXX. After that any white space is skipped, and whatever comes
next is the fixme message.
Given some subclassing you could whip up a format for FIXME messages with
metadata such as priorities, or whatnot. See the implementation of readfile.
The second interface is a compile time, somewhat more explicit way of emmitting messages.
use Devel::FIXME "broken";
This can be repeated for additional messages as needed. This is useful if you want your FIXMEs to ruin deployment, so you're forced to get rid of them. Make sure you run your final tests in a perl tree that doesn't have Devel::FIXME in it.
The third, and probably most problematic is a runtime, explicit way of emmitting messages:
use Devel::FIXME qw/FIXME/;
$code; FIXME("broken");
This relies on FIXME to have been imported into the current namespace, which is probably not always the case. Provided you know FIXME is loaded somewhere in the running perl interpreter, you can use a fully qualified version:
$code; Devel::FIXME::FIXME("broken");
or if you feel that repeating a word is clunky, do:
$code; Devel::FIXME->msg("broken");
# or
$code; Devel::FIXME::msg("broken");
But do use the first FIXME declaration style. Seriously.
There are some problems with simply grepping for occurances of FIXME:
The solution to the first two problems is to make the reporting smart, so that it decides which FIXMEs are printed and which arent.
The solution to the last problem is to have it happen automatically whenever the source code in question is used, and furthermore, to report context too.
The way FIXMEs are filtered is similar to how a firewall filters packets.
Each FIXME statement is considered as it is found, by iterating through some rules, which ultimately decide whether to print the statement or not.
This may sound a bit overkill, but I think it's useful.
What it means is that you can get reminded of FIXMEs in source files that are more than a week old, or when your release schedule reaches feature freeze, or if your program is in the stable tree if your source management repository, or whatever.
There are many modules that know how to parse SCM meta data, for CVS, Perforce, SVN, and so forth. File::Find::Rule can be used in nasty ways to ask questions about files (like was it modified in the last week?). The possibilities are quite vast.
Currently the FIXMEs are filtered by calling the class method rules, and
evaluating the subroutine references that are returned, as methods on the fixme
object.
The subclass Devel::FIXME::Rules::PerlFile is a convenient way to get rules from a file.
When require is called and the @INC hook is entered, it makes sure that it's
first in the @INC array. If it isn't, some files might be required without
being filtered. If the global variable $Devel::FIXME::REPAIR_INC is set to a
true value (it's undef by default), then the magic sub will put itself back in
the begining of @INC as required.
If I had a nickle for every bug you could find in this module, I would have
$nickles >= 0.
Amongst them:
It will find FIXME's in a quoted string, or other such edge cases. I don't care. Patches welcome.
$nickles++;
Yuval Kogman <nothingmuch@woobling.org>
Copyright (c) 2004 Yuval Kogman. All rights reserved This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
Devel::Messenger, grep(1)
| Devel-FIXME documentation | Contained in the Devel-FIXME distribution. |
#!/usr/bin/perl package Devel::FIXME; use fields qw/text line file package script time/; use 5.008_000; # needs open to work on scalar ref use strict; use warnings; use Exporter; use Scalar::Util qw/reftype/; use List::Util qw/first/; use Carp qw/carp croak/; our @EXPORT_OK = qw/FIXME SHOUT DROP CONT/; our %EXPORT_TAGS = ( "constants" => \@EXPORT_OK ); our $VERSION = 0.01; # some constants for rules sub CONT () { 0 }; sub SHOUT () { 1 }; sub DROP () { 2 }; our $REPAIR_INC = undef; # do not "repair" @INC by default my %lock; # to prevent recursion our %rets; # return value cache our $cur; # the current file, used in an eval our $err; # the current error, for rethrowal our $inited; # whether the code ref was installed in @INC, and all { my $anon = ''; open my $fh, "<", \$anon or die $!; close $fh; } # otherwise perlio require stuff breaks sub init { my $pkg = shift; unless($inited){ $pkg->readfile($_) for ($0, sort grep { $_ ne __FILE__ } (values %INC)); # readfile on everything loaded, but not us (we don't want to match our own docs) $pkg->install_inc; } $inited = 1; } our $carprec = 0; sub install_inc { my $pkg = shift; unshift @INC, sub { # YUCK! but tying %INC didn't work, and source filters are applied per caller. XS for source filter purposes is yucki/er/ my $self = shift; my $file = shift; return undef if $lock{$file}; # if we're already processing the file, then we're in the eval several lines down. return. local $lock{$file} = 1; # set this lock that prevents recursion unless (ref $INC[0] and $INC[0] == $self){ # if this happens, some stuff won't be filtered. It shouldn't happen often though. local @INC = grep { !ref or $_ != $self } @INC; # make sure we don't recurse when carp loads it's various innards, it causes a mess carp "FIXME's magic sub is no longer first in \@INC" . ($REPAIR_INC ? ", repairing" : ""); if ($REPAIR_INC){ my $i = 0; while ($i < @INC) { ref $INC[$i] or next; if ($INC[$i] == $self) { unshift @INC, splice(@INC, $i, 1); last; } } continue { $i++; } } } # create some perl code that gives back the return value of the original package, and thus looks like you're really requiring the same thing my $buffer = "\${ delete \$Devel::FIXME::rets{q{$file}} };"; # return what the last module returned. I don't know why it doesn't work without refs # really load the file local $cur = $file; my $ret = eval 'require $Devel::FIXME::cur'; # require always evaluates the return from an evalfile in scalar context, so we don't need to worry about list ($err = "$@\n") =~ s/\nCompilation failed in require at \(eval \d+\)(?:\[.*?\])? line 1\.\n//s; # trim off the eval's appendix to the error $buffer = 'die $Devel::FIXME::err' if $@; # rethrow this way, so that base.pm shuts up # save the return value so that the original require can have it $rets{$file} = \$ret; # see above for why it's a ref # look for FIXME comments in the file that was really required $pkg->readfile($INC{$file}) if ($INC{$file}); # return a filehandle containing source code that simply returns the value the real file did open my $fh, "<", \$buffer; $fh; }; } sub regex { qr/#\s*(?:FIXME|XXX)\s+(.*)$/; # match a FIXME or an XXX, in a comment, with some lax whitespace rules, and suck in anything afterwords as the text } sub readfile { # FIXME refactor to something classier my $pkg = shift; my $file = shift; return unless -f $file; open my $src, "<", $file or die "couldn't open $file: $!"; local $_; while(<$src>){ $pkg->FIXME( # if the line matches the fixme, generate a fixme text => "$1", line => $., # the current line number for <$src> file => $file, ) if $_ =~ $pkg->regex; } continue { last if eof $src }; # is this a platform bug on OSX? close $src; } sub eval { # evaluates all the rules on a fixme object my __PACKAGE__ $self = shift; foreach my $rule ($self->can("rules") ? $self->rules : ()){ my $action = &$rule($self); # run the rule as a class method, and get back a return value if ($action == SHOUT){ # if the rule said to shout, we shout and stop return $self->shout; } elsif ($action == DROP){ # if the rule says to drop, we stop return undef; } # otherwise we keep looping through the rules } $self->shout; # and shout if there are no more rules left. } sub shout { # generate a pretty string and send it to STDERR my __PACKAGE__ $self = shift; warn("# FIXME: $self->{text} at $self->{file} line $self->{line}.\n"); } sub new { # an object per FIXME statement my $pkg = shift; my %args; if (@_ == 1){ # if we only have one arg if (ref $_[0] and reftype($_[0]) eq 'HASH'){ # and it's a hash ref, then we take the hashref to be our args %args = %{ $_[0] }; } else { # if it's one arg and not a hashref, then it's our text %args = ( text => $_[0] ); } } elsif (@_ % 2 == 0){ # if there's an even number of arguments, they are key value pairs %args = @_; } else { # if the argument list is anything else we complain croak "Invalid arguments"; } my __PACKAGE__ $self = $pkg->fields::new(); %$self = %args; # fill in some defaults $self->{package} ||= (caller(1))[0]; $self->{file} ||= (caller(1))[1]; $self->{line} ||= (caller(1))[2]; # these are mainly for rules $self->{script} ||= $0; $self->{time} ||= localtime; $self; } sub import { # export \&FIXME to our caller, /and/ generate a message if there is one to generate my $pkg = $_[0]; $pkg->init unless @_ > 1; if (@_ == 1 or @_ > 2 or (@_ == 2 and first { $_[1] eq $_ or $_[1] eq "&$_" } @EXPORT_OK, map { ":$_" } keys %EXPORT_TAGS)){ shift; local $Exporter::ExportLevel = $Exporter::ExportLevel + 1; $pkg->Exporter::import(@_); } else { $pkg->init; goto \&FIXME; } } sub FIXME { # generate a method my $pkg = __PACKAGE__; $pkg = shift if UNIVERSAL::can($_[0],"isa") and $_[0]->isa(__PACKAGE__); # it's a method or function, we don't care $pkg->new(@_)->eval; } *msg = \&FIXME; # booya. __PACKAGE__ __END__