add phpunit code coverage driver

This commit is contained in:
Andrew Dolgov 2023-12-02 17:45:25 +03:00
parent 2b8e344532
commit 09898ccbc8
No known key found for this signature in database
GPG Key ID: 1A56B4FA25D4AF2A
38 changed files with 647 additions and 407 deletions

View File

@ -28,6 +28,7 @@
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "1.10.3", "phpstan/phpstan": "1.10.3",
"phpunit/phpunit": "9.5.16" "phpunit/phpunit": "9.5.16",
"phpunit/php-code-coverage": "^9.2"
} }
} }

16
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "cbbbfbdbf1c5f659b8e34307411bc751", "content-hash": "d65a2e896d59d3d603fd6cda0db3b646",
"packages": [ "packages": [
{ {
"name": "beberlei/assert", "name": "beberlei/assert",
@ -2904,23 +2904,23 @@
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "9.2.15", "version": "9.2.24",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2cf940ebc6355a9d430462811b5aaa308b174bed",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"ext-xmlwriter": "*", "ext-xmlwriter": "*",
"nikic/php-parser": "^4.13.0", "nikic/php-parser": "^4.14",
"php": ">=7.3", "php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3", "phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2", "phpunit/php-text-template": "^2.0.2",
@ -2969,7 +2969,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.24"
}, },
"funding": [ "funding": [
{ {
@ -2977,7 +2977,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2022-03-07T09:28:20+00:00" "time": "2023-01-26T08:26:55+00:00"
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",

View File

@ -2073,24 +2073,24 @@
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "9.2.15", "version": "9.2.24",
"version_normalized": "9.2.15.0", "version_normalized": "9.2.24.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2cf940ebc6355a9d430462811b5aaa308b174bed",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"ext-xmlwriter": "*", "ext-xmlwriter": "*",
"nikic/php-parser": "^4.13.0", "nikic/php-parser": "^4.14",
"php": ">=7.3", "php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3", "phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2", "phpunit/php-text-template": "^2.0.2",
@ -2108,7 +2108,7 @@
"ext-pcov": "*", "ext-pcov": "*",
"ext-xdebug": "*" "ext-xdebug": "*"
}, },
"time": "2022-03-07T09:28:20+00:00", "time": "2023-01-26T08:26:55+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
@ -2141,7 +2141,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.24"
}, },
"funding": [ "funding": [
{ {

View File

@ -3,7 +3,7 @@
'name' => '__root__', 'name' => '__root__',
'pretty_version' => 'dev-master', 'pretty_version' => 'dev-master',
'version' => 'dev-master', 'version' => 'dev-master',
'reference' => '2b61052e8709283d89997e351173bcb43a3c2c61', 'reference' => '2b8e34453234b8b31ebc9e7020f8677bf3889898',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@ -13,7 +13,7 @@
'__root__' => array( '__root__' => array(
'pretty_version' => 'dev-master', 'pretty_version' => 'dev-master',
'version' => 'dev-master', 'version' => 'dev-master',
'reference' => '2b61052e8709283d89997e351173bcb43a3c2c61', 'reference' => '2b8e34453234b8b31ebc9e7020f8677bf3889898',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@ -306,9 +306,9 @@
'dev_requirement' => true, 'dev_requirement' => true,
), ),
'phpunit/php-code-coverage' => array( 'phpunit/php-code-coverage' => array(
'pretty_version' => '9.2.15', 'pretty_version' => '9.2.24',
'version' => '9.2.15.0', 'version' => '9.2.24.0',
'reference' => '2e9da11878c4202f97915c1cb4bb1ca318a63f5f', 'reference' => '2cf940ebc6355a9d430462811b5aaa308b174bed',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../phpunit/php-code-coverage', 'install_path' => __DIR__ . '/../phpunit/php-code-coverage',
'aliases' => array(), 'aliases' => array(),

View File

@ -2,6 +2,72 @@
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## [9.2.24] - 2023-01-26
### Changed
* [#970](https://github.com/sebastianbergmann/php-code-coverage/issues/970): CSS and JavaScript assets are now referenced using `?v=%s` URLs in the HTML report to avoid cache issues
## [9.2.23] - 2022-12-28
### Fixed
* [#971](https://github.com/sebastianbergmann/php-code-coverage/issues/971): PHP report does not handle serialized code coverage data larger than 2 GB
* [#974](https://github.com/sebastianbergmann/php-code-coverage/issues/974): Executable line analysis fails for declarations with enumerations and unions
## [9.2.22] - 2022-12-18
### Fixed
* [#969](https://github.com/sebastianbergmann/php-code-coverage/pull/969): Fixed identifying line with `throw` as executable
## [9.2.21] - 2022-12-14
### Changed
* [#964](https://github.com/sebastianbergmann/php-code-coverage/pull/964): Changed how executable lines are identified
## [9.2.20] - 2022-12-13
### Fixed
* [#960](https://github.com/sebastianbergmann/php-code-coverage/issues/960): New body font-size is way too big
## [9.2.19] - 2022-11-18
### Fixed
* [#949](https://github.com/sebastianbergmann/php-code-coverage/pull/949): Various issues related to identifying executable lines
### Changed
* Tweaked CSS for HTML report
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.6.2 and jQuery 3.6.1
## [9.2.18] - 2022-10-27
### Fixed
* [#935](https://github.com/sebastianbergmann/php-code-coverage/pull/935): Cobertura package name attribute is always empty
* [#946](https://github.com/sebastianbergmann/php-code-coverage/issues/946): `return` with multiline constant expression must only contain the last line
## [9.2.17] - 2022-08-30
### Changed
* [#928](https://github.com/sebastianbergmann/php-code-coverage/pull/928): Avoid unnecessary `is_file()` calls
* [#931](https://github.com/sebastianbergmann/php-code-coverage/pull/931): Use MD5 instead of CRC32 for static analysis cache file identifier
### Fixed
* [#926](https://github.com/sebastianbergmann/php-code-coverage/pull/926): Static Analysis cache does not work with `open_basedir`
## [9.2.16] - 2022-08-20
### Fixed
* [#926](https://github.com/sebastianbergmann/php-code-coverage/issues/926): File view has wrong colouring for the first column
## [9.2.15] - 2022-03-07 ## [9.2.15] - 2022-03-07
### Fixed ### Fixed
@ -398,6 +464,15 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
* This component is no longer supported on PHP 7.1 * This component is no longer supported on PHP 7.1
[9.2.24]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.23...9.2.24
[9.2.23]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.22...9.2.23
[9.2.22]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.21...9.2.22
[9.2.21]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.20...9.2.21
[9.2.20]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.19...9.2.20
[9.2.19]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.18...9.2.19
[9.2.18]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.17...9.2.18
[9.2.17]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.16...9.2.17
[9.2.16]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.15...9.2.16
[9.2.15]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.14...9.2.15 [9.2.15]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.14...9.2.15
[9.2.14]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.13...9.2.14 [9.2.14]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.13...9.2.14
[9.2.13]: https://github.com/sebastianbergmann/php-code-coverage/compare/c011a0b6aaa4acd2f39b7f51fb4ad4442b6ec631...9.2.13 [9.2.13]: https://github.com/sebastianbergmann/php-code-coverage/compare/c011a0b6aaa4acd2f39b7f51fb4ad4442b6ec631...9.2.13

View File

@ -1,33 +1,29 @@
php-code-coverage BSD 3-Clause License
Copyright (c) 2009-2022, Sebastian Bergmann <sebastian@phpunit.de>. Copyright (c) 2009-2023, Sebastian Bergmann
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions modification, are permitted provided that the following conditions are met:
are met:
* Redistributions of source code must retain the above copyright 1. Redistributions of source code must retain the above copyright notice, this
notice, this list of conditions and the following disclaimer. list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright 2. Redistributions in binary form must reproduce the above copyright notice,
notice, this list of conditions and the following disclaimer in this list of conditions and the following disclaimer in the documentation
the documentation and/or other materials provided with the and/or other materials provided with the distribution.
distribution.
* Neither the name of Sebastian Bergmann nor the names of his 3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived contributors may be used to endorse or promote products derived from
from this software without specific prior written permission. this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@ -32,7 +32,7 @@
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"ext-xmlwriter": "*", "ext-xmlwriter": "*",
"nikic/php-parser": "^4.13.0", "nikic/php-parser": "^4.14",
"phpunit/php-file-iterator": "^3.0.3", "phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2", "phpunit/php-text-template": "^2.0.2",
"sebastian/code-unit-reverse-lookup": "^2.0.2", "sebastian/code-unit-reverse-lookup": "^2.0.2",

View File

@ -20,7 +20,6 @@ use function count;
use function explode; use function explode;
use function get_class; use function get_class;
use function is_array; use function is_array;
use function is_file;
use function sort; use function sort;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\PhptTestCase; use PHPUnit\Runner\PhptTestCase;
@ -77,7 +76,7 @@ final class CodeCoverage
private $ignoreDeprecatedCode = false; private $ignoreDeprecatedCode = false;
/** /**
* @var PhptTestCase|string|TestCase * @var null|PhptTestCase|string|TestCase
*/ */
private $currentId; private $currentId;
@ -486,9 +485,16 @@ final class CodeCoverage
continue; continue;
} }
$linesToBranchMap = $this->analyser()->executableLinesIn($filename);
$data->keepLineCoverageDataOnlyForLines( $data->keepLineCoverageDataOnlyForLines(
$filename, $filename,
$this->analyser()->executableLinesIn($filename) array_keys($linesToBranchMap)
);
$data->markExecutableLineByBranch(
$filename,
$linesToBranchMap
); );
} }
} }
@ -518,7 +524,7 @@ final class CodeCoverage
); );
foreach ($uncoveredFiles as $uncoveredFile) { foreach ($uncoveredFiles as $uncoveredFile) {
if (is_file($uncoveredFile)) { if ($this->filter->isFile($uncoveredFile)) {
$this->append( $this->append(
RawCodeCoverageData::fromUncoveredFile( RawCodeCoverageData::fromUncoveredFile(
$uncoveredFile, $uncoveredFile,
@ -543,7 +549,7 @@ final class CodeCoverage
$this->driver->start(); $this->driver->start();
foreach ($uncoveredFiles as $uncoveredFile) { foreach ($uncoveredFiles as $uncoveredFile) {
if (is_file($uncoveredFile)) { if ($this->filter->isFile($uncoveredFile)) {
include_once $uncoveredFile; include_once $uncoveredFile;
} }
} }
@ -644,7 +650,7 @@ final class CodeCoverage
} catch (\ReflectionException $e) { } catch (\ReflectionException $e) {
throw new ReflectionException( throw new ReflectionException(
$e->getMessage(), $e->getMessage(),
(int) $e->getCode(), $e->getCode(),
$e $e
); );
} }

View File

@ -100,11 +100,7 @@ final class Filter
public function isExcluded(string $filename): bool public function isExcluded(string $filename): bool
{ {
if (!$this->isFile($filename)) { return !isset($this->files[$filename]) || !$this->isFile($filename);
return true;
}
return !isset($this->files[$filename]);
} }
/** /**

View File

@ -74,8 +74,6 @@ final class Iterator implements RecursiveIterator
/** /**
* Returns the sub iterator for the current element. * Returns the sub iterator for the current element.
*
* @return Iterator
*/ */
public function getChildren(): self public function getChildren(): self
{ {

View File

@ -15,8 +15,12 @@ use function array_flip;
use function array_intersect; use function array_intersect;
use function array_intersect_key; use function array_intersect_key;
use function count; use function count;
use function explode;
use function file_get_contents;
use function in_array; use function in_array;
use function is_file;
use function range; use function range;
use function trim;
use SebastianBergmann\CodeCoverage\Driver\Driver; use SebastianBergmann\CodeCoverage\Driver\Driver;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser; use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
@ -87,7 +91,7 @@ final class RawCodeCoverageData
{ {
$lineCoverage = []; $lineCoverage = [];
foreach ($analyser->executableLinesIn($filename) as $line) { foreach ($analyser->executableLinesIn($filename) as $line => $branch) {
$lineCoverage[$line] = Driver::LINE_NOT_EXECUTED; $lineCoverage[$line] = Driver::LINE_NOT_EXECUTED;
} }
@ -137,6 +141,42 @@ final class RawCodeCoverageData
); );
} }
/**
* @param int[] $linesToBranchMap
*/
public function markExecutableLineByBranch(string $filename, array $linesToBranchMap): void
{
if (!isset($this->lineCoverage[$filename])) {
return;
}
$linesByBranch = [];
foreach ($linesToBranchMap as $line => $branch) {
$linesByBranch[$branch][] = $line;
}
foreach ($this->lineCoverage[$filename] as $line => $lineStatus) {
if (!isset($linesToBranchMap[$line])) {
continue;
}
$branch = $linesToBranchMap[$line];
if (!isset($linesByBranch[$branch])) {
continue;
}
foreach ($linesByBranch[$branch] as $lineInBranch) {
$this->lineCoverage[$filename][$lineInBranch] = $lineStatus;
}
if (Driver::LINE_EXECUTED === $lineStatus) {
unset($linesByBranch[$branch]);
}
}
}
/** /**
* @param int[] $lines * @param int[] $lines
*/ */

View File

@ -9,10 +9,13 @@
*/ */
namespace SebastianBergmann\CodeCoverage\Report; namespace SebastianBergmann\CodeCoverage\Report;
use function basename;
use function count; use function count;
use function dirname; use function dirname;
use function file_put_contents; use function file_put_contents;
use function preg_match;
use function range; use function range;
use function str_replace;
use function time; use function time;
use DOMImplementation; use DOMImplementation;
use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\CodeCoverage;
@ -25,7 +28,7 @@ final class Cobertura
/** /**
* @throws WriteOperationFailedException * @throws WriteOperationFailedException
*/ */
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string public function process(CodeCoverage $coverage, ?string $target = null): string
{ {
$time = (string) time(); $time = (string) time();
@ -84,9 +87,8 @@ final class Cobertura
$packageElement = $document->createElement('package'); $packageElement = $document->createElement('package');
$packageComplexity = 0; $packageComplexity = 0;
$packageName = $name ?? '';
$packageElement->setAttribute('name', $packageName); $packageElement->setAttribute('name', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString()));
$linesValid = $item->numberOfExecutableLines(); $linesValid = $item->numberOfExecutableLines();
$linesCovered = $item->numberOfExecutedLines(); $linesCovered = $item->numberOfExecutedLines();

View File

@ -117,7 +117,7 @@ final class Dashboard extends Renderer
private function coverageDistribution(array $classes): array private function coverageDistribution(array $classes): array
{ {
$result = [ $result = [
'class' => [ 'class' => [
'0%' => 0, '0%' => 0,
'0-10%' => 0, '0-10%' => 0,
'10-20%' => 0, '10-20%' => 0,

View File

@ -80,6 +80,8 @@ use const T_WHILE;
use const T_YIELD; use const T_YIELD;
use const T_YIELD_FROM; use const T_YIELD_FROM;
use function array_key_exists; use function array_key_exists;
use function array_keys;
use function array_merge;
use function array_pop; use function array_pop;
use function array_unique; use function array_unique;
use function constant; use function constant;
@ -89,6 +91,9 @@ use function explode;
use function file_get_contents; use function file_get_contents;
use function htmlspecialchars; use function htmlspecialchars;
use function is_string; use function is_string;
use function ksort;
use function range;
use function sort;
use function sprintf; use function sprintf;
use function str_replace; use function str_replace;
use function substr; use function substr;
@ -279,19 +284,19 @@ final class File extends Renderer
$buffer .= $this->renderItemTemplate( $buffer .= $this->renderItemTemplate(
$template, $template,
[ [
'name' => $this->abbreviateClassName($name), 'name' => $this->abbreviateClassName($name),
'numClasses' => $numClasses, 'numClasses' => $numClasses,
'numTestedClasses' => $numTestedClasses, 'numTestedClasses' => $numTestedClasses,
'numMethods' => $numMethods, 'numMethods' => $numMethods,
'numTestedMethods' => $numTestedMethods, 'numTestedMethods' => $numTestedMethods,
'linesExecutedPercent' => Percentage::fromFractionAndTotal( 'linesExecutedPercent' => Percentage::fromFractionAndTotal(
$item['executedLines'], $item['executedLines'],
$item['executableLines'], $item['executableLines'],
)->asFloat(), )->asFloat(),
'linesExecutedPercentAsString' => $linesExecutedPercentAsString, 'linesExecutedPercentAsString' => $linesExecutedPercentAsString,
'numExecutedLines' => $item['executedLines'], 'numExecutedLines' => $item['executedLines'],
'numExecutableLines' => $item['executableLines'], 'numExecutableLines' => $item['executableLines'],
'branchesExecutedPercent' => Percentage::fromFractionAndTotal( 'branchesExecutedPercent' => Percentage::fromFractionAndTotal(
$item['executedBranches'], $item['executedBranches'],
$item['executableBranches'], $item['executableBranches'],
)->asFloat(), )->asFloat(),
@ -302,14 +307,14 @@ final class File extends Renderer
$item['executedPaths'], $item['executedPaths'],
$item['executablePaths'] $item['executablePaths']
)->asFloat(), )->asFloat(),
'pathsExecutedPercentAsString' => $pathsExecutedPercentAsString, 'pathsExecutedPercentAsString' => $pathsExecutedPercentAsString,
'numExecutedPaths' => $item['executedPaths'], 'numExecutedPaths' => $item['executedPaths'],
'numExecutablePaths' => $item['executablePaths'], 'numExecutablePaths' => $item['executablePaths'],
'testedMethodsPercent' => $testedMethodsPercentage->asFloat(), 'testedMethodsPercent' => $testedMethodsPercentage->asFloat(),
'testedMethodsPercentAsString' => $testedMethodsPercentage->asString(), 'testedMethodsPercentAsString' => $testedMethodsPercentage->asString(),
'testedClassesPercent' => $testedClassesPercentage->asFloat(), 'testedClassesPercent' => $testedClassesPercentage->asFloat(),
'testedClassesPercentAsString' => $testedClassesPercentage->asString(), 'testedClassesPercentAsString' => $testedClassesPercentage->asString(),
'crap' => $item['crap'], 'crap' => $item['crap'],
] ]
); );
@ -379,7 +384,7 @@ final class File extends Renderer
return $this->renderItemTemplate( return $this->renderItemTemplate(
$template, $template,
[ [
'name' => sprintf( 'name' => sprintf(
'%s<a href="#%d"><abbr title="%s">%s</abbr></a>', '%s<a href="#%d"><abbr title="%s">%s</abbr></a>',
$indent, $indent,
$item['startLine'], $item['startLine'],
@ -797,8 +802,15 @@ final class File extends Renderer
$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}'); $singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');
$lines = ''; $lines = '';
$first = true;
foreach ($path['path'] as $branchId) { foreach ($path['path'] as $branchId) {
if ($first) {
$first = false;
} else {
$lines .= ' <tr><td colspan="2">&nbsp;</td></tr>' . "\n";
}
$branchLines = range($branches[$branchId]['line_start'], $branches[$branchId]['line_end']); $branchLines = range($branches[$branchId]['line_start'], $branches[$branchId]['line_end']);
sort($branchLines); // sometimes end_line < start_line sort($branchLines); // sometimes end_line < start_line
@ -834,6 +846,7 @@ final class File extends Renderer
$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]); $popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);
} }
$trClass = $lineCss . ' popin'; $trClass = $lineCss . ' popin';
} }

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,9 @@
body { body {
font-family: sans-serif;
font-size: 1em;
font-kerning: normal;
font-variant-ligatures: common-ligatures;
text-rendering: optimizeLegibility;
padding-top: 10px; padding-top: 10px;
} }
@ -8,6 +13,8 @@ body {
.octicon { .octicon {
margin-right:.25em; margin-right:.25em;
vertical-align: baseline;
width: 0.75em;
} }
.table-bordered>thead>tr>td { .table-bordered>thead>tr>td {
@ -57,6 +64,7 @@ body {
} }
td.big { td.big {
vertical-align: middle;
width: 117px; width: 117px;
} }

View File

@ -4,9 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Dashboard for {{full_path}}</title> <title>Dashboard for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/nv.d3.min.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/nv.d3.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head> </head>
<body> <body>
@ -137,9 +137,9 @@
</p> </p>
</footer> </footer>
</div> </div>
<script src="{{path_to_root}}_js/jquery.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/jquery.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/d3.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/d3.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/nv.d3.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/nv.d3.min.js?v={{version}}" type="text/javascript"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
nv.addGraph(function() { nv.addGraph(function() {

View File

@ -4,9 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Dashboard for {{full_path}}</title> <title>Dashboard for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/nv.d3.min.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/nv.d3.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head> </head>
<body> <body>
@ -137,9 +137,9 @@
</p> </p>
</footer> </footer>
</div> </div>
<script src="{{path_to_root}}_js/jquery.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/jquery.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/d3.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/d3.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/nv.d3.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/nv.d3.min.js?v={{version}}" type="text/javascript"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
nv.addGraph(function() { nv.addGraph(function() {

View File

@ -4,9 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title> <title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/octicons.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head> </head>
<body> <body>

View File

@ -4,9 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title> <title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/octicons.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head> </head>
<body> <body>

View File

@ -4,9 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title> <title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/octicons.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head> </head>
<body> <body>
@ -57,9 +57,9 @@
</a> </a>
</footer> </footer>
</div> </div>
<script src="{{path_to_root}}_js/jquery.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/jquery.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/popper.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/popper.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/bootstrap.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/bootstrap.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/file.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/file.js?v={{version}}" type="text/javascript"></script>
</body> </body>
</html> </html>

View File

@ -4,9 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title> <title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/octicons.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css"> <link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head> </head>
<body> <body>
@ -59,9 +59,9 @@
</a> </a>
</footer> </footer>
</div> </div>
<script src="{{path_to_root}}_js/jquery.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/jquery.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/popper.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/popper.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/bootstrap.min.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/bootstrap.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/file.js" type="text/javascript"></script> <script src="{{path_to_root}}_js/file.js?v={{version}}" type="text/javascript"></script>
</body> </body>
</html> </html>

View File

@ -1,5 +1,5 @@
<tr> <tr>
<td class="{{classes_level}}">{{name}}</td> <td class="{{lines_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td> <td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td> <td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td> <td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>

View File

@ -1,5 +1,5 @@
<tr> <tr>
<td class="{{classes_level}}">{{name}}</td> <td class="{{lines_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td> <td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td> <td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td> <td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
<tr> <tr>
<td class="{{methods_level}}">{{name}}</td> <td class="{{lines_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td> <td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td> <td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td> <td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>

View File

@ -1,5 +1,5 @@
<tr> <tr>
<td class="{{methods_level}}">{{name}}</td> <td class="{{lines_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td> <td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td> <td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td> <td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>

View File

@ -12,7 +12,6 @@ namespace SebastianBergmann\CodeCoverage\Report;
use function dirname; use function dirname;
use function file_put_contents; use function file_put_contents;
use function serialize; use function serialize;
use function sprintf;
use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException; use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Util\Filesystem; use SebastianBergmann\CodeCoverage\Util\Filesystem;
@ -21,14 +20,8 @@ final class PHP
{ {
public function process(CodeCoverage $coverage, ?string $target = null): string public function process(CodeCoverage $coverage, ?string $target = null): string
{ {
$buffer = sprintf( $buffer = "<?php
"<?php return \unserialize(<<<'END_OF_COVERAGE_SERIALIZATION'" . PHP_EOL . serialize($coverage) . PHP_EOL . 'END_OF_COVERAGE_SERIALIZATION' . PHP_EOL . ');';
return \unserialize(<<<'END_OF_COVERAGE_SERIALIZATION'%s%s%sEND_OF_COVERAGE_SERIALIZATION%s);",
PHP_EOL,
serialize($coverage),
PHP_EOL,
PHP_EOL
);
if ($target !== null) { if ($target !== null) {
Filesystem::createDirectory(dirname($target)); Filesystem::createDirectory(dirname($target));

View File

@ -37,7 +37,7 @@ final class Coverage
{ {
$this->contextNode = $context; $this->contextNode = $context;
$this->writer = new XMLWriter(); $this->writer = new XMLWriter;
$this->writer->openMemory(); $this->writer->openMemory();
$this->writer->startElementNS(null, $context->nodeName, 'https://schema.phpunit.de/coverage/1.0'); $this->writer->startElementNS(null, $context->nodeName, 'https://schema.phpunit.de/coverage/1.0');
$this->writer->writeAttribute('nr', $line); $this->writer->writeAttribute('nr', $line);

View File

@ -20,7 +20,7 @@ final class Report extends File
{ {
public function __construct(string $name) public function __construct(string $name)
{ {
$dom = new DOMDocument(); $dom = new DOMDocument;
$dom->loadXML('<?xml version="1.0" ?><phpunit xmlns="https://schema.phpunit.de/coverage/1.0"><file /></phpunit>'); $dom->loadXML('<?xml version="1.0" ?><phpunit xmlns="https://schema.phpunit.de/coverage/1.0"><file /></phpunit>');
$contextNode = $dom->getElementsByTagNameNS( $contextNode = $dom->getElementsByTagNameNS(

View File

@ -31,7 +31,7 @@ final class Source
{ {
$context = $this->context; $context = $this->context;
$tokens = (new Tokenizer())->parse($source); $tokens = (new Tokenizer)->parse($source);
$srcDom = (new XMLSerializer(new NamespaceUri($context->namespaceURI)))->toDom($tokens); $srcDom = (new XMLSerializer(new NamespaceUri($context->namespaceURI)))->toDom($tokens);
$context->parentNode->replaceChild( $context->parentNode->replaceChild(

View File

@ -9,15 +9,15 @@
*/ */
namespace SebastianBergmann\CodeCoverage\StaticAnalysis; namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
use function assert;
use function crc32;
use function file_get_contents; use function file_get_contents;
use function file_put_contents; use function file_put_contents;
use function implode;
use function is_file; use function is_file;
use function md5;
use function serialize; use function serialize;
use GlobIterator; use function unserialize;
use SebastianBergmann\CodeCoverage\Util\Filesystem; use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SplFileInfo; use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
/** /**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
@ -50,10 +50,6 @@ final class CachingFileAnalyser implements FileAnalyser
$this->analyser = $analyser; $this->analyser = $analyser;
$this->directory = $directory; $this->directory = $directory;
if (self::$cacheVersion === null) {
$this->calculateCacheVersion();
}
} }
public function classesIn(string $filename): array public function classesIn(string $filename): array
@ -165,19 +161,24 @@ final class CachingFileAnalyser implements FileAnalyser
private function cacheFile(string $filename): string private function cacheFile(string $filename): string
{ {
return $this->directory . DIRECTORY_SEPARATOR . hash('sha256', $filename . crc32(file_get_contents($filename)) . self::$cacheVersion); return $this->directory . DIRECTORY_SEPARATOR . md5($filename . "\0" . file_get_contents($filename) . "\0" . self::cacheVersion());
} }
private function calculateCacheVersion(): void private static function cacheVersion(): string
{ {
$buffer = ''; if (self::$cacheVersion !== null) {
return self::$cacheVersion;
foreach (new GlobIterator(__DIR__ . '/*.php') as $file) {
assert($file instanceof SplFileInfo);
$buffer .= file_get_contents($file->getPathname());
} }
self::$cacheVersion = (string) crc32($buffer); $buffer = [];
foreach ((new FileIteratorFacade)->getFilesAsArray(__DIR__, '.php') as $file) {
$buffer[] = $file;
$buffer[] = file_get_contents($file);
}
self::$cacheVersion = md5(implode("\0", $buffer));
return self::$cacheVersion;
} }
} }

View File

@ -9,6 +9,7 @@
*/ */
namespace SebastianBergmann\CodeCoverage\StaticAnalysis; namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
use function assert;
use function implode; use function implode;
use function rtrim; use function rtrim;
use function trim; use function trim;
@ -314,6 +315,8 @@ final class CodeUnitFindingVisitor extends NodeVisitorAbstract
if ($_type instanceof Name) { if ($_type instanceof Name) {
$types[] = $_type->toCodeString(); $types[] = $_type->toCodeString();
} else { } else {
assert($_type instanceof Identifier);
$types[] = $_type->toString(); $types[] = $_type->toString();
} }
} }

View File

@ -9,46 +9,19 @@
*/ */
namespace SebastianBergmann\CodeCoverage\StaticAnalysis; namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
use function array_diff_key;
use function assert;
use function count;
use function current;
use function end;
use function explode;
use function max;
use function preg_match;
use function preg_quote;
use function range;
use function reset;
use function sprintf;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\CallLike;
use PhpParser\Node\Expr\Cast;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\Match_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\NullsafePropertyFetch;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Expr\Ternary;
use PhpParser\Node\MatchArm;
use PhpParser\Node\Scalar\Encapsed;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Case_;
use PhpParser\Node\Stmt\Catch_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Continue_;
use PhpParser\Node\Stmt\Do_;
use PhpParser\Node\Stmt\Echo_;
use PhpParser\Node\Stmt\Else_;
use PhpParser\Node\Stmt\ElseIf_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Finally_;
use PhpParser\Node\Stmt\For_;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\Node\Stmt\Goto_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Property;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Stmt\Switch_;
use PhpParser\Node\Stmt\Throw_;
use PhpParser\Node\Stmt\TryCatch;
use PhpParser\Node\Stmt\Unset_;
use PhpParser\Node\Stmt\While_;
use PhpParser\NodeVisitorAbstract; use PhpParser\NodeVisitorAbstract;
/** /**
@ -57,217 +30,337 @@ use PhpParser\NodeVisitorAbstract;
final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
{ {
/** /**
* @psalm-var array<int, int> * @var int
*/ */
private $executableLines = []; private $nextBranch = 0;
/** /**
* @psalm-var array<int, int> * @var string
*/ */
private $propertyLines = []; private $source;
/** /**
* @psalm-var array<int, Return_> * @var array<int, int>
*/ */
private $returns = []; private $executableLinesGroupedByBranch = [];
/**
* @var array<int, bool>
*/
private $unsets = [];
/**
* @var array<int, string>
*/
private $commentsToCheckForUnset = [];
public function __construct(string $source)
{
$this->source = $source;
}
public function enterNode(Node $node): void public function enterNode(Node $node): void
{ {
$this->savePropertyLines($node); foreach ($node->getComments() as $comment) {
$commentLine = $comment->getStartLine();
if (!isset($this->executableLinesGroupedByBranch[$commentLine])) {
continue;
}
foreach (explode("\n", $comment->getText()) as $text) {
$this->commentsToCheckForUnset[$commentLine] = $text;
$commentLine++;
}
}
if ($node instanceof Node\Scalar\String_ ||
$node instanceof Node\Scalar\EncapsedStringPart) {
$startLine = $node->getStartLine() + 1;
$endLine = $node->getEndLine() - 1;
if ($startLine <= $endLine) {
foreach (range($startLine, $endLine) as $line) {
unset($this->executableLinesGroupedByBranch[$line]);
}
}
if (!$this->isExecutable($node)) {
return; return;
} }
foreach ($this->getLines($node) as $line) { if ($node instanceof Node\Stmt\Declare_ ||
if (isset($this->propertyLines[$line])) { $node instanceof Node\Stmt\DeclareDeclare ||
$node instanceof Node\Stmt\Else_ ||
$node instanceof Node\Stmt\EnumCase ||
$node instanceof Node\Stmt\Finally_ ||
$node instanceof Node\Stmt\Interface_ ||
$node instanceof Node\Stmt\Label ||
$node instanceof Node\Stmt\Namespace_ ||
$node instanceof Node\Stmt\Nop ||
$node instanceof Node\Stmt\Switch_ ||
$node instanceof Node\Stmt\TryCatch ||
$node instanceof Node\Stmt\Use_ ||
$node instanceof Node\Stmt\UseUse ||
$node instanceof Node\Expr\ConstFetch ||
$node instanceof Node\Expr\Match_ ||
$node instanceof Node\Expr\Variable ||
$node instanceof Node\ComplexType ||
$node instanceof Node\Const_ ||
$node instanceof Node\Identifier ||
$node instanceof Node\Name ||
$node instanceof Node\Param ||
$node instanceof Node\Scalar) {
return;
}
if ($node instanceof Node\Stmt\Throw_) {
$this->setLineBranch($node->expr->getEndLine(), $node->expr->getEndLine(), ++$this->nextBranch);
return;
}
if ($node instanceof Node\Stmt\Enum_ ||
$node instanceof Node\Stmt\Function_ ||
$node instanceof Node\Stmt\Class_ ||
$node instanceof Node\Stmt\ClassMethod ||
$node instanceof Node\Expr\Closure ||
$node instanceof Node\Stmt\Trait_) {
$isConcreteClassLike = $node instanceof Node\Stmt\Enum_ || $node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Trait_;
if (null !== $node->stmts) {
foreach ($node->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Nop) {
continue;
}
foreach (range($stmt->getStartLine(), $stmt->getEndLine()) as $line) {
unset($this->executableLinesGroupedByBranch[$line]);
if (
$isConcreteClassLike &&
!$stmt instanceof Node\Stmt\ClassMethod
) {
$this->unsets[$line] = true;
}
}
}
}
if ($isConcreteClassLike) {
return; return;
} }
$this->executableLines[$line] = $line; $hasEmptyBody = [] === $node->stmts ||
} null === $node->stmts ||
} (
1 === count($node->stmts) &&
$node->stmts[0] instanceof Node\Stmt\Nop
);
/** if ($hasEmptyBody) {
* @psalm-return array<int, int> if ($node->getEndLine() === $node->getStartLine()) {
*/ return;
public function executableLines(): array }
{
$this->computeReturns();
sort($this->executableLines); $this->setLineBranch($node->getEndLine(), $node->getEndLine(), ++$this->nextBranch);
return $this->executableLines; return;
} }
private function savePropertyLines(Node $node): void
{
if (!$node instanceof Property && !$node instanceof Node\Stmt\ClassConst) {
return; return;
} }
foreach (range($node->getStartLine(), $node->getEndLine()) as $index) { if ($node instanceof Node\Expr\ArrowFunction) {
$this->propertyLines[$index] = $index; $startLine = max(
} $node->getStartLine() + 1,
} $node->expr->getStartLine()
);
private function computeReturns(): void $endLine = $node->expr->getEndLine();
{
foreach ($this->returns as $return) { if ($endLine < $startLine) {
foreach (range($return->getStartLine(), $return->getEndLine()) as $loc) { return;
if (isset($this->executableLines[$loc])) { }
continue 2;
$this->setLineBranch($startLine, $endLine, ++$this->nextBranch);
return;
}
if ($node instanceof Node\Expr\Ternary) {
if (null !== $node->if &&
$node->getStartLine() !== $node->if->getEndLine()) {
$this->setLineBranch($node->if->getStartLine(), $node->if->getEndLine(), ++$this->nextBranch);
}
if ($node->getStartLine() !== $node->else->getEndLine()) {
$this->setLineBranch($node->else->getStartLine(), $node->else->getEndLine(), ++$this->nextBranch);
}
return;
}
if ($node instanceof Node\Expr\BinaryOp\Coalesce) {
if ($node->getStartLine() !== $node->getEndLine()) {
$this->setLineBranch($node->getEndLine(), $node->getEndLine(), ++$this->nextBranch);
}
return;
}
if ($node instanceof Node\Stmt\If_ ||
$node instanceof Node\Stmt\ElseIf_ ||
$node instanceof Node\Stmt\Case_) {
if (null === $node->cond) {
return;
}
$this->setLineBranch(
$node->cond->getStartLine(),
$node->cond->getStartLine(),
++$this->nextBranch
);
return;
}
if ($node instanceof Node\Stmt\For_) {
$startLine = null;
$endLine = null;
if ([] !== $node->init) {
$startLine = $node->init[0]->getStartLine();
end($node->init);
$endLine = current($node->init)->getEndLine();
reset($node->init);
}
if ([] !== $node->cond) {
if (null === $startLine) {
$startLine = $node->cond[0]->getStartLine();
} }
end($node->cond);
$endLine = current($node->cond)->getEndLine();
reset($node->cond);
} }
$line = $return->getEndLine(); if ([] !== $node->loop) {
if (null === $startLine) {
if ($return->expr !== null) { $startLine = $node->loop[0]->getStartLine();
$line = $return->expr->getStartLine();
}
$this->executableLines[$line] = $line;
}
}
/**
* @return int[]
*/
private function getLines(Node $node): array
{
if ($node instanceof Cast ||
$node instanceof PropertyFetch ||
$node instanceof NullsafePropertyFetch ||
$node instanceof StaticPropertyFetch) {
return [$node->getEndLine()];
}
if ($node instanceof ArrayDimFetch) {
if (null === $node->dim) {
return [];
}
return [$node->dim->getStartLine()];
}
if ($node instanceof Array_) {
$startLine = $node->getStartLine();
if (isset($this->executableLines[$startLine])) {
return [];
}
if ([] === $node->items) {
return [$node->getEndLine()];
}
if ($node->items[0] instanceof ArrayItem) {
return [$node->items[0]->getStartLine()];
}
}
if ($node instanceof ClassMethod) {
if ($node->name->name !== '__construct') {
return [];
}
$existsAPromotedProperty = false;
foreach ($node->getParams() as $param) {
if (0 !== ($param->flags & Class_::VISIBILITY_MODIFIER_MASK)) {
$existsAPromotedProperty = true;
break;
} }
end($node->loop);
$endLine = current($node->loop)->getEndLine();
reset($node->loop);
} }
if ($existsAPromotedProperty) { if (null === $startLine || null === $endLine) {
// Only the line with `function` keyword should be listed here return;
// but `nikic/php-parser` doesn't provide a way to fetch it
return range($node->getStartLine(), $node->name->getEndLine());
} }
return []; $this->setLineBranch(
$startLine,
$endLine,
++$this->nextBranch
);
return;
} }
if ($node instanceof MethodCall) { if ($node instanceof Node\Stmt\Foreach_) {
return [$node->name->getStartLine()]; $this->setLineBranch(
$node->expr->getStartLine(),
$node->valueVar->getEndLine(),
++$this->nextBranch
);
return;
} }
if ($node instanceof Ternary) { if ($node instanceof Node\Stmt\While_ ||
$lines = [$node->cond->getStartLine()]; $node instanceof Node\Stmt\Do_) {
$this->setLineBranch(
$node->cond->getStartLine(),
$node->cond->getEndLine(),
++$this->nextBranch
);
if (null !== $node->if) { return;
$lines[] = $node->if->getStartLine(); }
if ($node instanceof Node\Stmt\Catch_) {
assert([] !== $node->types);
$startLine = $node->types[0]->getStartLine();
end($node->types);
$endLine = current($node->types)->getEndLine();
$this->setLineBranch(
$startLine,
$endLine,
++$this->nextBranch
);
return;
}
if ($node instanceof Node\Expr\CallLike) {
if (isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) {
$branch = $this->executableLinesGroupedByBranch[$node->getStartLine()];
} else {
$branch = ++$this->nextBranch;
} }
$lines[] = $node->else->getStartLine(); $this->setLineBranch($node->getStartLine(), $node->getEndLine(), $branch);
return $lines; return;
} }
if ($node instanceof Match_) { if (isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) {
return [$node->cond->getStartLine()]; return;
} }
if ($node instanceof MatchArm) { $this->setLineBranch($node->getStartLine(), $node->getEndLine(), ++$this->nextBranch);
return [$node->body->getStartLine()];
}
if ($node instanceof Expression && (
$node->expr instanceof Cast ||
$node->expr instanceof Match_ ||
$node->expr instanceof MethodCall
)) {
return [];
}
if ($node instanceof Return_) {
$this->returns[] = $node;
return [];
}
return [$node->getStartLine()];
} }
private function isExecutable(Node $node): bool public function afterTraverse(array $nodes): void
{ {
return $node instanceof Assign || $lines = explode("\n", $this->source);
$node instanceof ArrayDimFetch ||
$node instanceof Array_ || foreach ($lines as $lineNumber => $line) {
$node instanceof BinaryOp || $lineNumber++;
$node instanceof Break_ ||
$node instanceof CallLike || if (1 === preg_match('/^\s*$/', $line) ||
$node instanceof Case_ || (
$node instanceof Cast || isset($this->commentsToCheckForUnset[$lineNumber]) &&
$node instanceof Catch_ || 1 === preg_match(sprintf('/^\s*%s\s*$/', preg_quote($this->commentsToCheckForUnset[$lineNumber], '/')), $line)
$node instanceof ClassMethod || )) {
$node instanceof Closure || unset($this->executableLinesGroupedByBranch[$lineNumber]);
$node instanceof Continue_ || }
$node instanceof Do_ || }
$node instanceof Echo_ ||
$node instanceof ElseIf_ || $this->executableLinesGroupedByBranch = array_diff_key(
$node instanceof Else_ || $this->executableLinesGroupedByBranch,
$node instanceof Encapsed || $this->unsets
$node instanceof Expression || );
$node instanceof Finally_ || }
$node instanceof For_ ||
$node instanceof Foreach_ || public function executableLinesGroupedByBranch(): array
$node instanceof Goto_ || {
$node instanceof If_ || return $this->executableLinesGroupedByBranch;
$node instanceof Match_ || }
$node instanceof MatchArm ||
$node instanceof MethodCall || private function setLineBranch(int $start, int $end, int $branch): void
$node instanceof NullsafePropertyFetch || {
$node instanceof PropertyFetch || foreach (range($start, $end) as $line) {
$node instanceof Return_ || $this->executableLinesGroupedByBranch[$line] = $branch;
$node instanceof StaticPropertyFetch || }
$node instanceof Switch_ ||
$node instanceof Ternary ||
$node instanceof Throw_ ||
$node instanceof TryCatch ||
$node instanceof Unset_ ||
$node instanceof While_;
} }
} }

View File

@ -10,9 +10,11 @@
namespace SebastianBergmann\CodeCoverage\StaticAnalysis; namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
use function array_merge; use function array_merge;
use function assert;
use function range; use function range;
use function strpos; use function strpos;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\Attribute;
use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\Function_;
@ -52,7 +54,8 @@ final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract
!$node instanceof Trait_ && !$node instanceof Trait_ &&
!$node instanceof Interface_ && !$node instanceof Interface_ &&
!$node instanceof ClassMethod && !$node instanceof ClassMethod &&
!$node instanceof Function_) { !$node instanceof Function_ &&
!$node instanceof Attribute) {
return; return;
} }
@ -60,11 +63,16 @@ final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract
return; return;
} }
// Workaround for https://bugs.xdebug.org/view.php?id=1798
if ($node instanceof Class_ || if ($node instanceof Class_ ||
$node instanceof Trait_ || $node instanceof Trait_ ||
$node instanceof Interface_) { $node instanceof Interface_ ||
$node instanceof Attribute) {
$this->ignoredLines[] = $node->getStartLine(); $this->ignoredLines[] = $node->getStartLine();
assert($node->name !== null);
// Workaround for https://github.com/nikic/PHP-Parser/issues/886
$this->ignoredLines[] = $node->name->getStartLine();
} }
if (!$this->useAnnotationsForIgnoringCode) { if (!$this->useAnnotationsForIgnoringCode) {
@ -75,6 +83,19 @@ final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract
return; return;
} }
$this->processDocComment($node);
}
/**
* @psalm-return list<int>
*/
public function ignoredLines(): array
{
return $this->ignoredLines;
}
private function processDocComment(Node $node): void
{
$docComment = $node->getDocComment(); $docComment = $node->getDocComment();
if ($docComment === null) { if ($docComment === null) {
@ -95,12 +116,4 @@ final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract
); );
} }
} }
/**
* @psalm-return list<int>
*/
public function ignoredLines(): array
{
return $this->ignoredLines;
}
} }

View File

@ -9,11 +9,14 @@
*/ */
namespace SebastianBergmann\CodeCoverage\StaticAnalysis; namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
use function array_merge;
use function array_unique; use function array_unique;
use function assert; use function assert;
use function file_get_contents; use function file_get_contents;
use function is_array; use function is_array;
use function max; use function max;
use function range;
use function sort;
use function sprintf; use function sprintf;
use function substr_count; use function substr_count;
use function token_get_all; use function token_get_all;
@ -153,7 +156,7 @@ final class ParsingFileAnalyser implements FileAnalyser
$codeUnitFindingVisitor = new CodeUnitFindingVisitor; $codeUnitFindingVisitor = new CodeUnitFindingVisitor;
$lineCountingVisitor = new LineCountingVisitor($linesOfCode); $lineCountingVisitor = new LineCountingVisitor($linesOfCode);
$ignoredLinesFindingVisitor = new IgnoredLinesFindingVisitor($this->useAnnotationsForIgnoringCode, $this->ignoreDeprecatedCode); $ignoredLinesFindingVisitor = new IgnoredLinesFindingVisitor($this->useAnnotationsForIgnoringCode, $this->ignoreDeprecatedCode);
$executableLinesFindingVisitor = new ExecutableLinesFindingVisitor; $executableLinesFindingVisitor = new ExecutableLinesFindingVisitor($source);
$traverser->addVisitor(new NameResolver); $traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ParentConnectingVisitor); $traverser->addVisitor(new ParentConnectingVisitor);
@ -172,7 +175,7 @@ final class ParsingFileAnalyser implements FileAnalyser
$filename, $filename,
$error->getMessage() $error->getMessage()
), ),
(int) $error->getCode(), $error->getCode(),
$error $error
); );
} }
@ -181,7 +184,7 @@ final class ParsingFileAnalyser implements FileAnalyser
$this->classes[$filename] = $codeUnitFindingVisitor->classes(); $this->classes[$filename] = $codeUnitFindingVisitor->classes();
$this->traits[$filename] = $codeUnitFindingVisitor->traits(); $this->traits[$filename] = $codeUnitFindingVisitor->traits();
$this->functions[$filename] = $codeUnitFindingVisitor->functions(); $this->functions[$filename] = $codeUnitFindingVisitor->functions();
$this->executableLines[$filename] = $executableLinesFindingVisitor->executableLines(); $this->executableLines[$filename] = $executableLinesFindingVisitor->executableLinesGroupedByBranch();
$this->ignoredLines[$filename] = []; $this->ignoredLines[$filename] = [];
$this->findLinesIgnoredByLineBasedAnnotations($filename, $source, $this->useAnnotationsForIgnoringCode); $this->findLinesIgnoredByLineBasedAnnotations($filename, $source, $this->useAnnotationsForIgnoringCode);
@ -206,45 +209,44 @@ final class ParsingFileAnalyser implements FileAnalyser
private function findLinesIgnoredByLineBasedAnnotations(string $filename, string $source, bool $useAnnotationsForIgnoringCode): void private function findLinesIgnoredByLineBasedAnnotations(string $filename, string $source, bool $useAnnotationsForIgnoringCode): void
{ {
$ignore = false; if (!$useAnnotationsForIgnoringCode) {
$stop = false; return;
}
$start = false;
foreach (token_get_all($source) as $token) { foreach (token_get_all($source) as $token) {
if (!is_array($token)) { if (!is_array($token) ||
!(T_COMMENT === $token[0] || T_DOC_COMMENT === $token[0])) {
continue; continue;
} }
switch ($token[0]) { $comment = trim($token[1]);
case T_COMMENT:
case T_DOC_COMMENT:
if (!$useAnnotationsForIgnoringCode) {
break;
}
$comment = trim($token[1]); if ($comment === '// @codeCoverageIgnore' ||
$comment === '//@codeCoverageIgnore') {
if ($comment === '// @codeCoverageIgnore' ||
$comment === '//@codeCoverageIgnore') {
$ignore = true;
$stop = true;
} elseif ($comment === '// @codeCoverageIgnoreStart' ||
$comment === '//@codeCoverageIgnoreStart') {
$ignore = true;
} elseif ($comment === '// @codeCoverageIgnoreEnd' ||
$comment === '//@codeCoverageIgnoreEnd') {
$stop = true;
}
break;
}
if ($ignore) {
$this->ignoredLines[$filename][] = $token[2]; $this->ignoredLines[$filename][] = $token[2];
if ($stop) { continue;
$ignore = false; }
$stop = false;
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])
);
} }
} }
} }

View File

@ -22,7 +22,7 @@ final class Version
public static function id(): string public static function id(): string
{ {
if (self::$version === null) { if (self::$version === null) {
self::$version = (new VersionId('9.2.15', dirname(__DIR__)))->getVersion(); self::$version = (new VersionId('9.2.24', dirname(__DIR__)))->getVersion();
} }
return self::$version; return self::$version;