leek
leek

Reputation: 12121

Easiest way to detect/remove unused `use` statements from PHP codebase

I've searched all over for something like this but I believe the word "use" is perhaps too common for any helpful results:

What is the easiest way to remove all un-used use statements from class files in a PHP codebase?

Edit: For the sake of simplicity, we can ignore detecting use statements that are used for annotations.

Upvotes: 21

Views: 14134

Answers (3)

dan-lee
dan-lee

Reputation: 14492

EDIT

I have completely rewritten it, so now it is much more powerful:

  • Traits and anonymous/lambda functions are ignored
  • Now taking care of catch blocks, class extensions and interfaces
  • Indentation and comments don't matter
  • Multiple declarations for namespace aliases work too
  • Static and object class calls are recognized as "usage" ($u->getUsages())
  • Full and half qualified usages are not treated

The test file, class.php:

use My\Full\Classname as Another, My\Full\NSname, Some\Other\Space;

/* some insane commentary */ use My\Full\NSname1; use ArrayObject;

$obj = new namespaced\Another;
$obj = new Another;

$a = new ArrayObject(array(1));

Space::call();

$a = function($a, $b, $c = 'test') use ($obj) {
  /* use */
};

class MyHelloWorld extends Base {
  use traits, hello, world;
}

And here the script:

class UseStatementSanitizer
{
  protected $content;

  public function __construct($file)
  {
    $this->content = token_get_all(file_get_contents($file));

    // we don't need and want them while parsing
    $this->removeTokens(T_COMMENT);
    $this->removeTokens(T_WHITESPACE);
  }

  public function getUnused()
  {
    $uses   = $this->getUseStatements();
    $usages = $this->getUsages();
    $unused = array();

    foreach($uses as $use) {
      if (!in_array($use, $usages)) {
        $unused[] =  $use;
      }
    }
    return $unused;
  }

  public function getUsages()
  {
    $usages = array();

    foreach($this->content as $key => $token) {

      if (!is_string($token)) {
        $t = $this->content;

        // for static calls
        if ($token[0] == T_DOUBLE_COLON) {
          // only if it is NOT full or half qualified namespace
          if ($t[$key-2][0] != T_NAMESPACE) {
            $usages[] = $t[$key-1][1];
          }
        }

        // for object instanciations
        if ($token[0] == T_NEW) {
          if ($t[$key+2][0] != T_NAMESPACE) {
            $usages[] = $t[$key+1][1];
          }
        }

        // for class extensions
        if ($token[0] == T_EXTENDS || $token[0] == T_IMPLEMENTS) {
          if ($t[$key+2][0] != T_NAMESPACE) {
            $usages[] = $t[$key+1][1];
          }
        }

        // for catch blocks
        if ($token[0] == T_CATCH) {
          if ($t[$key+3][0] != T_NAMESPACE) {
            $usages[] = $t[$key+2][1];
          }
        }
      }
    }
    return array_values(array_unique($usages));
  }

  public function getUseStatements()
  {
    $tokenUses = array();
    $level = 0;

    foreach($this->content as $key => $token) {

      // for traits, only first level uses should be captured
      if (is_string($token)) {
        if ($token == '{') {
          $level++;
        }
        if ($token == '}') {
          $level--;
        }
      }

      // capture all use statements besides trait-uses in class
      if (!is_string($token) && $token[0] == T_USE && $level == 0) {
        $tokenUses[] = $key;
      }
    }

    $useStatements = array();

    // get rid of uses in lambda functions
    foreach($tokenUses as $key => $tokenKey) {
      $i                   = $tokenKey;
      $char                = '';
      $useStatements[$key] = '';

      while($char != ';') {
        ++$i;
        $char = is_string($this->content[$i]) ? $this->content[$i] : $this->content[$i][1];

        if (!is_string($this->content[$i]) && $this->content[$i][0] == T_AS) {
          $useStatements[$key] .= ' AS ';
        } else {
          $useStatements[$key] .= $char;
        }

        if ($char == '(') {
          unset($useStatements[$key]);
          break;
        }
      }
    }

    $allUses = array();

    // get all use statements
    foreach($useStatements as $fullStmt) {
      $fullStmt = rtrim($fullStmt, ';');
      $fullStmt = preg_replace('/^.+ AS /', '', $fullStmt);
      $fullStmt = explode(',', $fullStmt);

      foreach($fullStmt as $singleStmt) {
        // $singleStmt only for full qualified use
        $fqUses[] = $singleStmt;

        $singleStmt = explode('\\', $singleStmt);
        $allUses[] = array_pop($singleStmt);
      }
    }
    return $allUses;
  }

  public function removeTokens($tokenId)
  {
    foreach($this->content as $key => $token) {
      if (isset($token[0]) && $token[0] == $tokenId) {
        unset($this->content[$key]);
      }
    }
    // reindex
    $this->content = array_values($this->content);
  }

}

$unused = new UseStatementSanitizer('class.php');

print_r($unused->getUnused());

Returns:

Array
(
  [0] => NSname
  [1] => NSname1
)

Upvotes: 8

wdev
wdev

Reputation: 2220

Check FriendsOfPHP's PHP-CS-Fixer https://github.com/FriendsOfPHP/PHP-CS-Fixer

Upvotes: 32

Arjan
Arjan

Reputation: 9874

It would probably depend on the way your code is set up. If your code uses namespaces like so:

namespace Foo
{
   <one or more classes in namespace Foo>
}

then you're probably fine if you just check each file individually. That still means you would have to parse the PHP code to find the use statements, and then to determine which statements are used.

The easy way is to use a tool that's already been built. I recently started using PhpStorm IDE (30 day free trail, or the early access program) and that can inform you when you have unused use statements in a file (and you can even specify whether that should come up as warnings or errors). It would still require you to open each file though. But you could also check files you are editing, then eventually your code will be cleaner.

Upvotes: 1

Related Questions