123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- <?php declare(strict_types=1);
- /*
- * This file is part of phpunit/php-code-coverage.
- *
- * (c) Sebastian Bergmann <sebastian@phpunit.de>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
- use function array_merge;
- use function array_unique;
- use function assert;
- use function file_get_contents;
- use function is_array;
- use function max;
- use function range;
- use function sort;
- use function sprintf;
- use function substr_count;
- use function token_get_all;
- use function trim;
- use PhpParser\Error;
- use PhpParser\NodeTraverser;
- use PhpParser\NodeVisitor\NameResolver;
- use PhpParser\NodeVisitor\ParentConnectingVisitor;
- use PhpParser\ParserFactory;
- use SebastianBergmann\CodeCoverage\ParserException;
- use SebastianBergmann\LinesOfCode\LineCountingVisitor;
- /**
- * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
- *
- * @psalm-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
- * @psalm-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
- * @psalm-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
- * @psalm-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
- * @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
- * @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
- */
- final class ParsingFileAnalyser implements FileAnalyser
- {
- /**
- * @psalm-var array<string, array<string, CodeUnitClassType>>
- */
- private array $classes = [];
- /**
- * @psalm-var array<string, array<string, CodeUnitTraitType>>
- */
- private array $traits = [];
- /**
- * @psalm-var array<string, array<string, CodeUnitFunctionType>>
- */
- private array $functions = [];
- /**
- * @var array<string, LinesOfCodeType>
- */
- private array $linesOfCode = [];
- /**
- * @var array<string, LinesType>
- */
- private array $ignoredLines = [];
- /**
- * @var array<string, LinesType>
- */
- private array $executableLines = [];
- private readonly bool $useAnnotationsForIgnoringCode;
- private readonly bool $ignoreDeprecatedCode;
- public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode)
- {
- $this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
- $this->ignoreDeprecatedCode = $ignoreDeprecatedCode;
- }
- public function classesIn(string $filename): array
- {
- $this->analyse($filename);
- return $this->classes[$filename];
- }
- public function traitsIn(string $filename): array
- {
- $this->analyse($filename);
- return $this->traits[$filename];
- }
- public function functionsIn(string $filename): array
- {
- $this->analyse($filename);
- return $this->functions[$filename];
- }
- public function linesOfCodeFor(string $filename): array
- {
- $this->analyse($filename);
- return $this->linesOfCode[$filename];
- }
- public function executableLinesIn(string $filename): array
- {
- $this->analyse($filename);
- return $this->executableLines[$filename];
- }
- public function ignoredLinesFor(string $filename): array
- {
- $this->analyse($filename);
- return $this->ignoredLines[$filename];
- }
- /**
- * @throws ParserException
- */
- private function analyse(string $filename): void
- {
- if (isset($this->classes[$filename])) {
- return;
- }
- $source = file_get_contents($filename);
- $linesOfCode = max(substr_count($source, "\n") + 1, substr_count($source, "\r") + 1);
- if ($linesOfCode === 0 && !empty($source)) {
- $linesOfCode = 1;
- }
- assert($linesOfCode > 0);
- $parser = (new ParserFactory)->createForHostVersion();
- try {
- $nodes = $parser->parse($source);
- assert($nodes !== null);
- $traverser = new NodeTraverser;
- $codeUnitFindingVisitor = new CodeUnitFindingVisitor;
- $lineCountingVisitor = new LineCountingVisitor($linesOfCode);
- $ignoredLinesFindingVisitor = new IgnoredLinesFindingVisitor($this->useAnnotationsForIgnoringCode, $this->ignoreDeprecatedCode);
- $executableLinesFindingVisitor = new ExecutableLinesFindingVisitor($source);
- $traverser->addVisitor(new NameResolver);
- $traverser->addVisitor(new ParentConnectingVisitor);
- $traverser->addVisitor($codeUnitFindingVisitor);
- $traverser->addVisitor($lineCountingVisitor);
- $traverser->addVisitor($ignoredLinesFindingVisitor);
- $traverser->addVisitor($executableLinesFindingVisitor);
- /* @noinspection UnusedFunctionResultInspection */
- $traverser->traverse($nodes);
- // @codeCoverageIgnoreStart
- } catch (Error $error) {
- throw new ParserException(
- sprintf(
- 'Cannot parse %s: %s',
- $filename,
- $error->getMessage(),
- ),
- $error->getCode(),
- $error,
- );
- }
- // @codeCoverageIgnoreEnd
- $this->classes[$filename] = $codeUnitFindingVisitor->classes();
- $this->traits[$filename] = $codeUnitFindingVisitor->traits();
- $this->functions[$filename] = $codeUnitFindingVisitor->functions();
- $this->executableLines[$filename] = $executableLinesFindingVisitor->executableLinesGroupedByBranch();
- $this->ignoredLines[$filename] = [];
- $this->findLinesIgnoredByLineBasedAnnotations($filename, $source, $this->useAnnotationsForIgnoringCode);
- $this->ignoredLines[$filename] = array_unique(
- array_merge(
- $this->ignoredLines[$filename],
- $ignoredLinesFindingVisitor->ignoredLines(),
- ),
- );
- sort($this->ignoredLines[$filename]);
- $result = $lineCountingVisitor->result();
- $this->linesOfCode[$filename] = [
- 'linesOfCode' => $result->linesOfCode(),
- 'commentLinesOfCode' => $result->commentLinesOfCode(),
- 'nonCommentLinesOfCode' => $result->nonCommentLinesOfCode(),
- ];
- }
- private function findLinesIgnoredByLineBasedAnnotations(string $filename, string $source, bool $useAnnotationsForIgnoringCode): void
- {
- if (!$useAnnotationsForIgnoringCode) {
- return;
- }
- $start = false;
- foreach (token_get_all($source) as $token) {
- if (!is_array($token) ||
- !(T_COMMENT === $token[0] || T_DOC_COMMENT === $token[0])) {
- continue;
- }
- $comment = trim($token[1]);
- if ($comment === '// @codeCoverageIgnore' ||
- $comment === '//@codeCoverageIgnore') {
- $this->ignoredLines[$filename][] = $token[2];
- continue;
- }
- if ($comment === '// @codeCoverageIgnoreStart' ||
- $comment === '//@codeCoverageIgnoreStart') {
- $start = $token[2];
- continue;
- }
- if ($comment === '// @codeCoverageIgnoreEnd' ||
- $comment === '//@codeCoverageIgnoreEnd') {
- if (false === $start) {
- $start = $token[2];
- }
- $this->ignoredLines[$filename] = array_merge(
- $this->ignoredLines[$filename],
- range($start, $token[2]),
- );
- }
- }
- }
- }
|