Elves Versus Unused Imports
Santa's elves are interested in code quality. This is something we touched on about 9 years ago in How Santa's Elves Keep their Workshop Tidy and, more recently, in Elves Versus Typos. Since the elves like keeping up with the times, they are currently experimenting with another, newer Perl code quality tool. It's finally time to take a close look at perlimports. perlimports is a fixer -- it can rewrite your code for you. It's also a linter, so you can use it to report on problems without fixing them. With this power comes great responsibility and perlimports
tries to be responsible. Inspired by goimports, perlimports
is an opinionated tool which tries to force its opinions on your code. What you get in return is a tool which takes much of the burden of managing imports out of your hands.
If you're interested in an in-depth discussion of how perlimports
and Perl's use
and require
work, check out this YouTube video from The Perl Conference in 2021: perlimports or "Where did that symbol come from?".
As a bonus, if you make it to the bottom of this article, we'll also explore a precious, which is a successor to Code::TidyAll.
Installation
Let's take a look at a typical getting started workflow. First the elves install the package from CPAN:
cpm install -g App::perlimports
Once that is done, the elves are ready to try this out. They'll probably just dive right in. The quick way to run perlimports
on a new repository might look something like this:
Getting Started
Clone a repo
Install *all* of the repository's dependencies (including recommended)
Run the test suite
Ensure all of the tests are passing. If there are test failures, fix those first
Run
perlimports
with the--lint
flag to see what changes it might makeTweak the configuration until we're happy with the linting results
Apply
perlimports
to the tests. We can do this viaperlimports -i t
Ensure all of the tests are still passing
Commit the changes
Move on to other parts of the code, like
lib
.perlimports --lint lib
In a best case scenario, this "just works". Let's try it on a really old repository of mine.
$ git clone https://github.com/oalders/acme-odometer.git
$ cd acme-odometer/
$ cpm install -g --with-recommends --cpanfile cpanfile
$ yath t
$ perlimports --lint t
And indeed, all of the steps "just worked". We ran the test(s) via yath and they passed. Why did we use yath rather than make test
or prove? We certainly could have done either of those things, but we're trying to get in the habit of using more modern tools. yath comes bundled with Test2::Harness. If you'd like to learn more about some of the features which Test2 provides, please see Santa’s Workshop Secrets: The Magical Test2 Suite (Part 1) and Santa’s Workshop Secrets: The Magical Test2 Suite (Part 2).
Now, let's see what the linting looks like:
$ perlimports --lint t
❌ Test::Most (import arguments need tidying) at t/load.t line 1
@@ -1 +1 @@
-use Test::Most;
+use Test::Most import => [ qw( done_testing ok ) ];
❌ Acme::Odometer (import arguments need tidying) at t/load.t line 3
@@ -3 +3 @@
-use Acme::Odometer;
+use Acme::Odometer ();
❌ Path::Class (import arguments need tidying) at t/load.t line 4
@@ -4 +4 @@
-use Path::Class qw(file);
+use Path::Class qw( file );
We can see three suggestions have been made. In the first suggestion, perlimports
has detected that done_testing
and ok
are the only functions exported by Test::Most which the test is using, so it has made this explicit.
In the second suggestion perlimports
has detected that the test is not importing any symbols from Acme::Odometer, so it has made this explicit by adding the empty round parens following the use
statement.
In the third suggestion we see that some whitespace padding has been added to the Path::Class import.
If we don't like these changes, we can tweak the configuration. To tell perlimports
to ignore Test::Most, we can change our incantation:
perlimports --lint --ignore-modules Test::Most t
If we also don't like the additional padding, we can turn that off:
perlimports --lint --ignore-modules Test::Most --no-padding t
Applying these settings we now get:
$ perlimports --lint --ignore-modules Test::Most --no-padding t
❌ Acme::Odometer (import arguments need tidying) at t/load.t line 3
@@ -3 +3 @@
-use Acme::Odometer;
+use Acme::Odometer ();
It's time to update the actual file. We'll use -i
for an inplace edit:
$ perlimports -i --ignore-modules Test::Most --no-padding t
The result is:
git --no-pager diff t
diff --git a/t/load.t b/t/load.t
index 503d560..d19688f 100644
--- a/t/load.t
+++ b/t/load.t
@@ -1,6 +1,6 @@
use Test::Most;
-use Acme::Odometer;
+use Acme::Odometer ();
use Path::Class qw(file);
my $path = file( 'assets', 'odometer' )->stringify;
Are the tests still passing?
yath t
** Defaulting to the 'test' command **
( PASSED ) job 1 t/load.t
Yath Result Summary
-----------------------------------------------------------------------------------
File Count: 1
Assertion Count: 3
Wall Time: 1.00 seconds
CPU Time: 1.42 seconds (usr: 0.32s | sys: 0.09s | cusr: 0.77s | csys: 0.24s)
CPU Usage: 142%
--> Result: PASSED <--
Excellent. Let's add the changes via git
and commit them. After that, let's turn to the lib
directory.
$ perlimports --lint --ignore-modules Test::Most --no-padding lib
❌ namespace::clean (appears to be unused and should be removed) at lib/Acme/Odometer.pm line 9
@@ -9 +8,0 @@
-use namespace::clean;
❌ GD (import arguments need tidying) at lib/Acme/Odometer.pm line 11
@@ -11 +11 @@
-use GD;
+use GD ();
❌ Memoize (import arguments need tidying) at lib/Acme/Odometer.pm line 12
@@ -12 +12 @@
-use Memoize;
+use Memoize qw(memoize);
❌ Path::Class (import arguments need tidying) at lib/Acme/Odometer.pm line 14
@@ -14 +14 @@
-use Path::Class qw( file );
+use Path::Class qw(file);
Now, we already see some issues. First off, perlimports
doesn't seem to know about namespace::clean. That's ok. We can ignore it.
perlimports --lint --ignore-modules namespace::clean,Test::Most --no-padding lib
As an aside, we could also update the code to use namespace::autoclean while we're poking around, but we're trying to make minimal changes in this first iteration.
The last suggestion is to remove the padding from the Path::Class imports. It's good to be consistent. The second and third suggestions look to be solid. Let's make this change.
perlimports -i --ignore-modules namespace::clean,Test::Most --no-padding lib
That gives us:
$ git --no-pager diff lib
diff --git a/lib/Acme/Odometer.pm b/lib/Acme/Odometer.pm
index 7fee773..cb1734e 100644
--- a/lib/Acme/Odometer.pm
+++ b/lib/Acme/Odometer.pm
@@ -8,10 +8,10 @@ package Acme::Odometer;
use Moo 1.001;
use namespace::clean;
-use GD;
-use Memoize;
+use GD ();
+use Memoize qw(memoize);
use MooX::Types::MooseLike::Numeric qw(PositiveInt PositiveOrZeroInt);
-use Path::Class qw( file );
+use Path::Class qw(file);
That's pretty good. Do the tests still pass? Yes, they do. So, we can commit this change as well.
A Configuration File
Now, this is all well and good, but what if we want to run perlimports
via the Perl Navigator Language Server? It would be better if we didn't have to worry about the custom command line switches. This sounds like a good time to create a config file.
perlimports --create-config-file perlimports.toml
Nice! We have a stub configuration file. Let's see what's inside perlimports.toml
# Valid log levels are:
# debug, info, notice, warning, error, critical, alert, emergency
# critical, alert and emergency are not currently used.
#
# Please use boolean values in this config file. Negated options (--no-*) are
# not permitted here. Explicitly set options to true or false.
#
# Some of these values deviate from the regular perlimports defaults. In
# particular, you're encouraged to leave preserve_duplicates and
# preserve_unused disabled.
cache = false # setting this to true is currently discouraged
ignore_modules = []
ignore_modules_filename = ""
ignore_modules_pattern = "" # regex like "^(Foo|Foo::Bar)"
ignore_modules_pattern_filename = ""
libs = ["lib", "t/lib"]
log_filename = ""
log_level = "warn"
never_export_modules = []
never_export_modules_filename = ""
padding = true
preserve_duplicates = false
preserve_unused = false
tidy_whitespace = true
Let's commit the stub file to git and then let's move our command line switches to the config file. The diff should look something like this:
$ git --no-pager diff perlimports.toml
diff --git a/perlimports.toml b/perlimports.toml
index d631998..1e54c9e 100644
--- a/perlimports.toml
+++ b/perlimports.toml
@@ -10,7 +10,7 @@
# preserve_unused disabled.
cache = false # setting this to true is currently discouraged
-ignore_modules = []
+ignore_modules = ["namespace::clean", "Test::Most"]
ignore_modules_filename = ""
ignore_modules_pattern = "" # regex like "^(Foo|Foo::Bar)"
ignore_modules_pattern_filename = ""
@@ -19,7 +19,7 @@ log_filename = ""
log_level = "warn"
never_export_modules = []
never_export_modules_filename = ""
-padding = true
+padding = false
preserve_duplicates = false
preserve_unused = false
tidy_whitespace = true
Integrations
That's it! Now, when we apply perlimports
via Perl Navigator, our custom configuration will be respected. Also, we can now run perlimports
from the command line without needing to include the --ignore-modules
and --no-padding
flags. We can think about adding perlimports
to our Continuous Integration and pre-commit
hooks as well, so that we maintain the changes we've just imposed.
As a bonus, if you are a neovim
user and you're using null-ls or its successor none-ls, then perlimports
is already available as a builtin. We can now apply perlimports
as part of your "format on save" behaviours.
The Power of precious
We talked about using Code::TidyAll in How Santa's Elves Keep their Workshop Tidy. tidyall is a wonderful tool that solves a lot of problems, but its design was not perfect and it's looking for a new maintainer. In the meantime, precious has drawn inspiration from tidyall and can be regarded as its spiritual successor, even if it's written in Rust. If we want to run perlimports
along with other fixing and linting tools, we can use precious
for this.
We won't cover installation here, but after installing precious
we can generate a stub config file:
$ precious config init --component perl
Writing precious.toml
The generated precious.toml requires the following tools to be installed:
https://metacpan.org/dist/Perl-Critic
https://metacpan.org/dist/Perl-Tidy
https://metacpan.org/dist/App-perlimports
https://metacpan.org/dist/Pod-Checker
https://metacpan.org/dist/Pod-Tidy
Let's have a look at the created file:
excludes = [
".build/**",
"blib/**",
]
[commands.perlcritic]
type = "lint"
include = [ "**/*.{pl,pm,t,psgi}" ]
cmd = [ "perlcritic", "--profile=$PRECIOUS_ROOT/perlcriticrc" ]
ok_exit_codes = 0
lint_failure_exit_codes = 2
[commands.perltidy]
type = "both"
include = [ "**/*.{pl,pm,t,psgi}" ]
cmd = [ "perltidy", "--profile=$PRECIOUS_ROOT/perltidyrc" ]
lint_flags = [ "--assert-tidy", "--no-standard-output", "--outfile=/dev/null" ]
tidy_flags = [ "--backup-and-modify-in-place", "--backup-file-extension=/" ]
ok_exit_codes = 0
lint_failure_exit_codes = 2
ignore_stderr = "Begin Error Output Stream"
[commands.perlimports]
type = "both"
include = [ "**/*.{pl,pm,t,psgi}" ]
cmd = [ "perlimports" ]
lint_flags = ["--lint" ]
tidy_flags = ["-i" ]
ok_exit_codes = 0
expect_stderr = true
[commands.podchecker]
type = "lint"
include = [ "**/*.{pl,pm,pod}" ]
cmd = [ "podchecker", "--warnings", "--warnings" ]
ok_exit_codes = [ 0, 2 ]
lint_failure_exit_codes = 1
ignore_stderr = [
".+ pod syntax OK",
".+ does not contain any pod commands",
]
[commands.podtidy]
type = "tidy"
include = [ "**/*.{pl,pm,pod}" ]
cmd = [ "podtidy", "--columns", "80", "--inplace", "--nobackup" ]
ok_exit_codes = 0
lint_failure_exit_codes = 1
We can see that the config already includes a linting and tidying configuration for lots of helpful Perl linters and tidiers, including perlimports
. Now, we can run precious tidy --all
or precious lint --all
to run all sorts of helpfu checks in pre-commit
hooks and other places where code quality needs to be ensured.
precious
is a powerful tool and it merits its own blog post, but let's leave this as a quick introduction. Perhaps you'll feel inspired to try it out.
The State of the Workshop
And as for Santa's elves? They have a large codebase which doesn't have 100% test coverage, so they're starting slowly with the changes introduced by perlimports
. After applying perlimports
to the test suite, they've also applied it to the files which have very good test coverage. Once they're confident in those changes, they'll move on to other parts of the codebase. Once they have an updated list of modules which perlimports
should always ignore, maybe they'll even send in a patch to the maintainer. 🤞