├── ChangeLog-12.3.md ├── LICENSE ├── README.md ├── SECURITY.md ├── composer.json └── src ├── CodeCoverage.php ├── Data ├── ProcessedCodeCoverageData.php └── RawCodeCoverageData.php ├── Driver ├── Driver.php ├── PcovDriver.php ├── Selector.php └── XdebugDriver.php ├── Exception ├── BranchAndPathCoverageNotSupportedException.php ├── DirectoryCouldNotBeCreatedException.php ├── Exception.php ├── FileCouldNotBeWrittenException.php ├── InvalidArgumentException.php ├── InvalidCodeCoverageTargetException.php ├── NoCodeCoverageDriverAvailableException.php ├── NoCodeCoverageDriverWithPathCoverageSupportAvailableException.php ├── ParserException.php ├── PathExistsButIsNotDirectoryException.php ├── PcovNotAvailableException.php ├── ReflectionException.php ├── ReportAlreadyFinalizedException.php ├── StaticAnalysisCacheNotConfiguredException.php ├── TestIdMissingException.php ├── UnintentionallyCoveredCodeException.php ├── WriteOperationFailedException.php ├── XdebugNotAvailableException.php ├── XdebugNotEnabledException.php ├── XdebugVersionNotSupportedException.php └── XmlException.php ├── Filter.php ├── Node ├── AbstractNode.php ├── Builder.php ├── CrapIndex.php ├── Directory.php ├── File.php └── Iterator.php ├── Report ├── Clover.php ├── Cobertura.php ├── Crap4j.php ├── Html │ ├── Colors.php │ ├── CustomCssFile.php │ ├── Facade.php │ ├── Renderer.php │ └── Renderer │ │ ├── Dashboard.php │ │ ├── Directory.php │ │ ├── File.php │ │ └── Template │ │ ├── branches.html.dist │ │ ├── coverage_bar.html.dist │ │ ├── coverage_bar_branch.html.dist │ │ ├── css │ │ ├── billboard.min.css │ │ ├── bootstrap.min.css │ │ ├── custom.css │ │ ├── octicons.css │ │ └── style.css │ │ ├── dashboard.html.dist │ │ ├── dashboard_branch.html.dist │ │ ├── directory.html.dist │ │ ├── directory_branch.html.dist │ │ ├── directory_item.html.dist │ │ ├── directory_item_branch.html.dist │ │ ├── file.html.dist │ │ ├── file_branch.html.dist │ │ ├── file_item.html.dist │ │ ├── file_item_branch.html.dist │ │ ├── icons │ │ ├── file-code.svg │ │ └── file-directory.svg │ │ ├── js │ │ ├── billboard.pkgd.min.js │ │ ├── bootstrap.bundle.min.js │ │ ├── file.js │ │ └── jquery.min.js │ │ ├── line.html.dist │ │ ├── lines.html.dist │ │ ├── method_item.html.dist │ │ ├── method_item_branch.html.dist │ │ └── paths.html.dist ├── OpenClover.php ├── PHP.php ├── Text.php ├── Thresholds.php └── Xml │ ├── BuildInformation.php │ ├── Coverage.php │ ├── Directory.php │ ├── Facade.php │ ├── File.php │ ├── Method.php │ ├── Node.php │ ├── Project.php │ ├── Report.php │ ├── Source.php │ ├── Tests.php │ ├── Totals.php │ └── Unit.php ├── StaticAnalysis ├── CacheWarmer.php ├── CachingSourceAnalyser.php ├── FileAnalyser.php ├── ParsingSourceAnalyser.php ├── SourceAnalyser.php ├── Value │ ├── AnalysisResult.php │ ├── Class_.php │ ├── Function_.php │ ├── Interface_.php │ ├── LinesOfCode.php │ ├── Method.php │ ├── Trait_.php │ └── Visibility.php └── Visitor │ ├── AttributeParentConnectingVisitor.php │ ├── CodeUnitFindingVisitor.php │ ├── ExecutableLinesFindingVisitor.php │ └── IgnoredLinesFindingVisitor.php ├── Target ├── Class_.php ├── ClassesThatExtendClass.php ├── ClassesThatImplementInterface.php ├── Function_.php ├── MapBuilder.php ├── Mapper.php ├── Method.php ├── Namespace_.php ├── Target.php ├── TargetCollection.php ├── TargetCollectionIterator.php ├── TargetCollectionValidator.php ├── Trait_.php ├── ValidationFailure.php ├── ValidationResult.php └── ValidationSuccess.php ├── TestSize ├── Known.php ├── Large.php ├── Medium.php ├── Small.php ├── TestSize.php └── Unknown.php ├── TestStatus ├── Failure.php ├── Known.php ├── Success.php ├── TestStatus.php └── Unknown.php ├── Util ├── Filesystem.php └── Percentage.php └── Version.php /ChangeLog-12.3.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 4 | 5 | ## [12.3.0] - 2025-05-23 6 | 7 | ### Changed 8 | 9 | * [#1080](https://github.com/sebastianbergmann/php-code-coverage/pull/1080): Support for reporting code coverage information in OpenClover XML format; unlike the existing Clover XML reporter, which remains unchanged, the XML documents generated by this new reporter validate against the OpenClover project's XML schema definition, with one exception: we do not generate the `` element. This feature is experimental and the generated XML might change in order to improve compliance with the OpenClover project's XML schema definition further. Such changes will be made in bugfix and/or minor releases even if they break backward compatibility. 10 | 11 | [12.3.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/12.2.1...12.3.0 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2009-2025, 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 are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phpunit/php-code-coverage 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/phpunit/php-code-coverage/v)](https://packagist.org/packages/phpunit/php-code-coverage) 4 | [![CI Status](https://github.com/sebastianbergmann/php-code-coverage/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/php-code-coverage/actions) 5 | [![codecov](https://codecov.io/gh/sebastianbergmann/php-code-coverage/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/php-code-coverage) 6 | 7 | Provides collection, processing, and rendering functionality for PHP code coverage information. 8 | 9 | ## Installation 10 | 11 | You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): 12 | 13 | ``` 14 | composer require phpunit/php-code-coverage 15 | ``` 16 | 17 | If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: 18 | 19 | ``` 20 | composer require --dev phpunit/php-code-coverage 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```php 26 | includeFiles( 35 | [ 36 | '/path/to/file.php', 37 | '/path/to/another_file.php', 38 | ] 39 | ); 40 | 41 | $coverage = new CodeCoverage( 42 | (new Selector)->forLineCoverage($filter), 43 | $filter 44 | ); 45 | 46 | $coverage->start(''); 47 | 48 | // ... 49 | 50 | $coverage->stop(); 51 | 52 | 53 | (new HtmlReport)->process($coverage, '/tmp/code-coverage-report'); 54 | ``` 55 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you believe you have found a security vulnerability in the library that is developed in this repository, please report it to us through coordinated disclosure. 4 | 5 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 6 | 7 | Instead, please email `sebastian@phpunit.de`. 8 | 9 | Please include as much of the information listed below as you can to help us better understand and resolve the issue: 10 | 11 | * The type of issue 12 | * Full paths of source file(s) related to the manifestation of the issue 13 | * The location of the affected source code (tag/branch/commit or direct URL) 14 | * Any special configuration required to reproduce the issue 15 | * Step-by-step instructions to reproduce the issue 16 | * Proof-of-concept or exploit code (if possible) 17 | * Impact of the issue, including how an attacker might exploit the issue 18 | 19 | This information will help us triage your report more quickly. 20 | 21 | ## Web Context 22 | 23 | The library that is developed in this repository was either extracted from [PHPUnit](https://github.com/sebastianbergmann/phpunit) or developed specifically as a dependency for PHPUnit. 24 | 25 | The library is developed with a focus on development environments and the command-line. No specific testing or hardening with regard to using the library in an HTTP or web context or with untrusted input data is performed. The library might also contain functionality that intentionally exposes internal application data for debugging purposes. 26 | 27 | If the library is used in a web application, the application developer is responsible for filtering inputs or escaping outputs as necessary and for verifying that the used functionality is safe for use within the intended context. 28 | 29 | Vulnerabilities specific to the use outside a development context will be fixed as applicable, provided that the fix does not have an averse effect on the primary use case for development purposes. 30 | 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpunit/php-code-coverage", 3 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 4 | "type": "library", 5 | "keywords": [ 6 | "coverage", 7 | "testing", 8 | "xunit" 9 | ], 10 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 11 | "license": "BSD-3-Clause", 12 | "authors": [ 13 | { 14 | "name": "Sebastian Bergmann", 15 | "email": "sebastian@phpunit.de", 16 | "role": "lead" 17 | } 18 | ], 19 | "support": { 20 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 21 | "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy" 22 | }, 23 | "config": { 24 | "platform": { 25 | "php": "8.3.0" 26 | }, 27 | "optimize-autoloader": true, 28 | "sort-packages": true 29 | }, 30 | "prefer-stable": true, 31 | "require": { 32 | "php": ">=8.3", 33 | "ext-dom": "*", 34 | "ext-libxml": "*", 35 | "ext-xmlwriter": "*", 36 | "nikic/php-parser": "^5.4.0", 37 | "phpunit/php-file-iterator": "^6.0", 38 | "phpunit/php-text-template": "^5.0", 39 | "sebastian/complexity": "^5.0", 40 | "sebastian/environment": "^8.0", 41 | "sebastian/lines-of-code": "^4.0", 42 | "sebastian/version": "^6.0", 43 | "theseer/tokenizer": "^1.2.3" 44 | }, 45 | "require-dev": { 46 | "phpunit/phpunit": "^12.1" 47 | }, 48 | "suggest": { 49 | "ext-pcov": "PHP extension that provides line coverage", 50 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" 51 | }, 52 | "autoload": { 53 | "classmap": [ 54 | "src/" 55 | ] 56 | }, 57 | "autoload-dev": { 58 | "classmap": [ 59 | "tests/" 60 | ] 61 | }, 62 | "extra": { 63 | "branch-alias": { 64 | "dev-main": "12.3.x-dev" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Driver/Driver.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 | namespace SebastianBergmann\CodeCoverage\Driver; 11 | 12 | use function sprintf; 13 | use SebastianBergmann\CodeCoverage\BranchAndPathCoverageNotSupportedException; 14 | use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData; 15 | 16 | /** 17 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 18 | */ 19 | abstract class Driver 20 | { 21 | /** 22 | * @see http://xdebug.org/docs/code_coverage 23 | */ 24 | public const int LINE_NOT_EXECUTABLE = -2; 25 | 26 | /** 27 | * @see http://xdebug.org/docs/code_coverage 28 | */ 29 | public const int LINE_NOT_EXECUTED = -1; 30 | 31 | /** 32 | * @see http://xdebug.org/docs/code_coverage 33 | */ 34 | public const int LINE_EXECUTED = 1; 35 | 36 | /** 37 | * @see http://xdebug.org/docs/code_coverage 38 | */ 39 | public const int BRANCH_NOT_HIT = 0; 40 | 41 | /** 42 | * @see http://xdebug.org/docs/code_coverage 43 | */ 44 | public const int BRANCH_HIT = 1; 45 | private bool $collectBranchAndPathCoverage = false; 46 | 47 | public function canCollectBranchAndPathCoverage(): bool 48 | { 49 | return false; 50 | } 51 | 52 | public function collectsBranchAndPathCoverage(): bool 53 | { 54 | return $this->collectBranchAndPathCoverage; 55 | } 56 | 57 | /** 58 | * @throws BranchAndPathCoverageNotSupportedException 59 | */ 60 | public function enableBranchAndPathCoverage(): void 61 | { 62 | if (!$this->canCollectBranchAndPathCoverage()) { 63 | throw new BranchAndPathCoverageNotSupportedException( 64 | sprintf( 65 | '%s does not support branch and path coverage', 66 | $this->nameAndVersion(), 67 | ), 68 | ); 69 | } 70 | 71 | $this->collectBranchAndPathCoverage = true; 72 | } 73 | 74 | public function disableBranchAndPathCoverage(): void 75 | { 76 | $this->collectBranchAndPathCoverage = false; 77 | } 78 | 79 | abstract public function nameAndVersion(): string; 80 | 81 | abstract public function start(): void; 82 | 83 | abstract public function stop(): RawCodeCoverageData; 84 | } 85 | -------------------------------------------------------------------------------- /src/Driver/PcovDriver.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 | namespace SebastianBergmann\CodeCoverage\Driver; 11 | 12 | use const pcov\inclusive; 13 | use function array_intersect; 14 | use function extension_loaded; 15 | use function pcov\clear; 16 | use function pcov\collect; 17 | use function pcov\start; 18 | use function pcov\stop; 19 | use function pcov\waiting; 20 | use function phpversion; 21 | use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData; 22 | use SebastianBergmann\CodeCoverage\Filter; 23 | 24 | /** 25 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 26 | */ 27 | final class PcovDriver extends Driver 28 | { 29 | private readonly Filter $filter; 30 | 31 | /** 32 | * @throws PcovNotAvailableException 33 | */ 34 | public function __construct(Filter $filter) 35 | { 36 | $this->ensurePcovIsAvailable(); 37 | 38 | $this->filter = $filter; 39 | } 40 | 41 | /** 42 | * @codeCoverageIgnore 43 | */ 44 | public function start(): void 45 | { 46 | start(); 47 | } 48 | 49 | public function stop(): RawCodeCoverageData 50 | { 51 | stop(); 52 | 53 | // @codeCoverageIgnoreStart 54 | $filesToCollectCoverageFor = waiting(); 55 | $collected = []; 56 | 57 | if ($filesToCollectCoverageFor !== []) { 58 | if (!$this->filter->isEmpty()) { 59 | $filesToCollectCoverageFor = array_intersect($filesToCollectCoverageFor, $this->filter->files()); 60 | } 61 | 62 | $collected = collect(inclusive, $filesToCollectCoverageFor); 63 | 64 | clear(); 65 | } 66 | 67 | return RawCodeCoverageData::fromXdebugWithoutPathCoverage($collected); 68 | // @codeCoverageIgnoreEnd 69 | } 70 | 71 | public function nameAndVersion(): string 72 | { 73 | return 'PCOV ' . phpversion('pcov'); 74 | } 75 | 76 | /** 77 | * @throws PcovNotAvailableException 78 | */ 79 | private function ensurePcovIsAvailable(): void 80 | { 81 | if (!extension_loaded('pcov')) { 82 | throw new PcovNotAvailableException; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Driver/Selector.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 | namespace SebastianBergmann\CodeCoverage\Driver; 11 | 12 | use SebastianBergmann\CodeCoverage\Filter; 13 | use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverAvailableException; 14 | use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverWithPathCoverageSupportAvailableException; 15 | use SebastianBergmann\Environment\Runtime; 16 | 17 | final class Selector 18 | { 19 | /** 20 | * @throws NoCodeCoverageDriverAvailableException 21 | * @throws PcovNotAvailableException 22 | * @throws XdebugNotAvailableException 23 | * @throws XdebugNotEnabledException 24 | * @throws XdebugVersionNotSupportedException 25 | */ 26 | public function forLineCoverage(Filter $filter): Driver 27 | { 28 | $runtime = new Runtime; 29 | 30 | if ($runtime->hasPCOV()) { 31 | return new PcovDriver($filter); 32 | } 33 | 34 | if ($runtime->hasXdebug()) { 35 | return new XdebugDriver($filter); 36 | } 37 | 38 | throw new NoCodeCoverageDriverAvailableException; 39 | } 40 | 41 | /** 42 | * @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException 43 | * @throws XdebugNotAvailableException 44 | * @throws XdebugNotEnabledException 45 | * @throws XdebugVersionNotSupportedException 46 | */ 47 | public function forLineAndPathCoverage(Filter $filter): Driver 48 | { 49 | if ((new Runtime)->hasXdebug()) { 50 | $driver = new XdebugDriver($filter); 51 | 52 | $driver->enableBranchAndPathCoverage(); 53 | 54 | return $driver; 55 | } 56 | 57 | throw new NoCodeCoverageDriverWithPathCoverageSupportAvailableException; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Driver/XdebugDriver.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 | namespace SebastianBergmann\CodeCoverage\Driver; 11 | 12 | use const XDEBUG_CC_BRANCH_CHECK; 13 | use const XDEBUG_CC_DEAD_CODE; 14 | use const XDEBUG_CC_UNUSED; 15 | use const XDEBUG_FILTER_CODE_COVERAGE; 16 | use const XDEBUG_PATH_INCLUDE; 17 | use function extension_loaded; 18 | use function in_array; 19 | use function phpversion; 20 | use function version_compare; 21 | use function xdebug_get_code_coverage; 22 | use function xdebug_info; 23 | use function xdebug_set_filter; 24 | use function xdebug_start_code_coverage; 25 | use function xdebug_stop_code_coverage; 26 | use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData; 27 | use SebastianBergmann\CodeCoverage\Filter; 28 | 29 | /** 30 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 31 | * 32 | * @see https://xdebug.org/docs/code_coverage#xdebug_get_code_coverage 33 | * 34 | * @phpstan-type XdebugLinesCoverageType array 35 | * @phpstan-type XdebugBranchCoverageType array{ 36 | * op_start: int, 37 | * op_end: int, 38 | * line_start: int, 39 | * line_end: int, 40 | * hit: int, 41 | * out: array, 42 | * out_hit: array, 43 | * } 44 | * @phpstan-type XdebugPathCoverageType array{ 45 | * path: array, 46 | * hit: int, 47 | * } 48 | * @phpstan-type XdebugFunctionCoverageType array{ 49 | * branches: array, 50 | * paths: array, 51 | * } 52 | * @phpstan-type XdebugFunctionsCoverageType array 53 | * @phpstan-type XdebugPathAndBranchesCoverageType array{ 54 | * lines: XdebugLinesCoverageType, 55 | * functions: XdebugFunctionsCoverageType, 56 | * } 57 | * @phpstan-type XdebugCodeCoverageWithoutPathCoverageType array 58 | * @phpstan-type XdebugCodeCoverageWithPathCoverageType array 59 | */ 60 | final class XdebugDriver extends Driver 61 | { 62 | /** 63 | * @throws XdebugNotAvailableException 64 | * @throws XdebugNotEnabledException 65 | * @throws XdebugVersionNotSupportedException 66 | */ 67 | public function __construct(Filter $filter) 68 | { 69 | $this->ensureXdebugIsAvailable(); 70 | 71 | if (!$filter->isEmpty()) { 72 | xdebug_set_filter( 73 | XDEBUG_FILTER_CODE_COVERAGE, 74 | XDEBUG_PATH_INCLUDE, 75 | $filter->files(), 76 | ); 77 | } 78 | } 79 | 80 | public function canCollectBranchAndPathCoverage(): bool 81 | { 82 | return true; 83 | } 84 | 85 | public function start(): void 86 | { 87 | $flags = XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE; 88 | 89 | if ($this->collectsBranchAndPathCoverage()) { 90 | $flags |= XDEBUG_CC_BRANCH_CHECK; 91 | } 92 | 93 | xdebug_start_code_coverage($flags); 94 | } 95 | 96 | public function stop(): RawCodeCoverageData 97 | { 98 | $data = xdebug_get_code_coverage(); 99 | 100 | xdebug_stop_code_coverage(); 101 | 102 | if ($this->collectsBranchAndPathCoverage()) { 103 | /* @var XdebugCodeCoverageWithPathCoverageType $data */ 104 | return RawCodeCoverageData::fromXdebugWithPathCoverage($data); 105 | } 106 | 107 | /* @var XdebugCodeCoverageWithoutPathCoverageType $data */ 108 | return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data); 109 | } 110 | 111 | public function nameAndVersion(): string 112 | { 113 | return 'Xdebug ' . phpversion('xdebug'); 114 | } 115 | 116 | /** 117 | * @throws XdebugNotAvailableException 118 | * @throws XdebugNotEnabledException 119 | * @throws XdebugVersionNotSupportedException 120 | */ 121 | private function ensureXdebugIsAvailable(): void 122 | { 123 | if (!extension_loaded('xdebug')) { 124 | throw new XdebugNotAvailableException; 125 | } 126 | 127 | if (!version_compare(phpversion('xdebug'), '3.1', '>=')) { 128 | throw new XdebugVersionNotSupportedException(phpversion('xdebug')); 129 | } 130 | 131 | if (!in_array('coverage', xdebug_info('mode'), true)) { 132 | throw new XdebugNotEnabledException; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Exception/BranchAndPathCoverageNotSupportedException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use RuntimeException; 13 | 14 | final class BranchAndPathCoverageNotSupportedException extends RuntimeException implements Exception 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/DirectoryCouldNotBeCreatedException.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 | namespace SebastianBergmann\CodeCoverage\Util; 11 | 12 | use RuntimeException; 13 | use SebastianBergmann\CodeCoverage\Exception; 14 | 15 | final class DirectoryCouldNotBeCreatedException extends RuntimeException implements Exception 16 | { 17 | } 18 | -------------------------------------------------------------------------------- /src/Exception/Exception.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use Throwable; 13 | 14 | interface Exception extends Throwable 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/FileCouldNotBeWrittenException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use RuntimeException; 13 | 14 | final class FileCouldNotBeWrittenException extends RuntimeException implements Exception 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | final class InvalidArgumentException extends \InvalidArgumentException implements Exception 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/InvalidCodeCoverageTargetException.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | use function sprintf; 13 | use RuntimeException; 14 | use SebastianBergmann\CodeCoverage\Exception; 15 | 16 | final class InvalidCodeCoverageTargetException extends RuntimeException implements Exception 17 | { 18 | public function __construct(Target $target) 19 | { 20 | parent::__construct( 21 | sprintf( 22 | '%s is not a valid target for code coverage', 23 | $target->description(), 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Exception/NoCodeCoverageDriverAvailableException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use RuntimeException; 13 | 14 | final class NoCodeCoverageDriverAvailableException extends RuntimeException implements Exception 15 | { 16 | public function __construct() 17 | { 18 | parent::__construct('No code coverage driver available'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/NoCodeCoverageDriverWithPathCoverageSupportAvailableException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use RuntimeException; 13 | 14 | final class NoCodeCoverageDriverWithPathCoverageSupportAvailableException extends RuntimeException implements Exception 15 | { 16 | public function __construct() 17 | { 18 | parent::__construct('No code coverage driver with path coverage support available'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/ParserException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use RuntimeException; 13 | 14 | final class ParserException extends RuntimeException implements Exception 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/PathExistsButIsNotDirectoryException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use function sprintf; 13 | use RuntimeException; 14 | 15 | final class PathExistsButIsNotDirectoryException extends RuntimeException implements Exception 16 | { 17 | public function __construct(string $path) 18 | { 19 | parent::__construct(sprintf('"%s" exists but is not a directory', $path)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/PcovNotAvailableException.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 | namespace SebastianBergmann\CodeCoverage\Driver; 11 | 12 | use RuntimeException; 13 | use SebastianBergmann\CodeCoverage\Exception; 14 | 15 | final class PcovNotAvailableException extends RuntimeException implements Exception 16 | { 17 | public function __construct() 18 | { 19 | parent::__construct('The PCOV extension is not available'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/ReflectionException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use RuntimeException; 13 | 14 | final class ReflectionException extends RuntimeException implements Exception 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/ReportAlreadyFinalizedException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use RuntimeException; 13 | 14 | final class ReportAlreadyFinalizedException extends RuntimeException implements Exception 15 | { 16 | public function __construct() 17 | { 18 | parent::__construct('The code coverage report has already been finalized'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/StaticAnalysisCacheNotConfiguredException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use RuntimeException; 13 | 14 | final class StaticAnalysisCacheNotConfiguredException extends RuntimeException implements Exception 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/TestIdMissingException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use RuntimeException; 13 | 14 | final class TestIdMissingException extends RuntimeException implements Exception 15 | { 16 | public function __construct() 17 | { 18 | parent::__construct('Test ID is missing'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/UnintentionallyCoveredCodeException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use RuntimeException; 13 | 14 | final class UnintentionallyCoveredCodeException extends RuntimeException implements Exception 15 | { 16 | /** 17 | * @var list 18 | */ 19 | private readonly array $unintentionallyCoveredUnits; 20 | 21 | /** 22 | * @param list $unintentionallyCoveredUnits 23 | */ 24 | public function __construct(array $unintentionallyCoveredUnits) 25 | { 26 | $this->unintentionallyCoveredUnits = $unintentionallyCoveredUnits; 27 | 28 | parent::__construct($this->toString()); 29 | } 30 | 31 | /** 32 | * @return list 33 | */ 34 | public function getUnintentionallyCoveredUnits(): array 35 | { 36 | return $this->unintentionallyCoveredUnits; 37 | } 38 | 39 | private function toString(): string 40 | { 41 | $message = ''; 42 | 43 | foreach ($this->unintentionallyCoveredUnits as $unit) { 44 | $message .= '- ' . $unit . "\n"; 45 | } 46 | 47 | return $message; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Exception/WriteOperationFailedException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use function sprintf; 13 | use RuntimeException; 14 | 15 | final class WriteOperationFailedException extends RuntimeException implements Exception 16 | { 17 | public function __construct(string $path) 18 | { 19 | parent::__construct(sprintf('Cannot write to "%s"', $path)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/XdebugNotAvailableException.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 | namespace SebastianBergmann\CodeCoverage\Driver; 11 | 12 | use RuntimeException; 13 | use SebastianBergmann\CodeCoverage\Exception; 14 | 15 | final class XdebugNotAvailableException extends RuntimeException implements Exception 16 | { 17 | public function __construct() 18 | { 19 | parent::__construct('The Xdebug extension is not available'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/XdebugNotEnabledException.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 | namespace SebastianBergmann\CodeCoverage\Driver; 11 | 12 | use RuntimeException; 13 | use SebastianBergmann\CodeCoverage\Exception; 14 | 15 | final class XdebugNotEnabledException extends RuntimeException implements Exception 16 | { 17 | public function __construct() 18 | { 19 | parent::__construct('XDEBUG_MODE=coverage (environment variable) or xdebug.mode=coverage (PHP configuration setting) has to be set'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/XdebugVersionNotSupportedException.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 | namespace SebastianBergmann\CodeCoverage\Driver; 11 | 12 | use function sprintf; 13 | use RuntimeException; 14 | use SebastianBergmann\CodeCoverage\Exception; 15 | 16 | final class XdebugVersionNotSupportedException extends RuntimeException implements Exception 17 | { 18 | /** 19 | * @param non-empty-string $version 20 | */ 21 | public function __construct(string $version) 22 | { 23 | parent::__construct( 24 | sprintf( 25 | 'Version %s of the Xdebug extension is not supported', 26 | $version, 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exception/XmlException.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use RuntimeException; 13 | 14 | final class XmlException extends RuntimeException implements Exception 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Filter.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use function array_keys; 13 | use function is_file; 14 | use function realpath; 15 | use function str_contains; 16 | use function str_starts_with; 17 | 18 | final class Filter 19 | { 20 | /** 21 | * @var array 22 | */ 23 | private array $files = []; 24 | 25 | /** 26 | * @var array 27 | */ 28 | private array $isFileCache = []; 29 | 30 | /** 31 | * @param list $filenames 32 | */ 33 | public function includeFiles(array $filenames): void 34 | { 35 | foreach ($filenames as $filename) { 36 | $this->includeFile($filename); 37 | } 38 | } 39 | 40 | public function includeFile(string $filename): void 41 | { 42 | $filename = realpath($filename); 43 | 44 | if (!$filename) { 45 | return; 46 | } 47 | 48 | $this->files[$filename] = true; 49 | } 50 | 51 | public function isFile(string $filename): bool 52 | { 53 | if (isset($this->isFileCache[$filename])) { 54 | return $this->isFileCache[$filename]; 55 | } 56 | 57 | if ($filename === '-' || 58 | str_starts_with($filename, 'vfs://') || 59 | str_contains($filename, 'xdebug://debug-eval') || 60 | str_contains($filename, 'eval()\'d code') || 61 | str_contains($filename, 'runtime-created function') || 62 | str_contains($filename, 'runkit created function') || 63 | str_contains($filename, 'assert code') || 64 | str_contains($filename, 'regexp code') || 65 | str_contains($filename, 'Standard input code')) { 66 | $isFile = false; 67 | } else { 68 | $isFile = is_file($filename); 69 | } 70 | 71 | $this->isFileCache[$filename] = $isFile; 72 | 73 | return $isFile; 74 | } 75 | 76 | public function isExcluded(string $filename): bool 77 | { 78 | return !isset($this->files[$filename]) || !$this->isFile($filename); 79 | } 80 | 81 | /** 82 | * @return list 83 | */ 84 | public function files(): array 85 | { 86 | return array_keys($this->files); 87 | } 88 | 89 | public function isEmpty(): bool 90 | { 91 | return $this->files === []; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Node/AbstractNode.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 | namespace SebastianBergmann\CodeCoverage\Node; 11 | 12 | use const DIRECTORY_SEPARATOR; 13 | use function array_merge; 14 | use function str_ends_with; 15 | use function str_replace; 16 | use function substr; 17 | use Countable; 18 | use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode; 19 | use SebastianBergmann\CodeCoverage\Util\Percentage; 20 | 21 | /** 22 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 23 | * 24 | * @phpstan-import-type ProcessedFunctionType from File 25 | * @phpstan-import-type ProcessedClassType from File 26 | * @phpstan-import-type ProcessedTraitType from File 27 | */ 28 | abstract class AbstractNode implements Countable 29 | { 30 | private readonly string $name; 31 | private string $pathAsString; 32 | 33 | /** 34 | * @var non-empty-list 35 | */ 36 | private array $pathAsArray; 37 | private readonly ?AbstractNode $parent; 38 | private string $id; 39 | 40 | public function __construct(string $name, ?self $parent = null) 41 | { 42 | if (str_ends_with($name, DIRECTORY_SEPARATOR)) { 43 | $name = substr($name, 0, -1); 44 | } 45 | 46 | $this->name = $name; 47 | $this->parent = $parent; 48 | 49 | $this->processId(); 50 | $this->processPath(); 51 | } 52 | 53 | public function name(): string 54 | { 55 | return $this->name; 56 | } 57 | 58 | public function id(): string 59 | { 60 | return $this->id; 61 | } 62 | 63 | public function pathAsString(): string 64 | { 65 | return $this->pathAsString; 66 | } 67 | 68 | /** 69 | * @return non-empty-list 70 | */ 71 | public function pathAsArray(): array 72 | { 73 | return $this->pathAsArray; 74 | } 75 | 76 | public function parent(): ?self 77 | { 78 | return $this->parent; 79 | } 80 | 81 | public function percentageOfTestedClasses(): Percentage 82 | { 83 | return Percentage::fromFractionAndTotal( 84 | $this->numberOfTestedClasses(), 85 | $this->numberOfClasses(), 86 | ); 87 | } 88 | 89 | public function percentageOfTestedTraits(): Percentage 90 | { 91 | return Percentage::fromFractionAndTotal( 92 | $this->numberOfTestedTraits(), 93 | $this->numberOfTraits(), 94 | ); 95 | } 96 | 97 | public function percentageOfTestedClassesAndTraits(): Percentage 98 | { 99 | return Percentage::fromFractionAndTotal( 100 | $this->numberOfTestedClassesAndTraits(), 101 | $this->numberOfClassesAndTraits(), 102 | ); 103 | } 104 | 105 | public function percentageOfTestedFunctions(): Percentage 106 | { 107 | return Percentage::fromFractionAndTotal( 108 | $this->numberOfTestedFunctions(), 109 | $this->numberOfFunctions(), 110 | ); 111 | } 112 | 113 | public function percentageOfTestedMethods(): Percentage 114 | { 115 | return Percentage::fromFractionAndTotal( 116 | $this->numberOfTestedMethods(), 117 | $this->numberOfMethods(), 118 | ); 119 | } 120 | 121 | public function percentageOfTestedFunctionsAndMethods(): Percentage 122 | { 123 | return Percentage::fromFractionAndTotal( 124 | $this->numberOfTestedFunctionsAndMethods(), 125 | $this->numberOfFunctionsAndMethods(), 126 | ); 127 | } 128 | 129 | public function percentageOfExecutedLines(): Percentage 130 | { 131 | return Percentage::fromFractionAndTotal( 132 | $this->numberOfExecutedLines(), 133 | $this->numberOfExecutableLines(), 134 | ); 135 | } 136 | 137 | public function percentageOfExecutedBranches(): Percentage 138 | { 139 | return Percentage::fromFractionAndTotal( 140 | $this->numberOfExecutedBranches(), 141 | $this->numberOfExecutableBranches(), 142 | ); 143 | } 144 | 145 | public function percentageOfExecutedPaths(): Percentage 146 | { 147 | return Percentage::fromFractionAndTotal( 148 | $this->numberOfExecutedPaths(), 149 | $this->numberOfExecutablePaths(), 150 | ); 151 | } 152 | 153 | public function numberOfClassesAndTraits(): int 154 | { 155 | return $this->numberOfClasses() + $this->numberOfTraits(); 156 | } 157 | 158 | public function numberOfTestedClassesAndTraits(): int 159 | { 160 | return $this->numberOfTestedClasses() + $this->numberOfTestedTraits(); 161 | } 162 | 163 | /** 164 | * @return array 165 | */ 166 | public function classesAndTraits(): array 167 | { 168 | return array_merge($this->classes(), $this->traits()); 169 | } 170 | 171 | public function numberOfFunctionsAndMethods(): int 172 | { 173 | return $this->numberOfFunctions() + $this->numberOfMethods(); 174 | } 175 | 176 | public function numberOfTestedFunctionsAndMethods(): int 177 | { 178 | return $this->numberOfTestedFunctions() + $this->numberOfTestedMethods(); 179 | } 180 | 181 | /** 182 | * @return non-negative-int 183 | */ 184 | public function cyclomaticComplexity(): int 185 | { 186 | $ccn = 0; 187 | 188 | foreach ($this->classesAndTraits() as $classLike) { 189 | $ccn += $classLike['ccn']; 190 | } 191 | 192 | foreach ($this->functions() as $function) { 193 | $ccn += $function['ccn']; 194 | } 195 | 196 | return $ccn; 197 | } 198 | 199 | /** 200 | * @return array 201 | */ 202 | abstract public function classes(): array; 203 | 204 | /** 205 | * @return array 206 | */ 207 | abstract public function traits(): array; 208 | 209 | /** 210 | * @return array 211 | */ 212 | abstract public function functions(): array; 213 | 214 | abstract public function linesOfCode(): LinesOfCode; 215 | 216 | abstract public function numberOfExecutableLines(): int; 217 | 218 | abstract public function numberOfExecutedLines(): int; 219 | 220 | abstract public function numberOfExecutableBranches(): int; 221 | 222 | abstract public function numberOfExecutedBranches(): int; 223 | 224 | abstract public function numberOfExecutablePaths(): int; 225 | 226 | abstract public function numberOfExecutedPaths(): int; 227 | 228 | abstract public function numberOfClasses(): int; 229 | 230 | abstract public function numberOfTestedClasses(): int; 231 | 232 | abstract public function numberOfTraits(): int; 233 | 234 | abstract public function numberOfTestedTraits(): int; 235 | 236 | abstract public function numberOfMethods(): int; 237 | 238 | abstract public function numberOfTestedMethods(): int; 239 | 240 | abstract public function numberOfFunctions(): int; 241 | 242 | abstract public function numberOfTestedFunctions(): int; 243 | 244 | private function processId(): void 245 | { 246 | if ($this->parent === null) { 247 | $this->id = 'index'; 248 | 249 | return; 250 | } 251 | 252 | $parentId = $this->parent->id(); 253 | 254 | if ($parentId === 'index') { 255 | $this->id = str_replace(':', '_', $this->name); 256 | } else { 257 | $this->id = $parentId . '/' . $this->name; 258 | } 259 | } 260 | 261 | private function processPath(): void 262 | { 263 | if ($this->parent === null) { 264 | $this->pathAsArray = [$this]; 265 | $this->pathAsString = $this->name; 266 | 267 | return; 268 | } 269 | 270 | $this->pathAsArray = $this->parent->pathAsArray(); 271 | $this->pathAsString = $this->parent->pathAsString() . DIRECTORY_SEPARATOR . $this->name; 272 | 273 | $this->pathAsArray[] = $this; 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/Node/CrapIndex.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 | namespace SebastianBergmann\CodeCoverage\Node; 11 | 12 | use function sprintf; 13 | 14 | /** 15 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final readonly class CrapIndex 18 | { 19 | private int $cyclomaticComplexity; 20 | private float $codeCoverage; 21 | 22 | public function __construct(int $cyclomaticComplexity, float $codeCoverage) 23 | { 24 | $this->cyclomaticComplexity = $cyclomaticComplexity; 25 | $this->codeCoverage = $codeCoverage; 26 | } 27 | 28 | public function asString(): string 29 | { 30 | if ($this->codeCoverage === 0.0) { 31 | return (string) ($this->cyclomaticComplexity ** 2 + $this->cyclomaticComplexity); 32 | } 33 | 34 | if ($this->codeCoverage >= 95) { 35 | return (string) $this->cyclomaticComplexity; 36 | } 37 | 38 | return sprintf( 39 | '%01.2F', 40 | $this->cyclomaticComplexity ** 2 * (1 - $this->codeCoverage / 100) ** 3 + $this->cyclomaticComplexity, 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Node/Iterator.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 | namespace SebastianBergmann\CodeCoverage\Node; 11 | 12 | use function assert; 13 | use function count; 14 | use RecursiveIterator; 15 | 16 | /** 17 | * @template-implements RecursiveIterator 18 | * 19 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 20 | */ 21 | final class Iterator implements RecursiveIterator 22 | { 23 | private int $position; 24 | 25 | /** 26 | * @var list 27 | */ 28 | private readonly array $nodes; 29 | 30 | public function __construct(Directory $node) 31 | { 32 | $this->nodes = $node->children(); 33 | } 34 | 35 | public function rewind(): void 36 | { 37 | $this->position = 0; 38 | } 39 | 40 | public function valid(): bool 41 | { 42 | return $this->position < count($this->nodes); 43 | } 44 | 45 | public function key(): int 46 | { 47 | return $this->position; 48 | } 49 | 50 | public function current(): ?AbstractNode 51 | { 52 | return $this->valid() ? $this->nodes[$this->position] : null; 53 | } 54 | 55 | public function next(): void 56 | { 57 | $this->position++; 58 | } 59 | 60 | public function getChildren(): self 61 | { 62 | assert($this->nodes[$this->position] instanceof Directory); 63 | 64 | return new self($this->nodes[$this->position]); 65 | } 66 | 67 | public function hasChildren(): bool 68 | { 69 | return $this->nodes[$this->position] instanceof Directory; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Report/Crap4j.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 | namespace SebastianBergmann\CodeCoverage\Report; 11 | 12 | use function date; 13 | use function dirname; 14 | use function file_put_contents; 15 | use function htmlspecialchars; 16 | use function is_string; 17 | use function round; 18 | use function str_contains; 19 | use DOMDocument; 20 | use SebastianBergmann\CodeCoverage\CodeCoverage; 21 | use SebastianBergmann\CodeCoverage\Node\File; 22 | use SebastianBergmann\CodeCoverage\Util\Filesystem; 23 | use SebastianBergmann\CodeCoverage\WriteOperationFailedException; 24 | 25 | final readonly class Crap4j 26 | { 27 | private int $threshold; 28 | 29 | public function __construct(int $threshold = 30) 30 | { 31 | $this->threshold = $threshold; 32 | } 33 | 34 | /** 35 | * @throws WriteOperationFailedException 36 | */ 37 | public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string 38 | { 39 | $document = new DOMDocument('1.0', 'UTF-8'); 40 | $document->formatOutput = true; 41 | 42 | $root = $document->createElement('crap_result'); 43 | $document->appendChild($root); 44 | 45 | $project = $document->createElement('project', is_string($name) ? $name : ''); 46 | $root->appendChild($project); 47 | $root->appendChild($document->createElement('timestamp', date('Y-m-d H:i:s'))); 48 | 49 | $stats = $document->createElement('stats'); 50 | $methodsNode = $document->createElement('methods'); 51 | 52 | $report = $coverage->getReport(); 53 | unset($coverage); 54 | 55 | $fullMethodCount = 0; 56 | $fullCrapMethodCount = 0; 57 | $fullCrapLoad = 0; 58 | $fullCrap = 0; 59 | 60 | foreach ($report as $item) { 61 | $namespace = 'global'; 62 | 63 | if (!$item instanceof File) { 64 | continue; 65 | } 66 | 67 | $file = $document->createElement('file'); 68 | $file->setAttribute('name', $item->pathAsString()); 69 | 70 | $classes = $item->classesAndTraits(); 71 | 72 | foreach ($classes as $className => $class) { 73 | foreach ($class['methods'] as $methodName => $method) { 74 | $crapLoad = $this->crapLoad((float) $method['crap'], $method['ccn'], $method['coverage']); 75 | 76 | $fullCrap += $method['crap']; 77 | $fullCrapLoad += $crapLoad; 78 | $fullMethodCount++; 79 | 80 | if ($method['crap'] >= $this->threshold) { 81 | $fullCrapMethodCount++; 82 | } 83 | 84 | $methodNode = $document->createElement('method'); 85 | 86 | if ($class['namespace'] !== '') { 87 | $namespace = $class['namespace']; 88 | } 89 | 90 | $methodNode->appendChild($document->createElement('package', $namespace)); 91 | $methodNode->appendChild($document->createElement('className', $className)); 92 | $methodNode->appendChild($document->createElement('methodName', $methodName)); 93 | $methodNode->appendChild($document->createElement('methodSignature', htmlspecialchars($method['signature']))); 94 | $methodNode->appendChild($document->createElement('fullMethod', htmlspecialchars($method['signature']))); 95 | $methodNode->appendChild($document->createElement('crap', (string) $this->roundValue((float) $method['crap']))); 96 | $methodNode->appendChild($document->createElement('complexity', (string) $method['ccn'])); 97 | $methodNode->appendChild($document->createElement('coverage', (string) $this->roundValue($method['coverage']))); 98 | $methodNode->appendChild($document->createElement('crapLoad', (string) round($crapLoad))); 99 | 100 | $methodsNode->appendChild($methodNode); 101 | } 102 | } 103 | } 104 | 105 | $stats->appendChild($document->createElement('name', 'Method Crap Stats')); 106 | $stats->appendChild($document->createElement('methodCount', (string) $fullMethodCount)); 107 | $stats->appendChild($document->createElement('crapMethodCount', (string) $fullCrapMethodCount)); 108 | $stats->appendChild($document->createElement('crapLoad', (string) round($fullCrapLoad))); 109 | $stats->appendChild($document->createElement('totalCrap', (string) $fullCrap)); 110 | 111 | $crapMethodPercent = 0; 112 | 113 | if ($fullMethodCount > 0) { 114 | $crapMethodPercent = $this->roundValue((100 * $fullCrapMethodCount) / $fullMethodCount); 115 | } 116 | 117 | $stats->appendChild($document->createElement('crapMethodPercent', (string) $crapMethodPercent)); 118 | 119 | $root->appendChild($stats); 120 | $root->appendChild($methodsNode); 121 | 122 | $buffer = $document->saveXML(); 123 | 124 | if ($target !== null) { 125 | if (!str_contains($target, '://')) { 126 | Filesystem::createDirectory(dirname($target)); 127 | } 128 | 129 | if (@file_put_contents($target, $buffer) === false) { 130 | throw new WriteOperationFailedException($target); 131 | } 132 | } 133 | 134 | return $buffer; 135 | } 136 | 137 | private function crapLoad(float $crapValue, int $cyclomaticComplexity, float $coveragePercent): float 138 | { 139 | $crapLoad = 0; 140 | 141 | if ($crapValue >= $this->threshold) { 142 | $crapLoad += $cyclomaticComplexity * (1.0 - $coveragePercent / 100); 143 | $crapLoad += $cyclomaticComplexity / $this->threshold; 144 | } 145 | 146 | return $crapLoad; 147 | } 148 | 149 | private function roundValue(float $value): float 150 | { 151 | return round($value, 2); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Report/Html/Colors.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 | namespace SebastianBergmann\CodeCoverage\Report\Html; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | final readonly class Colors 16 | { 17 | private string $successLow; 18 | private string $successMedium; 19 | private string $successHigh; 20 | private string $warning; 21 | private string $danger; 22 | 23 | public static function default(): self 24 | { 25 | return new self('#dff0d8', '#c3e3b5', '#99cb84', '#fcf8e3', '#f2dede'); 26 | } 27 | 28 | public static function from(string $successLow, string $successMedium, string $successHigh, string $warning, string $danger): self 29 | { 30 | return new self($successLow, $successMedium, $successHigh, $warning, $danger); 31 | } 32 | 33 | private function __construct(string $successLow, string $successMedium, string $successHigh, string $warning, string $danger) 34 | { 35 | $this->successLow = $successLow; 36 | $this->successMedium = $successMedium; 37 | $this->successHigh = $successHigh; 38 | $this->warning = $warning; 39 | $this->danger = $danger; 40 | } 41 | 42 | public function successLow(): string 43 | { 44 | return $this->successLow; 45 | } 46 | 47 | public function successMedium(): string 48 | { 49 | return $this->successMedium; 50 | } 51 | 52 | public function successHigh(): string 53 | { 54 | return $this->successHigh; 55 | } 56 | 57 | public function warning(): string 58 | { 59 | return $this->warning; 60 | } 61 | 62 | public function danger(): string 63 | { 64 | return $this->danger; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Report/Html/CustomCssFile.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 | namespace SebastianBergmann\CodeCoverage\Report\Html; 11 | 12 | use function is_file; 13 | use SebastianBergmann\CodeCoverage\InvalidArgumentException; 14 | 15 | /** 16 | * @immutable 17 | */ 18 | final readonly class CustomCssFile 19 | { 20 | private string $path; 21 | 22 | public static function default(): self 23 | { 24 | return new self(__DIR__ . '/Renderer/Template/css/custom.css'); 25 | } 26 | 27 | /** 28 | * @throws InvalidArgumentException 29 | */ 30 | public static function from(string $path): self 31 | { 32 | if (!is_file($path)) { 33 | throw new InvalidArgumentException( 34 | '$path does not exist', 35 | ); 36 | } 37 | 38 | return new self($path); 39 | } 40 | 41 | private function __construct(string $path) 42 | { 43 | $this->path = $path; 44 | } 45 | 46 | public function path(): string 47 | { 48 | return $this->path; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Report/Html/Facade.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 | namespace SebastianBergmann\CodeCoverage\Report\Html; 11 | 12 | use const DIRECTORY_SEPARATOR; 13 | use function copy; 14 | use function date; 15 | use function dirname; 16 | use function str_ends_with; 17 | use SebastianBergmann\CodeCoverage\CodeCoverage; 18 | use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException; 19 | use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode; 20 | use SebastianBergmann\CodeCoverage\Report\Thresholds; 21 | use SebastianBergmann\CodeCoverage\Util\Filesystem; 22 | use SebastianBergmann\Template\Exception; 23 | use SebastianBergmann\Template\Template; 24 | 25 | final readonly class Facade 26 | { 27 | private string $templatePath; 28 | private string $generator; 29 | private Colors $colors; 30 | private Thresholds $thresholds; 31 | private CustomCssFile $customCssFile; 32 | 33 | public function __construct(string $generator = '', ?Colors $colors = null, ?Thresholds $thresholds = null, ?CustomCssFile $customCssFile = null) 34 | { 35 | $this->generator = $generator; 36 | $this->colors = $colors ?? Colors::default(); 37 | $this->thresholds = $thresholds ?? Thresholds::default(); 38 | $this->customCssFile = $customCssFile ?? CustomCssFile::default(); 39 | $this->templatePath = __DIR__ . '/Renderer/Template/'; 40 | } 41 | 42 | public function process(CodeCoverage $coverage, string $target): void 43 | { 44 | $target = $this->directory($target); 45 | $report = $coverage->getReport(); 46 | $date = date('D M j G:i:s T Y'); 47 | 48 | $dashboard = new Dashboard( 49 | $this->templatePath, 50 | $this->generator, 51 | $date, 52 | $this->thresholds, 53 | $coverage->collectsBranchAndPathCoverage(), 54 | ); 55 | 56 | $directory = new Directory( 57 | $this->templatePath, 58 | $this->generator, 59 | $date, 60 | $this->thresholds, 61 | $coverage->collectsBranchAndPathCoverage(), 62 | ); 63 | 64 | $file = new File( 65 | $this->templatePath, 66 | $this->generator, 67 | $date, 68 | $this->thresholds, 69 | $coverage->collectsBranchAndPathCoverage(), 70 | ); 71 | 72 | $directory->render($report, $target . 'index.html'); 73 | $dashboard->render($report, $target . 'dashboard.html'); 74 | 75 | foreach ($report as $node) { 76 | $id = $node->id(); 77 | 78 | if ($node instanceof DirectoryNode) { 79 | Filesystem::createDirectory($target . $id); 80 | 81 | $directory->render($node, $target . $id . '/index.html'); 82 | $dashboard->render($node, $target . $id . '/dashboard.html'); 83 | } else { 84 | $dir = dirname($target . $id); 85 | 86 | Filesystem::createDirectory($dir); 87 | 88 | $file->render($node, $target . $id); 89 | } 90 | } 91 | 92 | $this->copyFiles($target); 93 | $this->renderCss($target); 94 | } 95 | 96 | private function copyFiles(string $target): void 97 | { 98 | $dir = $this->directory($target . '_css'); 99 | 100 | copy($this->templatePath . 'css/billboard.min.css', $dir . 'billboard.min.css'); 101 | copy($this->templatePath . 'css/bootstrap.min.css', $dir . 'bootstrap.min.css'); 102 | copy($this->customCssFile->path(), $dir . 'custom.css'); 103 | copy($this->templatePath . 'css/octicons.css', $dir . 'octicons.css'); 104 | 105 | $dir = $this->directory($target . '_icons'); 106 | copy($this->templatePath . 'icons/file-code.svg', $dir . 'file-code.svg'); 107 | copy($this->templatePath . 'icons/file-directory.svg', $dir . 'file-directory.svg'); 108 | 109 | $dir = $this->directory($target . '_js'); 110 | copy($this->templatePath . 'js/billboard.pkgd.min.js', $dir . 'billboard.pkgd.min.js'); 111 | copy($this->templatePath . 'js/bootstrap.bundle.min.js', $dir . 'bootstrap.bundle.min.js'); 112 | copy($this->templatePath . 'js/jquery.min.js', $dir . 'jquery.min.js'); 113 | copy($this->templatePath . 'js/file.js', $dir . 'file.js'); 114 | } 115 | 116 | private function renderCss(string $target): void 117 | { 118 | $template = new Template($this->templatePath . 'css/style.css', '{{', '}}'); 119 | 120 | $template->setVar( 121 | [ 122 | 'success-low' => $this->colors->successLow(), 123 | 'success-medium' => $this->colors->successMedium(), 124 | 'success-high' => $this->colors->successHigh(), 125 | 'warning' => $this->colors->warning(), 126 | 'danger' => $this->colors->danger(), 127 | ], 128 | ); 129 | 130 | try { 131 | $template->renderTo($this->directory($target . '_css') . 'style.css'); 132 | // @codeCoverageIgnoreStart 133 | } catch (Exception $e) { 134 | throw new FileCouldNotBeWrittenException( 135 | $e->getMessage(), 136 | $e->getCode(), 137 | $e, 138 | ); 139 | // @codeCoverageIgnoreEnd 140 | } 141 | } 142 | 143 | private function directory(string $directory): string 144 | { 145 | if (!str_ends_with($directory, DIRECTORY_SEPARATOR)) { 146 | $directory .= DIRECTORY_SEPARATOR; 147 | } 148 | 149 | Filesystem::createDirectory($directory); 150 | 151 | return $directory; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Directory.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 | namespace SebastianBergmann\CodeCoverage\Report\Html; 11 | 12 | use function count; 13 | use function sprintf; 14 | use function str_repeat; 15 | use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException; 16 | use SebastianBergmann\CodeCoverage\Node\AbstractNode as Node; 17 | use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode; 18 | use SebastianBergmann\Template\Exception; 19 | use SebastianBergmann\Template\Template; 20 | 21 | /** 22 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 23 | */ 24 | final class Directory extends Renderer 25 | { 26 | public function render(DirectoryNode $node, string $file): void 27 | { 28 | $templateName = $this->templatePath . ($this->hasBranchCoverage ? 'directory_branch.html' : 'directory.html'); 29 | $template = new Template($templateName, '{{', '}}'); 30 | 31 | $this->setCommonTemplateVariables($template, $node); 32 | 33 | $items = $this->renderItem($node, true); 34 | 35 | foreach ($node->directories() as $item) { 36 | $items .= $this->renderItem($item); 37 | } 38 | 39 | foreach ($node->files() as $item) { 40 | $items .= $this->renderItem($item); 41 | } 42 | 43 | $template->setVar( 44 | [ 45 | 'id' => $node->id(), 46 | 'items' => $items, 47 | ], 48 | ); 49 | 50 | try { 51 | $template->renderTo($file); 52 | } catch (Exception $e) { 53 | throw new FileCouldNotBeWrittenException( 54 | $e->getMessage(), 55 | $e->getCode(), 56 | $e, 57 | ); 58 | } 59 | } 60 | 61 | private function renderItem(Node $node, bool $total = false): string 62 | { 63 | $data = [ 64 | 'numClasses' => $node->numberOfClassesAndTraits(), 65 | 'numTestedClasses' => $node->numberOfTestedClassesAndTraits(), 66 | 'numMethods' => $node->numberOfFunctionsAndMethods(), 67 | 'numTestedMethods' => $node->numberOfTestedFunctionsAndMethods(), 68 | 'linesExecutedPercent' => $node->percentageOfExecutedLines()->asFloat(), 69 | 'linesExecutedPercentAsString' => $node->percentageOfExecutedLines()->asString(), 70 | 'numExecutedLines' => $node->numberOfExecutedLines(), 71 | 'numExecutableLines' => $node->numberOfExecutableLines(), 72 | 'branchesExecutedPercent' => $node->percentageOfExecutedBranches()->asFloat(), 73 | 'branchesExecutedPercentAsString' => $node->percentageOfExecutedBranches()->asString(), 74 | 'numExecutedBranches' => $node->numberOfExecutedBranches(), 75 | 'numExecutableBranches' => $node->numberOfExecutableBranches(), 76 | 'pathsExecutedPercent' => $node->percentageOfExecutedPaths()->asFloat(), 77 | 'pathsExecutedPercentAsString' => $node->percentageOfExecutedPaths()->asString(), 78 | 'numExecutedPaths' => $node->numberOfExecutedPaths(), 79 | 'numExecutablePaths' => $node->numberOfExecutablePaths(), 80 | 'testedMethodsPercent' => $node->percentageOfTestedFunctionsAndMethods()->asFloat(), 81 | 'testedMethodsPercentAsString' => $node->percentageOfTestedFunctionsAndMethods()->asString(), 82 | 'testedClassesPercent' => $node->percentageOfTestedClassesAndTraits()->asFloat(), 83 | 'testedClassesPercentAsString' => $node->percentageOfTestedClassesAndTraits()->asString(), 84 | ]; 85 | 86 | if ($total) { 87 | $data['name'] = 'Total'; 88 | } else { 89 | $up = str_repeat('../', count($node->pathAsArray()) - 2); 90 | $data['icon'] = sprintf('', $up); 91 | 92 | if ($node instanceof DirectoryNode) { 93 | $data['name'] = sprintf( 94 | '%s', 95 | $node->name(), 96 | $node->name(), 97 | ); 98 | $data['icon'] = sprintf('', $up); 99 | } elseif ($this->hasBranchCoverage) { 100 | $data['name'] = sprintf( 101 | '%s [line] [branch] [path]', 102 | $node->name(), 103 | $node->name(), 104 | $node->name(), 105 | $node->name(), 106 | ); 107 | } else { 108 | $data['name'] = sprintf( 109 | '%s', 110 | $node->name(), 111 | $node->name(), 112 | ); 113 | } 114 | } 115 | 116 | $templateName = $this->templatePath . ($this->hasBranchCoverage ? 'directory_item_branch.html' : 'directory_item.html'); 117 | 118 | return $this->renderItemTemplate( 119 | new Template($templateName, '{{', '}}'), 120 | $data, 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/branches.html.dist: -------------------------------------------------------------------------------- 1 |
2 |

