Removing Unused Variables Is Good
My favorite code clean is removing unused variables. This is right : variables that are hosted in the code, yet doing nothing. In fact, they are often toxic and grow bug-prone. Any old enough code source has them, and they tend to grow by the number. Have you ever tried spotting and removing those variables? Well, removing unused variables is my world and welcome to it!
What Is An Unused Variable
The simplest form and definition of unused variables are variables that are assigned, and never used. We can start with an example like this one:
<?php $unused = 1; $used = 2; echo $used; ?>
The $unused
variable is created, with the value 1
; Then, the script ends. On the other hand, the value 2
is assigned to the variable $used
, then used for display. It is not much, but it is honest work.
As usual, with code, this illustration has variations. In particular, variables are dependent on a context, so the end of the script is not necessary : the end of a method is sufficient. Also, note how $arg
, being a reference, is used once, but not unused : we need to check the calling context too. This would apply to global variables and properties.
<?php function foo(&$arg) { $arg = 3; $unused = 1; $used = 2; return $used; } ?>
The second variation style is when the variable is prepared. This is the case when it is augmented or formatted, before usage. That way, the variable is created, then used to update itself. Such a variable is unused if it is not used in the end. This may cover situations like the following :
<?php $unused = array(); $unused[] = 1; // This could have been merged with the above too. $unusedToo = 'The elephpant that roamed the world'; $unusedToo = strtolower($unusedToo); // Assigned, lower-cased and forgotten $unusedThree = range(0, 3); $unusedThree = array_merge($unusedToo, $b); // Completed with another variable. // Once $unusedThree is removed, $b will a natural candidate too ?>
The last example is the most complex of the three : the variable is created, augmented with a function call including itself and other values, and then, never used. The nature of the function called is important here : array_merge has no side effect, so this last expression doesn’t use the $unusedToo
variable, even as it merges it with $b
. It could have been very different if array_merge
was replaced by a methodcall, that stored the value for later.
Finding Unused Variables With Static analysis
Finding unused variable might be quite laborious, and easily automatised. So, our friends the static analysis tools will find those pesky variables for us, in no time. We’ll use Exakat to run the errands for us.
Exakat offers analysis rules for unused variables : Variables/UsedOncePerContext and Variables/WrittenOnlyVariables. We can also take a look at Classes/UsedOnceProperty, which extends the concept to class properties.
Running the audit
After the installation process, the simplest way to get the above mentioned results is to use the Diplomat report. It is the default run, so you can use the following commands in your terminal :
cd /some/folder/with/exakat
php exakat.phar init -p robo -R https://github.com/consolidation/Robo -v
php exakat.phar project -v -p robo
The result is available in HTML form, in projects\robo\report\index.html
.
If you prefer a text version of the results, you can read the Diplomat report :
cd /some/folder/with/exakat
php exakat.phar report -p robo --format Text -P Variables/VariableUsedOnceByContext -O Variables/WrittenOnlyVariable -P Classes/UsedOnceProperty
You will get a text result, similar to this one :
/src/Task/Archive/Extract.php:175 Variables/VariableUsedOnceByContext Used Once Variables (In Scope) $status /src/Task/Assets/ImageMinify.php:192 Variables/VariableUsedOnceByContext Used Once Variables (In Scope) $url /src/Task/Assets/ImageMinify.php:131 Classes/UsedOnceProperty Used Once Property $minifiers = ['optipng', 'gifsicle', 'jpegtran', 'svgo', 'pngquant', 'advpng', 'pngout', 'zopflipng', 'pngcrush', 'jpegoptim', 'jpeg-recompress', ] /RoboFile.php:25 Variables/VariableUsedOnceByContext Used Once Variables (In Scope) $taskPHPUnit /RoboFile.php:316 Variables/VariableUsedOnceByContext Used Once Variables (In Scope) $m /RoboFile.php:312 Variables/VariableUsedOnceByContext Used Once Variables (In Scope) $m /src/Runner.php:188 Variables/VariableUsedOnceByContext Used Once Variables (In Scope) $container /src/Runner.php:291 Variables/VariableUsedOnceByContext Used Once Variables (In Scope) $color /src/Runner.php:498 Variables/VariableUsedOnceByContext Used Once Variables (In Scope) $argv /src/Runner.php:149 Variables/VariableUsedOnceByContext Used Once Variables (In Scope) $argv /src/Log/ResultPrinter.php:98 Variables/VariableUsedOnceByContext Used Once Variables (In Scope) $task
Reviewing robo.li
We’ll use the Robo.li
, a smart task runner for PHP in PHP. The code is available at github, as you can see in the init
command. It is also available on packagist.org. It’s a very useful tool for automating development, and we like it very much. This is also why we’re using it in this article.
Removing unused variables
We’ll start with the ‘Used once variables’. Those are variable that are used only once in the entire application. All in all, there is only one mention of the variable. This is a good candidate to be an unused variable, but we need to check if it is worth removing directly. The first we can visit is Robofile.php:25
public function test(ConsoleIO $io, array $args, $options = [ 'coverage-html' => false, 'coverage' => false ]) { $io->warning("Deprecated: use 'composer test' instead. Codeception-based tests will fail."); $collection = $this->collectionBuilder($io); $taskPHPUnit = $collection->taskPHPUnit(); $taskCodecept = $collection->taskCodecept() ->args($args); if ($options['coverage']) { $taskCodecept->coverageXml('../../build/logs/clover.xml'); } if ($options['coverage-html']) { $taskCodecept->coverageHtml('../../build/logs/coverage'); } return $collection; }
We can see $taskPHPUnit
being assigned with the result of the task tastPHPunit()
, but never used again. tastPHPunit()
is the method that actually runs the tests, so, the method is important. Since the result is not use later in the method, there is no need to keep it. The next variable, $taskCodecept
, on the other hand, is collected then used twice.
Congratulations! We just spotted our first unused variable. Let’s see a few of other situations.
Iffectations
The second one we’ll check is $status
in the file src/Task/Archive/Extract.php:175
. The code is below. It is a classic example of an ‘iffectation’ : an affectation in a if() condition. Here, $status
is collected as the result of the condition, and not used later. The assignation may be removed, while keeping the !== comparison.
protected function extractZip($extractLocation) { if (!extension_loaded('zlib')) { return Result::errorMissingExtension($this, 'zlib', 'zip extracting'); } $zip = new \ZipArchive(); if (($status = $zip->open($this->filename)) !== true) { return Result::error($this, "Could not open zip archive {$this->filename}"); } if (!$zip->extractTo($extractLocation)) { return Result::error($this, "Could not extract zip archive {$this->filename}"); } $zip->close(); return Result::success($this); }
Blind variables
For the third one, we’ll switch to the ‘Used once variable (in scope)’ rule. You can select it in the Analysis column of the audit. We are looking at the file src/Collection/Collection.php:806
. It is the following code, about variable $name
. It is in the foreach() loop.
public function transferTasks($builder) { foreach ($this->taskList as $name => $taskGroup) { // TODO: We are abandoning all of our before and after tasks here. // At the moment, transferTasks is only called under conditions where // there will be none of these, but care should be taken if that changes. $task = $taskGroup->getTask(); $builder->addTaskToCollection($task); } $this->reset(); }
$name
is the key in the foreach loop. Only the value $taskGroup
is used in the loop, which makes $name
collected for nothing. It may be dropped, with a tiny improvement in performances. This is definitely a micro-optimisation, so you may assume it now worth your time.
Interestingly, there is another unused variable in another loop, in file src/Runner.php:291
. Here, we are looking at variable $color
(most of the code of the method has been omitted here. Just trust me). This is actually the opposite of the situation we just reviewed : the value is unused, while the key is used.
if ($statusCode) { foreach ($this->errorConditions as $msg => $color) { // TODO: This was 'yell'. Add styling? $output->writeln($msg); // used to wrap at 40 and write in $color } }
This time, it is not possible to drop the value, as it is required in the loop. Using array_keys() on $this->errorConditions
is possible, and make the intention very clear. However, the micro-optimisation we mentioned above will be void. So, at that point, the choice is yours.
Caught Exceptions
Next, we check src/Task/Base/ParallelExec.php:175
, for variable $e
. $e
is often used for exceptions in a catch clause, and this is exactly the case here.
try { $process->checkTimeout(); } catch (ProcessTimedOutException $e) { $this->printTaskWarning("Process timed out for {command}", ['command' => $process->getCommandLine(), '_style' => ['command' => 'fg=white;bg=magenta']]); }
In the catch clause, $e
is not mentioned. The exception is caught and processed. Thanks to its actual type, the logging doesn’t have to rely. Maybe this code will benefit from the Anonymous catches that PHP 8.0 will feature. Until then, this is actually an unused variable and a false positive that we can ignore.
Used once properties
Let’s round this up with a check on a property or two. Check the file src/Task/Development/SemVer.php:37
, for the property protected $specialSeparator
.
It is a protected property, so we need to check it with its parents. Since the class is not abstract
, there is no need to check with potential child classes : SemVer
is an autonomous class, and might be used just like that.
Here, class SemVer implements TaskInterface
doesn’t extend any other class. The protected
is actually a private
property (since no other class can access it, except itself).
/** * @param string $separator * * @return $this */ public function setPrereleaseSeparator($separator) { $this->specialSeparator = $separator; return $this; }
The only usage of the property is in the method above. The property is actually updated with a new separator. The method itself is never used in the rest of the robo code, so we might be looking at a dead method at the same time. That would require a different audit.
The most important part is that specialSeparator
is assigned, but never used. It is indeed a candidate for removal, along with its method.
Removing Variables Is Always Interesting
Out of the 6 examples that we reviewed, we spotted different kind of variables
- one unused return value
- one unused iffectation
- one property
- one caught exception
- two blind variables
and 4 of them are good candidates for removal. This is a meager harvest, over a code that is obviously reviewed by itself. Usually, unused variables are in greater numbers than those, and they might also hold results of significant computation. Ideally, we’d like to spot those to remove the variable and its calculations, earning a nice bump in speed.
When you want to try the same review on your own code, use the auditing command that we applied on this repository, using your repository during initialisation. There are hundreds of analysis rules to checks!
Happy PHP auditing with Exakat!