├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.xml ├── build ├── phar-autoload.php.in ├── phar-manifest.php ├── phpunit.xml └── travis-ci.xml ├── composer.json ├── phpdcd ├── src ├── Analyser.php ├── CLI │ ├── Application.php │ └── Command.php ├── Detector.php └── Log │ └── Text.php └── tests ├── AnalyserTest.php ├── DetectorTest.php └── _files ├── Interpolator.php ├── abstract_methods.php ├── declarations.php ├── function_call.php ├── function_call2.php ├── issue_18.php ├── issue_18_extra.php ├── issue_5.php ├── method_call.php ├── methods_vs_functions.php ├── parent_double_colon_handling.php └── static_method_call.php /.gitattributes: -------------------------------------------------------------------------------- 1 | *.php diff=php 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/phar 2 | build/SebastianBergmann 3 | build/phpcpd.bat 4 | build/phpcpd.php 5 | build/*.phar 6 | build/*.tgz 7 | .idea 8 | cache.properties 9 | composer.phar 10 | composer.lock 11 | vendor 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | install: 4 | - travis_retry composer install --no-interaction --prefer-source 5 | 6 | php: 7 | - 5.3.3 8 | - 5.3 9 | - 5.4 10 | - 5.5 11 | - 5.6 12 | - 7.0 13 | - hhvm 14 | 15 | script: ./vendor/bin/phpunit --configuration ./build/travis-ci.xml 16 | 17 | matrix: 18 | allow_failures: 19 | - php: hhvm 20 | 21 | notifications: 22 | email: false 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | phpdcd 2 | 3 | Copyright (c) 2009-2015, Sebastian Bergmann . 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | 18 | * Neither the name of Sebastian Bergmann nor the names of his 19 | contributors may be used to endorse or promote products derived 20 | from this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | POSSIBILITY OF SUCH DAMAGE. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This project is no longer maintained and its repository is only kept for archival purposes.** 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/sebastian/phpdcd/v/stable.png)](https://packagist.org/packages/sebastian/phpdcd) 4 | [![Build Status](https://travis-ci.org/sebastianbergmann/phpdcd.png?branch=master)](https://travis-ci.org/sebastianbergmann/phpdcd) 5 | 6 | # PHP Dead Code Detector (PHPDCD) 7 | 8 | **phpdcd** is a Dead Code Detector (DCD) for PHP code. It scans a PHP project for all declared functions and methods and reports those as being "dead code" that are not called at least once. 9 | 10 | ## Limitations 11 | 12 | As PHP is a very dynamic programming language, the static analysis performed by **phpdcd** does not recognize function or method calls that are performed using one of the following language features: 13 | 14 | * Reflection API 15 | * `call_user_func()` and `call_user_func_array()` 16 | * Usage of the `new` operator with variable class names 17 | * Variable class names for static method calls such as `$class::method()` 18 | * Variable function or method names such as `$function()` or `$object->$method()` 19 | * Automatic calls to methods such as `__toString()` or `Iterator::*()` 20 | 21 | Also note that infering the type of a variable is limited to type-hinted arguments (`function foo(Bar $bar) {}`) and direct object creation (`$object = new Clazz`) 22 | 23 | ## Installation 24 | 25 | ### PHP Archive (PHAR) 26 | 27 | The easiest way to obtain PHPDCD is to download a [PHP Archive (PHAR)](http://php.net/phar) that has all required dependencies of PHPDCD bundled in a single file: 28 | 29 | wget https://phar.phpunit.de/phpdcd.phar 30 | chmod +x phpdcd.phar 31 | mv phpdcd.phar /usr/local/bin/phpdcd 32 | 33 | You can also immediately use the PHAR after you have downloaded it, of course: 34 | 35 | wget https://phar.phpunit.de/phpdcd.phar 36 | php phpdcd.phar 37 | 38 | ### Composer 39 | 40 | Simply add a dependency on `sebastian/phpdcd` to your project's `composer.json` file if you use [Composer](http://getcomposer.org/) to manage the dependencies of your project. Here is a minimal example of a `composer.json` file that just defines a development-time dependency on PHPDCD: 41 | 42 | { 43 | "require-dev": { 44 | "sebastian/phpdcd": "*" 45 | } 46 | } 47 | 48 | For a system-wide installation via Composer, you can run: 49 | 50 | composer global require 'sebastian/phpdcd=*' 51 | 52 | Make sure you have `~/.composer/vendor/bin/` in your path. 53 | 54 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /build/phar-autoload.php.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 41 | 42 | __HALT_COMPILER(); 43 | -------------------------------------------------------------------------------- /build/phar-manifest.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | &1'); 6 | 7 | if (strpos($tag, '-') === false && strpos($tag, 'No names found') === false) { 8 | print $tag; 9 | } else { 10 | $branch = @exec('git rev-parse --abbrev-ref HEAD'); 11 | $hash = @exec('git log -1 --format="%H"'); 12 | print $branch . '@' . $hash; 13 | } 14 | 15 | print "\n"; 16 | 17 | $lock = json_decode(file_get_contents(__DIR__ . '/../composer.lock')); 18 | 19 | foreach ($lock->packages as $package) { 20 | print $package->name . ': ' . $package->version; 21 | 22 | if (!preg_match('/^[v= ]*(([0-9]+)(\\.([0-9]+)(\\.([0-9]+)(-([0-9]+))?(-?([a-zA-Z-+][a-zA-Z0-9\\.\\-:]*)?)?)?)?)$/', $package->version)) { 23 | print '@' . $package->source->reference; 24 | } 25 | 26 | print "\n"; 27 | } 28 | -------------------------------------------------------------------------------- /build/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ../tests 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /build/travis-ci.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ../tests 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ../src 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sebastian/phpdcd", 3 | "description": "Dead Code Detector (DCD) for PHP code.", 4 | "homepage": "https://github.com/sebastianbergmann/phpdcd", 5 | "license": "BSD-3-Clause", 6 | "authors": [ 7 | { 8 | "name": "Sebastian Bergmann", 9 | "email": "sebastian@phpunit.de", 10 | "role": "lead" 11 | } 12 | ], 13 | "support": { 14 | "issues": "https://github.com/sebastianbergmann/phpdcd/issues" 15 | }, 16 | "require": { 17 | "php": ">=5.3.3", 18 | "sebastian/finder-facade": "~1.1", 19 | "sebastian/version": "~1.0", 20 | "symfony/console": "~2.2", 21 | "phpunit/php-timer": ">=1.0.6", 22 | "phpunit/php-token-stream": "~1.1" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "~3.7" 26 | }, 27 | "autoload": { 28 | "classmap": [ 29 | "src/" 30 | ] 31 | }, 32 | "bin": [ 33 | "phpdcd" 34 | ], 35 | "extra": { 36 | "branch-alias": { 37 | "dev-master": "1.0-dev" 38 | } 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /phpdcd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | $loaded = false; 13 | 14 | foreach (array(__DIR__ . '/../../autoload.php', __DIR__ . '/vendor/autoload.php') as $file) { 15 | if (file_exists($file)) { 16 | require $file; 17 | $loaded = true; 18 | break; 19 | } 20 | } 21 | 22 | if (!$loaded) { 23 | die( 24 | 'You need to set up the project dependencies using the following commands:' . PHP_EOL . 25 | 'wget http://getcomposer.org/composer.phar' . PHP_EOL . 26 | 'php composer.phar install' . PHP_EOL 27 | ); 28 | } 29 | 30 | $application = new SebastianBergmann\PHPDCD\CLI\Application; 31 | $application->run(); 32 | 33 | -------------------------------------------------------------------------------- /src/Analyser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace SebastianBergmann\PHPDCD; 12 | 13 | /** 14 | * PHPDCD code analyser to be used on a body of source code. 15 | * 16 | * Analyses given source code (files) for declared and called functions 17 | * and aggregates this information. 18 | * 19 | * @since Class available since Release 1.0.0 20 | */ 21 | class Analyser 22 | { 23 | /** 24 | * Function declaration mapping: maps declared function name to file and line number 25 | * TODO: make mapping to file and line number optional for memory usage reduction? 26 | * @var array 27 | */ 28 | private $functionDeclarations = array(); 29 | 30 | /** 31 | * Function call mapping: maps "callees" to array of "callers" 32 | * TODO: make callers array optional for memory usage reduction? 33 | * @var array 34 | */ 35 | private $functionCalls = array(); 36 | 37 | /** 38 | * Class hierarchy data: maps classes to their direct parent. 39 | * @var array 40 | */ 41 | private $classParents = array(); 42 | 43 | public function getFunctionDeclarations() 44 | { 45 | return $this->functionDeclarations; 46 | } 47 | 48 | /** 49 | * Get function calls we detected 50 | * @return array maps "callees" to array of "callers" 51 | */ 52 | public function getFunctionCalls() 53 | { 54 | // Resolve parent(class) calls if possible 55 | foreach ($this->functionCalls as $call => $callers) { 56 | if (strpos($call, 'parent(') === 0) { 57 | preg_match('/parent\\((.*?)\\)::(.*)/', $call, $matches); 58 | $class = $matches[1]; 59 | $method = $matches[2]; 60 | foreach ($this->getAncestors($class) as $ancestor) { 61 | $resolvedCall = $ancestor . '::' . $method; 62 | if (isset($this->functionDeclarations[$resolvedCall])) { 63 | $this->functionCalls[$resolvedCall] = $callers; 64 | // TODO: also remove unresolved parent(class) entries? 65 | break; 66 | } 67 | } 68 | } 69 | } 70 | 71 | return $this->functionCalls; 72 | } 73 | 74 | /** 75 | * Get array of a class's ancestors. 76 | * @param $child 77 | * @return array of ancestors 78 | */ 79 | public function getAncestors($child) 80 | { 81 | $ancestors = array(); 82 | while (isset($this->classParents[$child])) { 83 | $child = $this->classParents[$child]; 84 | if (in_array($child, $ancestors)) { 85 | $cycle = implode(' -> ', $ancestors) . ' -> ' . $child; 86 | throw new \RuntimeException('Class hierarchy cycle detected: ' . $cycle); 87 | } 88 | $ancestors[] = $child; 89 | } 90 | 91 | return $ancestors; 92 | } 93 | 94 | /** 95 | * Build a mapping between parent classes and all their descendants 96 | * @return array maps each parent classes to array of its subclasses, subsubclasses, ... 97 | */ 98 | public function getClassDescendants() 99 | { 100 | $descendants = array(); 101 | foreach ($this->classParents as $child => $parent) { 102 | // Direct child 103 | $descendants[$parent][] = $child; 104 | // Store child for further ancestors 105 | $ancestor = $parent; 106 | while (isset($this->classParents[$ancestor])) { 107 | $ancestor = $this->classParents[$ancestor]; 108 | $descendants[$ancestor][] = $child; 109 | } 110 | } 111 | 112 | return $descendants; 113 | } 114 | 115 | /** 116 | * Analyse a PHP source code file for defined and called functions. 117 | * @param $filename 118 | */ 119 | public function analyseFile($filename) 120 | { 121 | $sourceCode = file_get_contents($filename); 122 | 123 | return $this->analyseSourceCode($sourceCode, $filename); 124 | } 125 | 126 | /** 127 | * Analyse PHP source code for defined and called functions 128 | * 129 | * @param string $sourceCode source code. 130 | * @param string $filename optional file name to use in declaration definition 131 | */ 132 | public function analyseSourceCode($sourceCode, $filename = 'undefined') 133 | { 134 | 135 | $blocks = array(); 136 | $currentBlock = null; 137 | $currentClass = ''; 138 | $currentFunction = ''; 139 | $currentInterface = ''; 140 | $namespace = ''; 141 | $variables = array(); 142 | 143 | $tokens = new \PHP_Token_Stream($sourceCode); 144 | $count = count($tokens); 145 | 146 | for ($i = 0; $i < $count; $i++) { 147 | if ($tokens[$i] instanceof \PHP_Token_NAMESPACE) { 148 | $namespace = $tokens[$i]->getName(); 149 | } elseif ($tokens[$i] instanceof \PHP_Token_CLASS) { 150 | $currentClass = $tokens[$i]->getName(); 151 | 152 | if ($namespace != '') { 153 | $currentClass = $namespace . '\\' . $currentClass; 154 | } 155 | 156 | $currentBlock = $currentClass; 157 | } elseif ($tokens[$i] instanceof \PHP_Token_EXTENDS 158 | && $tokens[$i+2] instanceof \PHP_Token_STRING) { 159 | // Store parent-child class relationship. 160 | $this->classParents[$currentClass] = (string) $tokens[$i+2]; 161 | } elseif ($tokens[$i] instanceof \PHP_Token_INTERFACE) { 162 | $currentInterface = $tokens[$i]->getName(); 163 | 164 | if ($namespace != '') { 165 | $currentInterface = $namespace . '\\' . $currentClass; 166 | } 167 | 168 | $currentBlock = $currentInterface; 169 | } elseif ($tokens[$i] instanceof \PHP_Token_NEW && 170 | !$tokens[$i+2] instanceof \PHP_Token_VARIABLE) { 171 | if ($tokens[$i-1] instanceof \PHP_Token_EQUAL) { 172 | $j = -1; 173 | } elseif ($tokens[$i-1] instanceof \PHP_Token_WHITESPACE && 174 | $tokens[$i-2] instanceof \PHP_Token_EQUAL) { 175 | $j = -2; 176 | } else { 177 | continue; 178 | } 179 | 180 | if ($tokens[$i+$j-1] instanceof \PHP_Token_WHITESPACE) { 181 | $j--; 182 | } 183 | 184 | if ($tokens[$i+$j-1] instanceof \PHP_Token_VARIABLE) { 185 | $name = (string) $tokens[$i+$j-1]; 186 | $variables[$name] = (string) $tokens[$i+2]; 187 | } elseif ($tokens[$i+$j-1] instanceof \PHP_Token_STRING && 188 | $tokens[$i+$j-2] instanceof \PHP_Token_OBJECT_OPERATOR && 189 | $tokens[$i+$j-3] instanceof \PHP_Token_VARIABLE) { 190 | $name = (string) $tokens[$i+$j-3] . '->' . 191 | (string) $tokens[$i+$j-1]; 192 | $variables[$name] = (string) $tokens[$i+2]; 193 | } 194 | } elseif ($tokens[$i] instanceof \PHP_Token_FUNCTION) { 195 | if ($currentInterface != '') { 196 | continue; 197 | } 198 | 199 | // Ignore abstract methods. 200 | for ($j=1; $j<=4; $j++) { 201 | if (isset($tokens[$i-$j]) && 202 | $tokens[$i-$j] instanceof \PHP_Token_ABSTRACT) { 203 | continue 2; 204 | } 205 | } 206 | 207 | $function = $tokens[$i]->getName(); 208 | 209 | if ($function == 'anonymous function') { 210 | continue; 211 | } 212 | 213 | $variables = $tokens[$i]->getArguments(); 214 | 215 | if ($currentClass != '') { 216 | $function = $currentClass . '::' . $function; 217 | $variables['$this'] = $currentClass; 218 | } 219 | 220 | $currentFunction = $function; 221 | $currentBlock = $currentFunction; 222 | 223 | $this->functionDeclarations[$function] = array( 224 | 'file' => $filename, 'line' => $tokens[$i]->getLine() 225 | ); 226 | } elseif ($tokens[$i] instanceof \PHP_Token_OPEN_CURLY 227 | || $tokens[$i] instanceof \PHP_Token_CURLY_OPEN 228 | || $tokens[$i] instanceof \PHP_Token_DOLLAR_OPEN_CURLY_BRACES ) { 229 | array_push($blocks, $currentBlock); 230 | $currentBlock = null; 231 | } elseif ($tokens[$i] instanceof \PHP_Token_CLOSE_CURLY) { 232 | $block = array_pop($blocks); 233 | 234 | if ($block == $currentClass) { 235 | $currentClass = ''; 236 | } elseif ($block == $currentFunction) { 237 | $this->functionDeclarations[$currentFunction]['loc'] = 238 | $tokens[$i]->getLine() - $this->functionDeclarations[$currentFunction]['line'] + 1; 239 | $currentFunction = ''; 240 | $variables = array(); 241 | } 242 | } elseif ($tokens[$i] instanceof \PHP_Token_OPEN_BRACKET) { 243 | for ($j = 1; $j <= 4; $j++) { 244 | if (isset($tokens[$i-$j]) && 245 | $tokens[$i-$j] instanceof \PHP_Token_FUNCTION) { 246 | continue 2; 247 | } 248 | } 249 | 250 | if ($tokens[$i-1] instanceof \PHP_Token_STRING) { 251 | $j = -1; 252 | } elseif ($tokens[$i-1] instanceof \PHP_Token_WHITESPACE && 253 | $tokens[$i-2] instanceof \PHP_Token_STRING) { 254 | $j = -2; 255 | } else { 256 | continue; 257 | } 258 | 259 | $function = (string) $tokens[$i+$j]; 260 | $lookForNamespace = true; 261 | 262 | if (isset($tokens[$i+$j-2]) && 263 | $tokens[$i+$j-2] instanceof \PHP_Token_NEW) { 264 | $function .= '::__construct'; 265 | } elseif ((isset($tokens[$i+$j-1]) && 266 | $tokens[$i+$j-1] instanceof \PHP_Token_OBJECT_OPERATOR) || 267 | (isset($tokens[$i+$j-2]) && 268 | $tokens[$i+$j-2] instanceof \PHP_Token_OBJECT_OPERATOR)) { 269 | $_function = $tokens[$i+$j]; 270 | $lookForNamespace = false; 271 | 272 | if ($tokens[$i+$j-1] instanceof \PHP_Token_OBJECT_OPERATOR) { 273 | $j -= 2; 274 | } else { 275 | $j -= 3; 276 | } 277 | 278 | if ($tokens[$i+$j] instanceof \PHP_Token_VARIABLE) { 279 | if (isset($variables[(string) $tokens[$i+$j]])) { 280 | $function = $variables[(string) $tokens[$i+$j]] . 281 | '::' . $_function; 282 | } else { 283 | $function = '::' . $_function; 284 | } 285 | } elseif ($tokens[$i+$j] instanceof \PHP_Token_STRING && 286 | $tokens[$i+$j-1] instanceof \PHP_Token_OBJECT_OPERATOR && 287 | $tokens[$i+$j-2] instanceof \PHP_Token_VARIABLE) { 288 | $variable = (string) $tokens[$i+$j-2] . '->' . 289 | (string) $tokens[$i+$j]; 290 | 291 | if (isset($variables[$variable])) { 292 | $function = $variables[$variable] . '::' . 293 | $_function; 294 | } 295 | } 296 | } elseif ($tokens[$i+$j-1] instanceof \PHP_Token_DOUBLE_COLON) { 297 | $class = (string) $tokens[$i+$j-2]; 298 | 299 | if ($class == 'self' || $class == 'static') { 300 | $class = $currentClass; 301 | } elseif ($class == 'parent') { 302 | $class = "parent($currentClass)"; 303 | } 304 | 305 | $function = $class . '::' . $function; 306 | $j -= 2; 307 | } 308 | 309 | if ($lookForNamespace) { 310 | while ($tokens[$i+$j-1] instanceof \PHP_Token_NS_SEPARATOR) { 311 | $function = $tokens[$i+$j-2] . '\\' . $function; 312 | $j -= 2; 313 | } 314 | } 315 | 316 | if (!isset($this->functionCalls[$function])) { 317 | $this->functionCalls[$function] = array(); 318 | } 319 | $this->functionCalls[$function][] = $currentFunction; 320 | } 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/CLI/Application.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace SebastianBergmann\PHPDCD\CLI; 12 | 13 | use SebastianBergmann\Version; 14 | use Symfony\Component\Console\Application as AbstractApplication; 15 | use Symfony\Component\Console\Input\InputInterface; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | use Symfony\Component\Console\Input\ArrayInput; 18 | 19 | /** 20 | * TextUI frontend for PHPDCD. 21 | * 22 | * @since Class available since Release 1.0.0 23 | */ 24 | class Application extends AbstractApplication 25 | { 26 | public function __construct() 27 | { 28 | $version = new Version('1.0.2', dirname(dirname(__DIR__))); 29 | parent::__construct('phpdcd', $version->getVersion()); 30 | } 31 | 32 | /** 33 | * Gets the name of the command based on input. 34 | * 35 | * @param InputInterface $input The input interface 36 | * 37 | * @return string The command name 38 | */ 39 | protected function getCommandName(InputInterface $input) 40 | { 41 | return 'phpdcd'; 42 | } 43 | 44 | /** 45 | * Gets the default commands that should always be available. 46 | * 47 | * @return array An array of default Command instances 48 | */ 49 | protected function getDefaultCommands() 50 | { 51 | $defaultCommands = parent::getDefaultCommands(); 52 | 53 | $defaultCommands[] = new Command; 54 | 55 | return $defaultCommands; 56 | } 57 | 58 | /** 59 | * Overridden so that the application doesn't expect the command 60 | * name to be the first argument. 61 | */ 62 | public function getDefinition() 63 | { 64 | $inputDefinition = parent::getDefinition(); 65 | $inputDefinition->setArguments(); 66 | 67 | return $inputDefinition; 68 | } 69 | 70 | /** 71 | * Runs the current application. 72 | * 73 | * @param InputInterface $input An Input instance 74 | * @param OutputInterface $output An Output instance 75 | * 76 | * @return int 0 if everything went fine, or an error code 77 | */ 78 | public function doRun(InputInterface $input, OutputInterface $output) 79 | { 80 | if (!$input->hasParameterOption('--quiet')) { 81 | $output->write( 82 | sprintf( 83 | "phpdcd %s by Sebastian Bergmann.\n\n", 84 | $this->getVersion() 85 | ) 86 | ); 87 | } 88 | 89 | if ($input->hasParameterOption('--version') || 90 | $input->hasParameterOption('-V')) { 91 | exit; 92 | } 93 | 94 | if (!$input->getFirstArgument()) { 95 | $input = new ArrayInput(array('--help')); 96 | } 97 | 98 | parent::doRun($input, $output); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/CLI/Command.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace SebastianBergmann\PHPDCD\CLI; 12 | 13 | use SebastianBergmann\PHPDCD\Detector; 14 | use SebastianBergmann\PHPDCD\Log\Text; 15 | use SebastianBergmann\FinderFacade\FinderFacade; 16 | use Symfony\Component\Console\Command\Command as AbstractCommand; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * @since Class available since Release 1.0.0 24 | */ 25 | class Command extends AbstractCommand 26 | { 27 | /** 28 | * Configures the current command. 29 | */ 30 | protected function configure() 31 | { 32 | $this->setName('phpdcd') 33 | ->setDefinition( 34 | array( 35 | new InputArgument( 36 | 'values', 37 | InputArgument::IS_ARRAY 38 | ) 39 | ) 40 | ) 41 | ->addOption( 42 | 'names', 43 | null, 44 | InputOption::VALUE_REQUIRED, 45 | 'A comma-separated list of file names to check', 46 | array('*.php') 47 | ) 48 | ->addOption( 49 | 'names-exclude', 50 | null, 51 | InputOption::VALUE_REQUIRED, 52 | 'A comma-separated list of file names to exclude', 53 | array() 54 | ) 55 | ->addOption( 56 | 'exclude', 57 | null, 58 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 59 | 'Exclude a directory from code analysis' 60 | ) 61 | ->addOption( 62 | 'recursive', 63 | null, 64 | InputOption::VALUE_NONE, 65 | 'Report code as dead if it is only called by dead code' 66 | ); 67 | } 68 | 69 | /** 70 | * Executes the current command. 71 | * 72 | * @param InputInterface $input An InputInterface instance 73 | * @param OutputInterface $output An OutputInterface instance 74 | * 75 | * @return null|int null or 0 if everything went fine, or an error code 76 | */ 77 | protected function execute(InputInterface $input, OutputInterface $output) 78 | { 79 | $finder = new FinderFacade( 80 | $input->getArgument('values'), 81 | $input->getOption('exclude'), 82 | $this->handleCSVOption($input, 'names'), 83 | $this->handleCSVOption($input, 'names-exclude') 84 | ); 85 | 86 | $files = $finder->findFiles(); 87 | 88 | if (empty($files)) { 89 | $output->writeln('No files found to scan'); 90 | exit(1); 91 | } 92 | 93 | $quiet = $output->getVerbosity() == OutputInterface::VERBOSITY_QUIET; 94 | 95 | $detector = new Detector; 96 | 97 | $result = $detector->detectDeadCode( 98 | $files, 99 | $input->getOption('recursive') 100 | ); 101 | 102 | if (!$quiet) { 103 | $printer = new Text; 104 | $printer->printResult($output, $result); 105 | 106 | $output->writeln(\PHP_Timer::resourceUsage()); 107 | } 108 | } 109 | 110 | /** 111 | * @param Symfony\Component\Console\Input\InputOption $input 112 | * @param string $option 113 | * @return array 114 | */ 115 | private function handleCSVOption(InputInterface $input, $option) 116 | { 117 | $result = $input->getOption($option); 118 | 119 | if (!is_array($result)) { 120 | $result = explode(',', $result); 121 | array_map('trim', $result); 122 | } 123 | 124 | return $result; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Detector.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace SebastianBergmann\PHPDCD; 12 | 13 | /** 14 | * PHPDCD detector for unused functions. 15 | * 16 | * @since Class available since Release 1.0.0 17 | */ 18 | class Detector 19 | { 20 | /** 21 | * @param array $files 22 | * @param bool $recursive 23 | * @return array 24 | */ 25 | public function detectDeadCode(array $files, $recursive = false) 26 | { 27 | 28 | // Analyse files and collect declared and called functions 29 | $analyser = new Analyser(); 30 | foreach ($files as $file) { 31 | $analyser->analyseFile($file); 32 | } 33 | 34 | // Get info on declared and called functions. 35 | $declared = $analyser->getFunctionDeclarations(); 36 | $called = $analyser->getFunctionCalls(); 37 | $classDescendants = $analyser->getClassDescendants(); 38 | 39 | // Search for declared, unused functions. 40 | $result = array(); 41 | foreach ($declared as $name => $source) { 42 | if (!isset($called[$name])) { 43 | // Unused function/method at first sight. 44 | $used = false; 45 | // For methods: check calls from subclass instances as well 46 | $parts = explode('::', $name); 47 | if (count($parts) == 2) { 48 | $class = $parts[0]; 49 | $subclasses = isset($classDescendants[$class]) ? $classDescendants[$class] : array(); 50 | foreach ($subclasses as $subclass) { 51 | if (isset($called[$subclass . '::' . $parts[1]])) { 52 | $used = true; 53 | break; 54 | } 55 | } 56 | } 57 | 58 | if (!$used) { 59 | $result[$name] = $source; 60 | } 61 | } 62 | } 63 | 64 | if ($recursive) { 65 | $done = false; 66 | 67 | while (!$done) { 68 | $done = true; 69 | 70 | foreach ($called as $callee => $callers) { 71 | $_called = false; 72 | 73 | foreach ($callers as $caller) { 74 | if (!isset($result[$caller])) { 75 | $_called = true; 76 | break; 77 | } 78 | } 79 | 80 | if (!$_called) { 81 | if (isset($declared[$callee])) { 82 | $result[$callee] = $declared[$callee]; 83 | } 84 | 85 | $done = false; 86 | 87 | unset($called[$callee]); 88 | } 89 | } 90 | } 91 | } 92 | 93 | ksort($result); 94 | 95 | return $result; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Log/Text.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace SebastianBergmann\PHPDCD\Log; 12 | 13 | use Symfony\Component\Console\Output\OutputInterface; 14 | 15 | /** 16 | * @since Class available since Release 1.0.0 17 | */ 18 | class Text 19 | { 20 | /** 21 | * Prints a result set from PHPDCD_Detector::detectDeadCode(). 22 | * 23 | * @param Symfony\Component\Console\Output\OutputInterface $output 24 | * @param array $result 25 | */ 26 | public function printResult(OutputInterface $output, array $result) 27 | { 28 | foreach ($result as $name => $source) { 29 | $output->writeln( 30 | sprintf( 31 | " - %s()\n LOC: %d, declared in %s:%d\n", 32 | $name, 33 | $source['loc'], 34 | $source['file'], 35 | $source['line'] 36 | ) 37 | ); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/AnalyserTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace SebastianBergmann\PHPDCD; 12 | 13 | use PHPUnit_Framework_TestCase; 14 | 15 | if (!defined('TEST_FILES_PATH')) { 16 | define( 17 | 'TEST_FILES_PATH', 18 | dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR 19 | ); 20 | } 21 | 22 | /** 23 | * Tests for the SebastianBergmann\PHPDCD\Analyser class. 24 | * 25 | * @since Class available since Release 1.0.0 26 | */ 27 | class AnalyserTest extends PHPUnit_Framework_TestCase 28 | { 29 | /** 30 | * @var Analyser 31 | */ 32 | protected $analyser; 33 | 34 | protected function setUp() 35 | { 36 | $this->analyser = new Analyser(); 37 | } 38 | 39 | /** 40 | * @covers SebastianBergmann\PHPDCD\Analyser::getFunctionDeclarations 41 | */ 42 | public function testDetectingDeclaredFunctionsAndMethodsWorks() 43 | { 44 | $file = TEST_FILES_PATH . 'declarations.php'; 45 | $this->analyser->analyseFile($file); 46 | $this->assertEquals( 47 | array('AClass::aStaticMethod', 'AClass::aMethod', 'a_function', 'another_function', 'yet_another_function'), 48 | array_keys($this->analyser->getFunctionDeclarations()) 49 | ); 50 | } 51 | 52 | /** 53 | * @covers SebastianBergmann\PHPDCD\Analyser::getFunctionCalls 54 | */ 55 | public function testDetectingCalledFunctionsAndMethodsWorks() 56 | { 57 | $file = TEST_FILES_PATH . 'declarations.php'; 58 | $this->analyser->analyseFile($file); 59 | $this->assertEquals( 60 | array('another_function'), 61 | array_keys($this->analyser->getFunctionCalls()) 62 | ); 63 | } 64 | 65 | /** 66 | * @covers SebastianBergmann\PHPDCD\Analyser::getFunctionDeclarations SebastianBergmann\PHPDCD\Analyser::getFunctionCalls SebastianBergmann\PHPDCD\Analyser::getClassDescendants 67 | */ 68 | public function testParentMethods() 69 | { 70 | $file = TEST_FILES_PATH . 'issue_18.php'; 71 | $this->analyser->analyseFile($file); 72 | $this->assertEquals( 73 | array('Animal::hasHead', 'Rabbit::hasFur', 'Rabbit::eatsCarrots'), 74 | array_keys($this->analyser->getFunctionDeclarations()) 75 | ); 76 | $this->assertEquals( 77 | array('Rabbit::__construct', 'Rabbit::hasHead', 'Rabbit::hasFur'), 78 | array_keys($this->analyser->getFunctionCalls()) 79 | ); 80 | $this->assertEquals( 81 | array('Animal' => array('Rabbit')), 82 | $this->analyser->getClassDescendants() 83 | ); 84 | } 85 | 86 | /** 87 | * @covers SebastianBergmann\PHPDCD\Analyser::getFunctionDeclarations SebastianBergmann\PHPDCD\Analyser::getFunctionCalls SebastianBergmann\PHPDCD\Analyser::getClassDescendants 88 | */ 89 | public function testGreatParentMethods() 90 | { 91 | $file = TEST_FILES_PATH . 'issue_18_extra.php'; 92 | $this->analyser->analyseFile($file); 93 | $this->assertEquals( 94 | array('Animal::hasHead', 'FurryAnimal::hasFur', 'Rabbit::isCute'), 95 | array_keys($this->analyser->getFunctionDeclarations()) 96 | ); 97 | $this->assertEquals( 98 | array('Rabbit::__construct', 'Rabbit::hasHead', 'Rabbit::hasFur', 'Rabbit::isCute'), 99 | array_keys($this->analyser->getFunctionCalls()) 100 | ); 101 | $this->assertEquals( 102 | array( 103 | 'Animal' => array('FurryAnimal', 'Rabbit'), 104 | 'FurryAnimal' => array('Rabbit') 105 | ), 106 | $this->analyser->getClassDescendants() 107 | ); 108 | } 109 | 110 | 111 | /** 112 | * @covers SebastianBergmann\PHPDCD\Analyser::getFunctionDeclarations SebastianBergmann\PHPDCD\Analyser::getFunctionCalls 113 | */ 114 | public function testParentDoubleColonHandling() 115 | { 116 | $file = TEST_FILES_PATH . 'parent_double_colon_handling.php'; 117 | $this->analyser->analyseFile($file); 118 | $this->assertEquals( 119 | array('Toy::ping', 'Ball::roll'), 120 | array_keys($this->analyser->getFunctionDeclarations()) 121 | ); 122 | $calls = $this->analyser->getFunctionCalls(); 123 | $this->assertArrayHasKey('Toy::ping', $calls); 124 | $this->assertArrayHasKey('Ball::__construct', $calls); 125 | $this->assertArrayHasKey('Ball::roll', $calls); 126 | } 127 | 128 | /** 129 | * @covers SebastianBergmann\PHPDCD\Analyser::getClassDescendants 130 | */ 131 | public function testGetClassDescendants() 132 | { 133 | $sourceCode = 'analyser->analyseSourceCode($sourceCode); 142 | $descendants = $this->analyser->getClassDescendants(); 143 | $expected = array( 144 | 'A' => array('B', 'C', 'D', 'E'), 145 | 'B' => array('D', 'E'), 146 | 'D' => array('E'), 147 | ); 148 | $this->assertSame($expected, $descendants); 149 | } 150 | 151 | /** 152 | * @covers SebastianBergmann\PHPDCD\Analyser::getAncestors 153 | * @dataProvider provideTestGetAncestors 154 | */ 155 | public function testGetAncestors($child, $expectedAncestors) 156 | { 157 | $sourceCode = 'analyser->analyseSourceCode($sourceCode); 166 | $ancestors = $this->analyser->getAncestors($child); 167 | sort($ancestors); 168 | sort($expectedAncestors); 169 | $this->assertSame($expectedAncestors, $ancestors); 170 | } 171 | 172 | /** 173 | * Data provider for testGetAncestors 174 | */ 175 | public function provideTestGetAncestors() 176 | { 177 | $data = array(); 178 | $data[] = array('A', array()); 179 | $data[] = array('B', array('A')); 180 | $data[] = array('C', array('A')); 181 | $data[] = array('D', array('A', 'B')); 182 | $data[] = array('E', array('A', 'B', 'D')); 183 | $data[] = array('Z', array()); 184 | return $data; 185 | } 186 | 187 | /** 188 | * @covers SebastianBergmann\PHPDCD\Analyser::getAncestors 189 | */ 190 | public function testGetAncestorsWithCycle() 191 | { 192 | // This code snippet is invalid PHP, 193 | // but PHPDCD should complain about this and not naively end up in an endless loop. 194 | $sourceCode = 'analyser->analyseSourceCode($sourceCode); 201 | $this->setExpectedException('RunTimeException', 'Class hierarchy cycle detected'); 202 | $this->analyser->getAncestors('A'); 203 | } 204 | 205 | /** 206 | * @see https://github.com/sebastianbergmann/phpdcd/issues/26 207 | * @covers SebastianBergmann\PHPDCD\Analyser::getFunctionDeclarations SebastianBergmann\PHPDCD\Analyser::getFunctionCalls 208 | */ 209 | public function testMethodsFunctionsMixup() 210 | { 211 | $file = TEST_FILES_PATH . 'methods_vs_functions.php'; 212 | $this->analyser->analyseFile($file); 213 | $this->assertEquals( 214 | array('Klass::doSomething', 'doSomething', 'main'), 215 | array_keys($this->analyser->getFunctionDeclarations()) 216 | ); 217 | $this->assertEquals( 218 | array('::doSomething', 'Klass::__construct', 'main'), 219 | array_keys($this->analyser->getFunctionCalls()) 220 | ); 221 | } 222 | 223 | 224 | /** 225 | * @see https://github.com/sebastianbergmann/phpdcd/issues/28 226 | * @covers SebastianBergmann\PHPDCD\Analyser::getFunctionDeclarations 227 | */ 228 | public function testComplexVariableInterpolation() 229 | { 230 | $file = TEST_FILES_PATH . 'Interpolator.php'; 231 | $this->analyser->analyseFile($file); 232 | $this->assertEquals( 233 | array('Interpolator::methodFoo', 'Interpolator::methodBar', 'Interpolator::methodBazBaz'), 234 | array_keys($this->analyser->getFunctionDeclarations()) 235 | ); 236 | } 237 | 238 | 239 | /** 240 | * @covers SebastianBergmann\PHPDCD\Analyser::getFunctionDeclarations 241 | */ 242 | public function testIgnoreAbstractMethods() 243 | { 244 | $file = TEST_FILES_PATH . 'abstract_methods.php'; 245 | $this->analyser->analyseFile($file); 246 | $this->assertEquals( 247 | array('Painting::getShape'), 248 | array_keys($this->analyser->getFunctionDeclarations()) 249 | ); 250 | } 251 | 252 | 253 | } 254 | -------------------------------------------------------------------------------- /tests/DetectorTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace SebastianBergmann\PHPDCD; 12 | 13 | use PHPUnit_Framework_TestCase; 14 | 15 | if (!defined('TEST_FILES_PATH')) { 16 | define( 17 | 'TEST_FILES_PATH', 18 | dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR 19 | ); 20 | } 21 | 22 | /** 23 | * Tests for the SebastianBergmann\PHPDCD\Detector class. 24 | * 25 | * @since Class available since Release 1.0.0 26 | */ 27 | class DetectorTest extends PHPUnit_Framework_TestCase 28 | { 29 | /** 30 | * @var Detector 31 | */ 32 | protected $detector; 33 | 34 | protected function setUp() 35 | { 36 | $this->detector = new Detector; 37 | } 38 | 39 | /** 40 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 41 | */ 42 | public function testDetectingDeclaredFunctionsAndMethodsWorks() 43 | { 44 | $this->assertEquals( 45 | array( 46 | 'AClass::aMethod' => array( 47 | 'file' => TEST_FILES_PATH . 'declarations.php', 48 | 'line' => 8, 49 | 'loc' => 3, 50 | ), 51 | 'AClass::aStaticMethod' => array( 52 | 'file' => TEST_FILES_PATH . 'declarations.php', 53 | 'line' => 4, 54 | 'loc' => 3, 55 | ), 56 | 'a_function' => array( 57 | 'file' => TEST_FILES_PATH . 'declarations.php', 58 | 'line' => 13, 59 | 'loc' => 4 60 | ), 61 | 'yet_another_function' => array( 62 | 'file' => TEST_FILES_PATH . 'declarations.php', 63 | 'line' => 22, 64 | 'loc' => 3, 65 | ) 66 | ), 67 | $this->detector->detectDeadCode( 68 | array(TEST_FILES_PATH . 'declarations.php'), FALSE 69 | ) 70 | ); 71 | } 72 | 73 | /** 74 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 75 | * @depends testDetectingDeclaredFunctionsAndMethodsWorks 76 | */ 77 | public function testDetectingDeclaredFunctionsAndMethodsWorks2() 78 | { 79 | $this->assertEquals( 80 | array( 81 | 'AClass::aMethod' => array( 82 | 'file' => TEST_FILES_PATH . 'declarations.php', 83 | 'line' => 8, 84 | 'loc' => 3, 85 | ), 86 | 'AClass::aStaticMethod' => array( 87 | 'file' => TEST_FILES_PATH . 'declarations.php', 88 | 'line' => 4, 89 | 'loc' => 3, 90 | ), 91 | 'a_function' => array( 92 | 'file' => TEST_FILES_PATH . 'declarations.php', 93 | 'line' => 13, 94 | 'loc' => 4 95 | ), 96 | 'another_function' => array( 97 | 'file' => TEST_FILES_PATH . 'declarations.php', 98 | 'line' => 18, 99 | 'loc' => 3, 100 | ), 101 | 'yet_another_function' => array( 102 | 'file' => TEST_FILES_PATH . 'declarations.php', 103 | 'line' => 22, 104 | 'loc' => 3, 105 | ) 106 | ), 107 | $this->detector->detectDeadCode( 108 | array(TEST_FILES_PATH . 'declarations.php'), TRUE 109 | ) 110 | ); 111 | } 112 | 113 | /** 114 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 115 | * @depends testDetectingDeclaredFunctionsAndMethodsWorks 116 | */ 117 | public function testDetectingFunctionCallsWorks() 118 | { 119 | $this->assertEquals( 120 | array( 121 | 'AClass::aMethod' => array( 122 | 'file' => TEST_FILES_PATH . 'declarations.php', 123 | 'line' => 8, 124 | 'loc' => 3, 125 | ), 126 | 'AClass::aStaticMethod' => array( 127 | 'file' => TEST_FILES_PATH . 'declarations.php', 128 | 'line' => 4, 129 | 'loc' => 3, 130 | ), 131 | 'a_function' => array( 132 | 'file' => TEST_FILES_PATH . 'declarations.php', 133 | 'line' => 13, 134 | 'loc' => 4 135 | ), 136 | ), 137 | $this->detector->detectDeadCode( 138 | array( 139 | TEST_FILES_PATH . 'declarations.php', 140 | TEST_FILES_PATH . 'function_call.php', 141 | ), 142 | FALSE 143 | ) 144 | ); 145 | } 146 | 147 | /** 148 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 149 | * @depends testDetectingFunctionCallsWorks 150 | */ 151 | public function testDetectingFunctionCallsWorks2() 152 | { 153 | $this->assertEquals( 154 | array( 155 | 'AClass::aMethod' => array( 156 | 'file' => TEST_FILES_PATH . 'declarations.php', 157 | 'line' => 8, 158 | 'loc' => 3, 159 | ), 160 | 'AClass::aStaticMethod' => array( 161 | 'file' => TEST_FILES_PATH . 'declarations.php', 162 | 'line' => 4, 163 | 'loc' => 3, 164 | ), 165 | 'a_function' => array( 166 | 'file' => TEST_FILES_PATH . 'declarations.php', 167 | 'line' => 13, 168 | 'loc' => 4 169 | ), 170 | 'another_function' => array( 171 | 'file' => TEST_FILES_PATH . 'declarations.php', 172 | 'line' => 18, 173 | 'loc' => 3, 174 | ), 175 | ), 176 | $this->detector->detectDeadCode( 177 | array( 178 | TEST_FILES_PATH . 'declarations.php', 179 | TEST_FILES_PATH . 'function_call.php', 180 | ), 181 | TRUE 182 | ) 183 | ); 184 | } 185 | 186 | /** 187 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 188 | * @depends testDetectingFunctionCallsWorks 189 | */ 190 | public function testDetectingFunctionCallsWorks3() 191 | { 192 | $this->assertEquals( 193 | array( 194 | 'AClass::aMethod' => array( 195 | 'file' => TEST_FILES_PATH . 'declarations.php', 196 | 'line' => 8, 197 | 'loc' => 3, 198 | ), 199 | 'AClass::aStaticMethod' => array( 200 | 'file' => TEST_FILES_PATH . 'declarations.php', 201 | 'line' => 4, 202 | 'loc' => 3, 203 | ), 204 | 'yet_another_function' => array( 205 | 'file' => TEST_FILES_PATH . 'declarations.php', 206 | 'line' => 22, 207 | 'loc' => 3, 208 | ) 209 | ), 210 | $this->detector->detectDeadCode( 211 | array( 212 | TEST_FILES_PATH . 'declarations.php', 213 | TEST_FILES_PATH . 'function_call2.php', 214 | ), 215 | FALSE 216 | ) 217 | ); 218 | } 219 | 220 | /** 221 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 222 | * @depends testDetectingFunctionCallsWorks3 223 | */ 224 | public function testDetectingFunctionCallsWorks4() 225 | { 226 | $this->assertEquals( 227 | array( 228 | 'AClass::aMethod' => array( 229 | 'file' => TEST_FILES_PATH . 'declarations.php', 230 | 'line' => 8, 231 | 'loc' => 3, 232 | ), 233 | 'AClass::aStaticMethod' => array( 234 | 'file' => TEST_FILES_PATH . 'declarations.php', 235 | 'line' => 4, 236 | 'loc' => 3, 237 | ), 238 | 'yet_another_function' => array( 239 | 'file' => TEST_FILES_PATH . 'declarations.php', 240 | 'line' => 22, 241 | 'loc' => 3, 242 | ) 243 | ), 244 | $this->detector->detectDeadCode( 245 | array( 246 | TEST_FILES_PATH . 'declarations.php', 247 | TEST_FILES_PATH . 'function_call2.php', 248 | ), 249 | TRUE 250 | ) 251 | ); 252 | } 253 | 254 | /** 255 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 256 | * @depends testDetectingDeclaredFunctionsAndMethodsWorks 257 | */ 258 | public function testDetectingStaticMethodCallsWorks() 259 | { 260 | $this->assertEquals( 261 | array( 262 | 'AClass::aMethod' => array( 263 | 'file' => TEST_FILES_PATH . 'declarations.php', 264 | 'line' => 8, 265 | 'loc' => 3, 266 | ), 267 | 'a_function' => array( 268 | 'file' => TEST_FILES_PATH . 'declarations.php', 269 | 'line' => 13, 270 | 'loc' => 4 271 | ), 272 | 'yet_another_function' => array( 273 | 'file' => TEST_FILES_PATH . 'declarations.php', 274 | 'line' => 22, 275 | 'loc' => 3, 276 | ) 277 | ), 278 | $this->detector->detectDeadCode( 279 | array( 280 | TEST_FILES_PATH . 'declarations.php', 281 | TEST_FILES_PATH . 'static_method_call.php', 282 | ), 283 | FALSE 284 | ) 285 | ); 286 | } 287 | 288 | /** 289 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 290 | * @depends testDetectingStaticMethodCallsWorks 291 | */ 292 | public function testDetectingStaticMethodCallsWorks2() 293 | { 294 | $this->assertEquals( 295 | array( 296 | 'AClass::aMethod' => array( 297 | 'file' => TEST_FILES_PATH . 'declarations.php', 298 | 'line' => 8, 299 | 'loc' => 3, 300 | ), 301 | 'a_function' => array( 302 | 'file' => TEST_FILES_PATH . 'declarations.php', 303 | 'line' => 13, 304 | 'loc' => 4 305 | ), 306 | 'another_function' => array( 307 | 'file' => TEST_FILES_PATH . 'declarations.php', 308 | 'line' => 18, 309 | 'loc' => 3, 310 | ), 311 | 'yet_another_function' => array( 312 | 'file' => TEST_FILES_PATH . 'declarations.php', 313 | 'line' => 22, 314 | 'loc' => 3, 315 | ) 316 | ), 317 | $this->detector->detectDeadCode( 318 | array( 319 | TEST_FILES_PATH . 'declarations.php', 320 | TEST_FILES_PATH . 'static_method_call.php', 321 | ), 322 | TRUE 323 | ) 324 | ); 325 | } 326 | 327 | /** 328 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 329 | * @depends testDetectingDeclaredFunctionsAndMethodsWorks 330 | */ 331 | public function testDetectingMethodCallsWorks() 332 | { 333 | $this->assertEquals( 334 | array( 335 | 'AClass::aStaticMethod' => array( 336 | 'file' => TEST_FILES_PATH . 'declarations.php', 337 | 'line' => 4, 338 | 'loc' => 3, 339 | ), 340 | 'a_function' => array( 341 | 'file' => TEST_FILES_PATH . 'declarations.php', 342 | 'line' => 13, 343 | 'loc' => 4 344 | ), 345 | 'yet_another_function' => array( 346 | 'file' => TEST_FILES_PATH . 'declarations.php', 347 | 'line' => 22, 348 | 'loc' => 3, 349 | ) 350 | ), 351 | $this->detector->detectDeadCode( 352 | array( 353 | TEST_FILES_PATH . 'declarations.php', 354 | TEST_FILES_PATH . 'method_call.php', 355 | ), 356 | FALSE 357 | ) 358 | ); 359 | } 360 | 361 | /** 362 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 363 | * @depends testDetectingMethodCallsWorks 364 | */ 365 | public function testDetectingMethodCallsWorks2() 366 | { 367 | $this->assertEquals( 368 | array( 369 | 'AClass::aStaticMethod' => array( 370 | 'file' => TEST_FILES_PATH . 'declarations.php', 371 | 'line' => 4, 372 | 'loc' => 3, 373 | ), 374 | 'a_function' => array( 375 | 'file' => TEST_FILES_PATH . 'declarations.php', 376 | 'line' => 13, 377 | 'loc' => 4 378 | ), 379 | 'another_function' => array( 380 | 'file' => TEST_FILES_PATH . 'declarations.php', 381 | 'line' => 18, 382 | 'loc' => 3, 383 | ), 384 | 'yet_another_function' => array( 385 | 'file' => TEST_FILES_PATH . 'declarations.php', 386 | 'line' => 22, 387 | 'loc' => 3, 388 | ) 389 | ), 390 | $this->detector->detectDeadCode( 391 | array( 392 | TEST_FILES_PATH . 'declarations.php', 393 | TEST_FILES_PATH . 'method_call.php', 394 | ), 395 | TRUE 396 | ) 397 | ); 398 | } 399 | 400 | /** 401 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 402 | */ 403 | public function testThisIsHandledCorrectly() 404 | { 405 | $this->assertEmpty( 406 | $this->detector->detectDeadCode( 407 | array(TEST_FILES_PATH . 'issue_5.php'), FALSE 408 | ) 409 | ); 410 | } 411 | 412 | /** 413 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 414 | */ 415 | public function testParentMethods() 416 | { 417 | $file = TEST_FILES_PATH . 'issue_18.php'; 418 | $this->assertEquals( 419 | array( 420 | 'Rabbit::eatsCarrots' => array( 421 | 'file' => $file, 422 | 'line' => 18, 423 | 'loc' => 4, 424 | ), 425 | ), 426 | $this->detector->detectDeadCode(array($file), FALSE) 427 | ); 428 | } 429 | 430 | /** 431 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 432 | */ 433 | public function testGreatParentMethods() 434 | { 435 | $file = TEST_FILES_PATH . 'issue_18_extra.php'; 436 | $this->assertEquals( 437 | array(), 438 | $this->detector->detectDeadCode(array($file), FALSE) 439 | ); 440 | } 441 | 442 | 443 | /** 444 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 445 | */ 446 | public function testParentDoubleColonHandling() 447 | { 448 | $file = TEST_FILES_PATH . 'parent_double_colon_handling.php'; 449 | $result = $this->detector->detectDeadCode(array($file), FALSE); 450 | $this->assertEquals(array(), $result); 451 | } 452 | 453 | 454 | /** 455 | * @see https://github.com/sebastianbergmann/phpdcd/issues/26 456 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 457 | */ 458 | public function testMethodsFunctionsMixup() 459 | { 460 | $file = TEST_FILES_PATH . 'methods_vs_functions.php'; 461 | $result = $this->detector->detectDeadCode(array($file), FALSE); 462 | $this->assertEquals(array('Klass::doSomething', 'doSomething'), array_keys($result)); 463 | } 464 | 465 | 466 | /** 467 | * @covers SebastianBergmann\PHPDCD\Detector::detectDeadCode 468 | */ 469 | public function testIgnoreAbstractMethods() 470 | { 471 | $file = TEST_FILES_PATH . 'abstract_methods.php'; 472 | $result = $this->detector->detectDeadCode(array($file), FALSE); 473 | $this->assertEquals(array('Painting::getShape'), array_keys($result)); 474 | 475 | } 476 | 477 | } 478 | -------------------------------------------------------------------------------- /tests/_files/Interpolator.php: -------------------------------------------------------------------------------- 1 | hasHead(); 27 | $f = $r->hasFur(); 28 | -------------------------------------------------------------------------------- /tests/_files/issue_18_extra.php: -------------------------------------------------------------------------------- 1 | hasHead(); 31 | $r->hasFur(); 32 | $r->isCute(); 33 | -------------------------------------------------------------------------------- /tests/_files/issue_5.php: -------------------------------------------------------------------------------- 1 | getClass()->test = 'a'; 9 | } 10 | } 11 | 12 | $a = new Test(); 13 | $a->callClass(); 14 | -------------------------------------------------------------------------------- /tests/_files/method_call.php: -------------------------------------------------------------------------------- 1 | aMethod(); 4 | -------------------------------------------------------------------------------- /tests/_files/methods_vs_functions.php: -------------------------------------------------------------------------------- 1 | doSomething(); 17 | } 18 | 19 | $x = new Klass(); 20 | main(); 21 | 22 | -------------------------------------------------------------------------------- /tests/_files/parent_double_colon_handling.php: -------------------------------------------------------------------------------- 1 | roll(); 22 | -------------------------------------------------------------------------------- /tests/_files/static_method_call.php: -------------------------------------------------------------------------------- 1 |