ParsingFileAnalyser.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of phpunit/php-code-coverage.
  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\CodeCoverage\StaticAnalysis;
  11. use function array_merge;
  12. use function array_unique;
  13. use function assert;
  14. use function file_get_contents;
  15. use function is_array;
  16. use function max;
  17. use function range;
  18. use function sort;
  19. use function sprintf;
  20. use function substr_count;
  21. use function token_get_all;
  22. use function trim;
  23. use PhpParser\Error;
  24. use PhpParser\NodeTraverser;
  25. use PhpParser\NodeVisitor\NameResolver;
  26. use PhpParser\NodeVisitor\ParentConnectingVisitor;
  27. use PhpParser\ParserFactory;
  28. use SebastianBergmann\CodeCoverage\ParserException;
  29. use SebastianBergmann\LinesOfCode\LineCountingVisitor;
  30. /**
  31. * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
  32. *
  33. * @psalm-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
  34. * @psalm-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
  35. * @psalm-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
  36. * @psalm-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
  37. * @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
  38. * @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
  39. */
  40. final class ParsingFileAnalyser implements FileAnalyser
  41. {
  42. /**
  43. * @psalm-var array<string, array<string, CodeUnitClassType>>
  44. */
  45. private array $classes = [];
  46. /**
  47. * @psalm-var array<string, array<string, CodeUnitTraitType>>
  48. */
  49. private array $traits = [];
  50. /**
  51. * @psalm-var array<string, array<string, CodeUnitFunctionType>>
  52. */
  53. private array $functions = [];
  54. /**
  55. * @var array<string, LinesOfCodeType>
  56. */
  57. private array $linesOfCode = [];
  58. /**
  59. * @var array<string, LinesType>
  60. */
  61. private array $ignoredLines = [];
  62. /**
  63. * @var array<string, LinesType>
  64. */
  65. private array $executableLines = [];
  66. private readonly bool $useAnnotationsForIgnoringCode;
  67. private readonly bool $ignoreDeprecatedCode;
  68. public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode)
  69. {
  70. $this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
  71. $this->ignoreDeprecatedCode = $ignoreDeprecatedCode;
  72. }
  73. public function classesIn(string $filename): array
  74. {
  75. $this->analyse($filename);
  76. return $this->classes[$filename];
  77. }
  78. public function traitsIn(string $filename): array
  79. {
  80. $this->analyse($filename);
  81. return $this->traits[$filename];
  82. }
  83. public function functionsIn(string $filename): array
  84. {
  85. $this->analyse($filename);
  86. return $this->functions[$filename];
  87. }
  88. public function linesOfCodeFor(string $filename): array
  89. {
  90. $this->analyse($filename);
  91. return $this->linesOfCode[$filename];
  92. }
  93. public function executableLinesIn(string $filename): array
  94. {
  95. $this->analyse($filename);
  96. return $this->executableLines[$filename];
  97. }
  98. public function ignoredLinesFor(string $filename): array
  99. {
  100. $this->analyse($filename);
  101. return $this->ignoredLines[$filename];
  102. }
  103. /**
  104. * @throws ParserException
  105. */
  106. private function analyse(string $filename): void
  107. {
  108. if (isset($this->classes[$filename])) {
  109. return;
  110. }
  111. $source = file_get_contents($filename);
  112. $linesOfCode = max(substr_count($source, "\n") + 1, substr_count($source, "\r") + 1);
  113. if ($linesOfCode === 0 && !empty($source)) {
  114. $linesOfCode = 1;
  115. }
  116. assert($linesOfCode > 0);
  117. $parser = (new ParserFactory)->createForHostVersion();
  118. try {
  119. $nodes = $parser->parse($source);
  120. assert($nodes !== null);
  121. $traverser = new NodeTraverser;
  122. $codeUnitFindingVisitor = new CodeUnitFindingVisitor;
  123. $lineCountingVisitor = new LineCountingVisitor($linesOfCode);
  124. $ignoredLinesFindingVisitor = new IgnoredLinesFindingVisitor($this->useAnnotationsForIgnoringCode, $this->ignoreDeprecatedCode);
  125. $executableLinesFindingVisitor = new ExecutableLinesFindingVisitor($source);
  126. $traverser->addVisitor(new NameResolver);
  127. $traverser->addVisitor(new ParentConnectingVisitor);
  128. $traverser->addVisitor($codeUnitFindingVisitor);
  129. $traverser->addVisitor($lineCountingVisitor);
  130. $traverser->addVisitor($ignoredLinesFindingVisitor);
  131. $traverser->addVisitor($executableLinesFindingVisitor);
  132. /* @noinspection UnusedFunctionResultInspection */
  133. $traverser->traverse($nodes);
  134. // @codeCoverageIgnoreStart
  135. } catch (Error $error) {
  136. throw new ParserException(
  137. sprintf(
  138. 'Cannot parse %s: %s',
  139. $filename,
  140. $error->getMessage(),
  141. ),
  142. $error->getCode(),
  143. $error,
  144. );
  145. }
  146. // @codeCoverageIgnoreEnd
  147. $this->classes[$filename] = $codeUnitFindingVisitor->classes();
  148. $this->traits[$filename] = $codeUnitFindingVisitor->traits();
  149. $this->functions[$filename] = $codeUnitFindingVisitor->functions();
  150. $this->executableLines[$filename] = $executableLinesFindingVisitor->executableLinesGroupedByBranch();
  151. $this->ignoredLines[$filename] = [];
  152. $this->findLinesIgnoredByLineBasedAnnotations($filename, $source, $this->useAnnotationsForIgnoringCode);
  153. $this->ignoredLines[$filename] = array_unique(
  154. array_merge(
  155. $this->ignoredLines[$filename],
  156. $ignoredLinesFindingVisitor->ignoredLines(),
  157. ),
  158. );
  159. sort($this->ignoredLines[$filename]);
  160. $result = $lineCountingVisitor->result();
  161. $this->linesOfCode[$filename] = [
  162. 'linesOfCode' => $result->linesOfCode(),
  163. 'commentLinesOfCode' => $result->commentLinesOfCode(),
  164. 'nonCommentLinesOfCode' => $result->nonCommentLinesOfCode(),
  165. ];
  166. }
  167. private function findLinesIgnoredByLineBasedAnnotations(string $filename, string $source, bool $useAnnotationsForIgnoringCode): void
  168. {
  169. if (!$useAnnotationsForIgnoringCode) {
  170. return;
  171. }
  172. $start = false;
  173. foreach (token_get_all($source) as $token) {
  174. if (!is_array($token) ||
  175. !(T_COMMENT === $token[0] || T_DOC_COMMENT === $token[0])) {
  176. continue;
  177. }
  178. $comment = trim($token[1]);
  179. if ($comment === '// @codeCoverageIgnore' ||
  180. $comment === '//@codeCoverageIgnore') {
  181. $this->ignoredLines[$filename][] = $token[2];
  182. continue;
  183. }
  184. if ($comment === '// @codeCoverageIgnoreStart' ||
  185. $comment === '//@codeCoverageIgnoreStart') {
  186. $start = $token[2];
  187. continue;
  188. }
  189. if ($comment === '// @codeCoverageIgnoreEnd' ||
  190. $comment === '//@codeCoverageIgnoreEnd') {
  191. if (false === $start) {
  192. $start = $token[2];
  193. }
  194. $this->ignoredLines[$filename] = array_merge(
  195. $this->ignoredLines[$filename],
  196. range($start, $token[2]),
  197. );
  198. }
  199. }
  200. }
  201. }