Branches

3 |

4 | Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not 5 | necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. 6 | Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement 7 | always has an else as part of its logical flow even if you didn't write one. 8 |

9 | {{branches}} 10 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/coverage_bar.html.dist: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{percent}}% covered ({{level}}) 4 |
5 |
6 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/coverage_bar_branch.html.dist: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{percent}}% covered ({{level}}) 4 |
5 |
6 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/css/billboard.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017 ~ present NAVER Corp. 3 | * billboard.js project is licensed under the MIT license 4 | * 5 | * billboard.js, JavaScript chart library 6 | * https://naver.github.io/billboard.js/ 7 | * 8 | * @version 3.15.1 9 | */ 10 | .bb svg{font:10px sans-serif;-webkit-tap-highlight-color:rgba(0,0,0,0)}.bb path,.bb line{fill:none;stroke:#000}.bb text,.bb .bb-button{-webkit-user-select:none;-moz-user-select:none;user-select:none}.bb-legend-item-tile,.bb-xgrid-focus,.bb-ygrid-focus,.bb-ygrid{shape-rendering:crispEdges}.bb-chart-arcs .bb-needle,.bb-chart-arc .bb-gauge-value{fill:#000}.bb-chart-arc path{stroke:#fff}.bb-chart-arc rect{stroke:#fff;stroke-width:1}.bb-chart-arc text{fill:#fff;font-size:13px}.bb-chart-funnels path{stroke-width:0}.bb-chart-funnels+.bb-chart-texts text{font-size:13px;fill:#fff}.bb-axis{shape-rendering:crispEdges}.bb-axis .bb-axis-x-tooltip,.bb-axis .bb-axis-y-tooltip,.bb-axis .bb-axis-y2-tooltip{font-size:1em;fill:#fff;white-space:nowrap}.bb-grid{pointer-events:none}.bb-grid line{stroke:#aaa}.bb-grid text{fill:#aaa}.bb-xgrid,.bb-ygrid{stroke-dasharray:3 3}.bb-text.bb-empty{fill:gray;font-size:2em}.bb-line{stroke-width:1px}.bb-circle._expanded_{stroke-width:1px;stroke:#fff}.bb-selected-circle{fill:#fff;stroke-width:2px}.bb-bar{stroke-width:0}.bb-bar._expanded_{fill-opacity:.75}.bb-candlestick{stroke-width:1px}.bb-candlestick._expanded_{fill-opacity:.75}.bb-target.bb-focused,.bb-circles.bb-focused{opacity:1}.bb-target.bb-focused path.bb-line,.bb-target.bb-focused path.bb-step,.bb-circles.bb-focused path.bb-line,.bb-circles.bb-focused path.bb-step{stroke-width:2px}.bb-target.bb-defocused,.bb-circles.bb-defocused{opacity:.3!important}.bb-target.bb-defocused .text-overlapping,.bb-circles.bb-defocused .text-overlapping{opacity:.05!important}.bb-region{fill:#4682b4}.bb-region rect{fill-opacity:.1}.bb-zoom-brush,.bb-brush .extent{fill-opacity:.1}.bb-legend-item{font-size:12px;user-select:none}.bb-legend-item-hidden{opacity:.15}.bb-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.bb-title{font:14px sans-serif}.bb-chart-treemaps rect{stroke:#fff;stroke-width:1px}.bb-tooltip-container{z-index:10;user-select:none}.bb-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;opacity:.9;box-shadow:7px 7px 12px -9px #777;white-space:nowrap}.bb-tooltip tr{border:1px solid #CCC}.bb-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#fff}.bb-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.bb-tooltip td>span,.bb-tooltip td>svg{display:inline-block;width:10px;height:10px;margin-right:6px}.bb-tooltip.value{text-align:right}.bb-area{stroke-width:0;opacity:.2}.bb-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}text.bb-chart-arcs-gauge-title{dominant-baseline:middle;font-size:2.7em}.bb-chart-arcs .bb-chart-arcs-background{fill:#e0e0e0;stroke:#fff}.bb-chart-arcs .bb-chart-arcs-gauge-unit{fill:#000;font-size:16px}.bb-chart-arcs .bb-chart-arcs-gauge-max,.bb-chart-arcs .bb-chart-arcs-gauge-min{fill:#777}.bb-chart-arcs .bb-levels circle{fill:none;stroke:#848282;stroke-width:.5px}.bb-chart-arcs .bb-levels text{fill:#848282}.bb-chart-radars .bb-levels polygon{fill:none;stroke:#848282;stroke-width:.5px}.bb-chart-radars .bb-levels text{fill:#848282}.bb-chart-radars .bb-axis line{stroke:#848282;stroke-width:.5px}.bb-chart-radars .bb-axis text{font-size:1.15em;cursor:default}.bb-chart-radars .bb-shapes polygon{fill-opacity:.2;stroke-width:1px}.bb-button{position:absolute;top:10px;right:10px}.bb-button .bb-zoom-reset{font-size:11px;border:solid 1px #ccc;background-color:#fff;padding:5px;border-radius:5px;cursor:pointer} 11 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/css/custom.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianbergmann/php-code-coverage/fdeebc707531f6e62dabefdc218e89015066b50f/src/Report/Html/Renderer/Template/css/custom.css -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/css/octicons.css: -------------------------------------------------------------------------------- 1 | .octicon { 2 | display: inline-block; 3 | vertical-align: text-top; 4 | fill: currentColor; 5 | } 6 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --phpunit-breadcrumbs: var(--bs-gray-200); 3 | --phpunit-success-bar: #28a745; 4 | --phpunit-success-high: {{success-high}}; 5 | --phpunit-success-medium: {{success-medium}}; 6 | --phpunit-success-low: {{success-low}}; 7 | --phpunit-warning: {{warning}}; 8 | --phpunit-warning-bar: #ffc107; 9 | --phpunit-danger: {{danger}}; 10 | --phpunit-danger-bar: #dc3545; 11 | } 12 | 13 | body { 14 | font-family: sans-serif; 15 | font-size: 1em; 16 | font-kerning: normal; 17 | font-variant-ligatures: common-ligatures; 18 | text-rendering: optimizeLegibility; 19 | padding-top: 10px; 20 | } 21 | 22 | nav .breadcrumb { 23 | border-radius: var(--bs-border-radius); 24 | background-color: var(--phpunit-breadcrumbs); 25 | padding: .75rem 1rem; 26 | } 27 | 28 | .popover { 29 | max-width: none; 30 | } 31 | 32 | .popover-body { 33 | max-height: 90vh; 34 | overflow-y: auto; 35 | } 36 | 37 | .octicon { 38 | margin-right:.25em; 39 | vertical-align: baseline; 40 | width: 0.75em; 41 | } 42 | 43 | .table-bordered>thead>tr>td { 44 | border-bottom-width: 1px; 45 | } 46 | 47 | .table tbody>tr>td, .table thead>tr>td { 48 | padding-top: 3px; 49 | padding-bottom: 3px; 50 | } 51 | 52 | .table-condensed tbody>tr>td { 53 | padding-top: 0; 54 | padding-bottom: 0; 55 | } 56 | 57 | .table .progress { 58 | margin-bottom: inherit; 59 | } 60 | 61 | .table-borderless th, .table-borderless td { 62 | border: 0 !important; 63 | } 64 | 65 | .table tbody tr.covered-by-large-tests, .table tbody tr.covered-by-large-tests td, li.covered-by-large-tests, tr.success, tr.success td, td.success, li.success, span.success { 66 | background-color: var(--phpunit-success-low); 67 | } 68 | 69 | .table tbody tr.covered-by-medium-tests, .table tbody tr.covered-by-medium-tests td, li.covered-by-medium-tests { 70 | background-color: var(--phpunit-success-medium); 71 | } 72 | 73 | .table tbody tr.covered-by-small-tests, .table tbody tr.covered-by-small-tests td, li.covered-by-small-tests { 74 | background-color: var(--phpunit-success-high); 75 | } 76 | 77 | .table tbody tr.warning, .table tbody tr.warning td, .table tbody td.warning, li.warning, span.warning { 78 | background-color: var(--phpunit-warning); 79 | } 80 | 81 | .table tbody tr.danger, .table tbody tr.danger td, .table tbody td.danger, li.danger, span.danger { 82 | background-color: var(--phpunit-danger); 83 | } 84 | 85 | .table tbody td.info { 86 | background-color: rgb(from var(--bs-info) r g b / 0.25); 87 | } 88 | 89 | td.big { 90 | vertical-align: middle; 91 | width: 117px; 92 | } 93 | 94 | td.small { 95 | } 96 | 97 | td.codeLine { 98 | font-family: "Source Code Pro", var(--bs-font-monospace); 99 | white-space: pre-wrap; 100 | } 101 | 102 | td span.comment { 103 | color: var(--bs-secondary-color); 104 | } 105 | 106 | td span.default { 107 | color: var(--bs-body-color); 108 | } 109 | 110 | td span.html { 111 | color: var(--bs-secondary-color); 112 | } 113 | 114 | td span.keyword { 115 | color: var(--bs-body-color); 116 | font-weight: bold; 117 | } 118 | 119 | pre span.string { 120 | color: var(--bs-body-color); 121 | } 122 | 123 | span.success, span.warning, span.danger { 124 | margin-right: 2px; 125 | padding-left: 10px; 126 | padding-right: 10px; 127 | text-align: center; 128 | } 129 | 130 | #toplink { 131 | position: fixed; 132 | left: 5px; 133 | bottom: 5px; 134 | outline: 0; 135 | } 136 | 137 | svg text { 138 | font-family: var(--bs-font-sans-serif); 139 | font-size: 11px; 140 | color: var(--bs-gray); 141 | fill: var(--bs-gray); 142 | } 143 | 144 | .scrollbox { 145 | height:245px; 146 | overflow-x:scroll; 147 | overflow-y:scroll; 148 | } 149 | 150 | table + .structure-heading { 151 | border-top: 1px solid var(--bs-gray-200); 152 | padding-top: 0.5em; 153 | } 154 | 155 | table#code td:first-of-type { 156 | padding-left: .75em; 157 | padding-right: .75em; 158 | } 159 | 160 | table#code td:first-of-type a { 161 | text-decoration: none; 162 | } 163 | 164 | .legend { 165 | font-weight: bold; 166 | margin-right: 2px; 167 | padding-left: 10px; 168 | padding-right: 10px; 169 | text-align: center; 170 | } 171 | 172 | .covered-by-small-tests { 173 | background-color: var(--phpunit-success-high); 174 | } 175 | 176 | .covered-by-medium-tests { 177 | background-color: var(--phpunit-success-medium); 178 | } 179 | 180 | .covered-by-large-tests { 181 | background-color: var(--phpunit-success-low); 182 | } 183 | 184 | .not-covered { 185 | background-color: var(--phpunit-danger); 186 | } 187 | 188 | .not-coverable { 189 | background-color: var(--phpunit-warning); 190 | } 191 | 192 | .progress-bar.bg-success { 193 | background-color: var(--phpunit-success-bar) !important; 194 | } 195 | 196 | .progress-bar.bg-warning { 197 | background-color: var(--phpunit-warning-bar) !important; 198 | } 199 | 200 | .progress-bar.bg-danger { 201 | background-color: var(--phpunit-danger-bar) !important; 202 | } -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/directory.html.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Code Coverage for {{full_path}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {{items}} 43 | 44 |
 
