Updates to vendors etc

This commit is contained in:
Chris Hunt
2025-07-11 15:57:48 +01:00
parent d972cbcd0a
commit 8fb6438254
8043 changed files with 248005 additions and 189479 deletions

View File

@@ -0,0 +1,132 @@
# ChangeLog
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## [10.1.16] - 2024-08-22
### Changed
* Updated dependencies (so that users that install using Composer's `--prefer-lowest` CLI option also get recent versions)
## [10.1.15] - 2024-06-29
### Fixed
* [#967](https://github.com/sebastianbergmann/php-code-coverage/issues/967): Identification of executable lines for `match` expressions does not work correctly
## [10.1.14] - 2024-03-12
### Fixed
* [#1033](https://github.com/sebastianbergmann/php-code-coverage/issues/1033): `@codeCoverageIgnore` annotation does not work on `enum`
## [10.1.13] - 2024-03-09
### Changed
* [#1032](https://github.com/sebastianbergmann/php-code-coverage/pull/1032): Pad lines in code coverage report only when colors are shown
## [10.1.12] - 2024-03-02
### Changed
* Do not use implicitly nullable parameters
## [10.1.11] - 2023-12-21
### Changed
* This component is now compatible with `nikic/php-parser` 5.0
## [10.1.10] - 2023-12-11
### Fixed
* [#1023](https://github.com/sebastianbergmann/php-code-coverage/issues/1023): Branch Coverage and Path Coverage are not correctly reported for traits
## [10.1.9] - 2023-11-23
### Fixed
* [#1020](https://github.com/sebastianbergmann/php-code-coverage/issues/1020): Single line method is ignored
## [10.1.8] - 2023-11-15
### Fixed
* [#1018](https://github.com/sebastianbergmann/php-code-coverage/issues/1018): Interface methods are not ignored when their signature is split over multiple lines
## [10.1.7] - 2023-10-04
### Fixed
* [#1014](https://github.com/sebastianbergmann/php-code-coverage/issues/1014): Incorrect statement count in coverage report for constructor property promotion
## [10.1.6] - 2023-09-19
### Fixed
* [#1012](https://github.com/sebastianbergmann/php-code-coverage/issues/1012): Cobertura report pulls functions from report scope, not the individual element
## [10.1.5] - 2023-09-12
### Changed
* [#1011](https://github.com/sebastianbergmann/php-code-coverage/pull/1011): Avoid serialization of cache data in PHP report
## [10.1.4] - 2023-08-31
### Fixed
* Exceptions of type `SebastianBergmann\Template\Exception` are now properly handled
## [10.1.3] - 2023-07-26
### Changed
* The result of `CodeCoverage::getReport()` is now cached
### Fixed
* Static analysis cache keys do not include configuration settings that affect source code parsing
* The Clover, Cobertura, Crap4j, and PHP report writers no longer create a `php:` directory when they should write to `php://stdout`, for instance
## [10.1.2] - 2023-05-22
### Fixed
* [#998](https://github.com/sebastianbergmann/php-code-coverage/pull/998): Group Use Declarations are not handled properly
## [10.1.1] - 2023-04-17
### Fixed
* [#994](https://github.com/sebastianbergmann/php-code-coverage/issues/994): Argument `$linesToBeIgnored` of `CodeCoverage::stop()` has no effect for files that are not executed at all
## [10.1.0] - 2023-04-13
### Added
* [#982](https://github.com/sebastianbergmann/php-code-coverage/issues/982): Add option to ignore lines from code coverage
### Deprecated
* The `SebastianBergmann\CodeCoverage\Filter::includeDirectory()`, `SebastianBergmann\CodeCoverage\Filter::excludeDirectory()`, and `SebastianBergmann\CodeCoverage\Filter::excludeFile()` methods are now deprecated
[10.1.16]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.15...10.1.16
[10.1.15]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.14...10.1.15
[10.1.14]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.13...10.1.14
[10.1.13]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.12...10.1.13
[10.1.12]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.11...10.1.12
[10.1.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.10...10.1.11
[10.1.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.9...10.1.10
[10.1.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.8...10.1.9
[10.1.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.7...10.1.8
[10.1.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.6...10.1.7
[10.1.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.5...10.1.6
[10.1.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.4...10.1.5
[10.1.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.3...10.1.4
[10.1.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.2...10.1.3
[10.1.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.1...10.1.2
[10.1.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1.0...10.1.1
[10.1.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.0.2...10.1.0

View File

@@ -1,570 +0,0 @@
# ChangeLog
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
## [9.2.30] - 2023-12-22
### Changed
* This component is now compatible with `nikic/php-parser` 5.0
## [9.2.29] - 2023-09-19
### Fixed
* [#1012](https://github.com/sebastianbergmann/php-code-coverage/issues/1012): Cobertura report pulls functions from report scope, not the individual element
## [9.2.28] - 2023-09-12
### Changed
* [#1011](https://github.com/sebastianbergmann/php-code-coverage/pull/1011): Avoid serialization of cache data in PHP report
## [9.2.27] - 2023-07-26
### Changed
* The result of `CodeCoverage::getReport()` is now cached
### Fixed
* Static analysis cache keys do not include configuration settings that affect source code parsing
* The Clover, Cobertura, Crap4j, and PHP report writers no longer create a `php:` directory when they should write to `php://stdout`, for instance
## [9.2.26] - 2023-03-06
### Changed
* Improved the legend on the file pages of the HTML code coverage report
## [9.2.25] - 2023-02-25
### Fixed
* [#981](https://github.com/sebastianbergmann/php-code-coverage/issues/981): `CodeUnitFindingVisitor` does not support DNF types
## [9.2.24] - 2023-01-26
### Changed
* [#970](https://github.com/sebastianbergmann/php-code-coverage/issues/970): CSS and JavaScript assets are now referenced using `?v=%s` URLs in the HTML report to avoid cache issues
## [9.2.23] - 2022-12-28
### Fixed
* [#971](https://github.com/sebastianbergmann/php-code-coverage/issues/971): PHP report does not handle serialized code coverage data larger than 2 GB
* [#974](https://github.com/sebastianbergmann/php-code-coverage/issues/974): Executable line analysis fails for declarations with enumerations and unions
## [9.2.22] - 2022-12-18
### Fixed
* [#969](https://github.com/sebastianbergmann/php-code-coverage/pull/969): Fixed identifying line with `throw` as executable
## [9.2.21] - 2022-12-14
### Changed
* [#964](https://github.com/sebastianbergmann/php-code-coverage/pull/964): Changed how executable lines are identified
## [9.2.20] - 2022-12-13
### Fixed
* [#960](https://github.com/sebastianbergmann/php-code-coverage/issues/960): New body font-size is way too big
## [9.2.19] - 2022-11-18
### Fixed
* [#949](https://github.com/sebastianbergmann/php-code-coverage/pull/949): Various issues related to identifying executable lines
### Changed
* Tweaked CSS for HTML report
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.6.2 and jQuery 3.6.1
## [9.2.18] - 2022-10-27
### Fixed
* [#935](https://github.com/sebastianbergmann/php-code-coverage/pull/935): Cobertura package name attribute is always empty
* [#946](https://github.com/sebastianbergmann/php-code-coverage/issues/946): `return` with multiline constant expression must only contain the last line
## [9.2.17] - 2022-08-30
### Changed
* [#928](https://github.com/sebastianbergmann/php-code-coverage/pull/928): Avoid unnecessary `is_file()` calls
* [#931](https://github.com/sebastianbergmann/php-code-coverage/pull/931): Use MD5 instead of CRC32 for static analysis cache file identifier
### Fixed
* [#926](https://github.com/sebastianbergmann/php-code-coverage/pull/926): Static Analysis cache does not work with `open_basedir`
## [9.2.16] - 2022-08-20
### Fixed
* [#926](https://github.com/sebastianbergmann/php-code-coverage/issues/926): File view has wrong colouring for the first column
## [9.2.15] - 2022-03-07
### Fixed
* [#885](https://github.com/sebastianbergmann/php-code-coverage/issues/885): Files that have only `\r` (CR, 0x0d) EOL characters are not handled correctly
* [#907](https://github.com/sebastianbergmann/php-code-coverage/issues/907): Line with only `return [` is not recognized as executable
## [9.2.14] - 2022-02-28
### Fixed
* [#904](https://github.com/sebastianbergmann/php-code-coverage/issues/904): Lines of code containing the `match` keyword were not recognized as executable correctly
* [#905](https://github.com/sebastianbergmann/php-code-coverage/issues/905): Lines of code in constructors were not recognized as executable correctly when constructor property promotion is used
## [9.2.13] - 2022-02-23
### Changed
* The contents of the static analysis sourcecode files is now used to generate the static analysis cache version identifier
### Fixed
* Reverted rename of `SebastianBergmann\CodeCoverage\ProcessedCodeCoverageData` to `SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData` (this class is marked as `@internal` and not covered by the backward compatibility promise, but it is (still) used directly by PHPUnit)
* Reverted rename of `SebastianBergmann\CodeCoverage\RawCodeCoverageData` to `SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData` (this class is marked as `@internal` and not covered by the backward compatibility promise, but it is (still) used directly by PHPUnit)
* The `ArrayDim`, `Cast`, and `MethodCall` nodes are now considered when determining whether a line of code is executable or not
## [9.2.12] - 2022-02-23 [YANKED]
### Changed
* [#898](https://github.com/sebastianbergmann/php-code-coverage/pull/898): Use content hash instead of `filemtime()` to determine cache hit/miss
### Fixed
* [#736](https://github.com/sebastianbergmann/php-code-coverage/issues/736): HTML report generator allows invalid values for low upper bound and high lower bound
* [#854](https://github.com/sebastianbergmann/php-code-coverage/issues/854): "Class Coverage Distribution" and "Class Complexity" graphs are not displayed at full width
* [#897](https://github.com/sebastianbergmann/php-code-coverage/issues/897): `declare(strict_types=1)` marked as uncovered
## [9.2.11] - 2022-02-18
### Changed
* `CoveredFileAnalyser` and `UncoveredFileAnalyser` have been combined to `FileAnalyser`
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.6.1, jQuery 3.6.0, and popper.js 1.16.1
### Fixed
* [#889](https://github.com/sebastianbergmann/php-code-coverage/issues/889): Code Coverage depends on autoload order
## [9.2.10] - 2021-12-05
### Fixed
* [#887](https://github.com/sebastianbergmann/php-code-coverage/issues/887): Document return type of `CodeUnitFindingVisitor::enterNode()` so that Symfony's DebugClassLoader does not trigger a deprecation warning
## [9.2.9] - 2021-11-19
### Fixed
* [#882](https://github.com/sebastianbergmann/php-code-coverage/issues/882): PHPUnit 9.2.8 has wrong version number
## [9.2.8] - 2021-10-30
### Fixed
* [#866](https://github.com/sebastianbergmann/php-code-coverage/issues/866): `CodeUnitFindingVisitor` does not handle `enum` type introduced in PHP 8.1
* [#868](https://github.com/sebastianbergmann/php-code-coverage/pull/868): Uncovered files should be ignored unless requested
* [#876](https://github.com/sebastianbergmann/php-code-coverage/issues/876): PCOV driver causes 2x slowdown after upgrade to PHPUnit 9.5
## [9.2.7] - 2021-09-17
### Fixed
* [#860](https://github.com/sebastianbergmann/php-code-coverage/pull/860): Empty value for `XDEBUG_MODE` environment variable is not handled correctly
## [9.2.6] - 2021-03-28
### Fixed
* [#846](https://github.com/sebastianbergmann/php-code-coverage/issues/846): Method name should not appear in the method signature attribute of Cobertura XML
## [9.2.5] - 2020-11-28
### Fixed
* [#831](https://github.com/sebastianbergmann/php-code-coverage/issues/831): Files that do not contain a newline are not handled correctly
## [9.2.4] - 2020-11-27
### Added
* [#834](https://github.com/sebastianbergmann/php-code-coverage/issues/834): Support `XDEBUG_MODE` environment variable
## [9.2.3] - 2020-10-30
### Changed
* Bumped required version of `nikic/php-parser`
## [9.2.2] - 2020-10-28
### Fixed
* [#820](https://github.com/sebastianbergmann/php-code-coverage/issues/820): Hidden dependency on PHPUnit
## [9.2.1] - 2020-10-26
### Fixed
* `SebastianBergmann\CodeCoverage\Exception` now correctly extends `\Throwable`
## [9.2.0] - 2020-10-02
### Added
* [#812](https://github.com/sebastianbergmann/php-code-coverage/pull/812): Support for Cobertura XML report format
### Changed
* Reduced the number of I/O operations performed by the static analysis cache
## [9.1.11] - 2020-09-19
### Fixed
* [#811](https://github.com/sebastianbergmann/php-code-coverage/issues/811): `T_FN` constant is used on PHP 7.3 where it is not available
## [9.1.10] - 2020-09-18
### Added
* `SebastianBergmann\CodeCoverage\Driver\Selector::forLineCoverage()` and `SebastianBergmann\CodeCoverage\Driver\Selector::forLineAndPathCoverage()` have been added
### Fixed
* [#810](https://github.com/sebastianbergmann/php-code-coverage/issues/810): `SebastianBergmann\CodeCoverage\Driver\Driver::forLineCoverage()` and `SebastianBergmann\CodeCoverage\Driver\Driver::forLineAndPathCoverage()` are marked as internal
### Removed
* `SebastianBergmann\CodeCoverage\Driver\Driver::forLineCoverage()` and `SebastianBergmann\CodeCoverage\Driver\Driver::forLineAndPathCoverage()` are now deprecated
## [9.1.9] - 2020-09-15
### Fixed
* [#808](https://github.com/sebastianbergmann/php-code-coverage/issues/808): `PHP Warning: Use of undefined constant T_MATCH`
## [9.1.8] - 2020-09-07
### Changed
* [#800](https://github.com/sebastianbergmann/php-code-coverage/pull/800): All files on the inclusion list are no longer loaded when `SebastianBergmann\CodeCoverage::start()` is called for the first time and `processUncoveredFiles` is set to `true`
### Fixed
* [#799](https://github.com/sebastianbergmann/php-code-coverage/issues/799): Uncovered new line at end of file
## [9.1.7] - 2020-09-03
### Fixed
* Fixed regressions introduced in versions 9.1.5 and 9.1.6
## [9.1.6] - 2020-08-31
### Fixed
* [#799](https://github.com/sebastianbergmann/php-code-coverage/issues/799): Uncovered new line at end of file
* [#803](https://github.com/sebastianbergmann/php-code-coverage/issues/803): HTML report does not sort directories and files anymore
## [9.1.5] - 2020-08-27
### Changed
* [#800](https://github.com/sebastianbergmann/php-code-coverage/pull/800): All files on the inclusion list are no longer loaded when `SebastianBergmann\CodeCoverage::start()` is called for the first time and `processUncoveredFiles` is set to `true`
### Fixed
* [#797](https://github.com/sebastianbergmann/php-code-coverage/pull/797): Class name is wrongly removed from namespace name
## [9.1.4] - 2020-08-13
### Fixed
* [#793](https://github.com/sebastianbergmann/php-code-coverage/issues/793): Lines with `::class` constant are not covered
## [9.1.3] - 2020-08-10
### Changed
* Changed PHP-Parser usage to parse sourcecode according to the PHP version we are currently running on instead of using emulative lexing
## [9.1.2] - 2020-08-10
### Fixed
* [#791](https://github.com/sebastianbergmann/php-code-coverage/pull/791): Cache Warmer does not warm all caches
## [9.1.1] - 2020-08-10
### Added
* Added `SebastianBergmann\CodeCoverage::cacheDirectory()` method for querying where the cache writes its files
## [9.1.0] - 2020-08-10
### Added
* Implemented a persistent cache for information gathered using PHP-Parser based static analysis (hereinafter referred to as "cache")
* Added `SebastianBergmann\CodeCoverage::cacheStaticAnalysis(string $cacheDirectory)` method for enabling the cache; it will write its files to `$directory`
* Added `SebastianBergmann\CodeCoverage::doNotCacheStaticAnalysis` method for disabling the cache
* Added `SebastianBergmann\CodeCoverage::cachesStaticAnalysis()` method for querying whether the cache is enabled
* Added `SebastianBergmann\CodeCoverage\StaticAnalysis\CacheWarmer::warmCache()` method for warming the cache
## [9.0.0] - 2020-08-07
### Added
* [#761](https://github.com/sebastianbergmann/php-code-coverage/pull/761): Support for Branch Coverage and Path Coverage
* Added `SebastianBergmann\CodeCoverage\Driver\Driver::forLineCoverage()` for selecting the best available driver for line coverage
* Added `SebastianBergmann\CodeCoverage\Driver\Driver::forLineAndPathCoverage()` for selecting the best available driver for path coverage
* This component is now supported on PHP 8
* This component now supports Xdebug 3
### Changed
* [#746](https://github.com/sebastianbergmann/php-code-coverage/pull/746): Remove some ancient workarounds for very old Xdebug versions
* [#747](https://github.com/sebastianbergmann/php-code-coverage/pull/747): Use native filtering in PCOV and Xdebug drivers
* [#748](https://github.com/sebastianbergmann/php-code-coverage/pull/748): Store raw code coverage in value objects instead of arrays
* [#749](https://github.com/sebastianbergmann/php-code-coverage/pull/749): Store processed code coverage in value objects instead of arrays
* [#752](https://github.com/sebastianbergmann/php-code-coverage/pull/752): Rework how code coverage settings are propagated to the driver
* [#754](https://github.com/sebastianbergmann/php-code-coverage/pull/754): Implement collection of raw branch and path coverage
* [#755](https://github.com/sebastianbergmann/php-code-coverage/pull/755): Implement processing of raw branch and path coverage
* [#756](https://github.com/sebastianbergmann/php-code-coverage/pull/756): Improve handling of uncovered files
* `SebastianBergmann\CodeCoverage\Filter::addDirectoryToWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::includeDirectory()`
* `SebastianBergmann\CodeCoverage\Filter::addFilesToWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::includeFiles()`
* `SebastianBergmann\CodeCoverage\Filter::addFileToWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::includeFile()`
* `SebastianBergmann\CodeCoverage\Filter::removeDirectoryFromWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::excludeDirectory()`
* `SebastianBergmann\CodeCoverage\Filter::removeFileFromWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::excludeFile()`
* `SebastianBergmann\CodeCoverage\Filter::isFiltered()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::isExcluded()`
* `SebastianBergmann\CodeCoverage\Filter::getWhitelist()` has been renamed to `SebastianBergmann\CodeCoverage\Filter::files()`
* The arguments for `CodeCoverage::__construct()` are no longer optional
### Fixed
* [#700](https://github.com/sebastianbergmann/php-code-coverage/pull/700): Throw an exception if code coverage fails to write to disk
### Removed
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCacheTokens()` and `SebastianBergmann\CodeCoverage\CodeCoverage::getCacheTokens()` have been removed
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCheckForUnintentionallyCoveredCode()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::enableCheckForUnintentionallyCoveredCode()` or `SebastianBergmann\CodeCoverage\CodeCoverage::disableCheckForUnintentionallyCoveredCode()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setSubclassesExcludedFromUnintentionallyCoveredCodeCheck()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setAddUncoveredFilesFromWhitelist()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::includeUncoveredFiles()` or `SebastianBergmann\CodeCoverage\CodeCoverage::excludeUncoveredFiles()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setProcessUncoveredFiles()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::processUncoveredFiles()` or `SebastianBergmann\CodeCoverage\CodeCoverage::doNotProcessUncoveredFiles()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setIgnoreDeprecatedCode()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::ignoreDeprecatedCode()` or `SebastianBergmann\CodeCoverage\CodeCoverage::doNotIgnoreDeprecatedCode()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setDisableIgnoredLines()` has been removed, please use `SebastianBergmann\CodeCoverage\CodeCoverage::enableAnnotationsForIgnoringCode()` or `SebastianBergmann\CodeCoverage\CodeCoverage::disableAnnotationsForIgnoringCode()` instead
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCheckForMissingCoversAnnotation()` has been removed
* `SebastianBergmann\CodeCoverage\CodeCoverage::setCheckForUnexecutedCoveredCode()` has been removed
* `SebastianBergmann\CodeCoverage\CodeCoverage::setForceCoversAnnotation()` has been removed
* `SebastianBergmann\CodeCoverage\Filter::hasWhitelist()` has been removed, please use `SebastianBergmann\CodeCoverage\Filter::isEmpty()` instead
* `SebastianBergmann\CodeCoverage\Filter::getWhitelistedFiles()` has been removed
* `SebastianBergmann\CodeCoverage\Filter::setWhitelistedFiles()` has been removed
## [8.0.2] - 2020-05-23
### Fixed
* [#750](https://github.com/sebastianbergmann/php-code-coverage/pull/750): Inconsistent handling of namespaces
* [#751](https://github.com/sebastianbergmann/php-code-coverage/pull/751): Dead code is not highlighted correctly
* [#753](https://github.com/sebastianbergmann/php-code-coverage/issues/753): Do not use `$_SERVER['REQUEST_TIME']` because the test(ed) code might unset it
## [8.0.1] - 2020-02-19
### Fixed
* [#731](https://github.com/sebastianbergmann/php-code-coverage/pull/731): Confusing footer in the HTML report
## [8.0.0] - 2020-02-07
### Fixed
* [#721](https://github.com/sebastianbergmann/php-code-coverage/pull/721): Workaround for PHP bug [#79191](https://bugs.php.net/bug.php?id=79191)
### Removed
* This component is no longer supported on PHP 7.2
## [7.0.15] - 2021-07-26
### Changed
* Bumped required version of php-token-stream
## [7.0.14] - 2020-12-02
### Changed
* [#837](https://github.com/sebastianbergmann/php-code-coverage/issues/837): Allow version 4 of php-token-stream
## [7.0.13] - 2020-11-30
### Changed
* Changed PHP version constraint in `composer.json` from `^7.2` to `>=7.2` to allow installation of this version of this library on PHP 8. However, this version of this library does not work on PHP 8. PHPUnit 8.5, which uses this version of this library, does not call into this library and instead shows a message that code coverage functionality is not available for PHPUnit 8.5 on PHP 8.
## [7.0.12] - 2020-11-27
### Added
* [#834](https://github.com/sebastianbergmann/php-code-coverage/issues/834): Support `XDEBUG_MODE` environment variable
## [7.0.11] - 2020-11-27
### Added
* Support for Xdebug 3
## [7.0.10] - 2019-11-20
### Fixed
* [#710](https://github.com/sebastianbergmann/php-code-coverage/pull/710): Code Coverage does not work in PhpStorm
## [7.0.9] - 2019-11-20
### Changed
* [#709](https://github.com/sebastianbergmann/php-code-coverage/pull/709): Prioritize PCOV over Xdebug
## [7.0.8] - 2019-09-17
### Changed
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.3.1, jQuery 3.4.1, and popper.js 1.15.0
## [7.0.7] - 2019-07-25
### Changed
* Bumped required version of php-token-stream
## [7.0.6] - 2019-07-08
### Changed
* Bumped required version of php-token-stream
## [7.0.5] - 2019-06-06
### Fixed
* [#681](https://github.com/sebastianbergmann/php-code-coverage/pull/681): `use function` statements are not ignored
## [7.0.4] - 2019-05-29
### Fixed
* [#682](https://github.com/sebastianbergmann/php-code-coverage/pull/682): Code that is not executed is reported as being executed when using PCOV
## [7.0.3] - 2019-02-26
### Fixed
* [#671](https://github.com/sebastianbergmann/php-code-coverage/issues/671): `TypeError` when directory name is a number
## [7.0.2] - 2019-02-15
### Changed
* Updated bundled CSS/JavaScript components used for HTML report: Bootstrap 4.3.0
### Fixed
* [#667](https://github.com/sebastianbergmann/php-code-coverage/pull/667): `TypeError` in PHP reporter
## [7.0.1] - 2019-02-01
### Fixed
* [#664](https://github.com/sebastianbergmann/php-code-coverage/issues/664): `TypeError` when whitelisted file does not exist
## [7.0.0] - 2019-02-01
### Added
* [#663](https://github.com/sebastianbergmann/php-code-coverage/pull/663): Support for PCOV
### Fixed
* [#654](https://github.com/sebastianbergmann/php-code-coverage/issues/654): HTML report fails to load assets
* [#655](https://github.com/sebastianbergmann/php-code-coverage/issues/655): Popin pops in outside of screen
### Removed
* This component is no longer supported on PHP 7.1
[9.2.30]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.29...9.2.30
[9.2.29]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.28...9.2.29
[9.2.28]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.27...9.2.28
[9.2.27]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.26...9.2.27
[9.2.26]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.25...9.2.26
[9.2.25]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.24...9.2.25
[9.2.24]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.23...9.2.24
[9.2.23]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.22...9.2.23
[9.2.22]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.21...9.2.22
[9.2.21]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.20...9.2.21
[9.2.20]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.19...9.2.20
[9.2.19]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.18...9.2.19
[9.2.18]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.17...9.2.18
[9.2.17]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.16...9.2.17
[9.2.16]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.15...9.2.16
[9.2.15]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.14...9.2.15
[9.2.14]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.13...9.2.14
[9.2.13]: https://github.com/sebastianbergmann/php-code-coverage/compare/c011a0b6aaa4acd2f39b7f51fb4ad4442b6ec631...9.2.13
[9.2.12]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.11...c011a0b6aaa4acd2f39b7f51fb4ad4442b6ec631
[9.2.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.10...9.2.11
[9.2.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.9...9.2.10
[9.2.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.8...9.2.9
[9.2.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.7...9.2.8
[9.2.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.6...9.2.7
[9.2.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.5...9.2.6
[9.2.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.4...9.2.5
[9.2.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.3...9.2.4
[9.2.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.2...9.2.3
[9.2.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.1...9.2.2
[9.2.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.2.0...9.2.1
[9.2.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.11...9.2.0
[9.1.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.10...9.1.11
[9.1.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.9...9.1.10
[9.1.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.8...9.1.9
[9.1.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.7...9.1.8
[9.1.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.6...9.1.7
[9.1.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.5...9.1.6
[9.1.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.4...9.1.5
[9.1.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.3...9.1.4
[9.1.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.2...9.1.3
[9.1.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.1...9.1.2
[9.1.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.1.0...9.1.1
[9.1.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/9.0.0...9.1.0
[9.0.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/8.0...9.0.0
[8.0.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/8.0.1...8.0.2
[8.0.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/8.0.0...8.0.1
[8.0.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.10...8.0.0
[7.0.15]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.14...7.0.15
[7.0.14]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.13...7.0.14
[7.0.13]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.12...7.0.13
[7.0.12]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.11...7.0.12
[7.0.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.10...7.0.11
[7.0.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.9...7.0.10
[7.0.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.8...7.0.9
[7.0.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.7...7.0.8
[7.0.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.6...7.0.7
[7.0.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.5...7.0.6
[7.0.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.4...7.0.5
[7.0.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.3...7.0.4
[7.0.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.2...7.0.3
[7.0.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.1...7.0.2
[7.0.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/7.0.0...7.0.1
[7.0.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/6.1.4...7.0.0

View File

@@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2009-2023, Sebastian Bergmann
Copyright (c) 2009-2024, Sebastian Bergmann
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@@ -3,6 +3,7 @@
[![Latest Stable Version](https://poser.pugx.org/phpunit/php-code-coverage/v/stable.png)](https://packagist.org/packages/phpunit/php-code-coverage)
[![CI Status](https://github.com/sebastianbergmann/php-code-coverage/workflows/CI/badge.svg)](https://github.com/sebastianbergmann/php-code-coverage/actions)
[![Type Coverage](https://shepherd.dev/github/sebastianbergmann/php-code-coverage/coverage.svg)](https://shepherd.dev/github/sebastianbergmann/php-code-coverage)
[![codecov](https://codecov.io/gh/sebastianbergmann/php-code-coverage/branch/main/graph/badge.svg)](https://codecov.io/gh/sebastianbergmann/php-code-coverage)
Provides collection, processing, and rendering functionality for PHP code coverage information.
@@ -30,7 +31,13 @@ use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Report\Html\Facade as HtmlReport;
$filter = new Filter;
$filter->includeDirectory('/path/to/directory');
$filter->includeFiles(
[
'/path/to/file.php',
'/path/to/another_file.php',
]
);
$coverage = new CodeCoverage(
(new Selector)->forLineCoverage($filter),

View File

@@ -0,0 +1,30 @@
# Security Policy
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.
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
Instead, please email `sebastian@phpunit.de`.
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
* The type of issue
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
## Web Context
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.
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.
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.
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.

View File

@@ -22,29 +22,29 @@
},
"config": {
"platform": {
"php": "7.3.0"
"php": "8.1.0"
},
"optimize-autoloader": true,
"sort-packages": true
},
"prefer-stable": true,
"require": {
"php": ">=7.3",
"php": ">=8.1",
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.18 || ^5.0",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
"sebastian/code-unit-reverse-lookup": "^2.0.2",
"sebastian/complexity": "^2.0",
"sebastian/environment": "^5.1.2",
"sebastian/lines-of-code": "^1.0.3",
"sebastian/version": "^3.0.1",
"theseer/tokenizer": "^1.2.0"
"nikic/php-parser": "^4.19.1 || ^5.1.0",
"phpunit/php-file-iterator": "^4.1.0",
"phpunit/php-text-template": "^3.0.1",
"sebastian/code-unit-reverse-lookup": "^3.0.0",
"sebastian/complexity": "^3.2.0",
"sebastian/environment": "^6.1.0",
"sebastian/lines-of-code": "^2.0.2",
"sebastian/version": "^4.0.1",
"theseer/tokenizer": "^1.2.3"
},
"require-dev": {
"phpunit/phpunit": "^9.3"
"phpunit/phpunit": "^10.1"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@@ -63,7 +63,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "9.2-dev"
"dev-main": "10.1.x-dev"
}
}
}

View File

@@ -14,110 +14,65 @@ use function array_diff_key;
use function array_flip;
use function array_keys;
use function array_merge;
use function array_merge_recursive;
use function array_unique;
use function array_values;
use function count;
use function explode;
use function get_class;
use function is_array;
use function is_file;
use function sort;
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\PhptTestCase;
use PHPUnit\Util\Test;
use ReflectionClass;
use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData;
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
use SebastianBergmann\CodeCoverage\Driver\Driver;
use SebastianBergmann\CodeCoverage\Node\Builder;
use SebastianBergmann\CodeCoverage\Node\Directory;
use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingFileAnalyser;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;
use SebastianBergmann\CodeCoverage\Test\TestSize\TestSize;
use SebastianBergmann\CodeCoverage\Test\TestStatus\TestStatus;
use SebastianBergmann\CodeUnitReverseLookup\Wizard;
/**
* Provides collection functionality for PHP code coverage information.
*
* @psalm-type TestType = array{
* size: string,
* status: string,
* }
*/
final class CodeCoverage
{
private const UNCOVERED_FILES = 'UNCOVERED_FILES';
private readonly Driver $driver;
private readonly Filter $filter;
private readonly Wizard $wizard;
private bool $checkForUnintentionallyCoveredCode = false;
private bool $includeUncoveredFiles = true;
private bool $ignoreDeprecatedCode = false;
private ?string $currentId = null;
private ?TestSize $currentSize = null;
private ProcessedCodeCoverageData $data;
private bool $useAnnotationsForIgnoringCode = true;
/**
* @var Driver
* @psalm-var array<string,list<int>>
*/
private $driver;
private array $linesToBeIgnored = [];
/**
* @var Filter
* @psalm-var array<string, TestType>
*/
private $filter;
/**
* @var Wizard
*/
private $wizard;
/**
* @var bool
*/
private $checkForUnintentionallyCoveredCode = false;
/**
* @var bool
*/
private $includeUncoveredFiles = true;
/**
* @var bool
*/
private $processUncoveredFiles = false;
/**
* @var bool
*/
private $ignoreDeprecatedCode = false;
/**
* @var null|PhptTestCase|string|TestCase
*/
private $currentId;
/**
* Code coverage data.
*
* @var ProcessedCodeCoverageData
*/
private $data;
/**
* @var bool
*/
private $useAnnotationsForIgnoringCode = true;
/**
* Test data.
*
* @var array
*/
private $tests = [];
private array $tests = [];
/**
* @psalm-var list<class-string>
*/
private $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
/**
* @var ?FileAnalyser
*/
private $analyser;
/**
* @var ?string
*/
private $cacheDirectory;
/**
* @var ?Directory
*/
private $cachedReport;
private array $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
private ?FileAnalyser $analyser = null;
private ?string $cacheDirectory = null;
private ?Directory $cachedReport = null;
public function __construct(Driver $driver, Filter $filter)
{
@@ -145,6 +100,7 @@ final class CodeCoverage
public function clear(): void
{
$this->currentId = null;
$this->currentSize = null;
$this->data = new ProcessedCodeCoverageData;
$this->tests = [];
$this->cachedReport = null;
@@ -172,9 +128,7 @@ final class CodeCoverage
public function getData(bool $raw = false): ProcessedCodeCoverageData
{
if (!$raw) {
if ($this->processUncoveredFiles) {
$this->processUncoveredFilesFromFilter();
} elseif ($this->includeUncoveredFiles) {
if ($this->includeUncoveredFiles) {
$this->addUncoveredFilesFromFilter();
}
}
@@ -191,7 +145,7 @@ final class CodeCoverage
}
/**
* Returns the test data.
* @psalm-return array<string, TestType>
*/
public function getTests(): array
{
@@ -199,25 +153,21 @@ final class CodeCoverage
}
/**
* Sets the test data.
* @psalm-param array<string, TestType> $tests
*/
public function setTests(array $tests): void
{
$this->tests = $tests;
}
/**
* Start collection of code coverage information.
*
* @param PhptTestCase|string|TestCase $id
*/
public function start($id, bool $clear = false): void
public function start(string $id, ?TestSize $size = null, bool $clear = false): void
{
if ($clear) {
$this->clear();
}
$this->currentId = $id;
$this->currentId = $id;
$this->currentSize = $size;
$this->driver->start();
@@ -225,38 +175,34 @@ final class CodeCoverage
}
/**
* Stop collection of code coverage information.
*
* @param array|false $linesToBeCovered
* @psalm-param array<string,list<int>> $linesToBeIgnored
*/
public function stop(bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): RawCodeCoverageData
public function stop(bool $append = true, ?TestStatus $status = null, array|false $linesToBeCovered = [], array $linesToBeUsed = [], array $linesToBeIgnored = []): RawCodeCoverageData
{
if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {
throw new InvalidArgumentException(
'$linesToBeCovered must be an array or false'
);
}
$data = $this->driver->stop();
$this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);
$this->linesToBeIgnored = array_merge_recursive(
$this->linesToBeIgnored,
$linesToBeIgnored,
);
$this->append($data, null, $append, $status, $linesToBeCovered, $linesToBeUsed, $linesToBeIgnored);
$this->currentId = null;
$this->currentSize = null;
$this->cachedReport = null;
return $data;
}
/**
* Appends code coverage data.
*
* @param PhptTestCase|string|TestCase $id
* @param array|false $linesToBeCovered
* @psalm-param array<string,list<int>> $linesToBeIgnored
*
* @throws ReflectionException
* @throws TestIdMissingException
* @throws UnintentionallyCoveredCodeException
*/
public function append(RawCodeCoverageData $rawData, $id = null, bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): void
public function append(RawCodeCoverageData $rawData, ?string $id = null, bool $append = true, ?TestStatus $status = null, array|false $linesToBeCovered = [], array $linesToBeUsed = [], array $linesToBeIgnored = []): void
{
if ($id === null) {
$id = $this->currentId;
@@ -268,12 +214,22 @@ final class CodeCoverage
$this->cachedReport = null;
if ($status === null) {
$status = TestStatus::unknown();
}
$size = $this->currentSize;
if ($size === null) {
$size = TestSize::unknown();
}
$this->applyFilter($rawData);
$this->applyExecutableLinesFilter($rawData);
if ($this->useAnnotationsForIgnoringCode) {
$this->applyIgnoredLinesFilter($rawData);
$this->applyIgnoredLinesFilter($rawData, $linesToBeIgnored);
}
$this->data->initializeUnseenData($rawData);
@@ -282,45 +238,27 @@ final class CodeCoverage
return;
}
if ($id !== self::UNCOVERED_FILES) {
$this->applyCoversAnnotationFilter(
$rawData,
$linesToBeCovered,
$linesToBeUsed
);
if (empty($rawData->lineCoverage())) {
return;
}
$size = 'unknown';
$status = -1;
$fromTestcase = false;
if ($id instanceof TestCase) {
$fromTestcase = true;
$_size = $id->getSize();
if ($_size === Test::SMALL) {
$size = 'small';
} elseif ($_size === Test::MEDIUM) {
$size = 'medium';
} elseif ($_size === Test::LARGE) {
$size = 'large';
}
$status = $id->getStatus();
$id = get_class($id) . '::' . $id->getName();
} elseif ($id instanceof PhptTestCase) {
$fromTestcase = true;
$size = 'large';
$id = $id->getName();
}
$this->tests[$id] = ['size' => $size, 'status' => $status, 'fromTestcase' => $fromTestcase];
$this->data->markCodeAsExecutedByTestCase($id, $rawData);
if ($id === self::UNCOVERED_FILES) {
return;
}
$this->applyCoversAndUsesFilter(
$rawData,
$linesToBeCovered,
$linesToBeUsed,
$size,
);
if (empty($rawData->lineCoverage())) {
return;
}
$this->tests[$id] = [
'size' => $size->asString(),
'status' => $status->asString(),
];
$this->data->markCodeAsExecutedByTestCase($id, $rawData);
}
/**
@@ -329,7 +267,7 @@ final class CodeCoverage
public function merge(self $that): void
{
$this->filter->includeFiles(
$that->filter()->files()
$that->filter()->files(),
);
$this->data->merge($that->data);
@@ -359,16 +297,6 @@ final class CodeCoverage
$this->includeUncoveredFiles = false;
}
public function processUncoveredFiles(): void
{
$this->processUncoveredFiles = true;
}
public function doNotProcessUncoveredFiles(): void
{
$this->processUncoveredFiles = false;
}
public function enableAnnotationsForIgnoringCode(): void
{
$this->useAnnotationsForIgnoringCode = true;
@@ -414,7 +342,7 @@ final class CodeCoverage
{
if (!$this->cachesStaticAnalysis()) {
throw new StaticAnalysisCacheNotConfiguredException(
'The static analysis cache is not configured'
'The static analysis cache is not configured',
);
}
@@ -450,14 +378,10 @@ final class CodeCoverage
}
/**
* Applies the @covers annotation filtering.
*
* @param array|false $linesToBeCovered
*
* @throws ReflectionException
* @throws UnintentionallyCoveredCodeException
*/
private function applyCoversAnnotationFilter(RawCodeCoverageData $rawData, $linesToBeCovered, array $linesToBeUsed): void
private function applyCoversAndUsesFilter(RawCodeCoverageData $rawData, array|false $linesToBeCovered, array $linesToBeUsed, TestSize $size): void
{
if ($linesToBeCovered === false) {
$rawData->clear();
@@ -469,9 +393,7 @@ final class CodeCoverage
return;
}
if ($this->checkForUnintentionallyCoveredCode &&
(!$this->currentId instanceof TestCase ||
(!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {
if ($this->checkForUnintentionallyCoveredCode && !$size->isMedium() && !$size->isLarge()) {
$this->performUnintentionallyCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed);
}
@@ -514,26 +436,36 @@ final class CodeCoverage
$data->keepLineCoverageDataOnlyForLines(
$filename,
array_keys($linesToBranchMap)
array_keys($linesToBranchMap),
);
$data->markExecutableLineByBranch(
$filename,
$linesToBranchMap
$linesToBranchMap,
);
}
}
private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void
/**
* @psalm-param array<string,list<int>> $linesToBeIgnored
*/
private function applyIgnoredLinesFilter(RawCodeCoverageData $data, array $linesToBeIgnored): void
{
foreach (array_keys($data->lineCoverage()) as $filename) {
if (!$this->filter->isFile($filename)) {
continue;
}
if (isset($linesToBeIgnored[$filename])) {
$data->removeCoverageDataForLines(
$filename,
$linesToBeIgnored[$filename],
);
}
$data->removeCoverageDataForLines(
$filename,
$this->analyser()->ignoredLinesFor($filename)
$this->analyser()->ignoredLinesFor($filename),
);
}
}
@@ -545,43 +477,23 @@ final class CodeCoverage
{
$uncoveredFiles = array_diff(
$this->filter->files(),
$this->data->coveredFiles()
$this->data->coveredFiles(),
);
foreach ($uncoveredFiles as $uncoveredFile) {
if ($this->filter->isFile($uncoveredFile)) {
if (is_file($uncoveredFile)) {
$this->append(
RawCodeCoverageData::fromUncoveredFile(
$uncoveredFile,
$this->analyser()
$this->analyser(),
),
self::UNCOVERED_FILES
self::UNCOVERED_FILES,
linesToBeIgnored: $this->linesToBeIgnored,
);
}
}
}
/**
* @throws UnintentionallyCoveredCodeException
*/
private function processUncoveredFilesFromFilter(): void
{
$uncoveredFiles = array_diff(
$this->filter->files(),
$this->data->coveredFiles()
);
$this->driver->start();
foreach ($uncoveredFiles as $uncoveredFile) {
if ($this->filter->isFile($uncoveredFile)) {
include_once $uncoveredFile;
}
}
$this->append($this->driver->stop(), self::UNCOVERED_FILES);
}
/**
* @throws ReflectionException
* @throws UnintentionallyCoveredCodeException
@@ -590,7 +502,7 @@ final class CodeCoverage
{
$allowedLines = $this->getAllowedLines(
$linesToBeCovered,
$linesToBeUsed
$linesToBeUsed,
);
$unintentionallyCoveredUnits = [];
@@ -607,7 +519,7 @@ final class CodeCoverage
if (!empty($unintentionallyCoveredUnits)) {
throw new UnintentionallyCoveredCodeException(
$unintentionallyCoveredUnits
$unintentionallyCoveredUnits,
);
}
}
@@ -623,7 +535,7 @@ final class CodeCoverage
$allowedLines[$file] = array_merge(
$allowedLines[$file],
$linesToBeCovered[$file]
$linesToBeCovered[$file],
);
}
@@ -634,13 +546,13 @@ final class CodeCoverage
$allowedLines[$file] = array_merge(
$allowedLines[$file],
$linesToBeUsed[$file]
$linesToBeUsed[$file],
);
}
foreach (array_keys($allowedLines) as $file) {
$allowedLines[$file] = array_flip(
array_unique($allowedLines[$file])
array_unique($allowedLines[$file]),
);
}
@@ -648,40 +560,50 @@ final class CodeCoverage
}
/**
* @param list<string> $unintentionallyCoveredUnits
*
* @throws ReflectionException
*
* @return list<string>
*/
private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array
{
$unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits);
sort($unintentionallyCoveredUnits);
$processed = [];
foreach (array_keys($unintentionallyCoveredUnits) as $k => $v) {
$unit = explode('::', $unintentionallyCoveredUnits[$k]);
foreach ($unintentionallyCoveredUnits as $unintentionallyCoveredUnit) {
$tmp = explode('::', $unintentionallyCoveredUnit);
if (count($tmp) !== 2) {
$processed[] = $unintentionallyCoveredUnit;
if (count($unit) !== 2) {
continue;
}
try {
$class = new ReflectionClass($unit[0]);
$class = new ReflectionClass($tmp[0]);
foreach ($this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck as $parentClass) {
if ($class->isSubclassOf($parentClass)) {
unset($unintentionallyCoveredUnits[$k]);
break;
continue 2;
}
}
} catch (\ReflectionException $e) {
throw new ReflectionException(
$e->getMessage(),
$e->getCode(),
$e
$e,
);
}
$processed[] = $tmp[0];
}
return array_values($unintentionallyCoveredUnits);
$processed = array_unique($processed);
sort($processed);
return $processed;
}
private function analyser(): FileAnalyser
@@ -692,7 +614,7 @@ final class CodeCoverage
$this->analyser = new ParsingFileAnalyser(
$this->useAnnotationsForIgnoringCode,
$this->ignoreDeprecatedCode
$this->ignoreDeprecatedCode,
);
if ($this->cachesStaticAnalysis()) {
@@ -700,7 +622,7 @@ final class CodeCoverage
$this->cacheDirectory,
$this->analyser,
$this->useAnnotationsForIgnoringCode,
$this->ignoreDeprecatedCode
$this->ignoreDeprecatedCode,
);
}

View File

@@ -7,7 +7,7 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
namespace SebastianBergmann\CodeCoverage\Data;
use function array_key_exists;
use function array_keys;
@@ -20,6 +20,10 @@ use SebastianBergmann\CodeCoverage\Driver\Driver;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type XdebugFunctionCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
*
* @psalm-type TestIdType = string
*/
final class ProcessedCodeCoverageData
{
@@ -27,18 +31,33 @@ final class ProcessedCodeCoverageData
* Line coverage data.
* An array of filenames, each having an array of linenumbers, each executable line having an array of testcase ids.
*
* @var array
* @psalm-var array<string, array<int, null|list<TestIdType>>>
*/
private $lineCoverage = [];
private array $lineCoverage = [];
/**
* Function coverage data.
* Maintains base format of raw data (@see https://xdebug.org/docs/code_coverage), but each 'hit' entry is an array
* of testcase ids.
*
* @var array
* @psalm-var array<string, array<string, array{
* branches: array<int, array{
* op_start: int,
* op_end: int,
* line_start: int,
* line_end: int,
* hit: list<TestIdType>,
* out: array<int, int>,
* out_hit: array<int, int>,
* }>,
* paths: array<int, array{
* path: array<int, int>,
* hit: list<TestIdType>,
* }>,
* hit: list<TestIdType>
* }>>
*/
private $functionCoverage = [];
private array $functionCoverage = [];
public function initializeUnseenData(RawCodeCoverageData $rawData): void
{
@@ -145,8 +164,8 @@ final class ProcessedCodeCoverageData
$compareLineNumbers = array_unique(
array_merge(
array_keys($this->lineCoverage[$file]),
array_keys($newData->lineCoverage[$file])
)
array_keys($newData->lineCoverage[$file]),
),
);
foreach ($compareLineNumbers as $line) {
@@ -157,7 +176,7 @@ final class ProcessedCodeCoverageData
$this->lineCoverage[$file][$line] = $newData->lineCoverage[$file][$line];
} elseif ($thatPriority === $thisPriority && is_array($this->lineCoverage[$file][$line])) {
$this->lineCoverage[$file][$line] = array_unique(
array_merge($this->lineCoverage[$file][$line], $newData->lineCoverage[$file][$line])
array_merge($this->lineCoverage[$file][$line], $newData->lineCoverage[$file][$line]),
);
}
}
@@ -217,6 +236,8 @@ final class ProcessedCodeCoverageData
/**
* For a function we have never seen before, copy all data over and simply init the 'hit' array.
*
* @psalm-param XdebugFunctionCoverageType $functionData
*/
private function initPreviouslyUnseenFunction(string $file, string $functionName, array $functionData): void
{
@@ -235,6 +256,8 @@ final class ProcessedCodeCoverageData
* For a function we have seen before, only copy over and init the 'hit' array for any unseen branches and paths.
* Techniques such as mocking and where the contents of a file are different vary during tests (e.g. compiling
* containers) mean that the functions inside a file cannot be relied upon to be static.
*
* @psalm-param XdebugFunctionCoverageType $functionData
*/
private function initPreviouslySeenFunction(string $file, string $functionName, array $functionData): void
{

View File

@@ -7,7 +7,7 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
namespace SebastianBergmann\CodeCoverage\Data;
use function array_diff;
use function array_diff_key;
@@ -19,65 +19,62 @@ use function explode;
use function file_get_contents;
use function in_array;
use function is_file;
use function preg_replace;
use function range;
use function str_ends_with;
use function str_starts_with;
use function trim;
use SebastianBergmann\CodeCoverage\Driver\Driver;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type XdebugFunctionsCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
* @psalm-import-type XdebugCodeCoverageWithoutPathCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
* @psalm-import-type XdebugCodeCoverageWithPathCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
*/
final class RawCodeCoverageData
{
/**
* @var array<string, array<int>>
*/
private static $emptyLineCache = [];
private static array $emptyLineCache = [];
/**
* @var array
*
* @see https://xdebug.org/docs/code_coverage for format
* @psalm-var XdebugCodeCoverageWithoutPathCoverageType
*/
private $lineCoverage;
private array $lineCoverage;
/**
* @var array
*
* @see https://xdebug.org/docs/code_coverage for format
* @psalm-var array<string, XdebugFunctionsCoverageType>
*/
private $functionCoverage;
private array $functionCoverage;
/**
* @psalm-param XdebugCodeCoverageWithoutPathCoverageType $rawCoverage
*/
public static function fromXdebugWithoutPathCoverage(array $rawCoverage): self
{
return new self($rawCoverage, []);
}
/**
* @psalm-param XdebugCodeCoverageWithPathCoverageType $rawCoverage
*/
public static function fromXdebugWithPathCoverage(array $rawCoverage): self
{
$lineCoverage = [];
$functionCoverage = [];
foreach ($rawCoverage as $file => $fileCoverageData) {
$lineCoverage[$file] = $fileCoverageData['lines'];
$functionCoverage[$file] = $fileCoverageData['functions'];
}
return new self($lineCoverage, $functionCoverage);
}
public static function fromXdebugWithMixedCoverage(array $rawCoverage): self
{
$lineCoverage = [];
$functionCoverage = [];
foreach ($rawCoverage as $file => $fileCoverageData) {
if (!isset($fileCoverageData['functions'])) {
// Current file does not have functions, so line coverage
// is stored in $fileCoverageData, not in $fileCoverageData['lines']
$lineCoverage[$file] = $fileCoverageData;
continue;
// Xdebug annotates the function name of traits, strip that off
foreach ($fileCoverageData['functions'] as $existingKey => $data) {
if (str_ends_with($existingKey, '}') && !str_starts_with($existingKey, '{')) { // don't want to catch {main}
$newKey = preg_replace('/\{.*}$/', '', $existingKey);
$fileCoverageData['functions'][$newKey] = $data;
unset($fileCoverageData['functions'][$existingKey]);
}
}
$lineCoverage[$file] = $fileCoverageData['lines'];
@@ -98,6 +95,10 @@ final class RawCodeCoverageData
return new self([$filename => $lineCoverage], []);
}
/**
* @psalm-param XdebugCodeCoverageWithoutPathCoverageType $lineCoverage
* @psalm-param array<string, XdebugFunctionsCoverageType> $functionCoverage
*/
private function __construct(array $lineCoverage, array $functionCoverage)
{
$this->lineCoverage = $lineCoverage;
@@ -111,11 +112,17 @@ final class RawCodeCoverageData
$this->lineCoverage = $this->functionCoverage = [];
}
/**
* @psalm-return XdebugCodeCoverageWithoutPathCoverageType
*/
public function lineCoverage(): array
{
return $this->lineCoverage;
}
/**
* @psalm-return array<string, XdebugFunctionsCoverageType>
*/
public function functionCoverage(): array
{
return $this->functionCoverage;
@@ -137,7 +144,7 @@ final class RawCodeCoverageData
$this->lineCoverage[$filename] = array_intersect_key(
$this->lineCoverage[$filename],
array_flip($lines)
array_flip($lines),
);
}
@@ -216,7 +223,7 @@ final class RawCodeCoverageData
$this->lineCoverage[$filename] = array_diff_key(
$this->lineCoverage[$filename],
array_flip($lines)
array_flip($lines),
);
if (isset($this->functionCoverage[$filename])) {

View File

@@ -11,11 +11,8 @@ namespace SebastianBergmann\CodeCoverage\Driver;
use function sprintf;
use SebastianBergmann\CodeCoverage\BranchAndPathCoverageNotSupportedException;
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
use SebastianBergmann\CodeCoverage\DeadCodeDetectionNotSupportedException;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverAvailableException;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverWithPathCoverageSupportAvailableException;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
@@ -55,45 +52,9 @@ abstract class Driver
*
* @see http://xdebug.org/docs/code_coverage
*/
public const BRANCH_HIT = 1;
/**
* @var bool
*/
private $collectBranchAndPathCoverage = false;
/**
* @var bool
*/
private $detectDeadCode = false;
/**
* @throws NoCodeCoverageDriverAvailableException
* @throws PcovNotAvailableException
* @throws PhpdbgNotAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
*
* @deprecated Use DriverSelector::forLineCoverage() instead
*/
public static function forLineCoverage(Filter $filter): self
{
return (new Selector)->forLineCoverage($filter);
}
/**
* @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
*
* @deprecated Use DriverSelector::forLineAndPathCoverage() instead
*/
public static function forLineAndPathCoverage(Filter $filter): self
{
return (new Selector)->forLineAndPathCoverage($filter);
}
public const BRANCH_HIT = 1;
private bool $collectBranchAndPathCoverage = false;
private bool $detectDeadCode = false;
public function canCollectBranchAndPathCoverage(): bool
{
@@ -114,8 +75,8 @@ abstract class Driver
throw new BranchAndPathCoverageNotSupportedException(
sprintf(
'%s does not support branch and path coverage',
$this->nameAndVersion()
)
$this->nameAndVersion(),
),
);
}
@@ -146,8 +107,8 @@ abstract class Driver
throw new DeadCodeDetectionNotSupportedException(
sprintf(
'%s does not support dead code detection',
$this->nameAndVersion()
)
$this->nameAndVersion(),
),
);
}

View File

@@ -18,27 +18,22 @@ use function pcov\start;
use function pcov\stop;
use function pcov\waiting;
use function phpversion;
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class PcovDriver extends Driver
{
/**
* @var Filter
*/
private $filter;
private readonly Filter $filter;
/**
* @throws PcovNotAvailableException
*/
public function __construct(Filter $filter)
{
if (!extension_loaded('pcov')) {
throw new PcovNotAvailableException;
}
$this->ensurePcovIsAvailable();
$this->filter = $filter;
}
@@ -72,4 +67,14 @@ final class PcovDriver extends Driver
{
return 'PCOV ' . phpversion('pcov');
}
/**
* @throws PcovNotAvailableException
*/
private function ensurePcovIsAvailable(): void
{
if (!extension_loaded('pcov')) {
throw new PcovNotAvailableException;
}
}
}

View File

@@ -1,93 +0,0 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use const PHP_SAPI;
use const PHP_VERSION;
use function array_diff;
use function array_keys;
use function array_merge;
use function get_included_files;
use function phpdbg_end_oplog;
use function phpdbg_get_executable;
use function phpdbg_start_oplog;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class PhpdbgDriver extends Driver
{
/**
* @throws PhpdbgNotAvailableException
*/
public function __construct()
{
if (PHP_SAPI !== 'phpdbg') {
throw new PhpdbgNotAvailableException;
}
}
public function start(): void
{
phpdbg_start_oplog();
}
public function stop(): RawCodeCoverageData
{
static $fetchedLines = [];
$dbgData = phpdbg_end_oplog();
if ($fetchedLines === []) {
$sourceLines = phpdbg_get_executable();
} else {
$newFiles = array_diff(get_included_files(), array_keys($fetchedLines));
$sourceLines = [];
if ($newFiles) {
$sourceLines = phpdbg_get_executable(['files' => $newFiles]);
}
}
foreach ($sourceLines as $file => $lines) {
foreach ($lines as $lineNo => $numExecuted) {
$sourceLines[$file][$lineNo] = self::LINE_NOT_EXECUTED;
}
}
$fetchedLines = array_merge($fetchedLines, $sourceLines);
return RawCodeCoverageData::fromXdebugWithoutPathCoverage(
$this->detectExecutedLines($fetchedLines, $dbgData)
);
}
public function nameAndVersion(): string
{
return 'PHPDBG ' . PHP_VERSION;
}
private function detectExecutedLines(array $sourceLines, array $dbgData): array
{
foreach ($dbgData as $file => $coveredLines) {
foreach ($coveredLines as $lineNo => $numExecuted) {
// phpdbg also reports $lineNo=0 when e.g. exceptions get thrown.
// make sure we only mark lines executed which are actually executable.
if (isset($sourceLines[$file][$lineNo])) {
$sourceLines[$file][$lineNo] = self::LINE_EXECUTED;
}
}
}
return $sourceLines;
}
}

View File

@@ -9,8 +9,6 @@
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use function phpversion;
use function version_compare;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverAvailableException;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverWithPathCoverageSupportAvailableException;
@@ -21,29 +19,19 @@ final class Selector
/**
* @throws NoCodeCoverageDriverAvailableException
* @throws PcovNotAvailableException
* @throws PhpdbgNotAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
* @throws XdebugNotEnabledException
*/
public function forLineCoverage(Filter $filter): Driver
{
$runtime = new Runtime;
if ($runtime->hasPHPDBGCodeCoverage()) {
return new PhpdbgDriver;
}
if ($runtime->hasPCOV()) {
return new PcovDriver($filter);
}
if ($runtime->hasXdebug()) {
if (version_compare(phpversion('xdebug'), '3', '>=')) {
$driver = new Xdebug3Driver($filter);
} else {
$driver = new Xdebug2Driver($filter);
}
$driver = new XdebugDriver($filter);
$driver->enableDeadCodeDetection();
@@ -55,18 +43,13 @@ final class Selector
/**
* @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
* @throws XdebugNotEnabledException
*/
public function forLineAndPathCoverage(Filter $filter): Driver
{
if ((new Runtime)->hasXdebug()) {
if (version_compare(phpversion('xdebug'), '3', '>=')) {
$driver = new Xdebug3Driver($filter);
} else {
$driver = new Xdebug2Driver($filter);
}
$driver = new XdebugDriver($filter);
$driver->enableDeadCodeDetection();
$driver->enableBranchAndPathCoverage();

View File

@@ -1,128 +0,0 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use const XDEBUG_CC_BRANCH_CHECK;
use const XDEBUG_CC_DEAD_CODE;
use const XDEBUG_CC_UNUSED;
use const XDEBUG_FILTER_CODE_COVERAGE;
use const XDEBUG_PATH_INCLUDE;
use const XDEBUG_PATH_WHITELIST;
use function defined;
use function extension_loaded;
use function ini_get;
use function phpversion;
use function sprintf;
use function version_compare;
use function xdebug_get_code_coverage;
use function xdebug_set_filter;
use function xdebug_start_code_coverage;
use function xdebug_stop_code_coverage;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Xdebug2Driver extends Driver
{
/**
* @var bool
*/
private $pathCoverageIsMixedCoverage;
/**
* @throws WrongXdebugVersionException
* @throws Xdebug2NotEnabledException
* @throws XdebugNotAvailableException
*/
public function __construct(Filter $filter)
{
if (!extension_loaded('xdebug')) {
throw new XdebugNotAvailableException;
}
if (version_compare(phpversion('xdebug'), '3', '>=')) {
throw new WrongXdebugVersionException(
sprintf(
'This driver requires Xdebug 2 but version %s is loaded',
phpversion('xdebug')
)
);
}
if (!ini_get('xdebug.coverage_enable')) {
throw new Xdebug2NotEnabledException;
}
if (!$filter->isEmpty()) {
if (defined('XDEBUG_PATH_WHITELIST')) {
$listType = XDEBUG_PATH_WHITELIST;
} else {
$listType = XDEBUG_PATH_INCLUDE;
}
xdebug_set_filter(
XDEBUG_FILTER_CODE_COVERAGE,
$listType,
$filter->files()
);
}
$this->pathCoverageIsMixedCoverage = version_compare(phpversion('xdebug'), '2.9.6', '<');
}
public function canCollectBranchAndPathCoverage(): bool
{
return true;
}
public function canDetectDeadCode(): bool
{
return true;
}
public function start(): void
{
$flags = XDEBUG_CC_UNUSED;
if ($this->detectsDeadCode() || $this->collectsBranchAndPathCoverage()) {
$flags |= XDEBUG_CC_DEAD_CODE;
}
if ($this->collectsBranchAndPathCoverage()) {
$flags |= XDEBUG_CC_BRANCH_CHECK;
}
xdebug_start_code_coverage($flags);
}
public function stop(): RawCodeCoverageData
{
$data = xdebug_get_code_coverage();
xdebug_stop_code_coverage();
if ($this->collectsBranchAndPathCoverage()) {
if ($this->pathCoverageIsMixedCoverage) {
return RawCodeCoverageData::fromXdebugWithMixedCoverage($data);
}
return RawCodeCoverageData::fromXdebugWithPathCoverage($data);
}
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data);
}
public function nameAndVersion(): string
{
return 'Xdebug ' . phpversion('xdebug');
}
}

View File

@@ -20,56 +20,62 @@ use function getenv;
use function in_array;
use function ini_get;
use function phpversion;
use function sprintf;
use function version_compare;
use function xdebug_get_code_coverage;
use function xdebug_info;
use function xdebug_set_filter;
use function xdebug_start_code_coverage;
use function xdebug_stop_code_coverage;
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @see https://xdebug.org/docs/code_coverage#xdebug_get_code_coverage
*
* @psalm-type XdebugLinesCoverageType = array<int, int>
* @psalm-type XdebugBranchCoverageType = array{
* op_start: int,
* op_end: int,
* line_start: int,
* line_end: int,
* hit: int,
* out: array<int, int>,
* out_hit: array<int, int>,
* }
* @psalm-type XdebugPathCoverageType = array{
* path: array<int, int>,
* hit: int,
* }
* @psalm-type XdebugFunctionCoverageType = array{
* branches: array<int, XdebugBranchCoverageType>,
* paths: array<int, XdebugPathCoverageType>,
* }
* @psalm-type XdebugFunctionsCoverageType = array<string, XdebugFunctionCoverageType>
* @psalm-type XdebugPathAndBranchesCoverageType = array{
* lines: XdebugLinesCoverageType,
* functions: XdebugFunctionsCoverageType,
* }
* @psalm-type XdebugCodeCoverageWithoutPathCoverageType = array<string, XdebugLinesCoverageType>
* @psalm-type XdebugCodeCoverageWithPathCoverageType = array<string, XdebugPathAndBranchesCoverageType>
*/
final class Xdebug3Driver extends Driver
final class XdebugDriver extends Driver
{
/**
* @throws WrongXdebugVersionException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
* @throws XdebugNotEnabledException
*/
public function __construct(Filter $filter)
{
if (!extension_loaded('xdebug')) {
throw new XdebugNotAvailableException;
}
if (version_compare(phpversion('xdebug'), '3', '<')) {
throw new WrongXdebugVersionException(
sprintf(
'This driver requires Xdebug 3 but version %s is loaded',
phpversion('xdebug')
)
);
}
$mode = getenv('XDEBUG_MODE');
if ($mode === false || $mode === '') {
$mode = ini_get('xdebug.mode');
}
if ($mode === false ||
!in_array('coverage', explode(',', $mode), true)) {
throw new Xdebug3NotEnabledException;
}
$this->ensureXdebugIsAvailable();
$this->ensureXdebugCodeCoverageFeatureIsEnabled();
if (!$filter->isEmpty()) {
xdebug_set_filter(
XDEBUG_FILTER_CODE_COVERAGE,
XDEBUG_PATH_INCLUDE,
$filter->files()
$filter->files(),
);
}
}
@@ -106,9 +112,11 @@ final class Xdebug3Driver extends Driver
xdebug_stop_code_coverage();
if ($this->collectsBranchAndPathCoverage()) {
/* @var XdebugCodeCoverageWithPathCoverageType $data */
return RawCodeCoverageData::fromXdebugWithPathCoverage($data);
}
/* @var XdebugCodeCoverageWithoutPathCoverageType $data */
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data);
}
@@ -116,4 +124,39 @@ final class Xdebug3Driver extends Driver
{
return 'Xdebug ' . phpversion('xdebug');
}
/**
* @throws XdebugNotAvailableException
*/
private function ensureXdebugIsAvailable(): void
{
if (!extension_loaded('xdebug')) {
throw new XdebugNotAvailableException;
}
}
/**
* @throws XdebugNotEnabledException
*/
private function ensureXdebugCodeCoverageFeatureIsEnabled(): void
{
if (version_compare(phpversion('xdebug'), '3.1', '>=')) {
if (!in_array('coverage', xdebug_info('mode'), true)) {
throw new XdebugNotEnabledException;
}
return;
}
$mode = getenv('XDEBUG_MODE');
if ($mode === false || $mode === '') {
$mode = ini_get('xdebug.mode');
}
if ($mode === false ||
!in_array('coverage', explode(',', $mode), true)) {
throw new XdebugNotEnabledException;
}
}
}

View File

@@ -7,11 +7,10 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
namespace SebastianBergmann\CodeCoverage;
use RuntimeException;
use SebastianBergmann\CodeCoverage\Exception;
final class WrongXdebugVersionException extends RuntimeException implements Exception
final class FileCouldNotBeWrittenException extends RuntimeException implements Exception
{
}

View File

@@ -14,10 +14,13 @@ use RuntimeException;
final class UnintentionallyCoveredCodeException extends RuntimeException implements Exception
{
/**
* @var array
* @var list<string>
*/
private $unintentionallyCoveredUnits;
private readonly array $unintentionallyCoveredUnits;
/**
* @param list<string> $unintentionallyCoveredUnits
*/
public function __construct(array $unintentionallyCoveredUnits)
{
$this->unintentionallyCoveredUnits = $unintentionallyCoveredUnits;
@@ -25,6 +28,9 @@ final class UnintentionallyCoveredCodeException extends RuntimeException impleme
parent::__construct($this->toString());
}
/**
* @return list<string>
*/
public function getUnintentionallyCoveredUnits(): array
{
return $this->unintentionallyCoveredUnits;

View File

@@ -1,21 +0,0 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use RuntimeException;
use SebastianBergmann\CodeCoverage\Exception;
final class Xdebug2NotEnabledException extends RuntimeException implements Exception
{
public function __construct()
{
parent::__construct('xdebug.coverage_enable=On has to be set');
}
}

View File

@@ -1,21 +0,0 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use RuntimeException;
use SebastianBergmann\CodeCoverage\Exception;
final class Xdebug3NotEnabledException extends RuntimeException implements Exception
{
public function __construct()
{
parent::__construct('XDEBUG_MODE=coverage or xdebug.mode=coverage has to be set');
}
}

View File

@@ -12,10 +12,10 @@ namespace SebastianBergmann\CodeCoverage\Driver;
use RuntimeException;
use SebastianBergmann\CodeCoverage\Exception;
final class PhpdbgNotAvailableException extends RuntimeException implements Exception
final class XdebugNotEnabledException extends RuntimeException implements Exception
{
public function __construct()
{
parent::__construct('The PHPDBG SAPI is not available');
parent::__construct('XDEBUG_MODE=coverage (environment variable) or xdebug.mode=coverage (PHP configuration setting) has to be set');
}
}

View File

@@ -12,7 +12,8 @@ namespace SebastianBergmann\CodeCoverage;
use function array_keys;
use function is_file;
use function realpath;
use function strpos;
use function str_contains;
use function str_starts_with;
use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
final class Filter
@@ -20,13 +21,16 @@ final class Filter
/**
* @psalm-var array<string,true>
*/
private $files = [];
private array $files = [];
/**
* @psalm-var array<string,bool>
*/
private $isFileCache = [];
private array $isFileCache = [];
/**
* @deprecated
*/
public function includeDirectory(string $directory, string $suffix = '.php', string $prefix = ''): void
{
foreach ((new FileIteratorFacade)->getFilesAsArray($directory, $suffix, $prefix) as $file) {
@@ -55,6 +59,9 @@ final class Filter
$this->files[$filename] = true;
}
/**
* @deprecated
*/
public function excludeDirectory(string $directory, string $suffix = '.php', string $prefix = ''): void
{
foreach ((new FileIteratorFacade)->getFilesAsArray($directory, $suffix, $prefix) as $file) {
@@ -62,6 +69,9 @@ final class Filter
}
}
/**
* @deprecated
*/
public function excludeFile(string $filename): void
{
$filename = realpath($filename);
@@ -80,14 +90,14 @@ final class Filter
}
if ($filename === '-' ||
strpos($filename, 'vfs://') === 0 ||
strpos($filename, 'xdebug://debug-eval') !== false ||
strpos($filename, 'eval()\'d code') !== false ||
strpos($filename, 'runtime-created function') !== false ||
strpos($filename, 'runkit created function') !== false ||
strpos($filename, 'assert code') !== false ||
strpos($filename, 'regexp code') !== false ||
strpos($filename, 'Standard input code') !== false) {
str_starts_with($filename, 'vfs://') ||
str_contains($filename, 'xdebug://debug-eval') ||
str_contains($filename, 'eval()\'d code') ||
str_contains($filename, 'runtime-created function') ||
str_contains($filename, 'runkit created function') ||
str_contains($filename, 'assert code') ||
str_contains($filename, 'regexp code') ||
str_contains($filename, 'Standard input code')) {
$isFile = false;
} else {
$isFile = is_file($filename);

View File

@@ -11,6 +11,7 @@ namespace SebastianBergmann\CodeCoverage\Node;
use const DIRECTORY_SEPARATOR;
use function array_merge;
use function str_ends_with;
use function str_replace;
use function substr;
use Countable;
@@ -18,42 +19,31 @@ use SebastianBergmann\CodeCoverage\Util\Percentage;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
* @psalm-import-type ProcessedFunctionType from \SebastianBergmann\CodeCoverage\Node\File
* @psalm-import-type ProcessedClassType from \SebastianBergmann\CodeCoverage\Node\File
* @psalm-import-type ProcessedTraitType from \SebastianBergmann\CodeCoverage\Node\File
*/
abstract class AbstractNode implements Countable
{
/**
* @var string
*/
private $name;
private readonly string $name;
private string $pathAsString;
private array $pathAsArray;
private readonly ?AbstractNode $parent;
private string $id;
/**
* @var string
*/
private $pathAsString;
/**
* @var array
*/
private $pathAsArray;
/**
* @var AbstractNode
*/
private $parent;
/**
* @var string
*/
private $id;
public function __construct(string $name, self $parent = null)
public function __construct(string $name, ?self $parent = null)
{
if (substr($name, -1) === DIRECTORY_SEPARATOR) {
if (str_ends_with($name, DIRECTORY_SEPARATOR)) {
$name = substr($name, 0, -1);
}
$this->name = $name;
$this->parent = $parent;
$this->processId();
$this->processPath();
}
public function name(): string
@@ -63,50 +53,16 @@ abstract class AbstractNode implements Countable
public function id(): string
{
if ($this->id === null) {
$parent = $this->parent();
if ($parent === null) {
$this->id = 'index';
} else {
$parentId = $parent->id();
if ($parentId === 'index') {
$this->id = str_replace(':', '_', $this->name);
} else {
$this->id = $parentId . '/' . $this->name;
}
}
}
return $this->id;
}
public function pathAsString(): string
{
if ($this->pathAsString === null) {
if ($this->parent === null) {
$this->pathAsString = $this->name;
} else {
$this->pathAsString = $this->parent->pathAsString() . DIRECTORY_SEPARATOR . $this->name;
}
}
return $this->pathAsString;
}
public function pathAsArray(): array
{
if ($this->pathAsArray === null) {
if ($this->parent === null) {
$this->pathAsArray = [];
} else {
$this->pathAsArray = $this->parent->pathAsArray();
}
$this->pathAsArray[] = $this;
}
return $this->pathAsArray;
}
@@ -175,7 +131,7 @@ abstract class AbstractNode implements Countable
{
return Percentage::fromFractionAndTotal(
$this->numberOfExecutedBranches(),
$this->numberOfExecutableBranches()
$this->numberOfExecutableBranches(),
);
}
@@ -183,7 +139,7 @@ abstract class AbstractNode implements Countable
{
return Percentage::fromFractionAndTotal(
$this->numberOfExecutedPaths(),
$this->numberOfExecutablePaths()
$this->numberOfExecutablePaths(),
);
}
@@ -212,14 +168,23 @@ abstract class AbstractNode implements Countable
return $this->numberOfTestedFunctions() + $this->numberOfTestedMethods();
}
/**
* @psalm-return array<string, ProcessedClassType>
*/
abstract public function classes(): array;
/**
* @psalm-return array<string, ProcessedTraitType>
*/
abstract public function traits(): array;
/**
* @psalm-return array<string, ProcessedFunctionType>
*/
abstract public function functions(): array;
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
* @psalm-return LinesOfCodeType
*/
abstract public function linesOfCode(): array;
@@ -250,4 +215,36 @@ abstract class AbstractNode implements Countable
abstract public function numberOfFunctions(): int;
abstract public function numberOfTestedFunctions(): int;
private function processId(): void
{
if ($this->parent === null) {
$this->id = 'index';
return;
}
$parentId = $this->parent->id();
if ($parentId === 'index') {
$this->id = str_replace(':', '_', $this->name);
} else {
$this->id = $parentId . '/' . $this->name;
}
}
private function processPath(): void
{
if ($this->parent === null) {
$this->pathAsArray = [$this];
$this->pathAsString = $this->name;
return;
}
$this->pathAsArray = $this->parent->pathAsArray();
$this->pathAsString = $this->parent->pathAsString() . DIRECTORY_SEPARATOR . $this->name;
$this->pathAsArray[] = $this;
}
}

View File

@@ -17,22 +17,22 @@ use function dirname;
use function explode;
use function implode;
use function is_file;
use function str_ends_with;
use function str_replace;
use function strpos;
use function str_starts_with;
use function substr;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\ProcessedCodeCoverageData;
use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type TestType from \SebastianBergmann\CodeCoverage\CodeCoverage
*/
final class Builder
{
/**
* @var FileAnalyser
*/
private $analyser;
private readonly FileAnalyser $analyser;
public function __construct(FileAnalyser $analyser)
{
@@ -45,24 +45,27 @@ final class Builder
$commonPath = $this->reducePaths($data);
$root = new Directory(
$commonPath,
null
null,
);
$this->addItems(
$root,
$this->buildDirectoryStructure($data),
$coverage->getTests()
$coverage->getTests(),
);
return $root;
}
/**
* @psalm-param array<string, TestType> $tests
*/
private function addItems(Directory $root, array $items, array $tests): void
{
foreach ($items as $key => $value) {
$key = (string) $key;
if (substr($key, -2) === '/f') {
if (str_ends_with($key, '/f')) {
$key = substr($key, 0, -2);
$filename = $root->pathAsString() . DIRECTORY_SEPARATOR . $key;
@@ -77,8 +80,8 @@ final class Builder
$this->analyser->classesIn($filename),
$this->analyser->traitsIn($filename),
$this->analyser->functionsIn($filename),
$this->analyser->linesOfCodeFor($filename)
)
$this->analyser->linesOfCodeFor($filename),
),
);
}
} else {
@@ -128,6 +131,8 @@ final class Builder
* )
* )
* </code>
*
* @psalm-return array<string, array<string, array{lineCoverage: array<int, int>, functionCoverage: array<string, array<int, int>>}>>
*/
private function buildDirectoryStructure(ProcessedCodeCoverageData $data): array
{
@@ -214,7 +219,7 @@ final class Builder
for ($i = 0; $i < $max; $i++) {
// strip phar:// prefixes
if (strpos($paths[$i], 'phar://') === 0) {
if (str_starts_with($paths[$i], 'phar://')) {
$paths[$i] = substr($paths[$i], 7);
$paths[$i] = str_replace('/', DIRECTORY_SEPARATOR, $paths[$i]);
}

View File

@@ -16,15 +16,8 @@ use function sprintf;
*/
final class CrapIndex
{
/**
* @var int
*/
private $cyclomaticComplexity;
/**
* @var float
*/
private $codeCoverage;
private readonly int $cyclomaticComplexity;
private readonly float $codeCoverage;
public function __construct(int $cyclomaticComplexity, float $codeCoverage)
{
@@ -44,7 +37,7 @@ final class CrapIndex
return sprintf(
'%01.2F',
$this->cyclomaticComplexity ** 2 * (1 - $this->codeCoverage / 100) ** 3 + $this->cyclomaticComplexity
$this->cyclomaticComplexity ** 2 * (1 - $this->codeCoverage / 100) ** 3 + $this->cyclomaticComplexity,
);
}
}

View File

@@ -16,118 +16,48 @@ use RecursiveIteratorIterator;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
*/
final class Directory extends AbstractNode implements IteratorAggregate
{
/**
* @var AbstractNode[]
* @var list<AbstractNode>
*/
private $children = [];
private array $children = [];
/**
* @var Directory[]
* @var list<Directory>
*/
private $directories = [];
private array $directories = [];
/**
* @var File[]
* @var list<File>
*/
private $files = [];
private array $files = [];
private ?array $classes = null;
private ?array $traits = null;
private ?array $functions = null;
/**
* @var array
* @psalm-var null|LinesOfCodeType
*/
private $classes;
/**
* @var array
*/
private $traits;
/**
* @var array
*/
private $functions;
/**
* @psalm-var null|array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
private $linesOfCode;
/**
* @var int
*/
private $numFiles = -1;
/**
* @var int
*/
private $numExecutableLines = -1;
/**
* @var int
*/
private $numExecutedLines = -1;
/**
* @var int
*/
private $numExecutableBranches = -1;
/**
* @var int
*/
private $numExecutedBranches = -1;
/**
* @var int
*/
private $numExecutablePaths = -1;
/**
* @var int
*/
private $numExecutedPaths = -1;
/**
* @var int
*/
private $numClasses = -1;
/**
* @var int
*/
private $numTestedClasses = -1;
/**
* @var int
*/
private $numTraits = -1;
/**
* @var int
*/
private $numTestedTraits = -1;
/**
* @var int
*/
private $numMethods = -1;
/**
* @var int
*/
private $numTestedMethods = -1;
/**
* @var int
*/
private $numFunctions = -1;
/**
* @var int
*/
private $numTestedFunctions = -1;
private ?array $linesOfCode = null;
private int $numFiles = -1;
private int $numExecutableLines = -1;
private int $numExecutedLines = -1;
private int $numExecutableBranches = -1;
private int $numExecutedBranches = -1;
private int $numExecutablePaths = -1;
private int $numExecutedPaths = -1;
private int $numClasses = -1;
private int $numTestedClasses = -1;
private int $numTraits = -1;
private int $numTestedTraits = -1;
private int $numMethods = -1;
private int $numTestedMethods = -1;
private int $numFunctions = -1;
private int $numTestedFunctions = -1;
public function count(): int
{
@@ -146,7 +76,7 @@ final class Directory extends AbstractNode implements IteratorAggregate
{
return new RecursiveIteratorIterator(
new Iterator($this),
RecursiveIteratorIterator::SELF_FIRST
RecursiveIteratorIterator::SELF_FIRST,
);
}
@@ -192,7 +122,7 @@ final class Directory extends AbstractNode implements IteratorAggregate
foreach ($this->children as $child) {
$this->classes = array_merge(
$this->classes,
$child->classes()
$child->classes(),
);
}
}
@@ -208,7 +138,7 @@ final class Directory extends AbstractNode implements IteratorAggregate
foreach ($this->children as $child) {
$this->traits = array_merge(
$this->traits,
$child->traits()
$child->traits(),
);
}
}
@@ -224,7 +154,7 @@ final class Directory extends AbstractNode implements IteratorAggregate
foreach ($this->children as $child) {
$this->functions = array_merge(
$this->functions,
$child->functions()
$child->functions(),
);
}
}
@@ -233,7 +163,7 @@ final class Directory extends AbstractNode implements IteratorAggregate
}
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
* @psalm-return LinesOfCodeType
*/
public function linesOfCode(): array
{
@@ -247,8 +177,8 @@ final class Directory extends AbstractNode implements IteratorAggregate
foreach ($this->children as $child) {
$childLinesOfCode = $child->linesOfCode();
$this->linesOfCode['linesOfCode'] += $childLinesOfCode['linesOfCode'];
$this->linesOfCode['commentLinesOfCode'] += $childLinesOfCode['commentLinesOfCode'];
$this->linesOfCode['linesOfCode'] += $childLinesOfCode['linesOfCode'];
$this->linesOfCode['commentLinesOfCode'] += $childLinesOfCode['commentLinesOfCode'];
$this->linesOfCode['nonCommentLinesOfCode'] += $childLinesOfCode['nonCommentLinesOfCode'];
}
}

View File

@@ -15,116 +15,134 @@ use function range;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
* @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
*
* @psalm-type ProcessedFunctionType = array{
* functionName: string,
* namespace: string,
* signature: string,
* startLine: int,
* endLine: int,
* executableLines: int,
* executedLines: int,
* executableBranches: int,
* executedBranches: int,
* executablePaths: int,
* executedPaths: int,
* ccn: int,
* coverage: int|float,
* crap: int|string,
* link: string
* }
* @psalm-type ProcessedMethodType = array{
* methodName: string,
* visibility: string,
* signature: string,
* startLine: int,
* endLine: int,
* executableLines: int,
* executedLines: int,
* executableBranches: int,
* executedBranches: int,
* executablePaths: int,
* executedPaths: int,
* ccn: int,
* coverage: float|int,
* crap: int|string,
* link: string
* }
* @psalm-type ProcessedClassType = array{
* className: string,
* namespace: string,
* methods: array<string, ProcessedMethodType>,
* startLine: int,
* executableLines: int,
* executedLines: int,
* executableBranches: int,
* executedBranches: int,
* executablePaths: int,
* executedPaths: int,
* ccn: int,
* coverage: int|float,
* crap: int|string,
* link: string
* }
* @psalm-type ProcessedTraitType = array{
* traitName: string,
* namespace: string,
* methods: array<string, ProcessedMethodType>,
* startLine: int,
* executableLines: int,
* executedLines: int,
* executableBranches: int,
* executedBranches: int,
* executablePaths: int,
* executedPaths: int,
* ccn: int,
* coverage: float|int,
* crap: int|string,
* link: string
* }
*/
final class File extends AbstractNode
{
/**
* @var array
* @psalm-var array<int, ?list<non-empty-string>>
*/
private $lineCoverageData;
private array $lineCoverageData;
private array $functionCoverageData;
private readonly array $testData;
private int $numExecutableLines = 0;
private int $numExecutedLines = 0;
private int $numExecutableBranches = 0;
private int $numExecutedBranches = 0;
private int $numExecutablePaths = 0;
private int $numExecutedPaths = 0;
/**
* @var array
* @psalm-var array<string, ProcessedClassType>
*/
private $functionCoverageData;
private array $classes = [];
/**
* @var array
* @psalm-var array<string, ProcessedTraitType>
*/
private $testData;
private array $traits = [];
/**
* @var int
* @psalm-var array<string, ProcessedFunctionType>
*/
private $numExecutableLines = 0;
private array $functions = [];
/**
* @var int
* @psalm-var LinesOfCodeType
*/
private $numExecutedLines = 0;
private readonly array $linesOfCode;
private ?int $numClasses = null;
private int $numTestedClasses = 0;
private ?int $numTraits = null;
private int $numTestedTraits = 0;
private ?int $numMethods = null;
private ?int $numTestedMethods = null;
private ?int $numTestedFunctions = null;
/**
* @var int
* @var array<int, array|array{0: CodeUnitClassType, 1: string}|array{0: CodeUnitFunctionType}|array{0: CodeUnitTraitType, 1: string}>
*/
private $numExecutableBranches = 0;
private array $codeUnitsByLine = [];
/**
* @var int
*/
private $numExecutedBranches = 0;
/**
* @var int
*/
private $numExecutablePaths = 0;
/**
* @var int
*/
private $numExecutedPaths = 0;
/**
* @var array
*/
private $classes = [];
/**
* @var array
*/
private $traits = [];
/**
* @var array
*/
private $functions = [];
/**
* @psalm-var array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
private $linesOfCode;
/**
* @var int
*/
private $numClasses;
/**
* @var int
*/
private $numTestedClasses = 0;
/**
* @var int
*/
private $numTraits;
/**
* @var int
*/
private $numTestedTraits = 0;
/**
* @var int
*/
private $numMethods;
/**
* @var int
*/
private $numTestedMethods;
/**
* @var int
*/
private $numTestedFunctions;
/**
* @var array
*/
private $codeUnitsByLine = [];
/**
* @psalm-param array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int} $linesOfCode
* @psalm-param array<int, ?list<non-empty-string>> $lineCoverageData
* @psalm-param LinesOfCodeType $linesOfCode
* @psalm-param array<string, CodeUnitClassType> $classes
* @psalm-param array<string, CodeUnitTraitType> $traits
* @psalm-param array<string, CodeUnitFunctionType> $functions
*/
public function __construct(string $name, AbstractNode $parent, array $lineCoverageData, array $functionCoverageData, array $testData, array $classes, array $traits, array $functions, array $linesOfCode)
{
@@ -143,6 +161,9 @@ final class File extends AbstractNode
return 1;
}
/**
* @psalm-return array<int, ?list<non-empty-string>>
*/
public function lineCoverageData(): array
{
return $this->lineCoverageData;
@@ -173,9 +194,6 @@ final class File extends AbstractNode
return $this->functions;
}
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
public function linesOfCode(): array
{
return $this->linesOfCode;
@@ -332,6 +350,11 @@ final class File extends AbstractNode
return $this->numTestedFunctions;
}
/**
* @psalm-param array<string, CodeUnitClassType> $classes
* @psalm-param array<string, CodeUnitTraitType> $traits
* @psalm-param array<string, CodeUnitFunctionType> $functions
*/
private function calculateStatistics(array $classes, array $traits, array $functions): void
{
foreach (range(1, $this->linesOfCode['linesOfCode']) as $lineNumber) {
@@ -434,6 +457,9 @@ final class File extends AbstractNode
}
}
/**
* @psalm-param array<string, CodeUnitClassType> $classes
*/
private function processClasses(array $classes): void
{
$link = $this->id() . '.html#';
@@ -461,14 +487,14 @@ final class File extends AbstractNode
$this->classes[$className]['methods'][$methodName] = $methodData;
$this->classes[$className]['executableBranches'] += $methodData['executableBranches'];
$this->classes[$className]['executedBranches'] += $methodData['executedBranches'];
$this->classes[$className]['executablePaths'] += $methodData['executablePaths'];
$this->classes[$className]['executedPaths'] += $methodData['executedPaths'];
$this->classes[$className]['executedBranches'] += $methodData['executedBranches'];
$this->classes[$className]['executablePaths'] += $methodData['executablePaths'];
$this->classes[$className]['executedPaths'] += $methodData['executedPaths'];
$this->numExecutableBranches += $methodData['executableBranches'];
$this->numExecutedBranches += $methodData['executedBranches'];
$this->numExecutablePaths += $methodData['executablePaths'];
$this->numExecutedPaths += $methodData['executedPaths'];
$this->numExecutedBranches += $methodData['executedBranches'];
$this->numExecutablePaths += $methodData['executablePaths'];
$this->numExecutedPaths += $methodData['executedPaths'];
foreach (range($method['startLine'], $method['endLine']) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [
@@ -480,6 +506,9 @@ final class File extends AbstractNode
}
}
/**
* @psalm-param array<string, CodeUnitTraitType> $traits
*/
private function processTraits(array $traits): void
{
$link = $this->id() . '.html#';
@@ -507,14 +536,14 @@ final class File extends AbstractNode
$this->traits[$traitName]['methods'][$methodName] = $methodData;
$this->traits[$traitName]['executableBranches'] += $methodData['executableBranches'];
$this->traits[$traitName]['executedBranches'] += $methodData['executedBranches'];
$this->traits[$traitName]['executablePaths'] += $methodData['executablePaths'];
$this->traits[$traitName]['executedPaths'] += $methodData['executedPaths'];
$this->traits[$traitName]['executedBranches'] += $methodData['executedBranches'];
$this->traits[$traitName]['executablePaths'] += $methodData['executablePaths'];
$this->traits[$traitName]['executedPaths'] += $methodData['executedPaths'];
$this->numExecutableBranches += $methodData['executableBranches'];
$this->numExecutedBranches += $methodData['executedBranches'];
$this->numExecutablePaths += $methodData['executablePaths'];
$this->numExecutedPaths += $methodData['executedPaths'];
$this->numExecutedBranches += $methodData['executedBranches'];
$this->numExecutablePaths += $methodData['executablePaths'];
$this->numExecutedPaths += $methodData['executedPaths'];
foreach (range($method['startLine'], $method['endLine']) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [
@@ -526,6 +555,9 @@ final class File extends AbstractNode
}
}
/**
* @psalm-param array<string, CodeUnitFunctionType> $functions
*/
private function processFunctions(array $functions): void
{
$link = $this->id() . '.html#';
@@ -555,7 +587,7 @@ final class File extends AbstractNode
if (isset($this->functionCoverageData[$functionName]['branches'])) {
$this->functions[$functionName]['executableBranches'] = count(
$this->functionCoverageData[$functionName]['branches']
$this->functionCoverageData[$functionName]['branches'],
);
$this->functions[$functionName]['executedBranches'] = count(
@@ -564,14 +596,14 @@ final class File extends AbstractNode
static function (array $branch)
{
return (bool) $branch['hit'];
}
)
},
),
);
}
if (isset($this->functionCoverageData[$functionName]['paths'])) {
$this->functions[$functionName]['executablePaths'] = count(
$this->functionCoverageData[$functionName]['paths']
$this->functionCoverageData[$functionName]['paths'],
);
$this->functions[$functionName]['executedPaths'] = count(
@@ -580,18 +612,23 @@ final class File extends AbstractNode
static function (array $path)
{
return (bool) $path['hit'];
}
)
},
),
);
}
$this->numExecutableBranches += $this->functions[$functionName]['executableBranches'];
$this->numExecutedBranches += $this->functions[$functionName]['executedBranches'];
$this->numExecutablePaths += $this->functions[$functionName]['executablePaths'];
$this->numExecutedPaths += $this->functions[$functionName]['executedPaths'];
$this->numExecutedBranches += $this->functions[$functionName]['executedBranches'];
$this->numExecutablePaths += $this->functions[$functionName]['executablePaths'];
$this->numExecutedPaths += $this->functions[$functionName]['executedPaths'];
}
}
/**
* @psalm-param CodeUnitMethodType $method
*
* @psalm-return ProcessedMethodType
*/
private function newMethod(string $className, string $methodName, array $method, string $link): array
{
$methodData = [
@@ -616,7 +653,7 @@ final class File extends AbstractNode
if (isset($this->functionCoverageData[$key]['branches'])) {
$methodData['executableBranches'] = count(
$this->functionCoverageData[$key]['branches']
$this->functionCoverageData[$key]['branches'],
);
$methodData['executedBranches'] = count(
@@ -625,14 +662,14 @@ final class File extends AbstractNode
static function (array $branch)
{
return (bool) $branch['hit'];
}
)
},
),
);
}
if (isset($this->functionCoverageData[$key]['paths'])) {
$methodData['executablePaths'] = count(
$this->functionCoverageData[$key]['paths']
$this->functionCoverageData[$key]['paths'],
);
$methodData['executedPaths'] = count(
@@ -641,8 +678,8 @@ final class File extends AbstractNode
static function (array $path)
{
return (bool) $path['hit'];
}
)
},
),
);
}

View File

@@ -17,15 +17,12 @@ use RecursiveIterator;
*/
final class Iterator implements RecursiveIterator
{
/**
* @var int
*/
private $position;
private int $position;
/**
* @var AbstractNode[]
* @var list<AbstractNode>
*/
private $nodes;
private readonly array $nodes;
public function __construct(Directory $node)
{

View File

@@ -16,7 +16,7 @@ use function is_string;
use function ksort;
use function max;
use function range;
use function strpos;
use function str_contains;
use function time;
use DOMDocument;
use SebastianBergmann\CodeCoverage\CodeCoverage;
@@ -79,7 +79,7 @@ final class Clover
}
$classMethods++;
$classStatements += $method['executableLines'];
$classStatements += $method['executableLines'];
$coveredClassStatements += $method['executedLines'];
if ($method['coverage'] == 100) {
@@ -89,7 +89,7 @@ final class Clover
$methodCount = 0;
foreach (range($method['startLine'], $method['endLine']) as $line) {
if (isset($coverageData[$line]) && ($coverageData[$line] !== null)) {
if (isset($coverageData[$line])) {
$methodCount = max($methodCount, count($coverageData[$line]));
}
}
@@ -115,28 +115,28 @@ final class Clover
if (!empty($class['package']['fullPackage'])) {
$xmlClass->setAttribute(
'fullPackage',
$class['package']['fullPackage']
$class['package']['fullPackage'],
);
}
if (!empty($class['package']['category'])) {
$xmlClass->setAttribute(
'category',
$class['package']['category']
$class['package']['category'],
);
}
if (!empty($class['package']['package'])) {
$xmlClass->setAttribute(
'package',
$class['package']['package']
$class['package']['package'],
);
}
if (!empty($class['package']['subpackage'])) {
$xmlClass->setAttribute(
'subpackage',
$class['package']['subpackage']
$class['package']['subpackage'],
);
}
@@ -213,7 +213,7 @@ final class Clover
} else {
if (!isset($packages[$namespace])) {
$packages[$namespace] = $xmlDocument->createElement(
'package'
'package',
);
$packages[$namespace]->setAttribute('name', $namespace);
@@ -244,7 +244,7 @@ final class Clover
$buffer = $xmlDocument->saveXML();
if ($target !== null) {
if (!strpos($target, '://') !== false) {
if (!str_contains($target, '://')) {
Filesystem::createDirectory(dirname($target));
}

View File

@@ -15,8 +15,8 @@ use function dirname;
use function file_put_contents;
use function preg_match;
use function range;
use function str_contains;
use function str_replace;
use function strpos;
use function time;
use DOMImplementation;
use SebastianBergmann\CodeCoverage\CodeCoverage;
@@ -40,7 +40,7 @@ final class Cobertura
$documentType = $implementation->createDocumentType(
'coverage',
'',
'http://cobertura.sourceforge.net/xml/coverage-04.dtd'
'http://cobertura.sourceforge.net/xml/coverage-04.dtd',
);
$document = $implementation->createDocument('', '', $documentType);
@@ -114,7 +114,7 @@ final class Cobertura
$coverageData = $item->lineCoverageData();
foreach ($classes as $className => $class) {
$complexity += $class['ccn'];
$complexity += $class['ccn'];
$packageComplexity += $class['ccn'];
if (!empty($class['package']['namespace'])) {
@@ -175,7 +175,7 @@ final class Cobertura
$methodElement->appendChild($methodLinesElement);
foreach (range($method['startLine'], $method['endLine']) as $line) {
if (!isset($coverageData[$line]) || $coverageData[$line] === null) {
if (!isset($coverageData[$line])) {
continue;
}
$methodLineElement = $document->createElement('line');
@@ -225,22 +225,22 @@ final class Cobertura
continue;
}
$complexity += $function['ccn'];
$packageComplexity += $function['ccn'];
$complexity += $function['ccn'];
$packageComplexity += $function['ccn'];
$functionsComplexity += $function['ccn'];
$linesValid = $function['executableLines'];
$linesCovered = $function['executedLines'];
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
$functionsLinesValid += $linesValid;
$functionsLinesValid += $linesValid;
$functionsLinesCovered += $linesCovered;
$branchesValid = $function['executableBranches'];
$branchesCovered = $function['executedBranches'];
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
$functionsBranchesValid += $branchesValid;
$functionsBranchesValid += $branchesValid;
$functionsBranchesCovered += $branchesValid;
$methodElement = $document->createElement('method');
@@ -256,7 +256,7 @@ final class Cobertura
$methodElement->appendChild($methodLinesElement);
foreach (range($function['startLine'], $function['endLine']) as $line) {
if (!isset($coverageData[$line]) || $coverageData[$line] === null) {
if (!isset($coverageData[$line])) {
continue;
}
$methodLineElement = $document->createElement('line');
@@ -295,7 +295,7 @@ final class Cobertura
$buffer = $document->saveXML();
if ($target !== null) {
if (!strpos($target, '://') !== false) {
if (!str_contains($target, '://')) {
Filesystem::createDirectory(dirname($target));
}

View File

@@ -15,7 +15,7 @@ use function file_put_contents;
use function htmlspecialchars;
use function is_string;
use function round;
use function strpos;
use function str_contains;
use DOMDocument;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
@@ -24,10 +24,7 @@ use SebastianBergmann\CodeCoverage\Util\Filesystem;
final class Crap4j
{
/**
* @var int
*/
private $threshold;
private readonly int $threshold;
public function __construct(int $threshold = 30)
{
@@ -76,7 +73,7 @@ final class Crap4j
foreach ($class['methods'] as $methodName => $method) {
$crapLoad = $this->crapLoad((float) $method['crap'], $method['ccn'], $method['coverage']);
$fullCrap += $method['crap'];
$fullCrap += $method['crap'];
$fullCrapLoad += $crapLoad;
$fullMethodCount++;
@@ -125,7 +122,7 @@ final class Crap4j
$buffer = $document->saveXML();
if ($target !== null) {
if (!strpos($target, '://') !== false) {
if (!str_contains($target, '://')) {
Filesystem::createDirectory(dirname($target));
}

View File

@@ -0,0 +1,66 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Html;
/**
* @psalm-immutable
*/
final class Colors
{
private readonly string $successLow;
private readonly string $successMedium;
private readonly string $successHigh;
private readonly string $warning;
private readonly string $danger;
public static function default(): self
{
return new self('#dff0d8', '#c3e3b5', '#99cb84', '#fcf8e3', '#f2dede');
}
public static function from(string $successLow, string $successMedium, string $successHigh, string $warning, string $danger): self
{
return new self($successLow, $successMedium, $successHigh, $warning, $danger);
}
private function __construct(string $successLow, string $successMedium, string $successHigh, string $warning, string $danger)
{
$this->successLow = $successLow;
$this->successMedium = $successMedium;
$this->successHigh = $successHigh;
$this->warning = $warning;
$this->danger = $danger;
}
public function successLow(): string
{
return $this->successLow;
}
public function successMedium(): string
{
return $this->successMedium;
}
public function successHigh(): string
{
return $this->successHigh;
}
public function warning(): string
{
return $this->warning;
}
public function danger(): string
{
return $this->danger;
}
}

View File

@@ -0,0 +1,50 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Html;
use function is_file;
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
/**
* @psalm-immutable
*/
final class CustomCssFile
{
private readonly string $path;
public static function default(): self
{
return new self(__DIR__ . '/Renderer/Template/css/custom.css');
}
/**
* @throws InvalidArgumentException
*/
public static function from(string $path): self
{
if (!is_file($path)) {
throw new InvalidArgumentException(
'$path does not exist',
);
}
return new self($path);
}
private function __construct(string $path)
{
$this->path = $path;
}
public function path(): string
{
return $this->path;
}
}

View File

@@ -13,46 +13,30 @@ use const DIRECTORY_SEPARATOR;
use function copy;
use function date;
use function dirname;
use function substr;
use function str_ends_with;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Report\Thresholds;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\Template\Exception;
use SebastianBergmann\Template\Template;
final class Facade
{
/**
* @var string
*/
private $templatePath;
private readonly string $templatePath;
private readonly string $generator;
private readonly Colors $colors;
private readonly Thresholds $thresholds;
private readonly CustomCssFile $customCssFile;
/**
* @var string
*/
private $generator;
/**
* @var int
*/
private $lowUpperBound;
/**
* @var int
*/
private $highLowerBound;
public function __construct(int $lowUpperBound = 50, int $highLowerBound = 90, string $generator = '')
public function __construct(string $generator = '', ?Colors $colors = null, ?Thresholds $thresholds = null, ?CustomCssFile $customCssFile = null)
{
if ($lowUpperBound > $highLowerBound) {
throw new InvalidArgumentException(
'$lowUpperBound must not be larger than $highLowerBound'
);
}
$this->generator = $generator;
$this->highLowerBound = $highLowerBound;
$this->lowUpperBound = $lowUpperBound;
$this->templatePath = __DIR__ . '/Renderer/Template/';
$this->generator = $generator;
$this->colors = $colors ?? Colors::default();
$this->thresholds = $thresholds ?? Thresholds::default();
$this->customCssFile = $customCssFile ?? CustomCssFile::default();
$this->templatePath = __DIR__ . '/Renderer/Template/';
}
public function process(CodeCoverage $coverage, string $target): void
@@ -65,27 +49,24 @@ final class Facade
$this->templatePath,
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound,
$coverage->collectsBranchAndPathCoverage()
$this->thresholds,
$coverage->collectsBranchAndPathCoverage(),
);
$directory = new Directory(
$this->templatePath,
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound,
$coverage->collectsBranchAndPathCoverage()
$this->thresholds,
$coverage->collectsBranchAndPathCoverage(),
);
$file = new File(
$this->templatePath,
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound,
$coverage->collectsBranchAndPathCoverage()
$this->thresholds,
$coverage->collectsBranchAndPathCoverage(),
);
$directory->render($report, $target . 'index.html');
@@ -109,6 +90,7 @@ final class Facade
}
$this->copyFiles($target);
$this->renderCss($target);
}
private function copyFiles(string $target): void
@@ -117,8 +99,7 @@ final class Facade
copy($this->templatePath . 'css/bootstrap.min.css', $dir . 'bootstrap.min.css');
copy($this->templatePath . 'css/nv.d3.min.css', $dir . 'nv.d3.min.css');
copy($this->templatePath . 'css/style.css', $dir . 'style.css');
copy($this->templatePath . 'css/custom.css', $dir . 'custom.css');
copy($this->customCssFile->path(), $dir . 'custom.css');
copy($this->templatePath . 'css/octicons.css', $dir . 'octicons.css');
$dir = $this->directory($target . '_icons');
@@ -134,9 +115,34 @@ final class Facade
copy($this->templatePath . 'js/file.js', $dir . 'file.js');
}
private function renderCss(string $target): void
{
$template = new Template($this->templatePath . 'css/style.css', '{{', '}}');
$template->setVar(
[
'success-low' => $this->colors->successLow(),
'success-medium' => $this->colors->successMedium(),
'success-high' => $this->colors->successHigh(),
'warning' => $this->colors->warning(),
'danger' => $this->colors->danger(),
],
);
try {
$template->renderTo($this->directory($target . '_css') . 'style.css');
} catch (Exception $e) {
throw new FileCouldNotBeWrittenException(
$e->getMessage(),
$e->getCode(),
$e,
);
}
}
private function directory(string $directory): string
{
if (substr($directory, -1, 1) != DIRECTORY_SEPARATOR) {
if (!str_ends_with($directory, DIRECTORY_SEPARATOR)) {
$directory .= DIRECTORY_SEPARATOR;
}

View File

@@ -17,6 +17,7 @@ use function substr_count;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\Report\Thresholds;
use SebastianBergmann\CodeCoverage\Version;
use SebastianBergmann\Environment\Runtime;
use SebastianBergmann\Template\Template;
@@ -26,48 +27,19 @@ use SebastianBergmann\Template\Template;
*/
abstract class Renderer
{
/**
* @var string
*/
protected $templatePath;
protected string $templatePath;
protected string $generator;
protected string $date;
protected Thresholds $thresholds;
protected bool $hasBranchCoverage;
protected string $version;
/**
* @var string
*/
protected $generator;
/**
* @var string
*/
protected $date;
/**
* @var int
*/
protected $lowUpperBound;
/**
* @var int
*/
protected $highLowerBound;
/**
* @var bool
*/
protected $hasBranchCoverage;
/**
* @var string
*/
protected $version;
public function __construct(string $templatePath, string $generator, string $date, int $lowUpperBound, int $highLowerBound, bool $hasBranchCoverage)
public function __construct(string $templatePath, string $generator, string $date, Thresholds $thresholds, bool $hasBranchCoverage)
{
$this->templatePath = $templatePath;
$this->generator = $generator;
$this->date = $date;
$this->lowUpperBound = $lowUpperBound;
$this->highLowerBound = $highLowerBound;
$this->thresholds = $thresholds;
$this->version = Version::id();
$this->hasBranchCoverage = $hasBranchCoverage;
}
@@ -83,7 +55,7 @@ abstract class Renderer
$data['numClasses'];
$classesBar = $this->coverageBar(
$data['testedClassesPercent']
$data['testedClassesPercent'],
);
} else {
$classesLevel = '';
@@ -99,7 +71,7 @@ abstract class Renderer
$data['numMethods'];
$methodsBar = $this->coverageBar(
$data['testedMethodsPercent']
$data['testedMethodsPercent'],
);
} else {
$methodsLevel = '';
@@ -115,7 +87,7 @@ abstract class Renderer
$data['numExecutableLines'];
$linesBar = $this->coverageBar(
$data['linesExecutedPercent']
$data['linesExecutedPercent'],
);
} else {
$linesLevel = '';
@@ -131,7 +103,7 @@ abstract class Renderer
$data['numExecutablePaths'];
$pathsBar = $this->coverageBar(
$data['pathsExecutedPercent']
$data['pathsExecutedPercent'],
);
} else {
$pathsLevel = '';
@@ -147,7 +119,7 @@ abstract class Renderer
$data['numExecutableBranches'];
$branchesBar = $this->coverageBar(
$data['branchesExecutedPercent']
$data['branchesExecutedPercent'],
);
} else {
$branchesLevel = '';
@@ -181,7 +153,7 @@ abstract class Renderer
'classes_tested_percent' => $data['testedClassesPercentAsString'] ?? '',
'classes_level' => $classesLevel,
'classes_number' => $classesNumber,
]
],
);
return $template->render();
@@ -199,9 +171,9 @@ abstract class Renderer
'version' => $this->version,
'runtime' => $this->runtimeString(),
'generator' => $this->generator,
'low_upper_bound' => $this->lowUpperBound,
'high_lower_bound' => $this->highLowerBound,
]
'low_upper_bound' => $this->thresholds->lowUpperBound(),
'high_lower_bound' => $this->thresholds->highLowerBound(),
],
);
}
@@ -224,7 +196,7 @@ abstract class Renderer
if ($step !== $node) {
$breadcrumbs .= $this->inactiveBreadcrumb(
$step,
array_pop($pathToRoot)
array_pop($pathToRoot),
);
} else {
$breadcrumbs .= $this->activeBreadcrumb($step);
@@ -238,7 +210,7 @@ abstract class Renderer
{
$buffer = sprintf(
' <li class="breadcrumb-item active">%s</li>' . "\n",
$node->name()
$node->name(),
);
if ($node instanceof DirectoryNode) {
@@ -253,7 +225,7 @@ abstract class Renderer
return sprintf(
' <li class="breadcrumb-item"><a href="%sindex.html">%s</a></li>' . "\n",
$pathToRoot,
$node->name()
$node->name(),
);
}
@@ -278,7 +250,7 @@ abstract class Renderer
$template = new Template(
$templateName,
'{{',
'}}'
'}}',
);
$template->setVar(['level' => $level, 'percent' => sprintf('%.2F', $percent)]);
@@ -288,12 +260,12 @@ abstract class Renderer
protected function colorLevel(float $percent): string
{
if ($percent <= $this->lowUpperBound) {
if ($percent <= $this->thresholds->lowUpperBound()) {
return 'danger';
}
if ($percent > $this->lowUpperBound &&
$percent < $this->highLowerBound) {
if ($percent > $this->thresholds->lowUpperBound() &&
$percent < $this->thresholds->highLowerBound()) {
return 'warning';
}
@@ -308,7 +280,7 @@ abstract class Renderer
'<a href="%s" target="_top">%s %s</a>',
$runtime->getVendorUrl(),
$runtime->getName(),
$runtime->getVersion()
$runtime->getVersion(),
);
}
}

View File

@@ -18,8 +18,10 @@ use function floor;
use function json_encode;
use function sprintf;
use function str_replace;
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\Template\Exception;
use SebastianBergmann\Template\Template;
/**
@@ -34,7 +36,7 @@ final class Dashboard extends Renderer
$template = new Template(
$templateName,
'{{',
'}}'
'}}',
);
$this->setCommonTemplateVariables($template, $node);
@@ -55,10 +57,18 @@ final class Dashboard extends Renderer
'complexity_method' => $complexity['method'],
'class_coverage_distribution' => $coverageDistribution['class'],
'method_coverage_distribution' => $coverageDistribution['method'],
]
],
);
$template->renderTo($file);
try {
$template->renderTo($file);
} catch (Exception $e) {
throw new FileCouldNotBeWrittenException(
$e->getMessage(),
$e->getCode(),
$e,
);
}
}
protected function activeBreadcrumb(AbstractNode $node): string
@@ -66,7 +76,7 @@ final class Dashboard extends Renderer
return sprintf(
' <li class="breadcrumb-item"><a href="index.html">%s</a></li>' . "\n" .
' <li class="breadcrumb-item active">(Dashboard)</li>' . "\n",
$node->name()
$node->name(),
);
}
@@ -89,7 +99,7 @@ final class Dashboard extends Renderer
sprintf(
'<a href="%s">%s</a>',
str_replace($baseLink, '', $method['link']),
$methodName
$methodName,
),
];
}
@@ -100,7 +110,7 @@ final class Dashboard extends Renderer
sprintf(
'<a href="%s">%s</a>',
str_replace($baseLink, '', $class['link']),
$className
$className,
),
];
}
@@ -188,7 +198,7 @@ final class Dashboard extends Renderer
foreach ($classes as $className => $class) {
foreach ($class['methods'] as $methodName => $method) {
if ($method['coverage'] < $this->highLowerBound) {
if ($method['coverage'] < $this->thresholds->highLowerBound()) {
$key = $methodName;
if ($className !== '*') {
@@ -199,7 +209,7 @@ final class Dashboard extends Renderer
}
}
if ($class['coverage'] < $this->highLowerBound) {
if ($class['coverage'] < $this->thresholds->highLowerBound()) {
$leastTestedClasses[$className] = $class['coverage'];
}
}
@@ -212,7 +222,7 @@ final class Dashboard extends Renderer
' <tr><td><a href="%s">%s</a></td><td class="text-right">%d%%</td></tr>' . "\n",
str_replace($baseLink, '', $classes[$className]['link']),
$className,
$coverage
$coverage,
);
}
@@ -224,7 +234,7 @@ final class Dashboard extends Renderer
str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']),
$methodName,
$method,
$coverage
$coverage,
);
}
@@ -242,7 +252,7 @@ final class Dashboard extends Renderer
foreach ($classes as $className => $class) {
foreach ($class['methods'] as $methodName => $method) {
if ($method['coverage'] < $this->highLowerBound && $method['ccn'] > 1) {
if ($method['coverage'] < $this->thresholds->highLowerBound() && $method['ccn'] > 1) {
$key = $methodName;
if ($className !== '*') {
@@ -253,7 +263,7 @@ final class Dashboard extends Renderer
}
}
if ($class['coverage'] < $this->highLowerBound &&
if ($class['coverage'] < $this->thresholds->highLowerBound() &&
$class['ccn'] > count($class['methods'])) {
$classRisks[$className] = $class['crap'];
}
@@ -267,7 +277,7 @@ final class Dashboard extends Renderer
' <tr><td><a href="%s">%s</a></td><td class="text-right">%d</td></tr>' . "\n",
str_replace($baseLink, '', $classes[$className]['link']),
$className,
$crap
$crap,
);
}
@@ -279,7 +289,7 @@ final class Dashboard extends Renderer
str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']),
$methodName,
$method,
$crap
$crap,
);
}

View File

@@ -12,8 +12,10 @@ namespace SebastianBergmann\CodeCoverage\Report\Html;
use function count;
use function sprintf;
use function str_repeat;
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
use SebastianBergmann\CodeCoverage\Node\AbstractNode as Node;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\Template\Exception;
use SebastianBergmann\Template\Template;
/**
@@ -42,10 +44,18 @@ final class Directory extends Renderer
[
'id' => $node->id(),
'items' => $items,
]
],
);
$template->renderTo($file);
try {
$template->renderTo($file);
} catch (Exception $e) {
throw new FileCouldNotBeWrittenException(
$e->getMessage(),
$e->getCode(),
$e,
);
}
}
private function renderItem(Node $node, bool $total = false): string
@@ -83,7 +93,7 @@ final class Directory extends Renderer
$data['name'] = sprintf(
'<a href="%s/index.html">%s</a>',
$node->name(),
$node->name()
$node->name(),
);
$data['icon'] = sprintf('<img src="%s_icons/file-directory.svg" class="octicon" />', $up);
} elseif ($this->hasBranchCoverage) {
@@ -92,13 +102,13 @@ final class Directory extends Renderer
$node->name(),
$node->name(),
$node->name(),
$node->name()
$node->name(),
);
} else {
$data['name'] = sprintf(
'<a href="%s.html">%s</a>',
$node->name(),
$node->name()
$node->name(),
);
}
}
@@ -107,7 +117,7 @@ final class Directory extends Renderer
return $this->renderItemTemplate(
new Template($templateName, '{{', '}}'),
$data
$data,
);
}
}

View File

@@ -84,9 +84,7 @@ use function array_keys;
use function array_merge;
use function array_pop;
use function array_unique;
use function constant;
use function count;
use function defined;
use function explode;
use function file_get_contents;
use function htmlspecialchars;
@@ -95,13 +93,14 @@ use function ksort;
use function range;
use function sort;
use function sprintf;
use function str_ends_with;
use function str_replace;
use function substr;
use function token_get_all;
use function trim;
use PHPUnit\Runner\BaseTestRunner;
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\Util\Percentage;
use SebastianBergmann\Template\Exception;
use SebastianBergmann\Template\Template;
/**
@@ -112,17 +111,78 @@ final class File extends Renderer
/**
* @psalm-var array<int,true>
*/
private static $keywordTokens = [];
/**
* @var array
*/
private static $formattedSourceCache = [];
/**
* @var int
*/
private $htmlSpecialCharsFlags = ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE;
private const KEYWORD_TOKENS = [
T_ABSTRACT => true,
T_ARRAY => true,
T_AS => true,
T_BREAK => true,
T_CALLABLE => true,
T_CASE => true,
T_CATCH => true,
T_CLASS => true,
T_CLONE => true,
T_CONST => true,
T_CONTINUE => true,
T_DECLARE => true,
T_DEFAULT => true,
T_DO => true,
T_ECHO => true,
T_ELSE => true,
T_ELSEIF => true,
T_EMPTY => true,
T_ENDDECLARE => true,
T_ENDFOR => true,
T_ENDFOREACH => true,
T_ENDIF => true,
T_ENDSWITCH => true,
T_ENDWHILE => true,
T_ENUM => true,
T_EVAL => true,
T_EXIT => true,
T_EXTENDS => true,
T_FINAL => true,
T_FINALLY => true,
T_FN => true,
T_FOR => true,
T_FOREACH => true,
T_FUNCTION => true,
T_GLOBAL => true,
T_GOTO => true,
T_HALT_COMPILER => true,
T_IF => true,
T_IMPLEMENTS => true,
T_INCLUDE => true,
T_INCLUDE_ONCE => true,
T_INSTANCEOF => true,
T_INSTEADOF => true,
T_INTERFACE => true,
T_ISSET => true,
T_LIST => true,
T_MATCH => true,
T_NAMESPACE => true,
T_NEW => true,
T_PRINT => true,
T_PRIVATE => true,
T_PROTECTED => true,
T_PUBLIC => true,
T_READONLY => true,
T_REQUIRE => true,
T_REQUIRE_ONCE => true,
T_RETURN => true,
T_STATIC => true,
T_SWITCH => true,
T_THROW => true,
T_TRAIT => true,
T_TRY => true,
T_UNSET => true,
T_USE => true,
T_VAR => true,
T_WHILE => true,
T_YIELD => true,
T_YIELD_FROM => true,
];
private static array $formattedSourceCache = [];
private int $htmlSpecialCharsFlags = ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE;
public function render(FileNode $node, string $file): void
{
@@ -136,10 +196,18 @@ final class File extends Renderer
'lines' => $this->renderSourceWithLineCoverage($node),
'legend' => '<p><span class="legend covered-by-small-tests">Covered by small (and larger) tests</span><span class="legend covered-by-medium-tests">Covered by medium (and large) tests</span><span class="legend covered-by-large-tests">Covered by large tests (and tests of unknown size)</span><span class="legend not-covered">Not covered</span><span class="legend not-coverable">Not coverable</span></p>',
'structure' => '',
]
],
);
$template->renderTo($file . '.html');
try {
$template->renderTo($file . '.html');
} catch (Exception $e) {
throw new FileCouldNotBeWrittenException(
$e->getMessage(),
$e->getCode(),
$e,
);
}
if ($this->hasBranchCoverage) {
$template->setVar(
@@ -148,10 +216,18 @@ final class File extends Renderer
'lines' => $this->renderSourceWithBranchCoverage($node),
'legend' => '<p><span class="success"><strong>Fully covered</strong></span><span class="warning"><strong>Partially covered</strong></span><span class="danger"><strong>Not covered</strong></span></p>',
'structure' => $this->renderBranchStructure($node),
]
],
);
$template->renderTo($file . '_branch.html');
try {
$template->renderTo($file . '_branch.html');
} catch (Exception $e) {
throw new FileCouldNotBeWrittenException(
$e->getMessage(),
$e->getCode(),
$e,
);
}
$template->setVar(
[
@@ -159,10 +235,18 @@ final class File extends Renderer
'lines' => $this->renderSourceWithPathCoverage($node),
'legend' => '<p><span class="success"><strong>Fully covered</strong></span><span class="warning"><strong>Partially covered</strong></span><span class="danger"><strong>Not covered</strong></span></p>',
'structure' => $this->renderPathStructure($node),
]
],
);
$template->renderTo($file . '_path.html');
try {
$template->renderTo($file . '_path.html');
} catch (Exception $e) {
throw new FileCouldNotBeWrittenException(
$e->getMessage(),
$e->getCode(),
$e,
);
}
}
}
@@ -175,7 +259,7 @@ final class File extends Renderer
$methodItemTemplate = new Template(
$methodTemplateName,
'{{',
'}}'
'}}',
);
$items = $this->renderItemTemplate(
@@ -203,24 +287,24 @@ final class File extends Renderer
'testedClassesPercent' => $node->percentageOfTestedClassesAndTraits()->asFloat(),
'testedClassesPercentAsString' => $node->percentageOfTestedClassesAndTraits()->asString(),
'crap' => '<abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr>',
]
],
);
$items .= $this->renderFunctionItems(
$node->functions(),
$methodItemTemplate
$methodItemTemplate,
);
$items .= $this->renderTraitOrClassItems(
$node->traits(),
$template,
$methodItemTemplate
$methodItemTemplate,
);
$items .= $this->renderTraitOrClassItems(
$node->classes(),
$template,
$methodItemTemplate
$methodItemTemplate,
);
return $items;
@@ -253,15 +337,15 @@ final class File extends Renderer
$numTestedClasses = $numTestedMethods === $numMethods ? 1 : 0;
$linesExecutedPercentAsString = Percentage::fromFractionAndTotal(
$item['executedLines'],
$item['executableLines']
$item['executableLines'],
)->asString();
$branchesExecutedPercentAsString = Percentage::fromFractionAndTotal(
$item['executedBranches'],
$item['executableBranches']
$item['executableBranches'],
)->asString();
$pathsExecutedPercentAsString = Percentage::fromFractionAndTotal(
$item['executedPaths'],
$item['executablePaths']
$item['executablePaths'],
)->asString();
} else {
$numClasses = 0;
@@ -273,12 +357,12 @@ final class File extends Renderer
$testedMethodsPercentage = Percentage::fromFractionAndTotal(
$numTestedMethods,
$numMethods
$numMethods,
);
$testedClassesPercentage = Percentage::fromFractionAndTotal(
$numTestedMethods === $numMethods ? 1 : 0,
1
1,
);
$buffer .= $this->renderItemTemplate(
@@ -305,7 +389,7 @@ final class File extends Renderer
'numExecutableBranches' => $item['executableBranches'],
'pathsExecutedPercent' => Percentage::fromFractionAndTotal(
$item['executedPaths'],
$item['executablePaths']
$item['executablePaths'],
)->asFloat(),
'pathsExecutedPercentAsString' => $pathsExecutedPercentAsString,
'numExecutedPaths' => $item['executedPaths'],
@@ -315,14 +399,14 @@ final class File extends Renderer
'testedClassesPercent' => $testedClassesPercentage->asFloat(),
'testedClassesPercentAsString' => $testedClassesPercentage->asString(),
'crap' => $item['crap'],
]
],
);
foreach ($item['methods'] as $method) {
$buffer .= $this->renderFunctionOrMethodItem(
$methodItemTemplate,
$method,
'&nbsp;'
'&nbsp;',
);
}
}
@@ -341,7 +425,7 @@ final class File extends Renderer
foreach ($functions as $function) {
$buffer .= $this->renderFunctionOrMethodItem(
$template,
$function
$function,
);
}
@@ -363,22 +447,22 @@ final class File extends Renderer
$executedLinesPercentage = Percentage::fromFractionAndTotal(
$item['executedLines'],
$item['executableLines']
$item['executableLines'],
);
$executedBranchesPercentage = Percentage::fromFractionAndTotal(
$item['executedBranches'],
$item['executableBranches']
$item['executableBranches'],
);
$executedPathsPercentage = Percentage::fromFractionAndTotal(
$item['executedPaths'],
$item['executablePaths']
$item['executablePaths'],
);
$testedMethodsPercentage = Percentage::fromFractionAndTotal(
$numTestedMethods,
1
1,
);
return $this->renderItemTemplate(
@@ -389,7 +473,7 @@ final class File extends Renderer
$indent,
$item['startLine'],
htmlspecialchars($item['signature'], $this->htmlSpecialCharsFlags),
$item['functionName'] ?? $item['methodName']
$item['functionName'] ?? $item['methodName'],
),
'numMethods' => $numMethods,
'numTestedMethods' => $numTestedMethods,
@@ -408,7 +492,7 @@ final class File extends Renderer
'testedMethodsPercent' => $testedMethodsPercentage->asFloat(),
'testedMethodsPercentAsString' => $testedMethodsPercentage->asString(),
'crap' => $item['crap'],
]
],
);
}
@@ -466,7 +550,7 @@ final class File extends Renderer
$popover = sprintf(
' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
);
}
@@ -553,7 +637,7 @@ final class File extends Renderer
$popover = sprintf(
' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
);
}
@@ -643,7 +727,7 @@ final class File extends Renderer
$popover = sprintf(
' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
);
}
@@ -741,7 +825,7 @@ final class File extends Renderer
$popover = sprintf(
' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
);
}
@@ -856,7 +940,7 @@ final class File extends Renderer
$popover = sprintf(
' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
);
}
@@ -881,7 +965,7 @@ final class File extends Renderer
'lineContent' => $lineContent,
'class' => $class,
'popover' => $popover,
]
],
);
return $template->render();
@@ -898,7 +982,7 @@ final class File extends Renderer
$result = [''];
$i = 0;
$stringFlag = false;
$fileEndsWithNewLine = substr($buffer, -1) === "\n";
$fileEndsWithNewLine = str_ends_with($buffer, "\n");
unset($buffer);
@@ -907,14 +991,14 @@ final class File extends Renderer
if ($token === '"' && $tokens[$j - 1] !== '\\') {
$result[$i] .= sprintf(
'<span class="string">%s</span>',
htmlspecialchars($token, $this->htmlSpecialCharsFlags)
htmlspecialchars($token, $this->htmlSpecialCharsFlags),
);
$stringFlag = !$stringFlag;
} else {
$result[$i] .= sprintf(
'<span class="keyword">%s</span>',
htmlspecialchars($token, $this->htmlSpecialCharsFlags)
htmlspecialchars($token, $this->htmlSpecialCharsFlags),
);
}
@@ -926,7 +1010,7 @@ final class File extends Renderer
$value = str_replace(
["\t", ' '],
['&nbsp;&nbsp;&nbsp;&nbsp;', '&nbsp;'],
htmlspecialchars($value, $this->htmlSpecialCharsFlags)
htmlspecialchars($value, $this->htmlSpecialCharsFlags),
);
if ($value === "\n") {
@@ -955,7 +1039,7 @@ final class File extends Renderer
$result[$i] .= sprintf(
'<span class="%s">%s</span>',
$colour,
$line
$line,
);
}
@@ -983,7 +1067,7 @@ final class File extends Renderer
$className = sprintf(
'<abbr title="%s">%s</abbr>',
$className,
array_pop($tmp)
array_pop($tmp),
);
}
@@ -1005,48 +1089,27 @@ final class File extends Renderer
{
$testCSS = '';
if ($testData['fromTestcase']) {
switch ($testData['status']) {
case BaseTestRunner::STATUS_PASSED:
switch ($testData['size']) {
case 'small':
$testCSS = ' class="covered-by-small-tests"';
switch ($testData['status']) {
case 'success':
$testCSS = match ($testData['size']) {
'small' => ' class="covered-by-small-tests"',
'medium' => ' class="covered-by-medium-tests"',
// no break
default => ' class="covered-by-large-tests"',
};
break;
break;
case 'medium':
$testCSS = ' class="covered-by-medium-tests"';
case 'failure':
$testCSS = ' class="danger"';
break;
default:
$testCSS = ' class="covered-by-large-tests"';
break;
}
break;
case BaseTestRunner::STATUS_SKIPPED:
case BaseTestRunner::STATUS_INCOMPLETE:
case BaseTestRunner::STATUS_RISKY:
case BaseTestRunner::STATUS_WARNING:
$testCSS = ' class="warning"';
break;
case BaseTestRunner::STATUS_FAILURE:
case BaseTestRunner::STATUS_ERROR:
$testCSS = ' class="danger"';
break;
}
break;
}
return sprintf(
'<li%s>%s</li>',
$testCSS,
htmlspecialchars($test, $this->htmlSpecialCharsFlags)
htmlspecialchars($test, $this->htmlSpecialCharsFlags),
);
}
@@ -1062,101 +1125,6 @@ final class File extends Renderer
private function isKeyword(int $token): bool
{
return isset(self::keywordTokens()[$token]);
}
/**
* @psalm-return array<int,true>
*/
private static function keywordTokens(): array
{
if (self::$keywordTokens !== []) {
return self::$keywordTokens;
}
self::$keywordTokens = [
T_ABSTRACT => true,
T_ARRAY => true,
T_AS => true,
T_BREAK => true,
T_CALLABLE => true,
T_CASE => true,
T_CATCH => true,
T_CLASS => true,
T_CLONE => true,
T_CONST => true,
T_CONTINUE => true,
T_DECLARE => true,
T_DEFAULT => true,
T_DO => true,
T_ECHO => true,
T_ELSE => true,
T_ELSEIF => true,
T_EMPTY => true,
T_ENDDECLARE => true,
T_ENDFOR => true,
T_ENDFOREACH => true,
T_ENDIF => true,
T_ENDSWITCH => true,
T_ENDWHILE => true,
T_EVAL => true,
T_EXIT => true,
T_EXTENDS => true,
T_FINAL => true,
T_FINALLY => true,
T_FOR => true,
T_FOREACH => true,
T_FUNCTION => true,
T_GLOBAL => true,
T_GOTO => true,
T_HALT_COMPILER => true,
T_IF => true,
T_IMPLEMENTS => true,
T_INCLUDE => true,
T_INCLUDE_ONCE => true,
T_INSTANCEOF => true,
T_INSTEADOF => true,
T_INTERFACE => true,
T_ISSET => true,
T_LIST => true,
T_NAMESPACE => true,
T_NEW => true,
T_PRINT => true,
T_PRIVATE => true,
T_PROTECTED => true,
T_PUBLIC => true,
T_REQUIRE => true,
T_REQUIRE_ONCE => true,
T_RETURN => true,
T_STATIC => true,
T_SWITCH => true,
T_THROW => true,
T_TRAIT => true,
T_TRY => true,
T_UNSET => true,
T_USE => true,
T_VAR => true,
T_WHILE => true,
T_YIELD => true,
T_YIELD_FROM => true,
];
if (defined('T_FN')) {
self::$keywordTokens[constant('T_FN')] = true;
}
if (defined('T_MATCH')) {
self::$keywordTokens[constant('T_MATCH')] = true;
}
if (defined('T_ENUM')) {
self::$keywordTokens[constant('T_ENUM')] = true;
}
if (defined('T_READONLY')) {
self::$keywordTokens[constant('T_READONLY')] = true;
}
return self::$keywordTokens;
return isset(self::KEYWORD_TOKENS[$token]);
}
}

View File

@@ -40,23 +40,23 @@ body {
}
.table tbody tr.covered-by-large-tests, li.covered-by-large-tests, tr.success, td.success, li.success, span.success {
background-color: #dff0d8;
background-color: {{success-low}};
}
.table tbody tr.covered-by-medium-tests, li.covered-by-medium-tests {
background-color: #c3e3b5;
background-color: {{success-medium}};
}
.table tbody tr.covered-by-small-tests, li.covered-by-small-tests {
background-color: #99cb84;
}
.table tbody tr.danger, .table tbody td.danger, li.danger, span.danger {
background-color: #f2dede;
background-color: {{success-high}};
}
.table tbody tr.warning, .table tbody td.warning, li.warning, span.warning {
background-color: #fcf8e3;
background-color: {{warning}};
}
.table tbody tr.danger, .table tbody td.danger, li.danger, span.danger {
background-color: {{danger}};
}
.table tbody td.info {
@@ -138,21 +138,21 @@ table + .structure-heading {
}
.covered-by-small-tests {
background-color: #99cb84;
background-color: {{success-high}};
}
.covered-by-medium-tests {
background-color: #c3e3b5;
background-color: {{success-medium}};
}
.covered-by-large-tests {
background-color: #dff0d8;
background-color: {{success-low}};
}
.not-covered {
background-color: #f2dede;
background-color: {{danger}};
}
.not-coverable {
background-color: #fcf8e3;
background-color: {{warning}};
}

View File

@@ -12,7 +12,7 @@ namespace SebastianBergmann\CodeCoverage\Report;
use function dirname;
use function file_put_contents;
use function serialize;
use function strpos;
use function str_contains;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
@@ -27,7 +27,7 @@ final class PHP
return \unserialize(<<<'END_OF_COVERAGE_SERIALIZATION'" . PHP_EOL . serialize($coverage) . PHP_EOL . 'END_OF_COVERAGE_SERIALIZATION' . PHP_EOL . ');';
if ($target !== null) {
if (!strpos($target, '://') !== false) {
if (!str_contains($target, '://')) {
Filesystem::createDirectory(dirname($target));
}

View File

@@ -47,36 +47,13 @@ final class Text
* @var string
*/
private const COLOR_RESET = "\x1b[0m";
private readonly Thresholds $thresholds;
private readonly bool $showUncoveredFiles;
private readonly bool $showOnlySummary;
/**
* @var string
*/
private const COLOR_EOL = "\x1b[2K";
/**
* @var int
*/
private $lowUpperBound;
/**
* @var int
*/
private $highLowerBound;
/**
* @var bool
*/
private $showUncoveredFiles;
/**
* @var bool
*/
private $showOnlySummary;
public function __construct(int $lowUpperBound = 50, int $highLowerBound = 90, bool $showUncoveredFiles = false, bool $showOnlySummary = false)
public function __construct(Thresholds $thresholds, bool $showUncoveredFiles = false, bool $showOnlySummary = false)
{
$this->lowUpperBound = $lowUpperBound;
$this->highLowerBound = $highLowerBound;
$this->thresholds = $thresholds;
$this->showUncoveredFiles = $showUncoveredFiles;
$this->showOnlySummary = $showOnlySummary;
}
@@ -96,48 +73,46 @@ final class Text
'branches' => '',
'paths' => '',
'reset' => '',
'eol' => '',
];
if ($showColors) {
$colors['classes'] = $this->coverageColor(
$report->numberOfTestedClassesAndTraits(),
$report->numberOfClassesAndTraits()
$report->numberOfClassesAndTraits(),
);
$colors['methods'] = $this->coverageColor(
$report->numberOfTestedMethods(),
$report->numberOfMethods()
$report->numberOfMethods(),
);
$colors['lines'] = $this->coverageColor(
$report->numberOfExecutedLines(),
$report->numberOfExecutableLines()
$report->numberOfExecutableLines(),
);
$colors['branches'] = $this->coverageColor(
$report->numberOfExecutedBranches(),
$report->numberOfExecutableBranches()
$report->numberOfExecutableBranches(),
);
$colors['paths'] = $this->coverageColor(
$report->numberOfExecutedPaths(),
$report->numberOfExecutablePaths()
$report->numberOfExecutablePaths(),
);
$colors['reset'] = self::COLOR_RESET;
$colors['header'] = self::COLOR_HEADER;
$colors['eol'] = self::COLOR_EOL;
}
$classes = sprintf(
' Classes: %6s (%d/%d)',
Percentage::fromFractionAndTotal(
$report->numberOfTestedClassesAndTraits(),
$report->numberOfClassesAndTraits()
$report->numberOfClassesAndTraits(),
)->asString(),
$report->numberOfTestedClassesAndTraits(),
$report->numberOfClassesAndTraits()
$report->numberOfClassesAndTraits(),
);
$methods = sprintf(
@@ -147,7 +122,7 @@ final class Text
$report->numberOfMethods(),
)->asString(),
$report->numberOfTestedMethods(),
$report->numberOfMethods()
$report->numberOfMethods(),
);
$paths = '';
@@ -161,7 +136,7 @@ final class Text
$report->numberOfExecutablePaths(),
)->asString(),
$report->numberOfExecutedPaths(),
$report->numberOfExecutablePaths()
$report->numberOfExecutablePaths(),
);
$branches = sprintf(
@@ -171,7 +146,7 @@ final class Text
$report->numberOfExecutableBranches(),
)->asString(),
$report->numberOfExecutedBranches(),
$report->numberOfExecutableBranches()
$report->numberOfExecutableBranches(),
);
}
@@ -182,7 +157,7 @@ final class Text
$report->numberOfExecutableLines(),
)->asString(),
$report->numberOfExecutedLines(),
$report->numberOfExecutableLines()
$report->numberOfExecutableLines(),
);
$padding = max(array_map('strlen', [$classes, $methods, $lines]));
@@ -240,12 +215,12 @@ final class Text
}
$classMethods++;
$classExecutableLines += $method['executableLines'];
$classExecutedLines += $method['executedLines'];
$classExecutableLines += $method['executableLines'];
$classExecutedLines += $method['executedLines'];
$classExecutableBranches += $method['executableBranches'];
$classExecutedBranches += $method['executedBranches'];
$classExecutablePaths += $method['executablePaths'];
$classExecutedPaths += $method['executedPaths'];
$classExecutedBranches += $method['executedBranches'];
$classExecutablePaths += $method['executablePaths'];
$classExecutedPaths += $method['executedPaths'];
if ($method['coverage'] == 100) {
$coveredMethods++;
@@ -303,14 +278,14 @@ final class Text
{
$coverage = Percentage::fromFractionAndTotal(
$numberOfCoveredElements,
$totalNumberOfElements
$totalNumberOfElements,
);
if ($coverage->asFloat() >= $this->highLowerBound) {
if ($coverage->asFloat() >= $this->thresholds->highLowerBound()) {
return self::COLOR_GREEN;
}
if ($coverage->asFloat() > $this->lowUpperBound) {
if ($coverage->asFloat() > $this->thresholds->lowUpperBound()) {
return self::COLOR_YELLOW;
}
@@ -323,19 +298,18 @@ final class Text
return Percentage::fromFractionAndTotal(
$numberOfCoveredElements,
$totalNumberOfElements
$totalNumberOfElements,
)->asFixedWidthString() .
' (' . sprintf($format, $numberOfCoveredElements) . '/' .
sprintf($format, $totalNumberOfElements) . ')';
}
/**
* @param false|string $string
*/
private function format(string $color, int $padding, $string): string
private function format(string $color, int $padding, false|string $string): string
{
$reset = $color ? self::COLOR_RESET : '';
if ($color === '') {
return (string) $string . PHP_EOL;
}
return $color . str_pad((string) $string, $padding) . $reset . PHP_EOL;
return $color . str_pad((string) $string, $padding) . self::COLOR_RESET . PHP_EOL;
}
}

View File

@@ -0,0 +1,56 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
/**
* @psalm-immutable
*/
final class Thresholds
{
private readonly int $lowUpperBound;
private readonly int $highLowerBound;
public static function default(): self
{
return new self(50, 90);
}
/**
* @throws InvalidArgumentException
*/
public static function from(int $lowUpperBound, int $highLowerBound): self
{
if ($lowUpperBound > $highLowerBound) {
throw new InvalidArgumentException(
'$lowUpperBound must not be larger than $highLowerBound',
);
}
return new self($lowUpperBound, $highLowerBound);
}
private function __construct(int $lowUpperBound, int $highLowerBound)
{
$this->lowUpperBound = $lowUpperBound;
$this->highLowerBound = $highLowerBound;
}
public function lowUpperBound(): int
{
return $this->lowUpperBound;
}
public function highLowerBound(): int
{
return $this->highLowerBound;
}
}

View File

@@ -9,7 +9,6 @@
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use function constant;
use function phpversion;
use DateTimeImmutable;
use DOMElement;
@@ -20,10 +19,7 @@ use SebastianBergmann\Environment\Runtime;
*/
final class BuildInformation
{
/**
* @var DOMElement
*/
private $contextNode;
private readonly DOMElement $contextNode;
public function __construct(DOMElement $contextNode)
{
@@ -40,11 +36,6 @@ final class BuildInformation
$driverNode = $this->nodeByName('driver');
if ($runtime->hasPHPDBGCodeCoverage()) {
$driverNode->setAttribute('name', 'phpdbg');
$driverNode->setAttribute('version', constant('PHPDBG_VERSION'));
}
if ($runtime->hasXdebug()) {
$driverNode->setAttribute('name', 'xdebug');
$driverNode->setAttribute('version', phpversion('xdebug'));
@@ -71,15 +62,15 @@ final class BuildInformation
{
$node = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
$name
$name,
)->item(0);
if (!$node) {
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
$name
)
$name,
),
);
}

View File

@@ -18,20 +18,9 @@ use XMLWriter;
*/
final class Coverage
{
/**
* @var XMLWriter
*/
private $writer;
/**
* @var DOMElement
*/
private $contextNode;
/**
* @var bool
*/
private $finalized = false;
private readonly XMLWriter $writer;
private readonly DOMElement $contextNode;
private bool $finalized = false;
public function __construct(DOMElement $context, string $line)
{
@@ -66,7 +55,7 @@ final class Coverage
$this->contextNode->parentNode->replaceChild(
$fragment,
$this->contextNode
$this->contextNode,
);
$this->finalized = true;

View File

@@ -40,20 +40,9 @@ use SebastianBergmann\Environment\Runtime;
final class Facade
{
/**
* @var string
*/
private $target;
/**
* @var Project
*/
private $project;
/**
* @var string
*/
private $phpUnitVersion;
private string $target;
private Project $project;
private readonly string $phpUnitVersion;
public function __construct(string $version)
{
@@ -75,7 +64,7 @@ final class Facade
$report = $coverage->getReport();
$this->project = new Project(
$coverage->getReport()->name()
$coverage->getReport()->name(),
);
$this->setBuildInformation();
@@ -143,14 +132,14 @@ final class Facade
{
$fileObject = $context->addFile(
$file->name(),
$file->id() . '.xml'
$file->id() . '.xml',
);
$this->setTotals($file, $fileObject->totals());
$path = substr(
$file->pathAsString(),
strlen($this->project->projectSourceDirectory())
strlen($this->project->projectSourceDirectory()),
);
$fileReport = new Report($path);
@@ -180,7 +169,7 @@ final class Facade
}
$fileReport->source()->setSourceCode(
file_get_contents($file->pathAsString())
file_get_contents($file->pathAsString()),
);
$this->saveDocument($fileReport->asDom(), $file->id());
@@ -197,7 +186,7 @@ final class Facade
$unitObject->setLines(
$unit['startLine'],
$unit['executableLines'],
$unit['executedLines']
$unit['executedLines'],
);
$unitObject->setCrap((float) $unit['crap']);
@@ -211,7 +200,7 @@ final class Facade
$methodObject->setTotals(
(string) $method['executableLines'],
(string) $method['executedLines'],
(string) $method['coverage']
(string) $method['coverage'],
);
}
}
@@ -244,27 +233,27 @@ final class Facade
$loc['commentLinesOfCode'],
$loc['nonCommentLinesOfCode'],
$node->numberOfExecutableLines(),
$node->numberOfExecutedLines()
$node->numberOfExecutedLines(),
);
$totals->setNumClasses(
$node->numberOfClasses(),
$node->numberOfTestedClasses()
$node->numberOfTestedClasses(),
);
$totals->setNumTraits(
$node->numberOfTraits(),
$node->numberOfTestedTraits()
$node->numberOfTestedTraits(),
);
$totals->setNumMethods(
$node->numberOfMethods(),
$node->numberOfTestedMethods()
$node->numberOfTestedMethods(),
);
$totals->setNumFunctions(
$node->numberOfFunctions(),
$node->numberOfTestedFunctions()
$node->numberOfTestedFunctions(),
);
}

View File

@@ -17,15 +17,8 @@ use DOMElement;
*/
class File
{
/**
* @var DOMDocument
*/
private $dom;
/**
* @var DOMElement
*/
private $contextNode;
private readonly DOMDocument $dom;
private readonly DOMElement $contextNode;
public function __construct(DOMElement $context)
{
@@ -41,8 +34,8 @@ class File
$totalsContainer = $this->contextNode->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'totals'
)
'totals',
),
);
}
@@ -53,23 +46,23 @@ class File
{
$coverage = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'coverage'
'coverage',
)->item(0);
if (!$coverage) {
$coverage = $this->contextNode->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'coverage'
)
'coverage',
),
);
}
$lineNode = $coverage->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'line'
)
'line',
),
);
return new Coverage($lineNode, $line);

View File

@@ -16,10 +16,7 @@ use DOMElement;
*/
final class Method
{
/**
* @var DOMElement
*/
private $contextNode;
private readonly DOMElement $contextNode;
public function __construct(DOMElement $context, string $name)
{

View File

@@ -17,15 +17,8 @@ use DOMElement;
*/
abstract class Node
{
/**
* @var DOMDocument
*/
private $dom;
/**
* @var DOMElement
*/
private $contextNode;
private DOMDocument $dom;
private DOMElement $contextNode;
public function __construct(DOMElement $context)
{
@@ -45,8 +38,8 @@ abstract class Node
$totalsContainer = $this->contextNode()->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'totals'
)
'totals',
),
);
}
@@ -57,7 +50,7 @@ abstract class Node
{
$dirNode = $this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'directory'
'directory',
);
$dirNode->setAttribute('name', $name);
@@ -70,7 +63,7 @@ abstract class Node
{
$fileNode = $this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'file'
'file',
);
$fileNode->setAttribute('name', $name);

View File

@@ -31,15 +31,15 @@ final class Project extends Node
{
$buildNode = $this->dom()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'build'
'build',
)->item(0);
if (!$buildNode) {
$buildNode = $this->dom()->documentElement->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'build'
)
'build',
),
);
}
@@ -50,15 +50,15 @@ final class Project extends Node
{
$testsNode = $this->contextNode()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'tests'
'tests',
)->item(0);
if (!$testsNode) {
$testsNode = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'tests'
)
'tests',
),
);
}
@@ -78,8 +78,8 @@ final class Project extends Node
$this->setContextNode(
$dom->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'project'
)->item(0)
'project',
)->item(0),
);
}

View File

@@ -25,7 +25,7 @@ final class Report extends File
$contextNode = $dom->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'file'
'file',
)->item(0);
parent::__construct($contextNode);
@@ -43,8 +43,8 @@ final class Report extends File
$node = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'function'
)
'function',
),
);
return new Method($node, $name);
@@ -64,15 +64,15 @@ final class Report extends File
{
$source = $this->contextNode()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'source'
'source',
)->item(0);
if (!$source) {
$source = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'source'
)
'source',
),
);
}
@@ -90,8 +90,8 @@ final class Report extends File
$node = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
$tagName
)
$tagName,
),
);
return new Unit($node, $name);

View File

@@ -19,8 +19,7 @@ use TheSeer\Tokenizer\XMLSerializer;
*/
final class Source
{
/** @var DOMElement */
private $context;
private readonly DOMElement $context;
public function __construct(DOMElement $context)
{
@@ -36,7 +35,7 @@ final class Source
$context->parentNode->replaceChild(
$context->ownerDocument->importNode($srcDom->documentElement, true),
$context
$context,
);
}
}

View File

@@ -13,38 +13,32 @@ use DOMElement;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type TestType from \SebastianBergmann\CodeCoverage\CodeCoverage
*/
final class Tests
{
private $contextNode;
private $codeMap = [
-1 => 'UNKNOWN', // PHPUnit_Runner_BaseTestRunner::STATUS_UNKNOWN
0 => 'PASSED', // PHPUnit_Runner_BaseTestRunner::STATUS_PASSED
1 => 'SKIPPED', // PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED
2 => 'INCOMPLETE', // PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE
3 => 'FAILURE', // PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE
4 => 'ERROR', // PHPUnit_Runner_BaseTestRunner::STATUS_ERROR
5 => 'RISKY', // PHPUnit_Runner_BaseTestRunner::STATUS_RISKY
6 => 'WARNING', // PHPUnit_Runner_BaseTestRunner::STATUS_WARNING
];
private readonly DOMElement $contextNode;
public function __construct(DOMElement $context)
{
$this->contextNode = $context;
}
/**
* @param TestType $result
*/
public function addTest(string $test, array $result): void
{
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'test'
)
'test',
),
);
$node->setAttribute('name', $test);
$node->setAttribute('size', $result['size']);
$node->setAttribute('result', (string) $result['status']);
$node->setAttribute('status', $this->codeMap[(int) $result['status']]);
$node->setAttribute('status', $result['status']);
}
}

View File

@@ -19,35 +19,12 @@ use SebastianBergmann\CodeCoverage\Util\Percentage;
*/
final class Totals
{
/**
* @var DOMNode
*/
private $container;
/**
* @var DOMElement
*/
private $linesNode;
/**
* @var DOMElement
*/
private $methodsNode;
/**
* @var DOMElement
*/
private $functionsNode;
/**
* @var DOMElement
*/
private $classesNode;
/**
* @var DOMElement
*/
private $traitsNode;
private readonly DOMNode $container;
private readonly DOMElement $linesNode;
private readonly DOMElement $methodsNode;
private readonly DOMElement $functionsNode;
private readonly DOMElement $classesNode;
private readonly DOMElement $traitsNode;
public function __construct(DOMElement $container)
{
@@ -56,27 +33,27 @@ final class Totals
$this->linesNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'lines'
'lines',
);
$this->methodsNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'methods'
'methods',
);
$this->functionsNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'functions'
'functions',
);
$this->classesNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'classes'
'classes',
);
$this->traitsNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'traits'
'traits',
);
$container->appendChild($this->linesNode);
@@ -100,7 +77,7 @@ final class Totals
$this->linesNode->setAttribute('executed', (string) $executed);
$this->linesNode->setAttribute(
'percent',
$executable === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($executed, $executable)->asFloat())
$executable === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($executed, $executable)->asFloat()),
);
}
@@ -110,7 +87,7 @@ final class Totals
$this->classesNode->setAttribute('tested', (string) $tested);
$this->classesNode->setAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
);
}
@@ -120,7 +97,7 @@ final class Totals
$this->traitsNode->setAttribute('tested', (string) $tested);
$this->traitsNode->setAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
);
}
@@ -130,7 +107,7 @@ final class Totals
$this->methodsNode->setAttribute('tested', (string) $tested);
$this->methodsNode->setAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
);
}
@@ -140,7 +117,7 @@ final class Totals
$this->functionsNode->setAttribute('tested', (string) $tested);
$this->functionsNode->setAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
);
}
}

View File

@@ -16,10 +16,7 @@ use DOMElement;
*/
final class Unit
{
/**
* @var DOMElement
*/
private $contextNode;
private readonly DOMElement $contextNode;
public function __construct(DOMElement $context, string $name)
{
@@ -44,15 +41,15 @@ final class Unit
{
$node = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'namespace'
'namespace',
)->item(0);
if (!$node) {
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'namespace'
)
'namespace',
),
);
}
@@ -64,8 +61,8 @@ final class Unit
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'method'
)
'method',
),
);
return new Method($node, $name);

View File

@@ -19,7 +19,7 @@ final class CacheWarmer
$cacheDirectory,
new ParsingFileAnalyser(
$useAnnotationsForIgnoringCode,
$ignoreDeprecatedCode
$ignoreDeprecatedCode,
),
$useAnnotationsForIgnoringCode,
$ignoreDeprecatedCode,

View File

@@ -21,38 +21,17 @@ use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
*/
final class CachingFileAnalyser implements FileAnalyser
{
/**
* @var ?string
*/
private static $cacheVersion;
/**
* @var string
*/
private $directory;
/**
* @var FileAnalyser
*/
private $analyser;
/**
* @var bool
*/
private $useAnnotationsForIgnoringCode;
/**
* @var bool
*/
private $ignoreDeprecatedCode;
/**
* @var array
*/
private $cache = [];
private static ?string $cacheVersion = null;
private readonly string $directory;
private readonly FileAnalyser $analyser;
private readonly bool $useAnnotationsForIgnoringCode;
private readonly bool $ignoreDeprecatedCode;
private array $cache = [];
public function __construct(string $directory, FileAnalyser $analyser, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode)
{
@@ -92,7 +71,7 @@ final class CachingFileAnalyser implements FileAnalyser
}
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
* @psalm-return LinesOfCodeType
*/
public function linesOfCodeFor(string $filename): array
{
@@ -143,10 +122,7 @@ final class CachingFileAnalyser implements FileAnalyser
$this->write($filename, $this->cache[$filename]);
}
/**
* @return mixed
*/
private function read(string $filename)
private function read(string $filename): array|false
{
$cacheFile = $this->cacheFile($filename);
@@ -156,18 +132,15 @@ final class CachingFileAnalyser implements FileAnalyser
return unserialize(
file_get_contents($cacheFile),
['allowed_classes' => false]
['allowed_classes' => false],
);
}
/**
* @param mixed $data
*/
private function write(string $filename, $data): void
private function write(string $filename, array $data): void
{
file_put_contents(
$this->cacheFile($filename),
serialize($data)
serialize($data),
);
}
@@ -182,8 +155,8 @@ final class CachingFileAnalyser implements FileAnalyser
self::cacheVersion(),
$this->useAnnotationsForIgnoringCode,
$this->ignoreDeprecatedCode,
]
)
],
),
);
return $this->directory . DIRECTORY_SEPARATOR . $cacheKey;

View File

@@ -26,30 +26,63 @@ use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\UnionType;
use PhpParser\NodeAbstract;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use SebastianBergmann\Complexity\CyclomaticComplexityCalculatingVisitor;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-type CodeUnitFunctionType = array{
* name: string,
* namespacedName: string,
* namespace: string,
* signature: string,
* startLine: int,
* endLine: int,
* ccn: int
* }
* @psalm-type CodeUnitMethodType = array{
* methodName: string,
* signature: string,
* visibility: string,
* startLine: int,
* endLine: int,
* ccn: int
* }
* @psalm-type CodeUnitClassType = array{
* name: string,
* namespacedName: string,
* namespace: string,
* startLine: int,
* endLine: int,
* methods: array<string, CodeUnitMethodType>
* }
* @psalm-type CodeUnitTraitType = array{
* name: string,
* namespacedName: string,
* namespace: string,
* startLine: int,
* endLine: int,
* methods: array<string, CodeUnitMethodType>
* }
*/
final class CodeUnitFindingVisitor extends NodeVisitorAbstract
{
/**
* @psalm-var array<string,array{name: string, namespacedName: string, namespace: string, startLine: int, endLine: int, methods: array<string,array{methodName: string, signature: string, visibility: string, startLine: int, endLine: int, ccn: int}>}>
* @psalm-var array<string, CodeUnitClassType>
*/
private $classes = [];
private array $classes = [];
/**
* @psalm-var array<string,array{name: string, namespacedName: string, namespace: string, startLine: int, endLine: int, methods: array<string,array{methodName: string, signature: string, visibility: string, startLine: int, endLine: int, ccn: int}>}>
* @psalm-var array<string, CodeUnitTraitType>
*/
private $traits = [];
private array $traits = [];
/**
* @psalm-var array<string,array{name: string, namespacedName: string, namespace: string, signature: string, startLine: int, endLine: int, ccn: int}>
* @psalm-var array<string, CodeUnitFunctionType>
*/
private $functions = [];
private array $functions = [];
public function enterNode(Node $node): void
{
@@ -85,7 +118,7 @@ final class CodeUnitFindingVisitor extends NodeVisitorAbstract
}
/**
* @psalm-return array<string,array{name: string, namespacedName: string, namespace: string, startLine: int, endLine: int, methods: array<string,array{methodName: string, signature: string, visibility: string, startLine: int, endLine: int, ccn: int}>}>
* @psalm-return array<string, CodeUnitClassType>
*/
public function classes(): array
{
@@ -93,7 +126,7 @@ final class CodeUnitFindingVisitor extends NodeVisitorAbstract
}
/**
* @psalm-return array<string,array{name: string, namespacedName: string, namespace: string, startLine: int, endLine: int, methods: array<string,array{methodName: string, signature: string, visibility: string, startLine: int, endLine: int, ccn: int}>}>
* @psalm-return array<string, CodeUnitTraitType>
*/
public function traits(): array
{
@@ -101,20 +134,15 @@ final class CodeUnitFindingVisitor extends NodeVisitorAbstract
}
/**
* @psalm-return array<string,array{name: string, namespacedName: string, namespace: string, signature: string, startLine: int, endLine: int, ccn: int}>
* @psalm-return array<string, CodeUnitFunctionType>
*/
public function functions(): array
{
return $this->functions;
}
/**
* @psalm-param ClassMethod|Function_ $node
*/
private function cyclomaticComplexity(Node $node): int
private function cyclomaticComplexity(ClassMethod|Function_ $node): int
{
assert($node instanceof ClassMethod || $node instanceof Function_);
$nodes = $node->getStmts();
if ($nodes === null) {
@@ -133,13 +161,8 @@ final class CodeUnitFindingVisitor extends NodeVisitorAbstract
return $cyclomaticComplexityCalculatingVisitor->cyclomaticComplexity();
}
/**
* @psalm-param ClassMethod|Function_ $node
*/
private function signature(Node $node): string
private function signature(ClassMethod|Function_ $node): string
{
assert($node instanceof ClassMethod || $node instanceof Function_);
$signature = ($node->returnsByRef() ? '&' : '') . $node->name->toString() . '(';
$parameters = [];
@@ -170,13 +193,8 @@ final class CodeUnitFindingVisitor extends NodeVisitorAbstract
return $signature;
}
/**
* @psalm-param Identifier|Name|ComplexType $type
*/
private function type(Node $type): string
private function type(ComplexType|Identifier|Name $type): string
{
assert($type instanceof Identifier || $type instanceof Name || $type instanceof ComplexType);
if ($type instanceof NullableType) {
return '?' . $type->type;
}
@@ -331,10 +349,7 @@ final class CodeUnitFindingVisitor extends NodeVisitorAbstract
return implode('&', $types);
}
/**
* @psalm-param Identifier|Name $node $node
*/
private function typeAsString(NodeAbstract $node): string
private function typeAsString(Identifier|Name $node): string
{
if ($node instanceof Name) {
return $node->toCodeString();

View File

@@ -26,33 +26,28 @@ use PhpParser\NodeVisitorAbstract;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
*/
final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
{
/**
* @var int
*/
private $nextBranch = 0;
private int $nextBranch = 0;
private readonly string $source;
/**
* @var string
* @psalm-var LinesType
*/
private $source;
private array $executableLinesGroupedByBranch = [];
/**
* @var array<int, int>
* @psalm-var array<int, bool>
*/
private $executableLinesGroupedByBranch = [];
private array $unsets = [];
/**
* @var array<int, bool>
* @psalm-var array<int, string>
*/
private $unsets = [];
/**
* @var array<int, string>
*/
private $commentsToCheckForUnset = [];
private array $commentsToCheckForUnset = [];
public function __construct(string $source)
{
@@ -110,7 +105,6 @@ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
$node instanceof Node\Stmt\Use_ ||
$node instanceof Node\Stmt\UseUse ||
$node instanceof Node\Expr\ConstFetch ||
$node instanceof Node\Expr\Match_ ||
$node instanceof Node\Expr\Variable ||
$node instanceof Node\Expr\Throw_ ||
$node instanceof Node\ComplexType ||
@@ -122,6 +116,18 @@ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
return;
}
if ($node instanceof Node\Expr\Match_) {
foreach ($node->arms as $arm) {
$this->setLineBranch(
$arm->body->getStartLine(),
$arm->body->getEndLine(),
++$this->nextBranch,
);
}
return;
}
/*
* nikic/php-parser ^4.18 represents <code>throw</code> statements
* as <code>Stmt\Throw_</code> objects
@@ -149,6 +155,20 @@ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
$node instanceof Node\Stmt\ClassMethod ||
$node instanceof Node\Expr\Closure ||
$node instanceof Node\Stmt\Trait_) {
if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
$unsets = [];
foreach ($node->getParams() as $param) {
foreach (range($param->getStartLine(), $param->getEndLine()) as $line) {
$unsets[$line] = true;
}
}
unset($unsets[$node->getEndLine()]);
$this->unsets += $unsets;
}
$isConcreteClassLike = $node instanceof Node\Stmt\Enum_ || $node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Trait_;
if (null !== $node->stmts) {
@@ -182,7 +202,7 @@ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
);
if ($hasEmptyBody) {
if ($node->getEndLine() === $node->getStartLine()) {
if ($node->getEndLine() === $node->getStartLine() && isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) {
return;
}
@@ -197,7 +217,7 @@ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
if ($node instanceof Node\Expr\ArrowFunction) {
$startLine = max(
$node->getStartLine() + 1,
$node->expr->getStartLine()
$node->expr->getStartLine(),
);
$endLine = $node->expr->getEndLine();
@@ -242,7 +262,7 @@ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
$this->setLineBranch(
$node->cond->getStartLine(),
$node->cond->getStartLine(),
++$this->nextBranch
++$this->nextBranch,
);
return;
@@ -293,7 +313,7 @@ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
$this->setLineBranch(
$startLine,
$endLine,
++$this->nextBranch
++$this->nextBranch,
);
return;
@@ -303,7 +323,7 @@ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
$this->setLineBranch(
$node->expr->getStartLine(),
$node->valueVar->getEndLine(),
++$this->nextBranch
++$this->nextBranch,
);
return;
@@ -314,7 +334,7 @@ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
$this->setLineBranch(
$node->cond->getStartLine(),
$node->cond->getEndLine(),
++$this->nextBranch
++$this->nextBranch,
);
return;
@@ -329,7 +349,7 @@ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
$this->setLineBranch(
$startLine,
$endLine,
++$this->nextBranch
++$this->nextBranch,
);
return;
@@ -372,10 +392,13 @@ final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
$this->executableLinesGroupedByBranch = array_diff_key(
$this->executableLinesGroupedByBranch,
$this->unsets
$this->unsets,
);
}
/**
* @psalm-return LinesType
*/
public function executableLinesGroupedByBranch(): array
{
return $this->executableLinesGroupedByBranch;

View File

@@ -11,21 +11,50 @@ namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
* @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
*
* @psalm-type LinesOfCodeType = array{
* linesOfCode: int,
* commentLinesOfCode: int,
* nonCommentLinesOfCode: int
* }
* @psalm-type LinesType = array<int, int>
*/
interface FileAnalyser
{
/**
* @psalm-return array<string, CodeUnitClassType>
*/
public function classesIn(string $filename): array;
/**
* @psalm-return array<string, CodeUnitTraitType>
*/
public function traitsIn(string $filename): array;
/**
* @psalm-return array<string, CodeUnitFunctionType>
*/
public function functionsIn(string $filename): array;
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
* @psalm-return LinesOfCodeType
*/
public function linesOfCodeFor(string $filename): array;
/**
* @psalm-return LinesType
*/
public function executableLinesIn(string $filename): array;
/**
* @psalm-return LinesType
*/
public function ignoredLinesFor(string $filename): array;
}

View File

@@ -9,14 +9,13 @@
*/
namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
use function array_merge;
use function assert;
use function range;
use function strpos;
use function str_contains;
use PhpParser\Node;
use PhpParser\Node\Attribute;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Trait_;
@@ -28,19 +27,11 @@ use PhpParser\NodeVisitorAbstract;
final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract
{
/**
* @psalm-var list<int>
* @psalm-var array<int>
*/
private $ignoredLines = [];
/**
* @var bool
*/
private $useAnnotationsForIgnoringCode;
/**
* @var bool
*/
private $ignoreDeprecated;
private array $ignoredLines = [];
private readonly bool $useAnnotationsForIgnoringCode;
private readonly bool $ignoreDeprecated;
public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecated)
{
@@ -53,6 +44,7 @@ final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract
if (!$node instanceof Class_ &&
!$node instanceof Trait_ &&
!$node instanceof Interface_ &&
!$node instanceof Enum_ &&
!$node instanceof ClassMethod &&
!$node instanceof Function_ &&
!$node instanceof Attribute) {
@@ -83,11 +75,23 @@ final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract
return;
}
if ($node instanceof Attribute &&
$node->name->toString() === 'PHPUnit\Framework\Attributes\CodeCoverageIgnore') {
$attributeGroup = $node->getAttribute('parent');
$attributedNode = $attributeGroup->getAttribute('parent');
for ($line = $attributedNode->getStartLine(); $line <= $attributedNode->getEndLine(); $line++) {
$this->ignoredLines[] = $line;
}
return;
}
$this->processDocComment($node);
}
/**
* @psalm-return list<int>
* @psalm-return array<int>
*/
public function ignoredLines(): array
{
@@ -102,18 +106,16 @@ final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract
return;
}
if (strpos($docComment->getText(), '@codeCoverageIgnore') !== false) {
$this->ignoredLines = array_merge(
$this->ignoredLines,
range($node->getStartLine(), $node->getEndLine())
);
if (str_contains($docComment->getText(), '@codeCoverageIgnore')) {
for ($line = $node->getStartLine(); $line <= $node->getEndLine(); $line++) {
$this->ignoredLines[] = $line;
}
}
if ($this->ignoreDeprecated && strpos($docComment->getText(), '@deprecated') !== false) {
$this->ignoredLines = array_merge(
$this->ignoredLines,
range($node->getStartLine(), $node->getEndLine())
);
if ($this->ignoreDeprecated && str_contains($docComment->getText(), '@deprecated')) {
for ($line = $node->getStartLine(); $line <= $node->getEndLine(); $line++) {
$this->ignoredLines[] = $line;
}
}
}
}

View File

@@ -31,48 +31,47 @@ use SebastianBergmann\LinesOfCode\LineCountingVisitor;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @psalm-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
* @psalm-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
* @psalm-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
*/
final class ParsingFileAnalyser implements FileAnalyser
{
/**
* @var array
* @psalm-var array<string, array<string, CodeUnitClassType>>
*/
private $classes = [];
private array $classes = [];
/**
* @var array
* @psalm-var array<string, array<string, CodeUnitTraitType>>
*/
private $traits = [];
private array $traits = [];
/**
* @var array
* @psalm-var array<string, array<string, CodeUnitFunctionType>>
*/
private $functions = [];
private array $functions = [];
/**
* @var array<string,array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}>
* @var array<string, LinesOfCodeType>
*/
private $linesOfCode = [];
private array $linesOfCode = [];
/**
* @var array
* @var array<string, LinesType>
*/
private $ignoredLines = [];
private array $ignoredLines = [];
/**
* @var array
* @var array<string, LinesType>
*/
private $executableLines = [];
/**
* @var bool
*/
private $useAnnotationsForIgnoringCode;
/**
* @var bool
*/
private $ignoreDeprecatedCode;
private array $executableLines = [];
private readonly bool $useAnnotationsForIgnoringCode;
private readonly bool $ignoreDeprecatedCode;
public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode)
{
@@ -101,9 +100,6 @@ final class ParsingFileAnalyser implements FileAnalyser
return $this->functions[$filename];
}
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
public function linesOfCodeFor(string $filename): array
{
$this->analyse($filename);
@@ -141,6 +137,8 @@ final class ParsingFileAnalyser implements FileAnalyser
$linesOfCode = 1;
}
assert($linesOfCode > 0);
$parser = (new ParserFactory)->createForHostVersion();
try {
@@ -169,10 +167,10 @@ final class ParsingFileAnalyser implements FileAnalyser
sprintf(
'Cannot parse %s: %s',
$filename,
$error->getMessage()
$error->getMessage(),
),
$error->getCode(),
$error
$error,
);
}
// @codeCoverageIgnoreEnd
@@ -188,8 +186,8 @@ final class ParsingFileAnalyser implements FileAnalyser
$this->ignoredLines[$filename] = array_unique(
array_merge(
$this->ignoredLines[$filename],
$ignoredLinesFindingVisitor->ignoredLines()
)
$ignoredLinesFindingVisitor->ignoredLines(),
),
);
sort($this->ignoredLines[$filename]);
@@ -241,7 +239,7 @@ final class ParsingFileAnalyser implements FileAnalyser
$this->ignoredLines[$filename] = array_merge(
$this->ignoredLines[$filename],
range($start, $token[2])
range($start, $token[2]),
);
}
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\TestSize;
/**
* @psalm-immutable
*/
abstract class Known extends TestSize
{
/**
* @psalm-assert-if-true Known $this
*/
public function isKnown(): bool
{
return true;
}
abstract public function isGreaterThan(self $other): bool;
}

View File

@@ -0,0 +1,34 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\TestSize;
/**
* @psalm-immutable
*/
final class Large extends Known
{
/**
* @psalm-assert-if-true Large $this
*/
public function isLarge(): bool
{
return true;
}
public function isGreaterThan(TestSize $other): bool
{
return !$other->isLarge();
}
public function asString(): string
{
return 'large';
}
}

View File

@@ -0,0 +1,34 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\TestSize;
/**
* @psalm-immutable
*/
final class Medium extends Known
{
/**
* @psalm-assert-if-true Medium $this
*/
public function isMedium(): bool
{
return true;
}
public function isGreaterThan(TestSize $other): bool
{
return $other->isSmall();
}
public function asString(): string
{
return 'medium';
}
}

View File

@@ -0,0 +1,34 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\TestSize;
/**
* @psalm-immutable
*/
final class Small extends Known
{
/**
* @psalm-assert-if-true Small $this
*/
public function isSmall(): bool
{
return true;
}
public function isGreaterThan(TestSize $other): bool
{
return false;
}
public function asString(): string
{
return 'small';
}
}

View File

@@ -0,0 +1,78 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\TestSize;
/**
* @psalm-immutable
*/
abstract class TestSize
{
public static function unknown(): self
{
return new Unknown;
}
public static function small(): self
{
return new Small;
}
public static function medium(): self
{
return new Medium;
}
public static function large(): self
{
return new Large;
}
/**
* @psalm-assert-if-true Known $this
*/
public function isKnown(): bool
{
return false;
}
/**
* @psalm-assert-if-true Unknown $this
*/
public function isUnknown(): bool
{
return false;
}
/**
* @psalm-assert-if-true Small $this
*/
public function isSmall(): bool
{
return false;
}
/**
* @psalm-assert-if-true Medium $this
*/
public function isMedium(): bool
{
return false;
}
/**
* @psalm-assert-if-true Large $this
*/
public function isLarge(): bool
{
return false;
}
abstract public function asString(): string;
}

View File

@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\TestSize;
/**
* @psalm-immutable
*/
final class Unknown extends TestSize
{
/**
* @psalm-assert-if-true Unknown $this
*/
public function isUnknown(): bool
{
return true;
}
public function asString(): string
{
return 'unknown';
}
}

View File

@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\TestStatus;
/**
* @psalm-immutable
*/
final class Failure extends Known
{
/**
* @psalm-assert-if-true Failure $this
*/
public function isFailure(): bool
{
return true;
}
public function asString(): string
{
return 'failure';
}
}

View File

@@ -0,0 +1,24 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\TestStatus;
/**
* @psalm-immutable
*/
abstract class Known extends TestStatus
{
/**
* @psalm-assert-if-true Known $this
*/
public function isKnown(): bool
{
return true;
}
}

View File

@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\TestStatus;
/**
* @psalm-immutable
*/
final class Success extends Known
{
/**
* @psalm-assert-if-true Success $this
*/
public function isSuccess(): bool
{
return true;
}
public function asString(): string
{
return 'success';
}
}

View File

@@ -0,0 +1,65 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\TestStatus;
/**
* @psalm-immutable
*/
abstract class TestStatus
{
public static function unknown(): self
{
return new Unknown;
}
public static function success(): self
{
return new Success;
}
public static function failure(): self
{
return new Failure;
}
/**
* @psalm-assert-if-true Known $this
*/
public function isKnown(): bool
{
return false;
}
/**
* @psalm-assert-if-true Unknown $this
*/
public function isUnknown(): bool
{
return false;
}
/**
* @psalm-assert-if-true Success $this
*/
public function isSuccess(): bool
{
return false;
}
/**
* @psalm-assert-if-true Failure $this
*/
public function isFailure(): bool
{
return false;
}
abstract public function asString(): string;
}

View File

@@ -0,0 +1,29 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\TestStatus;
/**
* @psalm-immutable
*/
final class Unknown extends TestStatus
{
/**
* @psalm-assert-if-true Unknown $this
*/
public function isUnknown(): bool
{
return true;
}
public function asString(): string
{
return 'unknown';
}
}

View File

@@ -23,14 +23,14 @@ final class Filesystem
*/
public static function createDirectory(string $directory): void
{
$success = !(!is_dir($directory) && !@mkdir($directory, 0777, true) && !is_dir($directory));
$success = !(!is_dir($directory) && !@mkdir($directory, 0o777, true) && !is_dir($directory));
if (!$success) {
throw new DirectoryCouldNotBeCreatedException(
sprintf(
'Directory "%s" could not be created',
$directory
)
$directory,
),
);
}
}

View File

@@ -16,15 +16,8 @@ use function sprintf;
*/
final class Percentage
{
/**
* @var float
*/
private $fraction;
/**
* @var float
*/
private $total;
private readonly float $fraction;
private readonly float $total;
public static function fromFractionAndTotal(float $fraction, float $total): self
{

View File

@@ -14,15 +14,12 @@ use SebastianBergmann\Version as VersionId;
final class Version
{
/**
* @var string
*/
private static $version;
private static string $version = '';
public static function id(): string
{
if (self::$version === null) {
self::$version = (new VersionId('9.2.30', dirname(__DIR__)))->getVersion();
if (self::$version === '') {
self::$version = (new VersionId('10.1.16', dirname(__DIR__)))->asString();
}
return self::$version;