PageRenderTime 43ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Analyser.php

http://github.com/sebastianbergmann/phpdcd
PHP | 323 lines | 227 code | 38 blank | 58 comment | 44 complexity | 0cab5bd1403be207a0151e06e50f8b79 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /*
  3. * This file is part of PHP Dead Code Detector (PHPDCD).
  4. *
  5. * (c) Sebastian Bergmann <sebastian@phpunit.de>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace SebastianBergmann\PHPDCD;
  11. /**
  12. * PHPDCD code analyser to be used on a body of source code.
  13. *
  14. * Analyses given source code (files) for declared and called functions
  15. * and aggregates this information.
  16. *
  17. * @since Class available since Release 1.0.0
  18. */
  19. class Analyser
  20. {
  21. /**
  22. * Function declaration mapping: maps declared function name to file and line number
  23. * TODO: make mapping to file and line number optional for memory usage reduction?
  24. * @var array
  25. */
  26. private $functionDeclarations = array();
  27. /**
  28. * Function call mapping: maps "callees" to array of "callers"
  29. * TODO: make callers array optional for memory usage reduction?
  30. * @var array
  31. */
  32. private $functionCalls = array();
  33. /**
  34. * Class hierarchy data: maps classes to their direct parent.
  35. * @var array
  36. */
  37. private $classParents = array();
  38. public function getFunctionDeclarations()
  39. {
  40. return $this->functionDeclarations;
  41. }
  42. /**
  43. * Get function calls we detected
  44. * @return array maps "callees" to array of "callers"
  45. */
  46. public function getFunctionCalls()
  47. {
  48. // Resolve parent(class) calls if possible
  49. foreach ($this->functionCalls as $call => $callers) {
  50. if (strpos($call, 'parent(') === 0) {
  51. preg_match('/parent\\((.*?)\\)::(.*)/', $call, $matches);
  52. $class = $matches[1];
  53. $method = $matches[2];
  54. foreach ($this->getAncestors($class) as $ancestor) {
  55. $resolvedCall = $ancestor . '::' . $method;
  56. if (isset($this->functionDeclarations[$resolvedCall])) {
  57. $this->functionCalls[$resolvedCall] = $callers;
  58. // TODO: also remove unresolved parent(class) entries?
  59. break;
  60. }
  61. }
  62. }
  63. }
  64. return $this->functionCalls;
  65. }
  66. /**
  67. * Get array of a class's ancestors.
  68. * @param $child
  69. * @return array of ancestors
  70. */
  71. public function getAncestors($child)
  72. {
  73. $ancestors = array();
  74. while (isset($this->classParents[$child])) {
  75. $child = $this->classParents[$child];
  76. if (in_array($child, $ancestors)) {
  77. $cycle = implode(' -> ', $ancestors) . ' -> ' . $child;
  78. throw new \RuntimeException('Class hierarchy cycle detected: ' . $cycle);
  79. }
  80. $ancestors[] = $child;
  81. }
  82. return $ancestors;
  83. }
  84. /**
  85. * Build a mapping between parent classes and all their descendants
  86. * @return array maps each parent classes to array of its subclasses, subsubclasses, ...
  87. */
  88. public function getClassDescendants()
  89. {
  90. $descendants = array();
  91. foreach ($this->classParents as $child => $parent) {
  92. // Direct child
  93. $descendants[$parent][] = $child;
  94. // Store child for further ancestors
  95. $ancestor = $parent;
  96. while (isset($this->classParents[$ancestor])) {
  97. $ancestor = $this->classParents[$ancestor];
  98. $descendants[$ancestor][] = $child;
  99. }
  100. }
  101. return $descendants;
  102. }
  103. /**
  104. * Analyse a PHP source code file for defined and called functions.
  105. * @param $filename
  106. */
  107. public function analyseFile($filename)
  108. {
  109. $sourceCode = file_get_contents($filename);
  110. return $this->analyseSourceCode($sourceCode, $filename);
  111. }
  112. /**
  113. * Analyse PHP source code for defined and called functions
  114. *
  115. * @param string $sourceCode source code.
  116. * @param string $filename optional file name to use in declaration definition
  117. */
  118. public function analyseSourceCode($sourceCode, $filename = 'undefined')
  119. {
  120. $blocks = array();
  121. $currentBlock = null;
  122. $currentClass = '';
  123. $currentFunction = '';
  124. $currentInterface = '';
  125. $namespace = '';
  126. $variables = array();
  127. $tokens = new \PHP_Token_Stream($sourceCode);
  128. $count = count($tokens);
  129. for ($i = 0; $i < $count; $i++) {
  130. if ($tokens[$i] instanceof \PHP_Token_NAMESPACE) {
  131. $namespace = $tokens[$i]->getName();
  132. } elseif ($tokens[$i] instanceof \PHP_Token_CLASS) {
  133. $currentClass = $tokens[$i]->getName();
  134. if ($namespace != '') {
  135. $currentClass = $namespace . '\\' . $currentClass;
  136. }
  137. $currentBlock = $currentClass;
  138. } elseif ($tokens[$i] instanceof \PHP_Token_EXTENDS
  139. && $tokens[$i+2] instanceof \PHP_Token_STRING) {
  140. // Store parent-child class relationship.
  141. $this->classParents[$currentClass] = (string) $tokens[$i+2];
  142. } elseif ($tokens[$i] instanceof \PHP_Token_INTERFACE) {
  143. $currentInterface = $tokens[$i]->getName();
  144. if ($namespace != '') {
  145. $currentInterface = $namespace . '\\' . $currentClass;
  146. }
  147. $currentBlock = $currentInterface;
  148. } elseif ($tokens[$i] instanceof \PHP_Token_NEW &&
  149. !$tokens[$i+2] instanceof \PHP_Token_VARIABLE) {
  150. if ($tokens[$i-1] instanceof \PHP_Token_EQUAL) {
  151. $j = -1;
  152. } elseif ($tokens[$i-1] instanceof \PHP_Token_WHITESPACE &&
  153. $tokens[$i-2] instanceof \PHP_Token_EQUAL) {
  154. $j = -2;
  155. } else {
  156. continue;
  157. }
  158. if ($tokens[$i+$j-1] instanceof \PHP_Token_WHITESPACE) {
  159. $j--;
  160. }
  161. if ($tokens[$i+$j-1] instanceof \PHP_Token_VARIABLE) {
  162. $name = (string) $tokens[$i+$j-1];
  163. $variables[$name] = (string) $tokens[$i+2];
  164. } elseif ($tokens[$i+$j-1] instanceof \PHP_Token_STRING &&
  165. $tokens[$i+$j-2] instanceof \PHP_Token_OBJECT_OPERATOR &&
  166. $tokens[$i+$j-3] instanceof \PHP_Token_VARIABLE) {
  167. $name = (string) $tokens[$i+$j-3] . '->' .
  168. (string) $tokens[$i+$j-1];
  169. $variables[$name] = (string) $tokens[$i+2];
  170. }
  171. } elseif ($tokens[$i] instanceof \PHP_Token_FUNCTION) {
  172. if ($currentInterface != '') {
  173. continue;
  174. }
  175. // Ignore abstract methods.
  176. for ($j=1; $j<=4; $j++) {
  177. if (isset($tokens[$i-$j]) &&
  178. $tokens[$i-$j] instanceof \PHP_Token_ABSTRACT) {
  179. continue 2;
  180. }
  181. }
  182. $function = $tokens[$i]->getName();
  183. if ($function == 'anonymous function') {
  184. continue;
  185. }
  186. $variables = $tokens[$i]->getArguments();
  187. if ($currentClass != '') {
  188. $function = $currentClass . '::' . $function;
  189. $variables['$this'] = $currentClass;
  190. }
  191. $currentFunction = $function;
  192. $currentBlock = $currentFunction;
  193. $this->functionDeclarations[$function] = array(
  194. 'file' => $filename, 'line' => $tokens[$i]->getLine()
  195. );
  196. } elseif ($tokens[$i] instanceof \PHP_Token_OPEN_CURLY
  197. || $tokens[$i] instanceof \PHP_Token_CURLY_OPEN
  198. || $tokens[$i] instanceof \PHP_Token_DOLLAR_OPEN_CURLY_BRACES ) {
  199. array_push($blocks, $currentBlock);
  200. $currentBlock = null;
  201. } elseif ($tokens[$i] instanceof \PHP_Token_CLOSE_CURLY) {
  202. $block = array_pop($blocks);
  203. if ($block == $currentClass) {
  204. $currentClass = '';
  205. } elseif ($block == $currentFunction) {
  206. $this->functionDeclarations[$currentFunction]['loc'] =
  207. $tokens[$i]->getLine() - $this->functionDeclarations[$currentFunction]['line'] + 1;
  208. $currentFunction = '';
  209. $variables = array();
  210. }
  211. } elseif ($tokens[$i] instanceof \PHP_Token_OPEN_BRACKET) {
  212. for ($j = 1; $j <= 4; $j++) {
  213. if (isset($tokens[$i-$j]) &&
  214. $tokens[$i-$j] instanceof \PHP_Token_FUNCTION) {
  215. continue 2;
  216. }
  217. }
  218. if ($tokens[$i-1] instanceof \PHP_Token_STRING) {
  219. $j = -1;
  220. } elseif ($tokens[$i-1] instanceof \PHP_Token_WHITESPACE &&
  221. $tokens[$i-2] instanceof \PHP_Token_STRING) {
  222. $j = -2;
  223. } else {
  224. continue;
  225. }
  226. $function = (string) $tokens[$i+$j];
  227. $lookForNamespace = true;
  228. if (isset($tokens[$i+$j-2]) &&
  229. $tokens[$i+$j-2] instanceof \PHP_Token_NEW) {
  230. $function .= '::__construct';
  231. } elseif ((isset($tokens[$i+$j-1]) &&
  232. $tokens[$i+$j-1] instanceof \PHP_Token_OBJECT_OPERATOR) ||
  233. (isset($tokens[$i+$j-2]) &&
  234. $tokens[$i+$j-2] instanceof \PHP_Token_OBJECT_OPERATOR)) {
  235. $_function = $tokens[$i+$j];
  236. $lookForNamespace = false;
  237. if ($tokens[$i+$j-1] instanceof \PHP_Token_OBJECT_OPERATOR) {
  238. $j -= 2;
  239. } else {
  240. $j -= 3;
  241. }
  242. if ($tokens[$i+$j] instanceof \PHP_Token_VARIABLE) {
  243. if (isset($variables[(string) $tokens[$i+$j]])) {
  244. $function = $variables[(string) $tokens[$i+$j]] .
  245. '::' . $_function;
  246. } else {
  247. $function = '::' . $_function;
  248. }
  249. } elseif ($tokens[$i+$j] instanceof \PHP_Token_STRING &&
  250. $tokens[$i+$j-1] instanceof \PHP_Token_OBJECT_OPERATOR &&
  251. $tokens[$i+$j-2] instanceof \PHP_Token_VARIABLE) {
  252. $variable = (string) $tokens[$i+$j-2] . '->' .
  253. (string) $tokens[$i+$j];
  254. if (isset($variables[$variable])) {
  255. $function = $variables[$variable] . '::' .
  256. $_function;
  257. }
  258. }
  259. } elseif ($tokens[$i+$j-1] instanceof \PHP_Token_DOUBLE_COLON) {
  260. $class = (string) $tokens[$i+$j-2];
  261. if ($class == 'self' || $class == 'static') {
  262. $class = $currentClass;
  263. } elseif ($class == 'parent') {
  264. $class = "parent($currentClass)";
  265. }
  266. $function = $class . '::' . $function;
  267. $j -= 2;
  268. }
  269. if ($lookForNamespace) {
  270. while ($tokens[$i+$j-1] instanceof \PHP_Token_NS_SEPARATOR) {
  271. $function = $tokens[$i+$j-2] . '\\' . $function;
  272. $j -= 2;
  273. }
  274. }
  275. if (!isset($this->functionCalls[$function])) {
  276. $this->functionCalls[$function] = array();
  277. }
  278. $this->functionCalls[$function][] = $currentFunction;
  279. }
  280. }
  281. }
  282. }