Code Coverage
 
Lines
Functions and Methods
Classes and Traits
45 |
46 |
47 |
48 |

Legend

49 |

50 | Low: 0% to {{low_upper_bound}}% 51 | Medium: {{low_upper_bound}}% to {{high_lower_bound}}% 52 | High: {{high_lower_bound}}% to 100% 53 |

54 |

55 | Generated by php-code-coverage {{version}} using {{runtime}}{{generator}} at {{date}}. 56 |

57 |
58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/directory_branch.html.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Code Coverage for {{full_path}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {{items}} 45 | 46 |
 
Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
47 |
48 |
49 |
50 |

Legend

51 |

52 | Low: 0% to {{low_upper_bound}}% 53 | Medium: {{low_upper_bound}}% to {{high_lower_bound}}% 54 | High: {{high_lower_bound}}% to 100% 55 |

56 |

57 | Generated by php-code-coverage {{version}} using {{runtime}}{{generator}} at {{date}}. 58 |

59 |
60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/directory_item.html.dist: -------------------------------------------------------------------------------- 1 | 2 | {{icon}}{{name}} 3 | {{lines_bar}} 4 |
{{lines_executed_percent}}
5 |
{{lines_number}}
6 | {{methods_bar}} 7 |
{{methods_tested_percent}}
8 |
{{methods_number}}
9 | {{classes_bar}} 10 |
{{classes_tested_percent}}
11 |
{{classes_number}}
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/directory_item_branch.html.dist: -------------------------------------------------------------------------------- 1 | 2 | {{icon}}{{name}} 3 | {{lines_bar}} 4 |
{{lines_executed_percent}}
5 |
{{lines_number}}
6 | {{branches_bar}} 7 |
{{branches_executed_percent}}
8 |
{{branches_number}}
9 | {{paths_bar}} 10 |
{{paths_executed_percent}}
11 |
{{paths_number}}
12 | {{methods_bar}} 13 |
{{methods_tested_percent}}
14 |
{{methods_number}}
15 | {{classes_bar}} 16 |
{{classes_tested_percent}}
17 |
{{classes_number}}
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/file.html.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Code Coverage for {{full_path}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {{items}} 43 | 44 |
 
Code Coverage
 
Lines
Functions and Methods
Classes and Traits
45 |
46 | {{lines}} 47 | {{structure}} 48 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/file_branch.html.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Code Coverage for {{full_path}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {{items}} 45 | 46 |
 
Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
47 |
48 | {{lines}} 49 | {{structure}} 50 | 61 |
62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/file_item.html.dist: -------------------------------------------------------------------------------- 1 | 2 | {{name}} 3 | {{lines_bar}} 4 |
{{lines_executed_percent}}
5 |
{{lines_number}}
6 | {{methods_bar}} 7 |
{{methods_tested_percent}}
8 |
{{methods_number}}
9 | {{crap}} 10 | {{classes_bar}} 11 |
{{classes_tested_percent}}
12 |
{{classes_number}}
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/file_item_branch.html.dist: -------------------------------------------------------------------------------- 1 | 2 | {{name}} 3 | {{lines_bar}} 4 |
{{lines_executed_percent}}
5 |
{{lines_number}}
6 | {{branches_bar}} 7 |
{{branches_executed_percent}}
8 |
{{branches_number}}
9 | {{paths_bar}} 10 |
{{paths_executed_percent}}
11 |
{{paths_number}}
12 | {{methods_bar}} 13 |
{{methods_tested_percent}}
14 |
{{methods_number}}
15 | {{crap}} 16 | {{classes_bar}} 17 |
{{classes_tested_percent}}
18 |
{{classes_number}}
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/icons/file-code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/icons/file-directory.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/js/file.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | var $window = $(window) 3 | , $top_link = $('#toplink') 4 | , $body = $('body, html') 5 | , offset = $('#code').offset().top; 6 | 7 | $top_link.hide().click(function (event) { 8 | event.preventDefault(); 9 | $body.animate({scrollTop: 0}, 800); 10 | }); 11 | 12 | $window.scroll(function () { 13 | if ($window.scrollTop() > offset) { 14 | $top_link.fadeIn(); 15 | } else { 16 | $top_link.fadeOut(); 17 | } 18 | }); 19 | 20 | var $popovers = $('.popin > :first-child'); 21 | $('.popin').on({ 22 | 'click.popover': function (event) { 23 | event.stopPropagation(); 24 | 25 | var $container = $(this).children().first(); 26 | 27 | //Close all other popovers: 28 | $popovers.each(function () { 29 | var $current = $(this); 30 | if (!$current.is($container)) { 31 | $current.popover('hide'); 32 | } 33 | }); 34 | 35 | // Toggle this popover: 36 | $container.popover('toggle'); 37 | }, 38 | }); 39 | 40 | //Hide all popovers on outside click: 41 | $(document).click(function (event) { 42 | if ($(event.target).closest($('.popover')).length === 0) { 43 | $popovers.popover('hide'); 44 | } 45 | }); 46 | 47 | //Hide all popovers on escape: 48 | $(document).keyup(function (event) { 49 | if (event.key === 'Escape') { 50 | $popovers.popover('hide'); 51 | } 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/line.html.dist: -------------------------------------------------------------------------------- 1 | {{lineNumber}}{{lineContent}} 2 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/lines.html.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{lines}} 4 | 5 |
6 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/method_item.html.dist: -------------------------------------------------------------------------------- 1 | 2 | {{name}} 3 | {{lines_bar}} 4 |
{{lines_executed_percent}}
5 |
{{lines_number}}
6 | {{methods_bar}} 7 |
{{methods_tested_percent}}
8 |
{{methods_number}}
9 | {{crap}} 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/method_item_branch.html.dist: -------------------------------------------------------------------------------- 1 | 2 | {{name}} 3 | {{lines_bar}} 4 |
{{lines_executed_percent}}
5 |
{{lines_number}}
6 | {{branches_bar}} 7 |
{{branches_executed_percent}}
8 |
{{branches_number}}
9 | {{paths_bar}} 10 |
{{paths_executed_percent}}
11 |
{{paths_number}}
12 | {{methods_bar}} 13 |
{{methods_tested_percent}}
14 |
{{methods_number}}
15 | {{crap}} 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Report/Html/Renderer/Template/paths.html.dist: -------------------------------------------------------------------------------- 1 |
2 |

Paths

3 |

4 | Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not 5 | necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once. 6 | Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement 7 | always has an else as part of its logical flow even if you didn't write one. 8 |

9 | {{paths}} 10 | -------------------------------------------------------------------------------- /src/Report/PHP.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 | namespace SebastianBergmann\CodeCoverage\Report; 11 | 12 | use const PHP_EOL; 13 | use function dirname; 14 | use function file_put_contents; 15 | use function serialize; 16 | use function str_contains; 17 | use SebastianBergmann\CodeCoverage\CodeCoverage; 18 | use SebastianBergmann\CodeCoverage\Util\Filesystem; 19 | use SebastianBergmann\CodeCoverage\WriteOperationFailedException; 20 | 21 | final class PHP 22 | { 23 | public function process(CodeCoverage $coverage, ?string $target = null): string 24 | { 25 | $coverage->clearCache(); 26 | 27 | $buffer = " 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\Report; 11 | 12 | use SebastianBergmann\CodeCoverage\InvalidArgumentException; 13 | 14 | /** 15 | * @immutable 16 | */ 17 | final readonly class Thresholds 18 | { 19 | private int $lowUpperBound; 20 | private int $highLowerBound; 21 | 22 | public static function default(): self 23 | { 24 | return new self(50, 90); 25 | } 26 | 27 | /** 28 | * @throws InvalidArgumentException 29 | */ 30 | public static function from(int $lowUpperBound, int $highLowerBound): self 31 | { 32 | if ($lowUpperBound > $highLowerBound) { 33 | throw new InvalidArgumentException( 34 | '$lowUpperBound must not be larger than $highLowerBound', 35 | ); 36 | } 37 | 38 | return new self($lowUpperBound, $highLowerBound); 39 | } 40 | 41 | private function __construct(int $lowUpperBound, int $highLowerBound) 42 | { 43 | $this->lowUpperBound = $lowUpperBound; 44 | $this->highLowerBound = $highLowerBound; 45 | } 46 | 47 | public function lowUpperBound(): int 48 | { 49 | return $this->lowUpperBound; 50 | } 51 | 52 | public function highLowerBound(): int 53 | { 54 | return $this->highLowerBound; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Report/Xml/BuildInformation.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | use function assert; 13 | use function phpversion; 14 | use DateTimeImmutable; 15 | use DOMElement; 16 | use SebastianBergmann\Environment\Runtime; 17 | 18 | /** 19 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 20 | */ 21 | final readonly class BuildInformation 22 | { 23 | private DOMElement $contextNode; 24 | 25 | public function __construct(DOMElement $contextNode) 26 | { 27 | $this->contextNode = $contextNode; 28 | } 29 | 30 | public function setRuntimeInformation(Runtime $runtime): void 31 | { 32 | $runtimeNode = $this->nodeByName('runtime'); 33 | 34 | $runtimeNode->setAttribute('name', $runtime->getName()); 35 | $runtimeNode->setAttribute('version', $runtime->getVersion()); 36 | $runtimeNode->setAttribute('url', $runtime->getVendorUrl()); 37 | 38 | $driverNode = $this->nodeByName('driver'); 39 | 40 | if ($runtime->hasXdebug()) { 41 | $driverNode->setAttribute('name', 'xdebug'); 42 | $driverNode->setAttribute('version', phpversion('xdebug')); 43 | } 44 | 45 | if ($runtime->hasPCOV()) { 46 | $driverNode->setAttribute('name', 'pcov'); 47 | $driverNode->setAttribute('version', phpversion('pcov')); 48 | } 49 | } 50 | 51 | public function setBuildTime(DateTimeImmutable $date): void 52 | { 53 | $this->contextNode->setAttribute('time', $date->format('D M j G:i:s T Y')); 54 | } 55 | 56 | public function setGeneratorVersions(string $phpUnitVersion, string $coverageVersion): void 57 | { 58 | $this->contextNode->setAttribute('phpunit', $phpUnitVersion); 59 | $this->contextNode->setAttribute('coverage', $coverageVersion); 60 | } 61 | 62 | private function nodeByName(string $name): DOMElement 63 | { 64 | $node = $this->contextNode->getElementsByTagNameNS( 65 | 'https://schema.phpunit.de/coverage/1.0', 66 | $name, 67 | )->item(0); 68 | 69 | if ($node === null) { 70 | $node = $this->contextNode->appendChild( 71 | $this->contextNode->ownerDocument->createElementNS( 72 | 'https://schema.phpunit.de/coverage/1.0', 73 | $name, 74 | ), 75 | ); 76 | } 77 | 78 | assert($node instanceof DOMElement); 79 | 80 | return $node; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Report/Xml/Coverage.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | use DOMElement; 13 | use SebastianBergmann\CodeCoverage\ReportAlreadyFinalizedException; 14 | use XMLWriter; 15 | 16 | /** 17 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 18 | */ 19 | final class Coverage 20 | { 21 | private readonly XMLWriter $writer; 22 | private readonly DOMElement $contextNode; 23 | private bool $finalized = false; 24 | 25 | public function __construct(DOMElement $context, string $line) 26 | { 27 | $this->contextNode = $context; 28 | 29 | $this->writer = new XMLWriter; 30 | $this->writer->openMemory(); 31 | $this->writer->startElementNs(null, $context->nodeName, 'https://schema.phpunit.de/coverage/1.0'); 32 | $this->writer->writeAttribute('nr', $line); 33 | } 34 | 35 | /** 36 | * @throws ReportAlreadyFinalizedException 37 | */ 38 | public function addTest(string $test): void 39 | { 40 | if ($this->finalized) { 41 | // @codeCoverageIgnoreStart 42 | throw new ReportAlreadyFinalizedException; 43 | // @codeCoverageIgnoreEnd 44 | } 45 | 46 | $this->writer->startElement('covered'); 47 | $this->writer->writeAttribute('by', $test); 48 | $this->writer->endElement(); 49 | } 50 | 51 | public function finalize(): void 52 | { 53 | $this->writer->endElement(); 54 | 55 | $fragment = $this->contextNode->ownerDocument->createDocumentFragment(); 56 | $fragment->appendXML($this->writer->outputMemory()); 57 | 58 | $this->contextNode->parentNode->replaceChild( 59 | $fragment, 60 | $this->contextNode, 61 | ); 62 | 63 | $this->finalized = true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Report/Xml/Directory.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | /** 13 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 14 | */ 15 | final class Directory extends Node 16 | { 17 | } 18 | -------------------------------------------------------------------------------- /src/Report/Xml/File.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | use function assert; 13 | use DOMDocument; 14 | use DOMElement; 15 | 16 | /** 17 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 18 | */ 19 | class File 20 | { 21 | private readonly DOMDocument $dom; 22 | private readonly DOMElement $contextNode; 23 | 24 | public function __construct(DOMElement $context) 25 | { 26 | $this->dom = $context->ownerDocument; 27 | $this->contextNode = $context; 28 | } 29 | 30 | public function totals(): Totals 31 | { 32 | $totalsContainer = $this->contextNode->firstChild; 33 | 34 | if ($totalsContainer === null) { 35 | $totalsContainer = $this->contextNode->appendChild( 36 | $this->dom->createElementNS( 37 | 'https://schema.phpunit.de/coverage/1.0', 38 | 'totals', 39 | ), 40 | ); 41 | } 42 | 43 | assert($totalsContainer instanceof DOMElement); 44 | 45 | return new Totals($totalsContainer); 46 | } 47 | 48 | public function lineCoverage(string $line): Coverage 49 | { 50 | $coverage = $this->contextNode->getElementsByTagNameNS( 51 | 'https://schema.phpunit.de/coverage/1.0', 52 | 'coverage', 53 | )->item(0); 54 | 55 | if ($coverage === null) { 56 | $coverage = $this->contextNode->appendChild( 57 | $this->dom->createElementNS( 58 | 'https://schema.phpunit.de/coverage/1.0', 59 | 'coverage', 60 | ), 61 | ); 62 | } 63 | 64 | $lineNode = $coverage->appendChild( 65 | $this->dom->createElementNS( 66 | 'https://schema.phpunit.de/coverage/1.0', 67 | 'line', 68 | ), 69 | ); 70 | 71 | assert($lineNode instanceof DOMElement); 72 | 73 | return new Coverage($lineNode, $line); 74 | } 75 | 76 | protected function contextNode(): DOMElement 77 | { 78 | return $this->contextNode; 79 | } 80 | 81 | protected function dom(): DOMDocument 82 | { 83 | return $this->dom; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Report/Xml/Method.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | use DOMElement; 13 | 14 | /** 15 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final readonly class Method 18 | { 19 | private DOMElement $contextNode; 20 | 21 | public function __construct(DOMElement $context, string $name) 22 | { 23 | $this->contextNode = $context; 24 | 25 | $this->setName($name); 26 | } 27 | 28 | public function setSignature(string $signature): void 29 | { 30 | $this->contextNode->setAttribute('signature', $signature); 31 | } 32 | 33 | public function setLines(string $start, ?string $end = null): void 34 | { 35 | $this->contextNode->setAttribute('start', $start); 36 | 37 | if ($end !== null) { 38 | $this->contextNode->setAttribute('end', $end); 39 | } 40 | } 41 | 42 | public function setTotals(string $executable, string $executed, string $coverage): void 43 | { 44 | $this->contextNode->setAttribute('executable', $executable); 45 | $this->contextNode->setAttribute('executed', $executed); 46 | $this->contextNode->setAttribute('coverage', $coverage); 47 | } 48 | 49 | public function setCrap(string $crap): void 50 | { 51 | $this->contextNode->setAttribute('crap', $crap); 52 | } 53 | 54 | private function setName(string $name): void 55 | { 56 | $this->contextNode->setAttribute('name', $name); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Report/Xml/Node.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | use function assert; 13 | use DOMDocument; 14 | use DOMElement; 15 | 16 | /** 17 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 18 | */ 19 | abstract class Node 20 | { 21 | private DOMDocument $dom; 22 | private DOMElement $contextNode; 23 | 24 | public function __construct(DOMElement $context) 25 | { 26 | $this->setContextNode($context); 27 | } 28 | 29 | public function dom(): DOMDocument 30 | { 31 | return $this->dom; 32 | } 33 | 34 | public function totals(): Totals 35 | { 36 | $totalsContainer = $this->contextNode()->firstChild; 37 | 38 | if ($totalsContainer === null) { 39 | $totalsContainer = $this->contextNode()->appendChild( 40 | $this->dom->createElementNS( 41 | 'https://schema.phpunit.de/coverage/1.0', 42 | 'totals', 43 | ), 44 | ); 45 | } 46 | 47 | assert($totalsContainer instanceof DOMElement); 48 | 49 | return new Totals($totalsContainer); 50 | } 51 | 52 | public function addDirectory(string $name): Directory 53 | { 54 | $dirNode = $this->dom()->createElementNS( 55 | 'https://schema.phpunit.de/coverage/1.0', 56 | 'directory', 57 | ); 58 | 59 | $dirNode->setAttribute('name', $name); 60 | $this->contextNode()->appendChild($dirNode); 61 | 62 | return new Directory($dirNode); 63 | } 64 | 65 | public function addFile(string $name, string $href): File 66 | { 67 | $fileNode = $this->dom()->createElementNS( 68 | 'https://schema.phpunit.de/coverage/1.0', 69 | 'file', 70 | ); 71 | 72 | $fileNode->setAttribute('name', $name); 73 | $fileNode->setAttribute('href', $href); 74 | $this->contextNode()->appendChild($fileNode); 75 | 76 | return new File($fileNode); 77 | } 78 | 79 | protected function setContextNode(DOMElement $context): void 80 | { 81 | $this->dom = $context->ownerDocument; 82 | $this->contextNode = $context; 83 | } 84 | 85 | protected function contextNode(): DOMElement 86 | { 87 | return $this->contextNode; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Report/Xml/Project.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | use function assert; 13 | use DOMDocument; 14 | use DOMElement; 15 | 16 | /** 17 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 18 | */ 19 | final class Project extends Node 20 | { 21 | /** 22 | * @phpstan-ignore constructor.missingParentCall 23 | */ 24 | public function __construct(string $directory) 25 | { 26 | $this->init(); 27 | $this->setProjectSourceDirectory($directory); 28 | } 29 | 30 | public function projectSourceDirectory(): string 31 | { 32 | return $this->contextNode()->getAttribute('source'); 33 | } 34 | 35 | public function buildInformation(): BuildInformation 36 | { 37 | $buildNode = $this->dom()->getElementsByTagNameNS( 38 | 'https://schema.phpunit.de/coverage/1.0', 39 | 'build', 40 | )->item(0); 41 | 42 | if ($buildNode === null) { 43 | $buildNode = $this->dom()->documentElement->appendChild( 44 | $this->dom()->createElementNS( 45 | 'https://schema.phpunit.de/coverage/1.0', 46 | 'build', 47 | ), 48 | ); 49 | } 50 | 51 | assert($buildNode instanceof DOMElement); 52 | 53 | return new BuildInformation($buildNode); 54 | } 55 | 56 | public function tests(): Tests 57 | { 58 | $testsNode = $this->contextNode()->getElementsByTagNameNS( 59 | 'https://schema.phpunit.de/coverage/1.0', 60 | 'tests', 61 | )->item(0); 62 | 63 | if ($testsNode === null) { 64 | $testsNode = $this->contextNode()->appendChild( 65 | $this->dom()->createElementNS( 66 | 'https://schema.phpunit.de/coverage/1.0', 67 | 'tests', 68 | ), 69 | ); 70 | } 71 | 72 | assert($testsNode instanceof DOMElement); 73 | 74 | return new Tests($testsNode); 75 | } 76 | 77 | public function asDom(): DOMDocument 78 | { 79 | return $this->dom(); 80 | } 81 | 82 | private function init(): void 83 | { 84 | $dom = new DOMDocument; 85 | $dom->loadXML(''); 86 | 87 | $this->setContextNode( 88 | $dom->getElementsByTagNameNS( 89 | 'https://schema.phpunit.de/coverage/1.0', 90 | 'project', 91 | )->item(0), 92 | ); 93 | } 94 | 95 | private function setProjectSourceDirectory(string $name): void 96 | { 97 | $this->contextNode()->setAttribute('source', $name); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Report/Xml/Report.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | use function assert; 13 | use function basename; 14 | use function dirname; 15 | use DOMDocument; 16 | use DOMElement; 17 | 18 | /** 19 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 20 | */ 21 | final class Report extends File 22 | { 23 | public function __construct(string $name) 24 | { 25 | $dom = new DOMDocument; 26 | $dom->loadXML(''); 27 | 28 | $contextNode = $dom->getElementsByTagNameNS( 29 | 'https://schema.phpunit.de/coverage/1.0', 30 | 'file', 31 | )->item(0); 32 | 33 | parent::__construct($contextNode); 34 | 35 | $this->setName($name); 36 | } 37 | 38 | public function asDom(): DOMDocument 39 | { 40 | return $this->dom(); 41 | } 42 | 43 | public function functionObject(string $name): Method 44 | { 45 | $node = $this->contextNode()->appendChild( 46 | $this->dom()->createElementNS( 47 | 'https://schema.phpunit.de/coverage/1.0', 48 | 'function', 49 | ), 50 | ); 51 | 52 | assert($node instanceof DOMElement); 53 | 54 | return new Method($node, $name); 55 | } 56 | 57 | public function classObject(string $name): Unit 58 | { 59 | return $this->unitObject('class', $name); 60 | } 61 | 62 | public function traitObject(string $name): Unit 63 | { 64 | return $this->unitObject('trait', $name); 65 | } 66 | 67 | public function source(): Source 68 | { 69 | $source = $this->contextNode()->getElementsByTagNameNS( 70 | 'https://schema.phpunit.de/coverage/1.0', 71 | 'source', 72 | )->item(0); 73 | 74 | if ($source === null) { 75 | $source = $this->contextNode()->appendChild( 76 | $this->dom()->createElementNS( 77 | 'https://schema.phpunit.de/coverage/1.0', 78 | 'source', 79 | ), 80 | ); 81 | } 82 | 83 | assert($source instanceof DOMElement); 84 | 85 | return new Source($source); 86 | } 87 | 88 | private function setName(string $name): void 89 | { 90 | $this->contextNode()->setAttribute('name', basename($name)); 91 | $this->contextNode()->setAttribute('path', dirname($name)); 92 | } 93 | 94 | private function unitObject(string $tagName, string $name): Unit 95 | { 96 | $node = $this->contextNode()->appendChild( 97 | $this->dom()->createElementNS( 98 | 'https://schema.phpunit.de/coverage/1.0', 99 | $tagName, 100 | ), 101 | ); 102 | 103 | assert($node instanceof DOMElement); 104 | 105 | return new Unit($node, $name); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Report/Xml/Source.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | use DOMElement; 13 | use TheSeer\Tokenizer\NamespaceUri; 14 | use TheSeer\Tokenizer\Tokenizer; 15 | use TheSeer\Tokenizer\XMLSerializer; 16 | 17 | /** 18 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 19 | */ 20 | final readonly class Source 21 | { 22 | private DOMElement $context; 23 | 24 | public function __construct(DOMElement $context) 25 | { 26 | $this->context = $context; 27 | } 28 | 29 | public function setSourceCode(string $source): void 30 | { 31 | $context = $this->context; 32 | 33 | $tokens = (new Tokenizer)->parse($source); 34 | $srcDom = (new XMLSerializer(new NamespaceUri($context->namespaceURI)))->toDom($tokens); 35 | 36 | $context->parentNode->replaceChild( 37 | $context->ownerDocument->importNode($srcDom->documentElement, true), 38 | $context, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Report/Xml/Tests.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | use function assert; 13 | use DOMElement; 14 | use SebastianBergmann\CodeCoverage\CodeCoverage; 15 | 16 | /** 17 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 18 | * 19 | * @phpstan-import-type TestType from CodeCoverage 20 | */ 21 | final readonly class Tests 22 | { 23 | private DOMElement $contextNode; 24 | 25 | public function __construct(DOMElement $context) 26 | { 27 | $this->contextNode = $context; 28 | } 29 | 30 | /** 31 | * @param TestType $result 32 | */ 33 | public function addTest(string $test, array $result): void 34 | { 35 | $node = $this->contextNode->appendChild( 36 | $this->contextNode->ownerDocument->createElementNS( 37 | 'https://schema.phpunit.de/coverage/1.0', 38 | 'test', 39 | ), 40 | ); 41 | 42 | assert($node instanceof DOMElement); 43 | 44 | $node->setAttribute('name', $test); 45 | $node->setAttribute('size', $result['size']); 46 | $node->setAttribute('status', $result['status']); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Report/Xml/Totals.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | use function sprintf; 13 | use DOMElement; 14 | use SebastianBergmann\CodeCoverage\Util\Percentage; 15 | 16 | /** 17 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 18 | */ 19 | final readonly class Totals 20 | { 21 | private DOMElement $linesNode; 22 | private DOMElement $methodsNode; 23 | private DOMElement $functionsNode; 24 | private DOMElement $classesNode; 25 | private DOMElement $traitsNode; 26 | 27 | public function __construct(DOMElement $container) 28 | { 29 | $dom = $container->ownerDocument; 30 | 31 | $this->linesNode = $dom->createElementNS( 32 | 'https://schema.phpunit.de/coverage/1.0', 33 | 'lines', 34 | ); 35 | 36 | $this->methodsNode = $dom->createElementNS( 37 | 'https://schema.phpunit.de/coverage/1.0', 38 | 'methods', 39 | ); 40 | 41 | $this->functionsNode = $dom->createElementNS( 42 | 'https://schema.phpunit.de/coverage/1.0', 43 | 'functions', 44 | ); 45 | 46 | $this->classesNode = $dom->createElementNS( 47 | 'https://schema.phpunit.de/coverage/1.0', 48 | 'classes', 49 | ); 50 | 51 | $this->traitsNode = $dom->createElementNS( 52 | 'https://schema.phpunit.de/coverage/1.0', 53 | 'traits', 54 | ); 55 | 56 | $container->appendChild($this->linesNode); 57 | $container->appendChild($this->methodsNode); 58 | $container->appendChild($this->functionsNode); 59 | $container->appendChild($this->classesNode); 60 | $container->appendChild($this->traitsNode); 61 | } 62 | 63 | public function setNumLines(int $loc, int $cloc, int $ncloc, int $executable, int $executed): void 64 | { 65 | $this->linesNode->setAttribute('total', (string) $loc); 66 | $this->linesNode->setAttribute('comments', (string) $cloc); 67 | $this->linesNode->setAttribute('code', (string) $ncloc); 68 | $this->linesNode->setAttribute('executable', (string) $executable); 69 | $this->linesNode->setAttribute('executed', (string) $executed); 70 | $this->linesNode->setAttribute( 71 | 'percent', 72 | $executable === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($executed, $executable)->asFloat()), 73 | ); 74 | } 75 | 76 | public function setNumClasses(int $count, int $tested): void 77 | { 78 | $this->classesNode->setAttribute('count', (string) $count); 79 | $this->classesNode->setAttribute('tested', (string) $tested); 80 | $this->classesNode->setAttribute( 81 | 'percent', 82 | $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), 83 | ); 84 | } 85 | 86 | public function setNumTraits(int $count, int $tested): void 87 | { 88 | $this->traitsNode->setAttribute('count', (string) $count); 89 | $this->traitsNode->setAttribute('tested', (string) $tested); 90 | $this->traitsNode->setAttribute( 91 | 'percent', 92 | $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), 93 | ); 94 | } 95 | 96 | public function setNumMethods(int $count, int $tested): void 97 | { 98 | $this->methodsNode->setAttribute('count', (string) $count); 99 | $this->methodsNode->setAttribute('tested', (string) $tested); 100 | $this->methodsNode->setAttribute( 101 | 'percent', 102 | $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), 103 | ); 104 | } 105 | 106 | public function setNumFunctions(int $count, int $tested): void 107 | { 108 | $this->functionsNode->setAttribute('count', (string) $count); 109 | $this->functionsNode->setAttribute('tested', (string) $tested); 110 | $this->functionsNode->setAttribute( 111 | 'percent', 112 | $count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()), 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Report/Xml/Unit.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 | namespace SebastianBergmann\CodeCoverage\Report\Xml; 11 | 12 | use function assert; 13 | use DOMElement; 14 | 15 | /** 16 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 17 | */ 18 | final readonly class Unit 19 | { 20 | private DOMElement $contextNode; 21 | 22 | public function __construct(DOMElement $context, string $name) 23 | { 24 | $this->contextNode = $context; 25 | 26 | $this->setName($name); 27 | } 28 | 29 | public function setLines(int $start, int $executable, int $executed): void 30 | { 31 | $this->contextNode->setAttribute('start', (string) $start); 32 | $this->contextNode->setAttribute('executable', (string) $executable); 33 | $this->contextNode->setAttribute('executed', (string) $executed); 34 | } 35 | 36 | public function setCrap(float $crap): void 37 | { 38 | $this->contextNode->setAttribute('crap', (string) $crap); 39 | } 40 | 41 | public function setNamespace(string $namespace): void 42 | { 43 | $node = $this->contextNode->getElementsByTagNameNS( 44 | 'https://schema.phpunit.de/coverage/1.0', 45 | 'namespace', 46 | )->item(0); 47 | 48 | if ($node === null) { 49 | $node = $this->contextNode->appendChild( 50 | $this->contextNode->ownerDocument->createElementNS( 51 | 'https://schema.phpunit.de/coverage/1.0', 52 | 'namespace', 53 | ), 54 | ); 55 | } 56 | 57 | assert($node instanceof DOMElement); 58 | 59 | $node->setAttribute('name', $namespace); 60 | } 61 | 62 | public function addMethod(string $name): Method 63 | { 64 | $node = $this->contextNode->appendChild( 65 | $this->contextNode->ownerDocument->createElementNS( 66 | 'https://schema.phpunit.de/coverage/1.0', 67 | 'method', 68 | ), 69 | ); 70 | 71 | assert($node instanceof DOMElement); 72 | 73 | return new Method($node, $name); 74 | } 75 | 76 | private function setName(string $name): void 77 | { 78 | $this->contextNode->setAttribute('name', $name); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/StaticAnalysis/CacheWarmer.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | use function file_get_contents; 13 | use SebastianBergmann\CodeCoverage\Filter; 14 | 15 | /** 16 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 17 | */ 18 | final readonly class CacheWarmer 19 | { 20 | /** 21 | * @return array{cacheHits: non-negative-int, cacheMisses: non-negative-int} 22 | */ 23 | public function warmCache(string $cacheDirectory, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode, Filter $filter): array 24 | { 25 | $analyser = new CachingSourceAnalyser( 26 | $cacheDirectory, 27 | new ParsingSourceAnalyser, 28 | ); 29 | 30 | foreach ($filter->files() as $file) { 31 | $analyser->analyse( 32 | $file, 33 | file_get_contents($file), 34 | $useAnnotationsForIgnoringCode, 35 | $ignoreDeprecatedCode, 36 | ); 37 | } 38 | 39 | return [ 40 | 'cacheHits' => $analyser->cacheHits(), 41 | 'cacheMisses' => $analyser->cacheMisses(), 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/StaticAnalysis/CachingSourceAnalyser.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | use const DIRECTORY_SEPARATOR; 13 | use function file_get_contents; 14 | use function file_put_contents; 15 | use function hash; 16 | use function implode; 17 | use function is_file; 18 | use function serialize; 19 | use function unserialize; 20 | use SebastianBergmann\CodeCoverage\Util\Filesystem; 21 | use SebastianBergmann\CodeCoverage\Version; 22 | 23 | /** 24 | * @internal This interface is not covered by the backward compatibility promise for phpunit/php-code-coverage 25 | */ 26 | final class CachingSourceAnalyser implements SourceAnalyser 27 | { 28 | /** 29 | * @var non-empty-string 30 | */ 31 | private readonly string $directory; 32 | private readonly SourceAnalyser $sourceAnalyser; 33 | 34 | /** 35 | * @var non-negative-int 36 | */ 37 | private int $cacheHits = 0; 38 | 39 | /** 40 | * @var non-negative-int 41 | */ 42 | private int $cacheMisses = 0; 43 | 44 | public function __construct(string $directory, SourceAnalyser $sourceAnalyser) 45 | { 46 | Filesystem::createDirectory($directory); 47 | 48 | $this->directory = $directory; 49 | $this->sourceAnalyser = $sourceAnalyser; 50 | } 51 | 52 | /** 53 | * @param non-empty-string $sourceCodeFile 54 | */ 55 | public function analyse(string $sourceCodeFile, string $sourceCode, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode): AnalysisResult 56 | { 57 | $cacheFile = $this->cacheFile( 58 | $sourceCode, 59 | $useAnnotationsForIgnoringCode, 60 | $ignoreDeprecatedCode, 61 | ); 62 | 63 | $cachedAnalysisResult = $this->read($cacheFile); 64 | 65 | if ($cachedAnalysisResult !== false) { 66 | $this->cacheHits++; 67 | 68 | return $cachedAnalysisResult; 69 | } 70 | 71 | $this->cacheMisses++; 72 | 73 | $analysisResult = $this->sourceAnalyser->analyse( 74 | $sourceCodeFile, 75 | $sourceCode, 76 | $useAnnotationsForIgnoringCode, 77 | $ignoreDeprecatedCode, 78 | ); 79 | 80 | $this->write($cacheFile, $analysisResult); 81 | 82 | return $analysisResult; 83 | } 84 | 85 | /** 86 | * @return non-negative-int 87 | */ 88 | public function cacheHits(): int 89 | { 90 | return $this->cacheHits; 91 | } 92 | 93 | /** 94 | * @return non-negative-int 95 | */ 96 | public function cacheMisses(): int 97 | { 98 | return $this->cacheMisses; 99 | } 100 | 101 | /** 102 | * @param non-empty-string $cacheFile 103 | */ 104 | private function read(string $cacheFile): AnalysisResult|false 105 | { 106 | if (!is_file($cacheFile)) { 107 | return false; 108 | } 109 | 110 | return unserialize( 111 | file_get_contents($cacheFile), 112 | [ 113 | 'allowed_classes' => [ 114 | AnalysisResult::class, 115 | Class_::class, 116 | Function_::class, 117 | Interface_::class, 118 | LinesOfCode::class, 119 | Method::class, 120 | Trait_::class, 121 | ], 122 | ], 123 | ); 124 | } 125 | 126 | /** 127 | * @param non-empty-string $cacheFile 128 | */ 129 | private function write(string $cacheFile, AnalysisResult $result): void 130 | { 131 | file_put_contents( 132 | $cacheFile, 133 | serialize($result), 134 | ); 135 | } 136 | 137 | private function cacheFile(string $source, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode): string 138 | { 139 | $cacheKey = hash( 140 | 'sha256', 141 | implode( 142 | "\0", 143 | [ 144 | $source, 145 | Version::id(), 146 | $useAnnotationsForIgnoringCode, 147 | $ignoreDeprecatedCode, 148 | ], 149 | ), 150 | ); 151 | 152 | return $this->directory . DIRECTORY_SEPARATOR . $cacheKey; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/StaticAnalysis/FileAnalyser.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | use function file_get_contents; 13 | 14 | /** 15 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final class FileAnalyser 18 | { 19 | private readonly SourceAnalyser $sourceAnalyser; 20 | private readonly bool $useAnnotationsForIgnoringCode; 21 | private readonly bool $ignoreDeprecatedCode; 22 | 23 | /** 24 | * @var array 25 | */ 26 | private array $cache = []; 27 | 28 | public function __construct(SourceAnalyser $sourceAnalyser, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode) 29 | { 30 | $this->sourceAnalyser = $sourceAnalyser; 31 | $this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode; 32 | $this->ignoreDeprecatedCode = $ignoreDeprecatedCode; 33 | } 34 | 35 | /** 36 | * @param non-empty-string $sourceCodeFile 37 | */ 38 | public function analyse(string $sourceCodeFile): AnalysisResult 39 | { 40 | if (isset($this->cache[$sourceCodeFile])) { 41 | return $this->cache[$sourceCodeFile]; 42 | } 43 | 44 | $this->cache[$sourceCodeFile] = $this->sourceAnalyser->analyse( 45 | $sourceCodeFile, 46 | file_get_contents($sourceCodeFile), 47 | $this->useAnnotationsForIgnoringCode, 48 | $this->ignoreDeprecatedCode, 49 | ); 50 | 51 | return $this->cache[$sourceCodeFile]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/StaticAnalysis/ParsingSourceAnalyser.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | use const T_COMMENT; 13 | use const T_DOC_COMMENT; 14 | use function array_merge; 15 | use function array_unique; 16 | use function assert; 17 | use function is_array; 18 | use function max; 19 | use function range; 20 | use function sort; 21 | use function sprintf; 22 | use function substr_count; 23 | use function token_get_all; 24 | use function trim; 25 | use PhpParser\Error; 26 | use PhpParser\NodeTraverser; 27 | use PhpParser\NodeVisitor\NameResolver; 28 | use PhpParser\ParserFactory; 29 | use SebastianBergmann\CodeCoverage\ParserException; 30 | use SebastianBergmann\LinesOfCode\LineCountingVisitor; 31 | 32 | /** 33 | * @internal This interface is not covered by the backward compatibility promise for phpunit/php-code-coverage 34 | */ 35 | final readonly class ParsingSourceAnalyser implements SourceAnalyser 36 | { 37 | /** 38 | * @param non-empty-string $sourceCodeFile 39 | */ 40 | public function analyse(string $sourceCodeFile, string $sourceCode, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode): AnalysisResult 41 | { 42 | $linesOfCode = max(substr_count($sourceCode, "\n") + 1, substr_count($sourceCode, "\r") + 1); 43 | 44 | if ($linesOfCode === 0 && $sourceCode !== '') { 45 | $linesOfCode = 1; 46 | } 47 | 48 | assert($linesOfCode > 0); 49 | 50 | $parser = (new ParserFactory)->createForHostVersion(); 51 | 52 | try { 53 | $nodes = $parser->parse($sourceCode); 54 | 55 | assert($nodes !== null); 56 | 57 | $traverser = new NodeTraverser; 58 | $codeUnitFindingVisitor = new CodeUnitFindingVisitor($sourceCodeFile); 59 | $lineCountingVisitor = new LineCountingVisitor($linesOfCode); 60 | $ignoredLinesFindingVisitor = new IgnoredLinesFindingVisitor($useAnnotationsForIgnoringCode, $ignoreDeprecatedCode); 61 | $executableLinesFindingVisitor = new ExecutableLinesFindingVisitor($sourceCode); 62 | 63 | $traverser->addVisitor(new NameResolver); 64 | $traverser->addVisitor(new AttributeParentConnectingVisitor); 65 | $traverser->addVisitor($codeUnitFindingVisitor); 66 | $traverser->addVisitor($lineCountingVisitor); 67 | $traverser->addVisitor($ignoredLinesFindingVisitor); 68 | $traverser->addVisitor($executableLinesFindingVisitor); 69 | 70 | /* @noinspection UnusedFunctionResultInspection */ 71 | $traverser->traverse($nodes); 72 | // @codeCoverageIgnoreStart 73 | } catch (Error $error) { 74 | throw new ParserException( 75 | sprintf( 76 | 'Cannot parse %s: %s', 77 | $sourceCodeFile, 78 | $error->getMessage(), 79 | ), 80 | $error->getCode(), 81 | $error, 82 | ); 83 | } 84 | // @codeCoverageIgnoreEnd 85 | 86 | $ignoredLines = array_unique( 87 | array_merge( 88 | $this->findLinesIgnoredByLineBasedAnnotations( 89 | $sourceCodeFile, 90 | $sourceCode, 91 | $useAnnotationsForIgnoringCode, 92 | ), 93 | $ignoredLinesFindingVisitor->ignoredLines(), 94 | ), 95 | ); 96 | 97 | sort($ignoredLines); 98 | 99 | return new AnalysisResult( 100 | $codeUnitFindingVisitor->interfaces(), 101 | $codeUnitFindingVisitor->classes(), 102 | $codeUnitFindingVisitor->traits(), 103 | $codeUnitFindingVisitor->functions(), 104 | new LinesOfCode( 105 | $lineCountingVisitor->result()->linesOfCode(), 106 | $lineCountingVisitor->result()->commentLinesOfCode(), 107 | $lineCountingVisitor->result()->nonCommentLinesOfCode(), 108 | ), 109 | $executableLinesFindingVisitor->executableLinesGroupedByBranch(), 110 | $ignoredLines, 111 | ); 112 | } 113 | 114 | /** 115 | * @return array 116 | */ 117 | private function findLinesIgnoredByLineBasedAnnotations(string $filename, string $source, bool $useAnnotationsForIgnoringCode): array 118 | { 119 | if (!$useAnnotationsForIgnoringCode) { 120 | return []; 121 | } 122 | 123 | $result = []; 124 | $start = false; 125 | 126 | foreach (token_get_all($source) as $token) { 127 | if (!is_array($token) || 128 | !(T_COMMENT === $token[0] || T_DOC_COMMENT === $token[0])) { 129 | continue; 130 | } 131 | 132 | $comment = trim($token[1]); 133 | 134 | if ($comment === '// @codeCoverageIgnore' || 135 | $comment === '//@codeCoverageIgnore') { 136 | $result[] = $token[2]; 137 | 138 | continue; 139 | } 140 | 141 | if ($comment === '// @codeCoverageIgnoreStart' || 142 | $comment === '//@codeCoverageIgnoreStart') { 143 | $start = $token[2]; 144 | 145 | continue; 146 | } 147 | 148 | if ($comment === '// @codeCoverageIgnoreEnd' || 149 | $comment === '//@codeCoverageIgnoreEnd') { 150 | if (false === $start) { 151 | $start = $token[2]; 152 | } 153 | 154 | $result = array_merge( 155 | $result, 156 | range($start, $token[2]), 157 | ); 158 | } 159 | } 160 | 161 | return $result; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/StaticAnalysis/SourceAnalyser.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | /** 13 | * @internal This interface is not covered by the backward compatibility promise for phpunit/php-code-coverage 14 | */ 15 | interface SourceAnalyser 16 | { 17 | /** 18 | * @param non-empty-string $sourceCodeFile 19 | */ 20 | public function analyse(string $sourceCodeFile, string $sourceCode, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode): AnalysisResult; 21 | } 22 | -------------------------------------------------------------------------------- /src/StaticAnalysis/Value/AnalysisResult.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | /** 13 | * @phpstan-type LinesType array 14 | * 15 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final readonly class AnalysisResult 18 | { 19 | /** 20 | * @var array 21 | */ 22 | private array $interfaces; 23 | 24 | /** 25 | * @var array 26 | */ 27 | private array $classes; 28 | 29 | /** 30 | * @var array 31 | */ 32 | private array $traits; 33 | 34 | /** 35 | * @var array 36 | */ 37 | private array $functions; 38 | private LinesOfCode $linesOfCode; 39 | 40 | /** 41 | * @var LinesType 42 | */ 43 | private array $executableLines; 44 | 45 | /** 46 | * @var LinesType 47 | */ 48 | private array $ignoredLines; 49 | 50 | /** 51 | * @param array $interfaces 52 | * @param array $classes 53 | * @param array $traits 54 | * @param array $functions 55 | * @param LinesType $executableLines 56 | * @param LinesType $ignoredLines 57 | */ 58 | public function __construct(array $interfaces, array $classes, array $traits, array $functions, LinesOfCode $linesOfCode, array $executableLines, array $ignoredLines) 59 | { 60 | $this->interfaces = $interfaces; 61 | $this->classes = $classes; 62 | $this->traits = $traits; 63 | $this->functions = $functions; 64 | $this->linesOfCode = $linesOfCode; 65 | $this->executableLines = $executableLines; 66 | $this->ignoredLines = $ignoredLines; 67 | } 68 | 69 | /** 70 | * @return array 71 | */ 72 | public function interfaces(): array 73 | { 74 | return $this->interfaces; 75 | } 76 | 77 | /** 78 | * @return array 79 | */ 80 | public function classes(): array 81 | { 82 | return $this->classes; 83 | } 84 | 85 | /** 86 | * @return array 87 | */ 88 | public function traits(): array 89 | { 90 | return $this->traits; 91 | } 92 | 93 | /** 94 | * @return array 95 | */ 96 | public function functions(): array 97 | { 98 | return $this->functions; 99 | } 100 | 101 | public function linesOfCode(): LinesOfCode 102 | { 103 | return $this->linesOfCode; 104 | } 105 | 106 | /** 107 | * @return LinesType 108 | */ 109 | public function executableLines(): array 110 | { 111 | return $this->executableLines; 112 | } 113 | 114 | /** 115 | * @return LinesType 116 | */ 117 | public function ignoredLines(): array 118 | { 119 | return $this->ignoredLines; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/StaticAnalysis/Value/Class_.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | /** 13 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 14 | */ 15 | final readonly class Class_ 16 | { 17 | /** 18 | * @var non-empty-string 19 | */ 20 | private string $name; 21 | 22 | /** 23 | * @var non-empty-string 24 | */ 25 | private string $namespacedName; 26 | private string $namespace; 27 | 28 | /** 29 | * @var non-empty-string 30 | */ 31 | private string $file; 32 | 33 | /** 34 | * @var non-negative-int 35 | */ 36 | private int $startLine; 37 | 38 | /** 39 | * @var non-negative-int 40 | */ 41 | private int $endLine; 42 | 43 | /** 44 | * @var ?non-empty-string 45 | */ 46 | private ?string $parentClass; 47 | 48 | /** 49 | * @var list 50 | */ 51 | private array $interfaces; 52 | 53 | /** 54 | * @var list 55 | */ 56 | private array $traits; 57 | 58 | /** 59 | * @var array 60 | */ 61 | private array $methods; 62 | 63 | /** 64 | * @param non-empty-string $name 65 | * @param non-empty-string $namespacedName 66 | * @param non-empty-string $file 67 | * @param non-negative-int $startLine 68 | * @param non-negative-int $endLine 69 | * @param ?non-empty-string $parentClass 70 | * @param list $interfaces 71 | * @param list $traits 72 | * @param array $methods 73 | */ 74 | public function __construct(string $name, string $namespacedName, string $namespace, string $file, int $startLine, int $endLine, ?string $parentClass, array $interfaces, array $traits, array $methods) 75 | { 76 | $this->name = $name; 77 | $this->namespacedName = $namespacedName; 78 | $this->namespace = $namespace; 79 | $this->file = $file; 80 | $this->startLine = $startLine; 81 | $this->endLine = $endLine; 82 | $this->parentClass = $parentClass; 83 | $this->interfaces = $interfaces; 84 | $this->traits = $traits; 85 | $this->methods = $methods; 86 | } 87 | 88 | /** 89 | * @return non-empty-string 90 | */ 91 | public function name(): string 92 | { 93 | return $this->name; 94 | } 95 | 96 | /** 97 | * @return non-empty-string 98 | */ 99 | public function namespacedName(): string 100 | { 101 | return $this->namespacedName; 102 | } 103 | 104 | public function isNamespaced(): bool 105 | { 106 | return $this->namespace !== ''; 107 | } 108 | 109 | public function namespace(): string 110 | { 111 | return $this->namespace; 112 | } 113 | 114 | /** 115 | * @return non-empty-string 116 | */ 117 | public function file(): string 118 | { 119 | return $this->file; 120 | } 121 | 122 | /** 123 | * @return non-negative-int 124 | */ 125 | public function startLine(): int 126 | { 127 | return $this->startLine; 128 | } 129 | 130 | /** 131 | * @return non-negative-int 132 | */ 133 | public function endLine(): int 134 | { 135 | return $this->endLine; 136 | } 137 | 138 | public function hasParent(): bool 139 | { 140 | return $this->parentClass !== null; 141 | } 142 | 143 | /** 144 | * @return ?non-empty-string 145 | */ 146 | public function parentClass(): ?string 147 | { 148 | return $this->parentClass; 149 | } 150 | 151 | /** 152 | * @return list 153 | */ 154 | public function interfaces(): array 155 | { 156 | return $this->interfaces; 157 | } 158 | 159 | /** 160 | * @return list 161 | */ 162 | public function traits(): array 163 | { 164 | return $this->traits; 165 | } 166 | 167 | /** 168 | * @return array 169 | */ 170 | public function methods(): array 171 | { 172 | return $this->methods; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/StaticAnalysis/Value/Function_.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | /** 13 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 14 | */ 15 | final readonly class Function_ 16 | { 17 | /** 18 | * @var non-empty-string 19 | */ 20 | private string $name; 21 | 22 | /** 23 | * @var non-empty-string 24 | */ 25 | private string $namespacedName; 26 | private string $namespace; 27 | 28 | /** 29 | * @var non-negative-int 30 | */ 31 | private int $startLine; 32 | 33 | /** 34 | * @var non-negative-int 35 | */ 36 | private int $endLine; 37 | 38 | /** 39 | * @var non-empty-string 40 | */ 41 | private string $signature; 42 | 43 | /** 44 | * @var positive-int 45 | */ 46 | private int $cyclomaticComplexity; 47 | 48 | /** 49 | * @param non-empty-string $name 50 | * @param non-empty-string $namespacedName 51 | * @param non-negative-int $startLine 52 | * @param non-negative-int $endLine 53 | * @param non-empty-string $signature 54 | * @param positive-int $cyclomaticComplexity 55 | */ 56 | public function __construct(string $name, string $namespacedName, string $namespace, int $startLine, int $endLine, string $signature, int $cyclomaticComplexity) 57 | { 58 | $this->name = $name; 59 | $this->namespacedName = $namespacedName; 60 | $this->namespace = $namespace; 61 | $this->startLine = $startLine; 62 | $this->endLine = $endLine; 63 | $this->signature = $signature; 64 | $this->cyclomaticComplexity = $cyclomaticComplexity; 65 | } 66 | 67 | /** 68 | * @return non-empty-string 69 | */ 70 | public function name(): string 71 | { 72 | return $this->name; 73 | } 74 | 75 | /** 76 | * @return non-empty-string 77 | */ 78 | public function namespacedName(): string 79 | { 80 | return $this->namespacedName; 81 | } 82 | 83 | public function isNamespaced(): bool 84 | { 85 | return $this->namespace !== ''; 86 | } 87 | 88 | public function namespace(): string 89 | { 90 | return $this->namespace; 91 | } 92 | 93 | /** 94 | * @return non-negative-int 95 | */ 96 | public function startLine(): int 97 | { 98 | return $this->startLine; 99 | } 100 | 101 | /** 102 | * @return non-negative-int 103 | */ 104 | public function endLine(): int 105 | { 106 | return $this->endLine; 107 | } 108 | 109 | /** 110 | * @return non-empty-string 111 | */ 112 | public function signature(): string 113 | { 114 | return $this->signature; 115 | } 116 | 117 | /** 118 | * @return positive-int 119 | */ 120 | public function cyclomaticComplexity(): int 121 | { 122 | return $this->cyclomaticComplexity; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/StaticAnalysis/Value/Interface_.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | /** 13 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 14 | */ 15 | final readonly class Interface_ 16 | { 17 | /** 18 | * @var non-empty-string 19 | */ 20 | private string $name; 21 | 22 | /** 23 | * @var non-empty-string 24 | */ 25 | private string $namespacedName; 26 | private string $namespace; 27 | 28 | /** 29 | * @var non-negative-int 30 | */ 31 | private int $startLine; 32 | 33 | /** 34 | * @var non-negative-int 35 | */ 36 | private int $endLine; 37 | 38 | /** 39 | * @var list 40 | */ 41 | private array $parentInterfaces; 42 | 43 | /** 44 | * @param non-empty-string $name 45 | * @param non-empty-string $namespacedName 46 | * @param non-negative-int $startLine 47 | * @param non-negative-int $endLine 48 | * @param list $parentInterfaces 49 | */ 50 | public function __construct(string $name, string $namespacedName, string $namespace, int $startLine, int $endLine, array $parentInterfaces) 51 | { 52 | $this->name = $name; 53 | $this->namespacedName = $namespacedName; 54 | $this->namespace = $namespace; 55 | $this->startLine = $startLine; 56 | $this->endLine = $endLine; 57 | $this->parentInterfaces = $parentInterfaces; 58 | } 59 | 60 | /** 61 | * @return non-empty-string 62 | */ 63 | public function name(): string 64 | { 65 | return $this->name; 66 | } 67 | 68 | /** 69 | * @return non-empty-string 70 | */ 71 | public function namespacedName(): string 72 | { 73 | return $this->namespacedName; 74 | } 75 | 76 | public function isNamespaced(): bool 77 | { 78 | return $this->namespace !== ''; 79 | } 80 | 81 | public function namespace(): string 82 | { 83 | return $this->namespace; 84 | } 85 | 86 | /** 87 | * @return non-negative-int 88 | */ 89 | public function startLine(): int 90 | { 91 | return $this->startLine; 92 | } 93 | 94 | /** 95 | * @return non-negative-int 96 | */ 97 | public function endLine(): int 98 | { 99 | return $this->endLine; 100 | } 101 | 102 | /** 103 | * @return list 104 | */ 105 | public function parentInterfaces(): array 106 | { 107 | return $this->parentInterfaces; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/StaticAnalysis/Value/LinesOfCode.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | /** 13 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 14 | */ 15 | final readonly class LinesOfCode 16 | { 17 | /** 18 | * @var non-negative-int 19 | */ 20 | private int $linesOfCode; 21 | 22 | /** 23 | * @var non-negative-int 24 | */ 25 | private int $commentLinesOfCode; 26 | 27 | /** 28 | * @var non-negative-int 29 | */ 30 | private int $nonCommentLinesOfCode; 31 | 32 | /** 33 | * @param non-negative-int $linesOfCode 34 | * @param non-negative-int $commentLinesOfCode 35 | * @param non-negative-int $nonCommentLinesOfCode 36 | */ 37 | public function __construct(int $linesOfCode, int $commentLinesOfCode, int $nonCommentLinesOfCode) 38 | { 39 | $this->linesOfCode = $linesOfCode; 40 | $this->commentLinesOfCode = $commentLinesOfCode; 41 | $this->nonCommentLinesOfCode = $nonCommentLinesOfCode; 42 | } 43 | 44 | /** 45 | * @return non-negative-int 46 | */ 47 | public function linesOfCode(): int 48 | { 49 | return $this->linesOfCode; 50 | } 51 | 52 | /** 53 | * @return non-negative-int 54 | */ 55 | public function commentLinesOfCode(): int 56 | { 57 | return $this->commentLinesOfCode; 58 | } 59 | 60 | /** 61 | * @return non-negative-int 62 | */ 63 | public function nonCommentLinesOfCode(): int 64 | { 65 | return $this->nonCommentLinesOfCode; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/StaticAnalysis/Value/Method.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | /** 13 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 14 | */ 15 | final readonly class Method 16 | { 17 | /** 18 | * @var non-empty-string 19 | */ 20 | private string $name; 21 | 22 | /** 23 | * @var non-negative-int 24 | */ 25 | private int $startLine; 26 | 27 | /** 28 | * @var non-negative-int 29 | */ 30 | private int $endLine; 31 | private Visibility $visibility; 32 | 33 | /** 34 | * @var non-empty-string 35 | */ 36 | private string $signature; 37 | 38 | /** 39 | * @var positive-int 40 | */ 41 | private int $cyclomaticComplexity; 42 | 43 | /** 44 | * @param non-empty-string $name 45 | * @param non-negative-int $startLine 46 | * @param non-negative-int $endLine 47 | * @param non-empty-string $signature 48 | * @param positive-int $cyclomaticComplexity 49 | */ 50 | public function __construct(string $name, int $startLine, int $endLine, string $signature, Visibility $visibility, int $cyclomaticComplexity) 51 | { 52 | $this->name = $name; 53 | $this->startLine = $startLine; 54 | $this->endLine = $endLine; 55 | $this->signature = $signature; 56 | $this->visibility = $visibility; 57 | $this->cyclomaticComplexity = $cyclomaticComplexity; 58 | } 59 | 60 | /** 61 | * @return non-empty-string 62 | */ 63 | public function name(): string 64 | { 65 | return $this->name; 66 | } 67 | 68 | /** 69 | * @return non-negative-int 70 | */ 71 | public function startLine(): int 72 | { 73 | return $this->startLine; 74 | } 75 | 76 | /** 77 | * @return non-negative-int 78 | */ 79 | public function endLine(): int 80 | { 81 | return $this->endLine; 82 | } 83 | 84 | /** 85 | * @return non-empty-string 86 | */ 87 | public function signature(): string 88 | { 89 | return $this->signature; 90 | } 91 | 92 | public function visibility(): Visibility 93 | { 94 | return $this->visibility; 95 | } 96 | 97 | /** 98 | * @return positive-int 99 | */ 100 | public function cyclomaticComplexity(): int 101 | { 102 | return $this->cyclomaticComplexity; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/StaticAnalysis/Value/Trait_.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | /** 13 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 14 | */ 15 | final readonly class Trait_ 16 | { 17 | /** 18 | * @var non-empty-string 19 | */ 20 | private string $name; 21 | 22 | /** 23 | * @var non-empty-string 24 | */ 25 | private string $namespacedName; 26 | private string $namespace; 27 | 28 | /** 29 | * @var non-empty-string 30 | */ 31 | private string $file; 32 | 33 | /** 34 | * @var non-negative-int 35 | */ 36 | private int $startLine; 37 | 38 | /** 39 | * @var non-negative-int 40 | */ 41 | private int $endLine; 42 | 43 | /** 44 | * @var list 45 | */ 46 | private array $traits; 47 | 48 | /** 49 | * @var array 50 | */ 51 | private array $methods; 52 | 53 | /** 54 | * @param non-empty-string $name 55 | * @param non-empty-string $namespacedName 56 | * @param non-empty-string $file 57 | * @param non-negative-int $startLine 58 | * @param non-negative-int $endLine 59 | * @param list $traits 60 | * @param array $methods 61 | */ 62 | public function __construct(string $name, string $namespacedName, string $namespace, string $file, int $startLine, int $endLine, array $traits, array $methods) 63 | { 64 | $this->name = $name; 65 | $this->namespacedName = $namespacedName; 66 | $this->namespace = $namespace; 67 | $this->file = $file; 68 | $this->startLine = $startLine; 69 | $this->endLine = $endLine; 70 | $this->traits = $traits; 71 | $this->methods = $methods; 72 | } 73 | 74 | /** 75 | * @return non-empty-string 76 | */ 77 | public function name(): string 78 | { 79 | return $this->name; 80 | } 81 | 82 | /** 83 | * @return non-empty-string 84 | */ 85 | public function namespacedName(): string 86 | { 87 | return $this->namespacedName; 88 | } 89 | 90 | public function isNamespaced(): bool 91 | { 92 | return $this->namespace !== ''; 93 | } 94 | 95 | public function namespace(): string 96 | { 97 | return $this->namespace; 98 | } 99 | 100 | /** 101 | * @return non-empty-string 102 | */ 103 | public function file(): string 104 | { 105 | return $this->file; 106 | } 107 | 108 | /** 109 | * @return non-negative-int 110 | */ 111 | public function startLine(): int 112 | { 113 | return $this->startLine; 114 | } 115 | 116 | /** 117 | * @return non-negative-int 118 | */ 119 | public function endLine(): int 120 | { 121 | return $this->endLine; 122 | } 123 | 124 | /** 125 | * @return list 126 | */ 127 | public function traits(): array 128 | { 129 | return $this->traits; 130 | } 131 | 132 | /** 133 | * @return array 134 | */ 135 | public function methods(): array 136 | { 137 | return $this->methods; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/StaticAnalysis/Value/Visibility.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | /** 13 | * @internal This enumeration is not covered by the backward compatibility promise for phpunit/php-code-coverage 14 | */ 15 | enum Visibility: string 16 | { 17 | case Public = 'public'; 18 | case Protected = 'protected'; 19 | case Private = 'private'; 20 | } 21 | -------------------------------------------------------------------------------- /src/StaticAnalysis/Visitor/AttributeParentConnectingVisitor.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 12 | 13 | use function array_pop; 14 | use function count; 15 | use PhpParser\Node; 16 | use PhpParser\NodeVisitor; 17 | 18 | /** 19 | * Visitor that connects a child node to its parent node optimized for Attribute nodes. 20 | * 21 | * On the child node, the parent node can be accessed through 22 | * $node->getAttribute('parent'). 23 | */ 24 | final class AttributeParentConnectingVisitor implements NodeVisitor 25 | { 26 | /** 27 | * @var Node[] 28 | */ 29 | private array $stack = []; 30 | 31 | public function beforeTraverse(array $nodes): null 32 | { 33 | $this->stack = []; 34 | 35 | return null; 36 | } 37 | 38 | public function enterNode(Node $node): null 39 | { 40 | if ($this->stack !== [] && 41 | ($node instanceof Node\Attribute || $node instanceof Node\AttributeGroup)) { 42 | $node->setAttribute('parent', $this->stack[count($this->stack) - 1]); 43 | } 44 | 45 | $this->stack[] = $node; 46 | 47 | return null; 48 | } 49 | 50 | public function leaveNode(Node $node): null 51 | { 52 | array_pop($this->stack); 53 | 54 | return null; 55 | } 56 | 57 | public function afterTraverse(array $nodes): null 58 | { 59 | return null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/StaticAnalysis/Visitor/IgnoredLinesFindingVisitor.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 | namespace SebastianBergmann\CodeCoverage\StaticAnalysis; 11 | 12 | use function assert; 13 | use function str_contains; 14 | use PhpParser\Node; 15 | use PhpParser\Node\Attribute; 16 | use PhpParser\Node\Stmt\Class_; 17 | use PhpParser\Node\Stmt\ClassMethod; 18 | use PhpParser\Node\Stmt\Enum_; 19 | use PhpParser\Node\Stmt\Function_; 20 | use PhpParser\Node\Stmt\Interface_; 21 | use PhpParser\Node\Stmt\Trait_; 22 | use PhpParser\NodeVisitorAbstract; 23 | 24 | /** 25 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 26 | */ 27 | final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract 28 | { 29 | /** 30 | * @var array 31 | */ 32 | private array $ignoredLines = []; 33 | private readonly bool $useAnnotationsForIgnoringCode; 34 | private readonly bool $ignoreDeprecated; 35 | 36 | public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecated) 37 | { 38 | $this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode; 39 | $this->ignoreDeprecated = $ignoreDeprecated; 40 | } 41 | 42 | public function enterNode(Node $node): null 43 | { 44 | if (!$node instanceof Class_ && 45 | !$node instanceof Trait_ && 46 | !$node instanceof Interface_ && 47 | !$node instanceof Enum_ && 48 | !$node instanceof ClassMethod && 49 | !$node instanceof Function_ && 50 | !$node instanceof Attribute) { 51 | return null; 52 | } 53 | 54 | if ($node instanceof Class_ && $node->isAnonymous()) { 55 | return null; 56 | } 57 | 58 | if ($node instanceof Class_ || 59 | $node instanceof Trait_ || 60 | $node instanceof Interface_ || 61 | $node instanceof Attribute) { 62 | $this->ignoredLines[] = $node->getStartLine(); 63 | 64 | assert($node->name !== null); 65 | 66 | // Workaround for https://github.com/nikic/PHP-Parser/issues/886 67 | $this->ignoredLines[] = $node->name->getStartLine(); 68 | } 69 | 70 | if (!$this->useAnnotationsForIgnoringCode) { 71 | return null; 72 | } 73 | 74 | if ($node instanceof Interface_) { 75 | return null; 76 | } 77 | 78 | if ($node instanceof Attribute && 79 | $node->name->toString() === 'PHPUnit\Framework\Attributes\CodeCoverageIgnore') { 80 | $attributeGroup = $node->getAttribute('parent'); 81 | $attributedNode = $attributeGroup->getAttribute('parent'); 82 | 83 | for ($line = $attributedNode->getStartLine(); $line <= $attributedNode->getEndLine(); $line++) { 84 | $this->ignoredLines[] = $line; 85 | } 86 | 87 | return null; 88 | } 89 | 90 | $this->processDocComment($node); 91 | 92 | return null; 93 | } 94 | 95 | /** 96 | * @return array 97 | */ 98 | public function ignoredLines(): array 99 | { 100 | return $this->ignoredLines; 101 | } 102 | 103 | private function processDocComment(Node $node): void 104 | { 105 | $docComment = $node->getDocComment(); 106 | 107 | if ($docComment === null) { 108 | return; 109 | } 110 | 111 | if (str_contains($docComment->getText(), '@codeCoverageIgnore')) { 112 | for ($line = $node->getStartLine(); $line <= $node->getEndLine(); $line++) { 113 | $this->ignoredLines[] = $line; 114 | } 115 | } 116 | 117 | if ($this->ignoreDeprecated && str_contains($docComment->getText(), '@deprecated')) { 118 | for ($line = $node->getStartLine(); $line <= $node->getEndLine(); $line++) { 119 | $this->ignoredLines[] = $line; 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Target/Class_.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | /** 13 | * @immutable 14 | * 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final class Class_ extends Target 18 | { 19 | /** 20 | * @var class-string 21 | */ 22 | private string $className; 23 | 24 | /** 25 | * @param class-string $className 26 | */ 27 | protected function __construct(string $className) 28 | { 29 | $this->className = $className; 30 | } 31 | 32 | public function isClass(): true 33 | { 34 | return true; 35 | } 36 | 37 | /** 38 | * @return class-string 39 | */ 40 | public function className(): string 41 | { 42 | return $this->className; 43 | } 44 | 45 | /** 46 | * @return non-empty-string 47 | */ 48 | public function key(): string 49 | { 50 | return 'classes'; 51 | } 52 | 53 | /** 54 | * @return non-empty-string 55 | */ 56 | public function target(): string 57 | { 58 | return $this->className; 59 | } 60 | 61 | /** 62 | * @return non-empty-string 63 | */ 64 | public function description(): string 65 | { 66 | return 'Class ' . $this->target(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Target/ClassesThatExtendClass.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | /** 13 | * @immutable 14 | * 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final class ClassesThatExtendClass extends Target 18 | { 19 | /** 20 | * @var class-string 21 | */ 22 | private string $className; 23 | 24 | /** 25 | * @param class-string $className 26 | */ 27 | protected function __construct(string $className) 28 | { 29 | $this->className = $className; 30 | } 31 | 32 | public function isClassesThatExtendClass(): true 33 | { 34 | return true; 35 | } 36 | 37 | /** 38 | * @return class-string 39 | */ 40 | public function className(): string 41 | { 42 | return $this->className; 43 | } 44 | 45 | /** 46 | * @return non-empty-string 47 | */ 48 | public function key(): string 49 | { 50 | return 'classesThatExtendClass'; 51 | } 52 | 53 | /** 54 | * @return non-empty-string 55 | */ 56 | public function target(): string 57 | { 58 | return $this->className; 59 | } 60 | 61 | /** 62 | * @return non-empty-string 63 | */ 64 | public function description(): string 65 | { 66 | return 'Classes that extend class ' . $this->target(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Target/ClassesThatImplementInterface.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | /** 13 | * @immutable 14 | * 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final class ClassesThatImplementInterface extends Target 18 | { 19 | /** 20 | * @var class-string 21 | */ 22 | private string $interfaceName; 23 | 24 | /** 25 | * @param class-string $interfaceName 26 | */ 27 | protected function __construct(string $interfaceName) 28 | { 29 | $this->interfaceName = $interfaceName; 30 | } 31 | 32 | public function isClassesThatImplementInterface(): true 33 | { 34 | return true; 35 | } 36 | 37 | /** 38 | * @return class-string 39 | */ 40 | public function interfaceName(): string 41 | { 42 | return $this->interfaceName; 43 | } 44 | 45 | /** 46 | * @return non-empty-string 47 | */ 48 | public function key(): string 49 | { 50 | return 'classesThatImplementInterface'; 51 | } 52 | 53 | /** 54 | * @return non-empty-string 55 | */ 56 | public function target(): string 57 | { 58 | return $this->interfaceName; 59 | } 60 | 61 | /** 62 | * @return non-empty-string 63 | */ 64 | public function description(): string 65 | { 66 | return 'Classes that implement interface ' . $this->target(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Target/Function_.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | /** 13 | * @immutable 14 | * 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final class Function_ extends Target 18 | { 19 | /** 20 | * @var non-empty-string 21 | */ 22 | private string $functionName; 23 | 24 | /** 25 | * @param non-empty-string $functionName 26 | */ 27 | protected function __construct(string $functionName) 28 | { 29 | $this->functionName = $functionName; 30 | } 31 | 32 | public function isFunction(): true 33 | { 34 | return true; 35 | } 36 | 37 | /** 38 | * @return non-empty-string 39 | */ 40 | public function functionName(): string 41 | { 42 | return $this->functionName; 43 | } 44 | 45 | /** 46 | * @return non-empty-string 47 | */ 48 | public function key(): string 49 | { 50 | return 'functions'; 51 | } 52 | 53 | /** 54 | * @return non-empty-string 55 | */ 56 | public function target(): string 57 | { 58 | return $this->functionName; 59 | } 60 | 61 | /** 62 | * @return non-empty-string 63 | */ 64 | public function description(): string 65 | { 66 | return 'Function ' . $this->target(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Target/Mapper.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | use function array_keys; 13 | use function array_merge; 14 | use function array_unique; 15 | use function strcasecmp; 16 | 17 | /** 18 | * @phpstan-type TargetMap array{namespaces: TargetMapPart, traits: TargetMapPart, classes: TargetMapPart, classesThatExtendClass: TargetMapPart, classesThatImplementInterface: TargetMapPart, methods: TargetMapPart, functions: TargetMapPart, reverseLookup: ReverseLookup} 19 | * @phpstan-type TargetMapPart array>> 20 | * @phpstan-type ReverseLookup array 21 | * 22 | * @immutable 23 | * 24 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 25 | * 26 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 27 | */ 28 | final readonly class Mapper 29 | { 30 | /** 31 | * @var TargetMap 32 | */ 33 | private array $map; 34 | 35 | /** 36 | * @param TargetMap $map 37 | */ 38 | public function __construct(array $map) 39 | { 40 | $this->map = $map; 41 | } 42 | 43 | /** 44 | * @return array> 45 | */ 46 | public function mapTargets(TargetCollection $targets): array 47 | { 48 | $result = []; 49 | 50 | foreach ($targets as $target) { 51 | foreach ($this->mapTarget($target) as $file => $lines) { 52 | if (!isset($result[$file])) { 53 | $result[$file] = $lines; 54 | 55 | continue; 56 | } 57 | 58 | $result[$file] = array_unique(array_merge($result[$file], $lines)); 59 | } 60 | } 61 | 62 | return $result; 63 | } 64 | 65 | /** 66 | * @throws InvalidCodeCoverageTargetException 67 | * 68 | * @return array> 69 | */ 70 | public function mapTarget(Target $target): array 71 | { 72 | if (isset($this->map[$target->key()][$target->target()])) { 73 | return $this->map[$target->key()][$target->target()]; 74 | } 75 | 76 | foreach (array_keys($this->map[$target->key()]) as $key) { 77 | if (strcasecmp($key, $target->target()) === 0) { 78 | return $this->map[$target->key()][$key]; 79 | } 80 | } 81 | 82 | throw new InvalidCodeCoverageTargetException($target); 83 | } 84 | 85 | /** 86 | * @param non-empty-string $file 87 | * @param positive-int $line 88 | * 89 | * @return non-empty-string 90 | */ 91 | public function lookup(string $file, int $line): string 92 | { 93 | $key = $file . ':' . $line; 94 | 95 | if (isset($this->map['reverseLookup'][$key])) { 96 | return $this->map['reverseLookup'][$key]; 97 | } 98 | 99 | return $key; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Target/Method.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | /** 13 | * @immutable 14 | * 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final class Method extends Target 18 | { 19 | /** 20 | * @var class-string 21 | */ 22 | private string $className; 23 | 24 | /** 25 | * @var non-empty-string 26 | */ 27 | private string $methodName; 28 | 29 | /** 30 | * @param class-string $className 31 | * @param non-empty-string $methodName 32 | */ 33 | protected function __construct(string $className, string $methodName) 34 | { 35 | $this->className = $className; 36 | $this->methodName = $methodName; 37 | } 38 | 39 | public function isMethod(): true 40 | { 41 | return true; 42 | } 43 | 44 | /** 45 | * @return class-string 46 | */ 47 | public function className(): string 48 | { 49 | return $this->className; 50 | } 51 | 52 | /** 53 | * @return non-empty-string 54 | */ 55 | public function methodName(): string 56 | { 57 | return $this->methodName; 58 | } 59 | 60 | /** 61 | * @return non-empty-string 62 | */ 63 | public function key(): string 64 | { 65 | return 'methods'; 66 | } 67 | 68 | /** 69 | * @return non-empty-string 70 | */ 71 | public function target(): string 72 | { 73 | return $this->className . '::' . $this->methodName; 74 | } 75 | 76 | /** 77 | * @return non-empty-string 78 | */ 79 | public function description(): string 80 | { 81 | return 'Method ' . $this->target(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Target/Namespace_.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | /** 13 | * @immutable 14 | * 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final class Namespace_ extends Target 18 | { 19 | /** 20 | * @var non-empty-string 21 | */ 22 | private string $namespace; 23 | 24 | /** 25 | * @param non-empty-string $namespace 26 | */ 27 | protected function __construct(string $namespace) 28 | { 29 | $this->namespace = $namespace; 30 | } 31 | 32 | public function isNamespace(): true 33 | { 34 | return true; 35 | } 36 | 37 | /** 38 | * @return non-empty-string 39 | */ 40 | public function namespace(): string 41 | { 42 | return $this->namespace; 43 | } 44 | 45 | /** 46 | * @return non-empty-string 47 | */ 48 | public function key(): string 49 | { 50 | return 'namespaces'; 51 | } 52 | 53 | /** 54 | * @return non-empty-string 55 | */ 56 | public function target(): string 57 | { 58 | return $this->namespace; 59 | } 60 | 61 | /** 62 | * @return non-empty-string 63 | */ 64 | public function description(): string 65 | { 66 | return 'Namespace ' . $this->target(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Target/Target.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | /** 13 | * @immutable 14 | * 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | abstract class Target 18 | { 19 | /** 20 | * @param non-empty-string $namespace 21 | */ 22 | public static function forNamespace(string $namespace): Namespace_ 23 | { 24 | return new Namespace_($namespace); 25 | } 26 | 27 | /** 28 | * @param class-string $className 29 | */ 30 | public static function forClass(string $className): Class_ 31 | { 32 | return new Class_($className); 33 | } 34 | 35 | /** 36 | * @param class-string $className 37 | * @param non-empty-string $methodName 38 | */ 39 | public static function forMethod(string $className, string $methodName): Method 40 | { 41 | return new Method($className, $methodName); 42 | } 43 | 44 | /** 45 | * @param class-string $interfaceName 46 | */ 47 | public static function forClassesThatImplementInterface(string $interfaceName): ClassesThatImplementInterface 48 | { 49 | return new ClassesThatImplementInterface($interfaceName); 50 | } 51 | 52 | /** 53 | * @param class-string $className 54 | */ 55 | public static function forClassesThatExtendClass(string $className): ClassesThatExtendClass 56 | { 57 | return new ClassesThatExtendClass($className); 58 | } 59 | 60 | /** 61 | * @param non-empty-string $functionName 62 | */ 63 | public static function forFunction(string $functionName): Function_ 64 | { 65 | return new Function_($functionName); 66 | } 67 | 68 | /** 69 | * @param trait-string $traitName 70 | */ 71 | public static function forTrait(string $traitName): Trait_ 72 | { 73 | return new Trait_($traitName); 74 | } 75 | 76 | public function isNamespace(): bool 77 | { 78 | return false; 79 | } 80 | 81 | public function isClass(): bool 82 | { 83 | return false; 84 | } 85 | 86 | public function isMethod(): bool 87 | { 88 | return false; 89 | } 90 | 91 | public function isClassesThatImplementInterface(): bool 92 | { 93 | return false; 94 | } 95 | 96 | public function isClassesThatExtendClass(): bool 97 | { 98 | return false; 99 | } 100 | 101 | public function isFunction(): bool 102 | { 103 | return false; 104 | } 105 | 106 | public function isTrait(): bool 107 | { 108 | return false; 109 | } 110 | 111 | /** 112 | * @return non-empty-string 113 | */ 114 | abstract public function key(): string; 115 | 116 | /** 117 | * @return non-empty-string 118 | */ 119 | abstract public function target(): string; 120 | 121 | /** 122 | * @return non-empty-string 123 | */ 124 | abstract public function description(): string; 125 | } 126 | -------------------------------------------------------------------------------- /src/Target/TargetCollection.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | use function count; 13 | use Countable; 14 | use IteratorAggregate; 15 | 16 | /** 17 | * @template-implements IteratorAggregate 18 | * 19 | * @immutable 20 | * 21 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 22 | */ 23 | final readonly class TargetCollection implements Countable, IteratorAggregate 24 | { 25 | /** 26 | * @var list 27 | */ 28 | private array $targets; 29 | 30 | /** 31 | * @param list $targets 32 | */ 33 | public static function fromArray(array $targets): self 34 | { 35 | return new self(...$targets); 36 | } 37 | 38 | private function __construct(Target ...$targets) 39 | { 40 | $this->targets = $targets; 41 | } 42 | 43 | /** 44 | * @return list 45 | */ 46 | public function asArray(): array 47 | { 48 | return $this->targets; 49 | } 50 | 51 | public function count(): int 52 | { 53 | return count($this->targets); 54 | } 55 | 56 | public function isEmpty(): bool 57 | { 58 | return $this->count() === 0; 59 | } 60 | 61 | public function isNotEmpty(): bool 62 | { 63 | return $this->count() > 0; 64 | } 65 | 66 | public function getIterator(): TargetCollectionIterator 67 | { 68 | return new TargetCollectionIterator($this); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Target/TargetCollectionIterator.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | use function count; 13 | use Iterator; 14 | 15 | /** 16 | * @template-implements Iterator 17 | * 18 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 19 | */ 20 | final class TargetCollectionIterator implements Iterator 21 | { 22 | /** 23 | * @var list 24 | */ 25 | private readonly array $targets; 26 | private int $position = 0; 27 | 28 | public function __construct(TargetCollection $metadata) 29 | { 30 | $this->targets = $metadata->asArray(); 31 | } 32 | 33 | public function rewind(): void 34 | { 35 | $this->position = 0; 36 | } 37 | 38 | public function valid(): bool 39 | { 40 | return $this->position < count($this->targets); 41 | } 42 | 43 | public function key(): int 44 | { 45 | return $this->position; 46 | } 47 | 48 | public function current(): Target 49 | { 50 | return $this->targets[$this->position]; 51 | } 52 | 53 | public function next(): void 54 | { 55 | $this->position++; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Target/TargetCollectionValidator.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | use function implode; 13 | 14 | /** 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | * 17 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 18 | */ 19 | final readonly class TargetCollectionValidator 20 | { 21 | public function validate(Mapper $mapper, TargetCollection $targets): ValidationResult 22 | { 23 | $errors = []; 24 | 25 | foreach ($targets as $target) { 26 | try { 27 | $mapper->mapTarget($target); 28 | } catch (InvalidCodeCoverageTargetException $e) { 29 | $errors[] = $e->getMessage(); 30 | } 31 | } 32 | 33 | if ($errors === []) { 34 | return ValidationResult::success(); 35 | } 36 | 37 | return ValidationResult::failure(implode("\n", $errors)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Target/Trait_.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | /** 13 | * @immutable 14 | * 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final class Trait_ extends Target 18 | { 19 | /** 20 | * @var trait-string 21 | */ 22 | private string $traitName; 23 | 24 | /** 25 | * @param trait-string $traitName 26 | */ 27 | protected function __construct(string $traitName) 28 | { 29 | $this->traitName = $traitName; 30 | } 31 | 32 | public function isTrait(): true 33 | { 34 | return true; 35 | } 36 | 37 | /** 38 | * @return trait-string 39 | */ 40 | public function traitName(): string 41 | { 42 | return $this->traitName; 43 | } 44 | 45 | /** 46 | * @return non-empty-string 47 | */ 48 | public function key(): string 49 | { 50 | return 'traits'; 51 | } 52 | 53 | /** 54 | * @return non-empty-string 55 | */ 56 | public function target(): string 57 | { 58 | return $this->traitName; 59 | } 60 | 61 | /** 62 | * @return non-empty-string 63 | */ 64 | public function description(): string 65 | { 66 | return 'Trait ' . $this->target(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Target/ValidationFailure.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | /** 13 | * @immutable 14 | * 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final readonly class ValidationFailure extends ValidationResult 18 | { 19 | /** 20 | * @var non-empty-string 21 | */ 22 | private string $message; 23 | 24 | /** 25 | * @param non-empty-string $message 26 | * 27 | * @noinspection PhpMissingParentConstructorInspection 28 | */ 29 | protected function __construct(string $message) 30 | { 31 | $this->message = $message; 32 | } 33 | 34 | public function isFailure(): true 35 | { 36 | return true; 37 | } 38 | 39 | /** 40 | * @return non-empty-string 41 | */ 42 | public function message(): string 43 | { 44 | return $this->message; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Target/ValidationResult.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | /** 13 | * @immutable 14 | * 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | abstract readonly class ValidationResult 18 | { 19 | public static function success(): ValidationSuccess 20 | { 21 | return new ValidationSuccess; 22 | } 23 | 24 | /** 25 | * @param non-empty-string $message 26 | */ 27 | public static function failure(string $message): ValidationFailure 28 | { 29 | return new ValidationFailure($message); 30 | } 31 | 32 | /** 33 | * @phpstan-assert-if-true ValidationSuccess $this 34 | */ 35 | public function isSuccess(): bool 36 | { 37 | return false; 38 | } 39 | 40 | /** 41 | * @phpstan-assert-if-true ValidationFailure $this 42 | */ 43 | public function isFailure(): bool 44 | { 45 | return false; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Target/ValidationSuccess.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 | namespace SebastianBergmann\CodeCoverage\Test\Target; 11 | 12 | /** 13 | * @immutable 14 | * 15 | * @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final readonly class ValidationSuccess extends ValidationResult 18 | { 19 | public function isSuccess(): true 20 | { 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/TestSize/Known.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 | namespace SebastianBergmann\CodeCoverage\Test\TestSize; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | abstract class Known extends TestSize 16 | { 17 | public function isKnown(): true 18 | { 19 | return true; 20 | } 21 | 22 | abstract public function isGreaterThan(self $other): bool; 23 | } 24 | -------------------------------------------------------------------------------- /src/TestSize/Large.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 | namespace SebastianBergmann\CodeCoverage\Test\TestSize; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | final class Large extends Known 16 | { 17 | public function isLarge(): true 18 | { 19 | return true; 20 | } 21 | 22 | public function isGreaterThan(TestSize $other): bool 23 | { 24 | return !$other->isLarge(); 25 | } 26 | 27 | public function asString(): string 28 | { 29 | return 'large'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/TestSize/Medium.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 | namespace SebastianBergmann\CodeCoverage\Test\TestSize; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | final class Medium extends Known 16 | { 17 | public function isMedium(): true 18 | { 19 | return true; 20 | } 21 | 22 | public function isGreaterThan(TestSize $other): bool 23 | { 24 | return $other->isSmall(); 25 | } 26 | 27 | public function asString(): string 28 | { 29 | return 'medium'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/TestSize/Small.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 | namespace SebastianBergmann\CodeCoverage\Test\TestSize; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | final class Small extends Known 16 | { 17 | public function isSmall(): true 18 | { 19 | return true; 20 | } 21 | 22 | public function isGreaterThan(TestSize $other): bool 23 | { 24 | return false; 25 | } 26 | 27 | public function asString(): string 28 | { 29 | return 'small'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/TestSize/TestSize.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 | namespace SebastianBergmann\CodeCoverage\Test\TestSize; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | abstract class TestSize 16 | { 17 | public static function unknown(): Unknown 18 | { 19 | return new Unknown; 20 | } 21 | 22 | public static function small(): Small 23 | { 24 | return new Small; 25 | } 26 | 27 | public static function medium(): Medium 28 | { 29 | return new Medium; 30 | } 31 | 32 | public static function large(): Large 33 | { 34 | return new Large; 35 | } 36 | 37 | /** 38 | * @phpstan-assert-if-true Known $this 39 | */ 40 | public function isKnown(): bool 41 | { 42 | return false; 43 | } 44 | 45 | /** 46 | * @phpstan-assert-if-true Unknown $this 47 | */ 48 | public function isUnknown(): bool 49 | { 50 | return false; 51 | } 52 | 53 | /** 54 | * @phpstan-assert-if-true Small $this 55 | */ 56 | public function isSmall(): bool 57 | { 58 | return false; 59 | } 60 | 61 | /** 62 | * @phpstan-assert-if-true Medium $this 63 | */ 64 | public function isMedium(): bool 65 | { 66 | return false; 67 | } 68 | 69 | /** 70 | * @phpstan-assert-if-true Large $this 71 | */ 72 | public function isLarge(): bool 73 | { 74 | return false; 75 | } 76 | 77 | abstract public function asString(): string; 78 | } 79 | -------------------------------------------------------------------------------- /src/TestSize/Unknown.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 | namespace SebastianBergmann\CodeCoverage\Test\TestSize; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | final class Unknown extends TestSize 16 | { 17 | public function isUnknown(): true 18 | { 19 | return true; 20 | } 21 | 22 | public function asString(): string 23 | { 24 | return 'unknown'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/TestStatus/Failure.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 | namespace SebastianBergmann\CodeCoverage\Test\TestStatus; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | final class Failure extends Known 16 | { 17 | public function isFailure(): true 18 | { 19 | return true; 20 | } 21 | 22 | public function asString(): string 23 | { 24 | return 'failure'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/TestStatus/Known.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 | namespace SebastianBergmann\CodeCoverage\Test\TestStatus; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | abstract class Known extends TestStatus 16 | { 17 | public function isKnown(): true 18 | { 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/TestStatus/Success.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 | namespace SebastianBergmann\CodeCoverage\Test\TestStatus; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | final class Success extends Known 16 | { 17 | public function isSuccess(): true 18 | { 19 | return true; 20 | } 21 | 22 | public function asString(): string 23 | { 24 | return 'success'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/TestStatus/TestStatus.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 | namespace SebastianBergmann\CodeCoverage\Test\TestStatus; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | abstract class TestStatus 16 | { 17 | public static function unknown(): self 18 | { 19 | return new Unknown; 20 | } 21 | 22 | public static function success(): self 23 | { 24 | return new Success; 25 | } 26 | 27 | public static function failure(): self 28 | { 29 | return new Failure; 30 | } 31 | 32 | /** 33 | * @phpstan-assert-if-true Known $this 34 | */ 35 | public function isKnown(): bool 36 | { 37 | return false; 38 | } 39 | 40 | /** 41 | * @phpstan-assert-if-true Unknown $this 42 | */ 43 | public function isUnknown(): bool 44 | { 45 | return false; 46 | } 47 | 48 | /** 49 | * @phpstan-assert-if-true Success $this 50 | */ 51 | public function isSuccess(): bool 52 | { 53 | return false; 54 | } 55 | 56 | /** 57 | * @phpstan-assert-if-true Failure $this 58 | */ 59 | public function isFailure(): bool 60 | { 61 | return false; 62 | } 63 | 64 | abstract public function asString(): string; 65 | } 66 | -------------------------------------------------------------------------------- /src/TestStatus/Unknown.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 | namespace SebastianBergmann\CodeCoverage\Test\TestStatus; 11 | 12 | /** 13 | * @immutable 14 | */ 15 | final class Unknown extends TestStatus 16 | { 17 | public function isUnknown(): true 18 | { 19 | return true; 20 | } 21 | 22 | public function asString(): string 23 | { 24 | return 'unknown'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Util/Filesystem.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 | namespace SebastianBergmann\CodeCoverage\Util; 11 | 12 | use function is_dir; 13 | use function mkdir; 14 | use function sprintf; 15 | 16 | /** 17 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 18 | */ 19 | final class Filesystem 20 | { 21 | /** 22 | * @throws DirectoryCouldNotBeCreatedException 23 | */ 24 | public static function createDirectory(string $directory): void 25 | { 26 | $success = !(!is_dir($directory) && !@mkdir($directory, 0o777, true) && !is_dir($directory)); 27 | 28 | if (!$success) { 29 | throw new DirectoryCouldNotBeCreatedException( 30 | sprintf( 31 | 'Directory "%s" could not be created', 32 | $directory, 33 | ), 34 | ); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Util/Percentage.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 | namespace SebastianBergmann\CodeCoverage\Util; 11 | 12 | use function sprintf; 13 | 14 | /** 15 | * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage 16 | */ 17 | final readonly class Percentage 18 | { 19 | private float $fraction; 20 | private float $total; 21 | 22 | public static function fromFractionAndTotal(float $fraction, float $total): self 23 | { 24 | return new self($fraction, $total); 25 | } 26 | 27 | private function __construct(float $fraction, float $total) 28 | { 29 | $this->fraction = $fraction; 30 | $this->total = $total; 31 | } 32 | 33 | public function asFloat(): float 34 | { 35 | if ($this->total > 0) { 36 | return ($this->fraction / $this->total) * 100; 37 | } 38 | 39 | return 100.0; 40 | } 41 | 42 | public function asString(): string 43 | { 44 | if ($this->total > 0) { 45 | return sprintf('%01.2F%%', $this->asFloat()); 46 | } 47 | 48 | return ''; 49 | } 50 | 51 | public function asFixedWidthString(): string 52 | { 53 | if ($this->total > 0) { 54 | return sprintf('%6.2F%%', $this->asFloat()); 55 | } 56 | 57 | return ''; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Version.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 | namespace SebastianBergmann\CodeCoverage; 11 | 12 | use function dirname; 13 | use SebastianBergmann\Version as VersionId; 14 | 15 | final class Version 16 | { 17 | private static string $version = ''; 18 | 19 | public static function id(): string 20 | { 21 | if (self::$version === '') { 22 | self::$version = (new VersionId('12.3.0', dirname(__DIR__)))->asString(); 23 | } 24 | 25 | return self::$version; 26 | } 27 | } 28 | --------------------------------------------------------------------------------