Finding unused variables in your Template stash
Quite often companies who use Catalyst (with Template Toolkit) find that after a while, they're over relying on the use of the stash as a global dumping ground. To deal with that, I wrote a highly experimental module to print out unused template variables.
It works like this:
my $template = Template->new({
STASH => Template::Stash::Unused->new({
template_dir => 'root',
debug_level => 1
})
});
When the stash goes out of scope, it dumps to STDERR
a report which looks like this:
# Stash variable 'customers' unused, but found in templates
# Stash variable 'orders' unused, but found in templates
# Stash variable 'item_count' unused, but found in templates
# Stash variable 'products' not found in root/
With Catalyst, you would use it like this:
package Veure::View::HTML;
use Modern::Perl;
use Template::Stash::Unused;
use parent 'Catalyst::View::TT';
__PACKAGE__->config(
TEMPLATE_EXTENSION => '.tt',
WRAPPER => 'site/wrapper',
EVAL_PERL => $Veure::DEBUG,
ENCODING => 'utf-8',
PLUGIN_BASE => 'Veure::Template::Plugin',
STASH => Template::Stash::Unused->new({
template_dir => 'root',
debug_level => 1,
}),
);
It's fairly naïve in it's approach, but so far for one $client I'm finding plenty of unused variables that we can delete from our code (note that it only works on Unix like systems because it calls out to the shell).
package Template::Stash::Unused;
use strict;
use warnings;
use parent 'Template::Stash';
# XXX strange accessor and internal names to try very hard to avoid stepping
# on Template internals
use constant SLOT => "__" . __PACKAGE__ . "_args__";
sub new {
my ( $class, $arg_for ) = @_;
my $self = $class->SUPER::new;
$self->{ +SLOT } = $arg_for;
return $self;
}
sub _this_template_dir { shift->{ +SLOT }{template_dir} }
sub _this_debug_level { shift->{ +SLOT }{debug_level} }
sub clone {
my ( $self, $params ) = @_;
my %vars = map { $_ => 1 } keys %$params;
delete $vars{template};
$self->{_unused_vars} = \%vars;
$self->{_num_tracked_vars} = scalar keys %vars;
$self->next::method(@_);
}
sub get {
my ( $self, $ident, $args ) = @_;
delete $self->{_unused_vars}{$ident};
$self->next::method( $ident, $args );
}
sub DESTROY {
my $self = shift;
if ( $self->_this_debug_level ) {
my $unused_vars = $self->{_unused_vars};
if ( my @unused = keys %$unused_vars ) {
my $template_dir = $self->_this_template_dir;
for my $var ( sort grep { !/^_/ } @unused ) {
my $output
= `count=\$(git grep -c $var $template_dir|wc -l); if [ \$count -eq 0 ]; then echo '#' Stash variable \\'$var\\' not found in view/; fi`;
if ($output) {
print STDERR $output;
}
elsif ( $self->_this_debug_level > 1 ) {
print STDERR
"# Stash variable '$var' unused, but found in templates\n";
}
}
}
}
}
1;
__END__
=encoding utf8
=head1 NAME
Template::Stash::Unused - show unused template stash variables
=head1 SYNOPSIS
use Template;
my $template = Template->new(
{ STASH => Template::Stash::Unused->new(
template_dir => 'root',
debug_level => 1
)
}
);
$template->process($template, $data);
=head1 DESCRIPTION
This is a highly experimental module which attempts to figure out which
variables passed to a template are not used. It uses a naïve heuristic to
determine if they are used in any template in the C<template_dir> directory.
If C<debug_level> is true, it will attempt to print to STDERR all template
variables which were unused I<and> which cannot be found anywhere in the
C<template_dir> directory (via a naïve grep).
Sample output:
# Stash variable 'products' not found in root/
If C<debug_level> > 1 then it will also print out template variables not used
in the current template, but which I<are> found in other templates (again, via
a naïve grep).
Sample output:
# Stash variable 'customers' unused, but found in templates
# Stash variable 'orders' unused, but found in templates
# Stash variable 'item_count' unused, but found in templates
# Stash variable 'products' not found in root/
You forgot to shift $self from @_ before calling next::method in clone.
Nifty approach, but a pity the catalyst docs still promote the approach where the stash in it's entirety is pushed into the view - following that example still creates piles of extra code, onfusion and "warn Dumper($c->stash)" debugging statements at $work today.
It's an approach that should be avoided in large and complex code bases IMHO.