fix:repo cleanup
This commit is contained in:
1204 files changed
+121800
-16971
No files matched your search
@@ -1,101 +0,0 @@
|
||||
# ChangeLog
|
||||
|
||||
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
|
||||
|
||||
## [11.0.11] - 2025-08-27
|
||||
|
||||
### Changed
|
||||
|
||||
* [#1085](https://github.com/sebastianbergmann/php-code-coverage/pull/1085): Improve performance by skipping empty lines after filter has been applied
|
||||
|
||||
## [11.0.10] - 2025-06-18
|
||||
|
||||
### Changed
|
||||
|
||||
* Changed CSS for HTML report to not use common ligatures as this sometimes lead to hard-to-read code
|
||||
* Updated Bootstrap to version 5.3.6 for HTML report
|
||||
|
||||
## [11.0.9] - 2025-02-25
|
||||
|
||||
### Changed
|
||||
|
||||
* Changed version identifier for static analysis cache from "MD5 over source code" to `Version::id()`
|
||||
* The `SebastianBergmann\CodeCoverage\Filter::includeUncoveredFiles()` and `SebastianBergmann\CodeCoverage\Filter::excludeUncoveredFiles()` methods are no longer deprecated
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1063](https://github.com/sebastianbergmann/php-code-coverage/issues/1063): HTML report highlights argument named `fn` differently than other named arguments
|
||||
|
||||
## [11.0.8] - 2024-12-11
|
||||
|
||||
### Changed
|
||||
|
||||
* [#1054](https://github.com/sebastianbergmann/php-code-coverage/pull/1054): Use click event for toggling "tests covering this line" popover in HTML report
|
||||
|
||||
## [11.0.7] - 2024-10-09
|
||||
|
||||
### Changed
|
||||
|
||||
* [#1037](https://github.com/sebastianbergmann/php-code-coverage/pull/1037): Upgrade Bootstrap to version 5.3.3 for HTML report
|
||||
* [#1046](https://github.com/sebastianbergmann/php-code-coverage/pull/1046): CSS fixes for HTML report
|
||||
|
||||
### Deprecated
|
||||
|
||||
* The `SebastianBergmann\CodeCoverage\Filter::includeUncoveredFiles()`, `SebastianBergmann\CodeCoverage\Filter::excludeUncoveredFiles()`, and `SebastianBergmann\CodeCoverage\Filter::excludeFile()` methods have been deprecated
|
||||
|
||||
## [11.0.6] - 2024-08-22
|
||||
|
||||
### Changed
|
||||
|
||||
* Updated dependencies (so that users that install using Composer's `--prefer-lowest` CLI option also get recent versions)
|
||||
|
||||
## [11.0.5] - 2024-07-03
|
||||
|
||||
### Changed
|
||||
|
||||
* This project now uses PHPStan instead of Psalm for static analysis
|
||||
|
||||
## [11.0.4] - 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
|
||||
|
||||
## [11.0.3] - 2024-03-12
|
||||
|
||||
### Fixed
|
||||
|
||||
* [#1033](https://github.com/sebastianbergmann/php-code-coverage/issues/1033): `@codeCoverageIgnore` annotation does not work on `enum`
|
||||
|
||||
## [11.0.2] - 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
|
||||
|
||||
## [11.0.1] - 2024-03-02
|
||||
|
||||
### Changed
|
||||
|
||||
* Do not use implicitly nullable parameters
|
||||
|
||||
## [11.0.0] - 2024-02-02
|
||||
|
||||
### Removed
|
||||
|
||||
* The `SebastianBergmann\CodeCoverage\Filter::includeDirectory()`, `SebastianBergmann\CodeCoverage\Filter::excludeDirectory()`, and `SebastianBergmann\CodeCoverage\Filter::excludeFile()` methods have been removed
|
||||
* This component now requires PHP-Parser 5
|
||||
* This component is no longer supported on PHP 8.1
|
||||
|
||||
[11.0.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.10...11.0.11
|
||||
[11.0.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.9...11.0.10
|
||||
[11.0.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.8...11.0.9
|
||||
[11.0.8]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.7...11.0.8
|
||||
[11.0.7]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.6...11.0.7
|
||||
[11.0.6]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.5...11.0.6
|
||||
[11.0.5]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.4...11.0.5
|
||||
[11.0.4]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.3...11.0.4
|
||||
[11.0.3]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.2...11.0.3
|
||||
[11.0.2]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.1...11.0.2
|
||||
[11.0.1]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.0...11.0.1
|
||||
[11.0.0]: https://github.com/sebastianbergmann/php-code-coverage/compare/10.1...11.0.0
|
||||
+11
-12
@@ -22,29 +22,28 @@
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.2.0"
|
||||
"php": "8.3.0"
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"php": ">=8.3",
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"nikic/php-parser": "^5.4.0",
|
||||
"phpunit/php-file-iterator": "^5.1.0",
|
||||
"phpunit/php-text-template": "^4.0.1",
|
||||
"sebastian/code-unit-reverse-lookup": "^4.0.1",
|
||||
"sebastian/complexity": "^4.0.1",
|
||||
"sebastian/environment": "^7.2.0",
|
||||
"sebastian/lines-of-code": "^3.0.1",
|
||||
"sebastian/version": "^5.0.2",
|
||||
"nikic/php-parser": "^5.6.1",
|
||||
"phpunit/php-file-iterator": "^6.0",
|
||||
"phpunit/php-text-template": "^5.0",
|
||||
"sebastian/complexity": "^5.0",
|
||||
"sebastian/environment": "^8.0.3",
|
||||
"sebastian/lines-of-code": "^4.0",
|
||||
"sebastian/version": "^6.0",
|
||||
"theseer/tokenizer": "^1.2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.5.2"
|
||||
"phpunit/phpunit": "^12.3.7"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcov": "PHP extension that provides line coverage",
|
||||
@@ -62,7 +61,7 @@
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "11.0.x-dev"
|
||||
"dev-main": "12.4.x-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
+122
-81
@@ -14,11 +14,9 @@ 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 count;
|
||||
use function explode;
|
||||
use function is_array;
|
||||
use function is_file;
|
||||
use function sort;
|
||||
use ReflectionClass;
|
||||
@@ -27,59 +25,76 @@ 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\CachingSourceAnalyser;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingSourceAnalyser;
|
||||
use SebastianBergmann\CodeCoverage\Test\Target\MapBuilder;
|
||||
use SebastianBergmann\CodeCoverage\Test\Target\Mapper;
|
||||
use SebastianBergmann\CodeCoverage\Test\Target\TargetCollection;
|
||||
use SebastianBergmann\CodeCoverage\Test\Target\TargetCollectionValidator;
|
||||
use SebastianBergmann\CodeCoverage\Test\Target\ValidationResult;
|
||||
use SebastianBergmann\CodeCoverage\Test\TestSize\TestSize;
|
||||
use SebastianBergmann\CodeCoverage\Test\TestStatus\TestStatus;
|
||||
use SebastianBergmann\CodeUnitReverseLookup\Wizard;
|
||||
|
||||
/**
|
||||
* Provides collection functionality for PHP code coverage information.
|
||||
*
|
||||
* @phpstan-type TestType = array{
|
||||
* size: string,
|
||||
* status: string,
|
||||
* }
|
||||
* @phpstan-type TestType array{size: string, status: string}
|
||||
* @phpstan-type TargetedLines array<non-empty-string, list<positive-int>>
|
||||
*/
|
||||
final class CodeCoverage
|
||||
{
|
||||
private const UNCOVERED_FILES = 'UNCOVERED_FILES';
|
||||
private const string UNCOVERED_FILES = 'UNCOVERED_FILES';
|
||||
private readonly Driver $driver;
|
||||
private readonly Filter $filter;
|
||||
private readonly Wizard $wizard;
|
||||
private ?FileAnalyser $analyser = null;
|
||||
private ?Mapper $targetMapper = null;
|
||||
private ?string $cacheDirectory = null;
|
||||
private bool $checkForUnintentionallyCoveredCode = false;
|
||||
private bool $collectBranchAndPathCoverage = 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 array<string,list<int>>
|
||||
*/
|
||||
private array $linesToBeIgnored = [];
|
||||
|
||||
/**
|
||||
* @var array<string, TestType>
|
||||
*/
|
||||
private array $tests = [];
|
||||
private bool $useAnnotationsForIgnoringCode = true;
|
||||
|
||||
/**
|
||||
* @var list<class-string>
|
||||
*/
|
||||
private array $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
|
||||
private ?FileAnalyser $analyser = null;
|
||||
private ?string $cacheDirectory = null;
|
||||
private ?Directory $cachedReport = null;
|
||||
private ?string $currentId = null;
|
||||
private ?TestSize $currentSize = null;
|
||||
private ProcessedCodeCoverageData $data;
|
||||
|
||||
/**
|
||||
* @var array<string, TestType>
|
||||
*/
|
||||
private array $tests = [];
|
||||
private ?Directory $cachedReport = null;
|
||||
|
||||
public function __construct(Driver $driver, Filter $filter)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
$this->filter = $filter;
|
||||
$this->data = new ProcessedCodeCoverageData;
|
||||
$this->wizard = new Wizard;
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
$prefix = "\x00" . self::class . "\x00";
|
||||
|
||||
return [
|
||||
// Configuration
|
||||
$prefix . 'cacheDirectory' => $this->cacheDirectory,
|
||||
$prefix . 'checkForUnintentionallyCoveredCode' => $this->checkForUnintentionallyCoveredCode,
|
||||
$prefix . 'includeUncoveredFiles' => $this->includeUncoveredFiles,
|
||||
$prefix . 'ignoreDeprecatedCode' => $this->ignoreDeprecatedCode,
|
||||
$prefix . 'parentClassesExcludedFromUnintentionallyCoveredCodeCheck' => $this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck,
|
||||
$prefix . 'useAnnotationsForIgnoringCode' => $this->useAnnotationsForIgnoringCode,
|
||||
$prefix . 'filter' => $this->filter,
|
||||
|
||||
// Data
|
||||
$prefix . 'data' => $this->data,
|
||||
$prefix . 'tests' => $this->tests,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,19 +189,11 @@ final class CodeCoverage
|
||||
$this->cachedReport = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,list<int>> $linesToBeIgnored
|
||||
*/
|
||||
public function stop(bool $append = true, ?TestStatus $status = null, array|false $linesToBeCovered = [], array $linesToBeUsed = [], array $linesToBeIgnored = []): RawCodeCoverageData
|
||||
public function stop(bool $append = true, ?TestStatus $status = null, null|false|TargetCollection $covers = null, ?TargetCollection $uses = null): RawCodeCoverageData
|
||||
{
|
||||
$data = $this->driver->stop();
|
||||
|
||||
$this->linesToBeIgnored = array_merge_recursive(
|
||||
$this->linesToBeIgnored,
|
||||
$linesToBeIgnored,
|
||||
);
|
||||
|
||||
$this->append($data, null, $append, $status, $linesToBeCovered, $linesToBeUsed, $linesToBeIgnored);
|
||||
$this->append($data, null, $append, $status, $covers, $uses);
|
||||
|
||||
$this->currentId = null;
|
||||
$this->currentSize = null;
|
||||
@@ -196,13 +203,11 @@ final class CodeCoverage
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,list<int>> $linesToBeIgnored
|
||||
*
|
||||
* @throws ReflectionException
|
||||
* @throws TestIdMissingException
|
||||
* @throws UnintentionallyCoveredCodeException
|
||||
*/
|
||||
public function append(RawCodeCoverageData $rawData, ?string $id = null, bool $append = true, ?TestStatus $status = null, array|false $linesToBeCovered = [], array $linesToBeUsed = [], array $linesToBeIgnored = []): void
|
||||
public function append(RawCodeCoverageData $rawData, ?string $id = null, bool $append = true, ?TestStatus $status = null, null|false|TargetCollection $covers = null, ?TargetCollection $uses = null): void
|
||||
{
|
||||
if ($id === null) {
|
||||
$id = $this->currentId;
|
||||
@@ -212,24 +217,32 @@ final class CodeCoverage
|
||||
throw new TestIdMissingException;
|
||||
}
|
||||
|
||||
$this->cachedReport = null;
|
||||
|
||||
if ($status === null) {
|
||||
$status = TestStatus::unknown();
|
||||
}
|
||||
|
||||
if ($covers === null) {
|
||||
$covers = TargetCollection::fromArray([]);
|
||||
}
|
||||
|
||||
if ($uses === null) {
|
||||
$uses = TargetCollection::fromArray([]);
|
||||
}
|
||||
|
||||
$size = $this->currentSize;
|
||||
|
||||
if ($size === null) {
|
||||
$size = TestSize::unknown();
|
||||
}
|
||||
|
||||
$this->cachedReport = null;
|
||||
|
||||
$this->applyFilter($rawData);
|
||||
|
||||
$this->applyExecutableLinesFilter($rawData);
|
||||
|
||||
if ($this->useAnnotationsForIgnoringCode) {
|
||||
$this->applyIgnoredLinesFilter($rawData, $linesToBeIgnored);
|
||||
$this->applyIgnoredLinesFilter($rawData);
|
||||
}
|
||||
|
||||
$this->data->initializeUnseenData($rawData);
|
||||
@@ -242,6 +255,17 @@ final class CodeCoverage
|
||||
return;
|
||||
}
|
||||
|
||||
$linesToBeCovered = false;
|
||||
$linesToBeUsed = [];
|
||||
|
||||
if ($covers !== false) {
|
||||
$linesToBeCovered = $this->targetMapper()->mapTargets($covers);
|
||||
}
|
||||
|
||||
if ($linesToBeCovered !== false) {
|
||||
$linesToBeUsed = $this->targetMapper()->mapTargets($uses);
|
||||
}
|
||||
|
||||
$this->applyCoversAndUsesFilter(
|
||||
$rawData,
|
||||
$linesToBeCovered,
|
||||
@@ -249,7 +273,7 @@ final class CodeCoverage
|
||||
$size,
|
||||
);
|
||||
|
||||
if (empty($rawData->lineCoverage())) {
|
||||
if ($rawData->lineCoverage() === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -360,24 +384,31 @@ final class CodeCoverage
|
||||
public function enableBranchAndPathCoverage(): void
|
||||
{
|
||||
$this->driver->enableBranchAndPathCoverage();
|
||||
|
||||
$this->collectBranchAndPathCoverage = true;
|
||||
}
|
||||
|
||||
public function disableBranchAndPathCoverage(): void
|
||||
{
|
||||
$this->driver->disableBranchAndPathCoverage();
|
||||
|
||||
$this->collectBranchAndPathCoverage = false;
|
||||
}
|
||||
|
||||
public function collectsBranchAndPathCoverage(): bool
|
||||
{
|
||||
return $this->driver->collectsBranchAndPathCoverage();
|
||||
return $this->collectBranchAndPathCoverage;
|
||||
}
|
||||
|
||||
public function detectsDeadCode(): bool
|
||||
public function validate(TargetCollection $targets): ValidationResult
|
||||
{
|
||||
return $this->driver->detectsDeadCode();
|
||||
return (new TargetCollectionValidator)->validate($this->targetMapper(), $targets);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param false|TargetedLines $linesToBeCovered
|
||||
* @param TargetedLines $linesToBeUsed
|
||||
*
|
||||
* @throws ReflectionException
|
||||
* @throws UnintentionallyCoveredCodeException
|
||||
*/
|
||||
@@ -389,7 +420,7 @@ final class CodeCoverage
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($linesToBeCovered)) {
|
||||
if ($linesToBeCovered === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -404,11 +435,9 @@ final class CodeCoverage
|
||||
$rawData->removeCoverageDataForFile($fileWithNoCoverage);
|
||||
}
|
||||
|
||||
if (is_array($linesToBeCovered)) {
|
||||
foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) {
|
||||
$rawData->keepLineCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
|
||||
$rawData->keepFunctionCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
|
||||
}
|
||||
foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) {
|
||||
$rawData->keepLineCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
|
||||
$rawData->keepFunctionCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +461,7 @@ final class CodeCoverage
|
||||
continue;
|
||||
}
|
||||
|
||||
$linesToBranchMap = $this->analyser()->executableLinesIn($filename);
|
||||
$linesToBranchMap = $this->analyser()->analyse($filename)->executableLines();
|
||||
|
||||
$data->keepLineCoverageDataOnlyForLines(
|
||||
$filename,
|
||||
@@ -446,26 +475,16 @@ final class CodeCoverage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,list<int>> $linesToBeIgnored
|
||||
*/
|
||||
private function applyIgnoredLinesFilter(RawCodeCoverageData $data, array $linesToBeIgnored): void
|
||||
private function applyIgnoredLinesFilter(RawCodeCoverageData $data): 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()->analyse($filename)->ignoredLines(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -488,13 +507,15 @@ final class CodeCoverage
|
||||
$this->analyser(),
|
||||
),
|
||||
self::UNCOVERED_FILES,
|
||||
linesToBeIgnored: $this->linesToBeIgnored,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TargetedLines $linesToBeCovered
|
||||
* @param TargetedLines $linesToBeUsed
|
||||
*
|
||||
* @throws ReflectionException
|
||||
* @throws UnintentionallyCoveredCodeException
|
||||
*/
|
||||
@@ -510,20 +531,26 @@ final class CodeCoverage
|
||||
foreach ($data->lineCoverage() as $file => $_data) {
|
||||
foreach ($_data as $line => $flag) {
|
||||
if ($flag === 1 && !isset($allowedLines[$file][$line])) {
|
||||
$unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line);
|
||||
$unintentionallyCoveredUnits[] = $this->targetMapper->lookup($file, $line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
|
||||
|
||||
if (!empty($unintentionallyCoveredUnits)) {
|
||||
if ($unintentionallyCoveredUnits !== []) {
|
||||
throw new UnintentionallyCoveredCodeException(
|
||||
$unintentionallyCoveredUnits,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TargetedLines $linesToBeCovered
|
||||
* @param TargetedLines $linesToBeUsed
|
||||
*
|
||||
* @return TargetedLines
|
||||
*/
|
||||
private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed): array
|
||||
{
|
||||
$allowedLines = [];
|
||||
@@ -606,26 +633,40 @@ final class CodeCoverage
|
||||
return $processed;
|
||||
}
|
||||
|
||||
private function targetMapper(): Mapper
|
||||
{
|
||||
if ($this->targetMapper !== null) {
|
||||
return $this->targetMapper;
|
||||
}
|
||||
|
||||
$this->targetMapper = new Mapper(
|
||||
(new MapBuilder)->build($this->filter, $this->analyser()),
|
||||
);
|
||||
|
||||
return $this->targetMapper;
|
||||
}
|
||||
|
||||
private function analyser(): FileAnalyser
|
||||
{
|
||||
if ($this->analyser !== null) {
|
||||
return $this->analyser;
|
||||
}
|
||||
|
||||
$this->analyser = new ParsingFileAnalyser(
|
||||
$sourceAnalyser = new ParsingSourceAnalyser;
|
||||
|
||||
if ($this->cachesStaticAnalysis()) {
|
||||
$sourceAnalyser = new CachingSourceAnalyser(
|
||||
$this->cacheDirectory,
|
||||
$sourceAnalyser,
|
||||
);
|
||||
}
|
||||
|
||||
$this->analyser = new FileAnalyser(
|
||||
$sourceAnalyser,
|
||||
$this->useAnnotationsForIgnoringCode,
|
||||
$this->ignoreDeprecatedCode,
|
||||
);
|
||||
|
||||
if ($this->cachesStaticAnalysis()) {
|
||||
$this->analyser = new CachingFileAnalyser(
|
||||
$this->cacheDirectory,
|
||||
$this->analyser,
|
||||
$this->useAnnotationsForIgnoringCode,
|
||||
$this->ignoreDeprecatedCode,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->analyser;
|
||||
}
|
||||
}
|
||||
+25
-20
@@ -17,13 +17,31 @@ use function count;
|
||||
use function is_array;
|
||||
use function ksort;
|
||||
use SebastianBergmann\CodeCoverage\Driver\Driver;
|
||||
use SebastianBergmann\CodeCoverage\Driver\XdebugDriver;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type XdebugFunctionCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
|
||||
* @phpstan-import-type XdebugFunctionCoverageType from XdebugDriver
|
||||
*
|
||||
* @phpstan-type TestIdType = string
|
||||
* @phpstan-type TestIdType string
|
||||
* @phpstan-type FunctionCoverageDataType 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>
|
||||
* }
|
||||
* @phpstan-type FunctionCoverageType array<string, array<string, FunctionCoverageDataType>>
|
||||
*/
|
||||
final class ProcessedCodeCoverageData
|
||||
{
|
||||
@@ -40,22 +58,7 @@ final class ProcessedCodeCoverageData
|
||||
* 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<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>
|
||||
* }>>
|
||||
* @var FunctionCoverageType
|
||||
*/
|
||||
private array $functionCoverage = [];
|
||||
|
||||
@@ -216,6 +219,8 @@ final class ProcessedCodeCoverageData
|
||||
* 4 = the line has been tested
|
||||
*
|
||||
* During a merge, a higher number is better.
|
||||
*
|
||||
* @return 1|2|3|4
|
||||
*/
|
||||
private function priorityForLine(array $data, int $line): int
|
||||
{
|
||||
@@ -237,7 +242,7 @@ final class ProcessedCodeCoverageData
|
||||
/**
|
||||
* For a function we have never seen before, copy all data over and simply init the 'hit' array.
|
||||
*
|
||||
* @param XdebugFunctionCoverageType $functionData
|
||||
* @param FunctionCoverageDataType|XdebugFunctionCoverageType $functionData
|
||||
*/
|
||||
private function initPreviouslyUnseenFunction(string $file, string $functionName, array $functionData): void
|
||||
{
|
||||
@@ -257,7 +262,7 @@ final class ProcessedCodeCoverageData
|
||||
* 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.
|
||||
*
|
||||
* @param XdebugFunctionCoverageType $functionData
|
||||
* @param FunctionCoverageDataType|XdebugFunctionCoverageType $functionData
|
||||
*/
|
||||
private function initPreviouslySeenFunction(string $file, string $functionName, array $functionData): void
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ use function array_diff_key;
|
||||
use function array_flip;
|
||||
use function array_intersect;
|
||||
use function array_intersect_key;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function file_get_contents;
|
||||
@@ -25,14 +26,15 @@ use function str_ends_with;
|
||||
use function str_starts_with;
|
||||
use function trim;
|
||||
use SebastianBergmann\CodeCoverage\Driver\Driver;
|
||||
use SebastianBergmann\CodeCoverage\Driver\XdebugDriver;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type XdebugFunctionsCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
|
||||
* @phpstan-import-type XdebugCodeCoverageWithoutPathCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
|
||||
* @phpstan-import-type XdebugCodeCoverageWithPathCoverageType from \SebastianBergmann\CodeCoverage\Driver\XdebugDriver
|
||||
* @phpstan-import-type XdebugFunctionsCoverageType from XdebugDriver
|
||||
* @phpstan-import-type XdebugCodeCoverageWithoutPathCoverageType from XdebugDriver
|
||||
* @phpstan-import-type XdebugCodeCoverageWithPathCoverageType from XdebugDriver
|
||||
*/
|
||||
final class RawCodeCoverageData
|
||||
{
|
||||
@@ -86,11 +88,10 @@ final class RawCodeCoverageData
|
||||
|
||||
public static function fromUncoveredFile(string $filename, FileAnalyser $analyser): self
|
||||
{
|
||||
$lineCoverage = [];
|
||||
|
||||
foreach ($analyser->executableLinesIn($filename) as $line => $branch) {
|
||||
$lineCoverage[$line] = Driver::LINE_NOT_EXECUTED;
|
||||
}
|
||||
$lineCoverage = array_map(
|
||||
static fn (): int => Driver::LINE_NOT_EXECUTED,
|
||||
$analyser->analyse($filename)->executableLines(),
|
||||
);
|
||||
|
||||
return new self([$filename => $lineCoverage], []);
|
||||
}
|
||||
@@ -211,7 +212,7 @@ final class RawCodeCoverageData
|
||||
*/
|
||||
public function removeCoverageDataForLines(string $filename, array $lines): void
|
||||
{
|
||||
if (empty($lines)) {
|
||||
if ($lines === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -258,6 +259,9 @@ final class RawCodeCoverageData
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
private function getEmptyLinesForFile(string $filename): array
|
||||
{
|
||||
if (!isset(self::$emptyLineCache[$filename])) {
|
||||
|
||||
+5
-49
@@ -12,7 +12,6 @@ namespace SebastianBergmann\CodeCoverage\Driver;
|
||||
use function sprintf;
|
||||
use SebastianBergmann\CodeCoverage\BranchAndPathCoverageNotSupportedException;
|
||||
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
|
||||
use SebastianBergmann\CodeCoverage\DeadCodeDetectionNotSupportedException;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
@@ -20,41 +19,30 @@ use SebastianBergmann\CodeCoverage\DeadCodeDetectionNotSupportedException;
|
||||
abstract class Driver
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const LINE_NOT_EXECUTABLE = -2;
|
||||
public const int LINE_NOT_EXECUTABLE = -2;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const LINE_NOT_EXECUTED = -1;
|
||||
public const int LINE_NOT_EXECUTED = -1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const LINE_EXECUTED = 1;
|
||||
public const int LINE_EXECUTED = 1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const BRANCH_NOT_HIT = 0;
|
||||
public const int BRANCH_NOT_HIT = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const BRANCH_HIT = 1;
|
||||
public const int BRANCH_HIT = 1;
|
||||
private bool $collectBranchAndPathCoverage = false;
|
||||
private bool $detectDeadCode = false;
|
||||
|
||||
public function canCollectBranchAndPathCoverage(): bool
|
||||
{
|
||||
@@ -88,38 +76,6 @@ abstract class Driver
|
||||
$this->collectBranchAndPathCoverage = false;
|
||||
}
|
||||
|
||||
public function canDetectDeadCode(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function detectsDeadCode(): bool
|
||||
{
|
||||
return $this->detectDeadCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DeadCodeDetectionNotSupportedException
|
||||
*/
|
||||
public function enableDeadCodeDetection(): void
|
||||
{
|
||||
if (!$this->canDetectDeadCode()) {
|
||||
throw new DeadCodeDetectionNotSupportedException(
|
||||
sprintf(
|
||||
'%s does not support dead code detection',
|
||||
$this->nameAndVersion(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$this->detectDeadCode = true;
|
||||
}
|
||||
|
||||
public function disableDeadCodeDetection(): void
|
||||
{
|
||||
$this->detectDeadCode = false;
|
||||
}
|
||||
|
||||
abstract public function nameAndVersion(): string;
|
||||
|
||||
abstract public function start(): void;
|
||||
|
||||
@@ -38,6 +38,9 @@ final class PcovDriver extends Driver
|
||||
$this->filter = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function start(): void
|
||||
{
|
||||
start();
|
||||
@@ -47,10 +50,11 @@ final class PcovDriver extends Driver
|
||||
{
|
||||
stop();
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
$filesToCollectCoverageFor = waiting();
|
||||
$collected = [];
|
||||
|
||||
if ($filesToCollectCoverageFor) {
|
||||
if ($filesToCollectCoverageFor !== []) {
|
||||
if (!$this->filter->isEmpty()) {
|
||||
$filesToCollectCoverageFor = array_intersect($filesToCollectCoverageFor, $this->filter->files());
|
||||
}
|
||||
@@ -61,6 +65,7 @@ final class PcovDriver extends Driver
|
||||
}
|
||||
|
||||
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($collected);
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
public function nameAndVersion(): string
|
||||
|
||||
@@ -21,6 +21,7 @@ final class Selector
|
||||
* @throws PcovNotAvailableException
|
||||
* @throws XdebugNotAvailableException
|
||||
* @throws XdebugNotEnabledException
|
||||
* @throws XdebugVersionNotSupportedException
|
||||
*/
|
||||
public function forLineCoverage(Filter $filter): Driver
|
||||
{
|
||||
@@ -31,11 +32,7 @@ final class Selector
|
||||
}
|
||||
|
||||
if ($runtime->hasXdebug()) {
|
||||
$driver = new XdebugDriver($filter);
|
||||
|
||||
$driver->enableDeadCodeDetection();
|
||||
|
||||
return $driver;
|
||||
return new XdebugDriver($filter);
|
||||
}
|
||||
|
||||
throw new NoCodeCoverageDriverAvailableException;
|
||||
@@ -45,13 +42,13 @@ final class Selector
|
||||
* @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException
|
||||
* @throws XdebugNotAvailableException
|
||||
* @throws XdebugNotEnabledException
|
||||
* @throws XdebugVersionNotSupportedException
|
||||
*/
|
||||
public function forLineAndPathCoverage(Filter $filter): Driver
|
||||
{
|
||||
if ((new Runtime)->hasXdebug()) {
|
||||
$driver = new XdebugDriver($filter);
|
||||
|
||||
$driver->enableDeadCodeDetection();
|
||||
$driver->enableBranchAndPathCoverage();
|
||||
|
||||
return $driver;
|
||||
|
||||
@@ -14,11 +14,8 @@ use const XDEBUG_CC_DEAD_CODE;
|
||||
use const XDEBUG_CC_UNUSED;
|
||||
use const XDEBUG_FILTER_CODE_COVERAGE;
|
||||
use const XDEBUG_PATH_INCLUDE;
|
||||
use function explode;
|
||||
use function extension_loaded;
|
||||
use function getenv;
|
||||
use function in_array;
|
||||
use function ini_get;
|
||||
use function phpversion;
|
||||
use function version_compare;
|
||||
use function xdebug_get_code_coverage;
|
||||
@@ -34,8 +31,8 @@ use SebastianBergmann\CodeCoverage\Filter;
|
||||
*
|
||||
* @see https://xdebug.org/docs/code_coverage#xdebug_get_code_coverage
|
||||
*
|
||||
* @phpstan-type XdebugLinesCoverageType = array<int, int>
|
||||
* @phpstan-type XdebugBranchCoverageType = array{
|
||||
* @phpstan-type XdebugLinesCoverageType array<int, int>
|
||||
* @phpstan-type XdebugBranchCoverageType array{
|
||||
* op_start: int,
|
||||
* op_end: int,
|
||||
* line_start: int,
|
||||
@@ -44,32 +41,32 @@ use SebastianBergmann\CodeCoverage\Filter;
|
||||
* out: array<int, int>,
|
||||
* out_hit: array<int, int>,
|
||||
* }
|
||||
* @phpstan-type XdebugPathCoverageType = array{
|
||||
* @phpstan-type XdebugPathCoverageType array{
|
||||
* path: array<int, int>,
|
||||
* hit: int,
|
||||
* }
|
||||
* @phpstan-type XdebugFunctionCoverageType = array{
|
||||
* @phpstan-type XdebugFunctionCoverageType array{
|
||||
* branches: array<int, XdebugBranchCoverageType>,
|
||||
* paths: array<int, XdebugPathCoverageType>,
|
||||
* }
|
||||
* @phpstan-type XdebugFunctionsCoverageType = array<string, XdebugFunctionCoverageType>
|
||||
* @phpstan-type XdebugPathAndBranchesCoverageType = array{
|
||||
* @phpstan-type XdebugFunctionsCoverageType array<string, XdebugFunctionCoverageType>
|
||||
* @phpstan-type XdebugPathAndBranchesCoverageType array{
|
||||
* lines: XdebugLinesCoverageType,
|
||||
* functions: XdebugFunctionsCoverageType,
|
||||
* }
|
||||
* @phpstan-type XdebugCodeCoverageWithoutPathCoverageType = array<string, XdebugLinesCoverageType>
|
||||
* @phpstan-type XdebugCodeCoverageWithPathCoverageType = array<string, XdebugPathAndBranchesCoverageType>
|
||||
* @phpstan-type XdebugCodeCoverageWithoutPathCoverageType array<string, XdebugLinesCoverageType>
|
||||
* @phpstan-type XdebugCodeCoverageWithPathCoverageType array<string, XdebugPathAndBranchesCoverageType>
|
||||
*/
|
||||
final class XdebugDriver extends Driver
|
||||
{
|
||||
/**
|
||||
* @throws XdebugNotAvailableException
|
||||
* @throws XdebugNotEnabledException
|
||||
* @throws XdebugVersionNotSupportedException
|
||||
*/
|
||||
public function __construct(Filter $filter)
|
||||
{
|
||||
$this->ensureXdebugIsAvailable();
|
||||
$this->ensureXdebugCodeCoverageFeatureIsEnabled();
|
||||
|
||||
if (!$filter->isEmpty()) {
|
||||
xdebug_set_filter(
|
||||
@@ -85,18 +82,9 @@ final class XdebugDriver extends Driver
|
||||
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;
|
||||
}
|
||||
$flags = XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE;
|
||||
|
||||
if ($this->collectsBranchAndPathCoverage()) {
|
||||
$flags |= XDEBUG_CC_BRANCH_CHECK;
|
||||
@@ -127,35 +115,20 @@ final class XdebugDriver extends Driver
|
||||
|
||||
/**
|
||||
* @throws XdebugNotAvailableException
|
||||
* @throws XdebugNotEnabledException
|
||||
* @throws XdebugVersionNotSupportedException
|
||||
*/
|
||||
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;
|
||||
if (!version_compare(phpversion('xdebug'), '3.1', '>=')) {
|
||||
throw new XdebugVersionNotSupportedException(phpversion('xdebug'));
|
||||
}
|
||||
|
||||
$mode = getenv('XDEBUG_MODE');
|
||||
|
||||
if ($mode === false || $mode === '') {
|
||||
$mode = ini_get('xdebug.mode');
|
||||
}
|
||||
|
||||
if ($mode === false ||
|
||||
!in_array('coverage', explode(',', $mode), true)) {
|
||||
if (!in_array('coverage', xdebug_info('mode'), true)) {
|
||||
throw new XdebugNotEnabledException;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-16
@@ -1,16 +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;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class DeadCodeDetectionNotSupportedException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
Vendored
+1
-2
@@ -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 function sprintf;
|
||||
use RuntimeException;
|
||||
use SebastianBergmann\CodeCoverage\Exception;
|
||||
|
||||
final class PathExistsButIsNotDirectoryException extends RuntimeException implements Exception
|
||||
{
|
||||
|
||||
+1
-2
@@ -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 function sprintf;
|
||||
use RuntimeException;
|
||||
use SebastianBergmann\CodeCoverage\Exception;
|
||||
|
||||
final class WriteOperationFailedException extends RuntimeException implements Exception
|
||||
{
|
||||
|
||||
+1
-1
@@ -88,6 +88,6 @@ final class Filter
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->files);
|
||||
return $this->files === [];
|
||||
}
|
||||
}
|
||||
@@ -15,20 +15,24 @@ use function str_ends_with;
|
||||
use function str_replace;
|
||||
use function substr;
|
||||
use Countable;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode;
|
||||
use SebastianBergmann\CodeCoverage\Util\Percentage;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
* @phpstan-import-type ProcessedFunctionType from \SebastianBergmann\CodeCoverage\Node\File
|
||||
* @phpstan-import-type ProcessedClassType from \SebastianBergmann\CodeCoverage\Node\File
|
||||
* @phpstan-import-type ProcessedTraitType from \SebastianBergmann\CodeCoverage\Node\File
|
||||
* @phpstan-import-type ProcessedFunctionType from File
|
||||
* @phpstan-import-type ProcessedClassType from File
|
||||
* @phpstan-import-type ProcessedTraitType from File
|
||||
*/
|
||||
abstract class AbstractNode implements Countable
|
||||
{
|
||||
private readonly string $name;
|
||||
private string $pathAsString;
|
||||
|
||||
/**
|
||||
* @var non-empty-list<self>
|
||||
*/
|
||||
private array $pathAsArray;
|
||||
private readonly ?AbstractNode $parent;
|
||||
private string $id;
|
||||
@@ -61,6 +65,9 @@ abstract class AbstractNode implements Countable
|
||||
return $this->pathAsString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<self>
|
||||
*/
|
||||
public function pathAsArray(): array
|
||||
{
|
||||
return $this->pathAsArray;
|
||||
@@ -171,6 +178,24 @@ abstract class AbstractNode implements Countable
|
||||
return $this->numberOfTestedFunctions() + $this->numberOfTestedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-negative-int
|
||||
*/
|
||||
public function cyclomaticComplexity(): int
|
||||
{
|
||||
$ccn = 0;
|
||||
|
||||
foreach ($this->classesAndTraits() as $classLike) {
|
||||
$ccn += $classLike['ccn'];
|
||||
}
|
||||
|
||||
foreach ($this->functions() as $function) {
|
||||
$ccn += $function['ccn'];
|
||||
}
|
||||
|
||||
return $ccn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedClassType>
|
||||
*/
|
||||
@@ -186,10 +211,7 @@ abstract class AbstractNode implements Countable
|
||||
*/
|
||||
abstract public function functions(): array;
|
||||
|
||||
/**
|
||||
* @return LinesOfCodeType
|
||||
*/
|
||||
abstract public function linesOfCode(): array;
|
||||
abstract public function linesOfCode(): LinesOfCode;
|
||||
|
||||
abstract public function numberOfExecutableLines(): int;
|
||||
|
||||
|
||||
+12
-9
@@ -28,11 +28,11 @@ use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type TestType from \SebastianBergmann\CodeCoverage\CodeCoverage
|
||||
* @phpstan-import-type TestType from CodeCoverage
|
||||
*/
|
||||
final class Builder
|
||||
final readonly class Builder
|
||||
{
|
||||
private readonly FileAnalyser $analyser;
|
||||
private FileAnalyser $analyser;
|
||||
|
||||
public function __construct(FileAnalyser $analyser)
|
||||
{
|
||||
@@ -70,6 +70,8 @@ final class Builder
|
||||
$filename = $root->pathAsString() . DIRECTORY_SEPARATOR . $key;
|
||||
|
||||
if (is_file($filename)) {
|
||||
$analysisResult = $this->analyser->analyse($filename);
|
||||
|
||||
$root->addFile(
|
||||
new File(
|
||||
$key,
|
||||
@@ -77,10 +79,10 @@ final class Builder
|
||||
$value['lineCoverage'],
|
||||
$value['functionCoverage'],
|
||||
$tests,
|
||||
$this->analyser->classesIn($filename),
|
||||
$this->analyser->traitsIn($filename),
|
||||
$this->analyser->functionsIn($filename),
|
||||
$this->analyser->linesOfCodeFor($filename),
|
||||
$analysisResult->classes(),
|
||||
$analysisResult->traits(),
|
||||
$analysisResult->functions(),
|
||||
$analysisResult->linesOfCode(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -201,7 +203,7 @@ final class Builder
|
||||
*/
|
||||
private function reducePaths(ProcessedCodeCoverageData $coverage): string
|
||||
{
|
||||
if (empty($coverage->coveredFiles())) {
|
||||
if ($coverage->coveredFiles() === []) {
|
||||
return '.';
|
||||
}
|
||||
|
||||
@@ -223,9 +225,10 @@ final class Builder
|
||||
$paths[$i] = substr($paths[$i], 7);
|
||||
$paths[$i] = str_replace('/', DIRECTORY_SEPARATOR, $paths[$i]);
|
||||
}
|
||||
|
||||
$paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]);
|
||||
|
||||
if (empty($paths[$i][0])) {
|
||||
if ($paths[$i][0] === '') {
|
||||
$paths[$i][0] = DIRECTORY_SEPARATOR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ use function sprintf;
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class CrapIndex
|
||||
final readonly class CrapIndex
|
||||
{
|
||||
private readonly int $cyclomaticComplexity;
|
||||
private readonly float $codeCoverage;
|
||||
private int $cyclomaticComplexity;
|
||||
private float $codeCoverage;
|
||||
|
||||
public function __construct(int $cyclomaticComplexity, float $codeCoverage)
|
||||
{
|
||||
|
||||
+51
-20
@@ -14,11 +14,16 @@ use function assert;
|
||||
use function count;
|
||||
use IteratorAggregate;
|
||||
use RecursiveIteratorIterator;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
* @template-implements IteratorAggregate<int, AbstractNode>
|
||||
*
|
||||
* @phpstan-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
* @phpstan-import-type ProcessedFunctionType from File
|
||||
* @phpstan-import-type ProcessedClassType from File
|
||||
* @phpstan-import-type ProcessedTraitType from File
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Directory extends AbstractNode implements IteratorAggregate
|
||||
{
|
||||
@@ -35,15 +40,23 @@ final class Directory extends AbstractNode implements IteratorAggregate
|
||||
/**
|
||||
* @var list<File>
|
||||
*/
|
||||
private array $files = [];
|
||||
private ?array $classes = null;
|
||||
private ?array $traits = null;
|
||||
private ?array $functions = null;
|
||||
private array $files = [];
|
||||
|
||||
/**
|
||||
* @var null|LinesOfCodeType
|
||||
* @var ?array<string, ProcessedClassType>
|
||||
*/
|
||||
private ?array $linesOfCode = null;
|
||||
private ?array $classes = null;
|
||||
|
||||
/**
|
||||
* @var ?array<string, ProcessedTraitType>
|
||||
*/
|
||||
private ?array $traits = null;
|
||||
|
||||
/**
|
||||
* @var ?array<string, ProcessedFunctionType>
|
||||
*/
|
||||
private ?array $functions = null;
|
||||
private ?LinesOfCode $linesOfCode = null;
|
||||
private int $numFiles = -1;
|
||||
private int $numExecutableLines = -1;
|
||||
private int $numExecutedLines = -1;
|
||||
@@ -73,6 +86,9 @@ final class Directory extends AbstractNode implements IteratorAggregate
|
||||
return $this->numFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RecursiveIteratorIterator<Iterator<AbstractNode>>
|
||||
*/
|
||||
public function getIterator(): RecursiveIteratorIterator
|
||||
{
|
||||
return new RecursiveIteratorIterator(
|
||||
@@ -102,21 +118,33 @@ final class Directory extends AbstractNode implements IteratorAggregate
|
||||
$this->numExecutedLines = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Directory>
|
||||
*/
|
||||
public function directories(): array
|
||||
{
|
||||
return $this->directories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<File>
|
||||
*/
|
||||
public function files(): array
|
||||
{
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Directory|File>
|
||||
*/
|
||||
public function children(): array
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedClassType>
|
||||
*/
|
||||
public function classes(): array
|
||||
{
|
||||
if ($this->classes === null) {
|
||||
@@ -133,6 +161,9 @@ final class Directory extends AbstractNode implements IteratorAggregate
|
||||
return $this->classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedTraitType>
|
||||
*/
|
||||
public function traits(): array
|
||||
{
|
||||
if ($this->traits === null) {
|
||||
@@ -149,6 +180,9 @@ final class Directory extends AbstractNode implements IteratorAggregate
|
||||
return $this->traits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedFunctionType>
|
||||
*/
|
||||
public function functions(): array
|
||||
{
|
||||
if ($this->functions === null) {
|
||||
@@ -165,25 +199,22 @@ final class Directory extends AbstractNode implements IteratorAggregate
|
||||
return $this->functions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinesOfCodeType
|
||||
*/
|
||||
public function linesOfCode(): array
|
||||
public function linesOfCode(): LinesOfCode
|
||||
{
|
||||
if ($this->linesOfCode === null) {
|
||||
$this->linesOfCode = [
|
||||
'linesOfCode' => 0,
|
||||
'commentLinesOfCode' => 0,
|
||||
'nonCommentLinesOfCode' => 0,
|
||||
];
|
||||
$linesOfCode = 0;
|
||||
$commentLinesOfCode = 0;
|
||||
$nonCommentLinesOfCode = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$childLinesOfCode = $child->linesOfCode();
|
||||
|
||||
$this->linesOfCode['linesOfCode'] += $childLinesOfCode['linesOfCode'];
|
||||
$this->linesOfCode['commentLinesOfCode'] += $childLinesOfCode['commentLinesOfCode'];
|
||||
$this->linesOfCode['nonCommentLinesOfCode'] += $childLinesOfCode['nonCommentLinesOfCode'];
|
||||
$linesOfCode += $childLinesOfCode->linesOfCode();
|
||||
$commentLinesOfCode += $childLinesOfCode->commentLinesOfCode();
|
||||
$nonCommentLinesOfCode += $childLinesOfCode->nonCommentLinesOfCode();
|
||||
}
|
||||
|
||||
$this->linesOfCode = new LinesOfCode($linesOfCode, $commentLinesOfCode, $nonCommentLinesOfCode);
|
||||
}
|
||||
|
||||
return $this->linesOfCode;
|
||||
|
||||
+89
-85
@@ -12,18 +12,21 @@ namespace SebastianBergmann\CodeCoverage\Node;
|
||||
use function array_filter;
|
||||
use function count;
|
||||
use function range;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\AnalysisResult;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\Class_;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\Function_;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\Method;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\Trait_;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
* @phpstan-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
* @phpstan-import-type TestType from CodeCoverage
|
||||
* @phpstan-import-type LinesType from AnalysisResult
|
||||
*
|
||||
* @phpstan-type ProcessedFunctionType = array{
|
||||
* @phpstan-type ProcessedFunctionType array{
|
||||
* functionName: string,
|
||||
* namespace: string,
|
||||
* signature: string,
|
||||
@@ -40,7 +43,7 @@ use function range;
|
||||
* crap: int|string,
|
||||
* link: string
|
||||
* }
|
||||
* @phpstan-type ProcessedMethodType = array{
|
||||
* @phpstan-type ProcessedMethodType array{
|
||||
* methodName: string,
|
||||
* visibility: string,
|
||||
* signature: string,
|
||||
@@ -57,7 +60,7 @@ use function range;
|
||||
* crap: int|string,
|
||||
* link: string
|
||||
* }
|
||||
* @phpstan-type ProcessedClassType = array{
|
||||
* @phpstan-type ProcessedClassType array{
|
||||
* className: string,
|
||||
* namespace: string,
|
||||
* methods: array<string, ProcessedMethodType>,
|
||||
@@ -73,7 +76,7 @@ use function range;
|
||||
* crap: int|string,
|
||||
* link: string
|
||||
* }
|
||||
* @phpstan-type ProcessedTraitType = array{
|
||||
* @phpstan-type ProcessedTraitType array{
|
||||
* traitName: string,
|
||||
* namespace: string,
|
||||
* methods: array<string, ProcessedMethodType>,
|
||||
@@ -97,6 +100,10 @@ final class File extends AbstractNode
|
||||
*/
|
||||
private array $lineCoverageData;
|
||||
private array $functionCoverageData;
|
||||
|
||||
/**
|
||||
* @var array<string, TestType>
|
||||
*/
|
||||
private readonly array $testData;
|
||||
private int $numExecutableLines = 0;
|
||||
private int $numExecutedLines = 0;
|
||||
@@ -119,11 +126,7 @@ final class File extends AbstractNode
|
||||
* @var array<string, ProcessedFunctionType>
|
||||
*/
|
||||
private array $functions = [];
|
||||
|
||||
/**
|
||||
* @var LinesOfCodeType
|
||||
*/
|
||||
private readonly array $linesOfCode;
|
||||
private readonly LinesOfCode $linesOfCode;
|
||||
private ?int $numClasses = null;
|
||||
private int $numTestedClasses = 0;
|
||||
private ?int $numTraits = null;
|
||||
@@ -133,18 +136,18 @@ final class File extends AbstractNode
|
||||
private ?int $numTestedFunctions = null;
|
||||
|
||||
/**
|
||||
* @var array<int, array|array{0: CodeUnitClassType, 1: string}|array{0: CodeUnitFunctionType}|array{0: CodeUnitTraitType, 1: string}>
|
||||
* @var array<int, array|array{0: Class_, 1: string}|array{0: Function_}|array{0: Trait_, 1: string}>
|
||||
*/
|
||||
private array $codeUnitsByLine = [];
|
||||
|
||||
/**
|
||||
* @param array<int, ?list<non-empty-string>> $lineCoverageData
|
||||
* @param array<string, CodeUnitClassType> $classes
|
||||
* @param array<string, CodeUnitTraitType> $traits
|
||||
* @param array<string, CodeUnitFunctionType> $functions
|
||||
* @param LinesOfCodeType $linesOfCode
|
||||
* @param array<string, TestType> $testData
|
||||
* @param array<string, Class_> $classes
|
||||
* @param array<string, Trait_> $traits
|
||||
* @param array<string, Function_> $functions
|
||||
*/
|
||||
public function __construct(string $name, AbstractNode $parent, array $lineCoverageData, array $functionCoverageData, array $testData, array $classes, array $traits, array $functions, array $linesOfCode)
|
||||
public function __construct(string $name, AbstractNode $parent, array $lineCoverageData, array $functionCoverageData, array $testData, array $classes, array $traits, array $functions, LinesOfCode $linesOfCode)
|
||||
{
|
||||
parent::__construct($name, $parent);
|
||||
|
||||
@@ -174,6 +177,9 @@ final class File extends AbstractNode
|
||||
return $this->functionCoverageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, TestType>
|
||||
*/
|
||||
public function testData(): array
|
||||
{
|
||||
return $this->testData;
|
||||
@@ -203,7 +209,7 @@ final class File extends AbstractNode
|
||||
return $this->functions;
|
||||
}
|
||||
|
||||
public function linesOfCode(): array
|
||||
public function linesOfCode(): LinesOfCode
|
||||
{
|
||||
return $this->linesOfCode;
|
||||
}
|
||||
@@ -360,13 +366,13 @@ final class File extends AbstractNode
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, CodeUnitClassType> $classes
|
||||
* @param array<string, CodeUnitTraitType> $traits
|
||||
* @param array<string, CodeUnitFunctionType> $functions
|
||||
* @param array<string, Class_> $classes
|
||||
* @param array<string, Trait_> $traits
|
||||
* @param array<string, Function_> $functions
|
||||
*/
|
||||
private function calculateStatistics(array $classes, array $traits, array $functions): void
|
||||
{
|
||||
foreach (range(1, $this->linesOfCode['linesOfCode']) as $lineNumber) {
|
||||
foreach (range(1, $this->linesOfCode->linesOfCode()) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [];
|
||||
}
|
||||
|
||||
@@ -374,7 +380,7 @@ final class File extends AbstractNode
|
||||
$this->processTraits($traits);
|
||||
$this->processFunctions($functions);
|
||||
|
||||
foreach (range(1, $this->linesOfCode['linesOfCode']) as $lineNumber) {
|
||||
foreach (range(1, $this->linesOfCode->linesOfCode()) as $lineNumber) {
|
||||
if (isset($this->lineCoverageData[$lineNumber])) {
|
||||
foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) {
|
||||
$codeUnit['executableLines']++;
|
||||
@@ -398,24 +404,24 @@ final class File extends AbstractNode
|
||||
|
||||
foreach ($this->traits as &$trait) {
|
||||
foreach ($trait['methods'] as &$method) {
|
||||
$methodLineCoverage = $method['executableLines'] ? ($method['executedLines'] / $method['executableLines']) * 100 : 100;
|
||||
$methodBranchCoverage = $method['executableBranches'] ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0;
|
||||
$methodPathCoverage = $method['executablePaths'] ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0;
|
||||
$methodLineCoverage = $method['executableLines'] > 0 ? ($method['executedLines'] / $method['executableLines']) * 100 : 100;
|
||||
$methodBranchCoverage = $method['executableBranches'] > 0 ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0;
|
||||
$methodPathCoverage = $method['executablePaths'] > 0 ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0;
|
||||
|
||||
$method['coverage'] = $methodBranchCoverage ?: $methodLineCoverage;
|
||||
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage ?: $methodLineCoverage))->asString();
|
||||
$method['coverage'] = $methodBranchCoverage > 0 ? $methodBranchCoverage : $methodLineCoverage;
|
||||
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage > 0 ? $methodPathCoverage : $methodLineCoverage))->asString();
|
||||
|
||||
$trait['ccn'] += $method['ccn'];
|
||||
}
|
||||
|
||||
unset($method);
|
||||
|
||||
$traitLineCoverage = $trait['executableLines'] ? ($trait['executedLines'] / $trait['executableLines']) * 100 : 100;
|
||||
$traitBranchCoverage = $trait['executableBranches'] ? ($trait['executedBranches'] / $trait['executableBranches']) * 100 : 0;
|
||||
$traitPathCoverage = $trait['executablePaths'] ? ($trait['executedPaths'] / $trait['executablePaths']) * 100 : 0;
|
||||
$traitLineCoverage = $trait['executableLines'] > 0 ? ($trait['executedLines'] / $trait['executableLines']) * 100 : 100;
|
||||
$traitBranchCoverage = $trait['executableBranches'] > 0 ? ($trait['executedBranches'] / $trait['executableBranches']) * 100 : 0;
|
||||
$traitPathCoverage = $trait['executablePaths'] > 0 ? ($trait['executedPaths'] / $trait['executablePaths']) * 100 : 0;
|
||||
|
||||
$trait['coverage'] = $traitBranchCoverage ?: $traitLineCoverage;
|
||||
$trait['crap'] = (new CrapIndex($trait['ccn'], $traitPathCoverage ?: $traitLineCoverage))->asString();
|
||||
$trait['coverage'] = $traitBranchCoverage > 0 ? $traitBranchCoverage : $traitLineCoverage;
|
||||
$trait['crap'] = (new CrapIndex($trait['ccn'], $traitPathCoverage > 0 ? $traitPathCoverage : $traitLineCoverage))->asString();
|
||||
|
||||
if ($trait['executableLines'] > 0 && $trait['coverage'] === 100) {
|
||||
$this->numTestedClasses++;
|
||||
@@ -426,24 +432,24 @@ final class File extends AbstractNode
|
||||
|
||||
foreach ($this->classes as &$class) {
|
||||
foreach ($class['methods'] as &$method) {
|
||||
$methodLineCoverage = $method['executableLines'] ? ($method['executedLines'] / $method['executableLines']) * 100 : 100;
|
||||
$methodBranchCoverage = $method['executableBranches'] ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0;
|
||||
$methodPathCoverage = $method['executablePaths'] ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0;
|
||||
$methodLineCoverage = $method['executableLines'] > 0 ? ($method['executedLines'] / $method['executableLines']) * 100 : 100;
|
||||
$methodBranchCoverage = $method['executableBranches'] > 0 ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0;
|
||||
$methodPathCoverage = $method['executablePaths'] > 0 ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0;
|
||||
|
||||
$method['coverage'] = $methodBranchCoverage ?: $methodLineCoverage;
|
||||
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage ?: $methodLineCoverage))->asString();
|
||||
$method['coverage'] = $methodBranchCoverage > 0 ? $methodBranchCoverage : $methodLineCoverage;
|
||||
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage > 0 ? $methodPathCoverage : $methodLineCoverage))->asString();
|
||||
|
||||
$class['ccn'] += $method['ccn'];
|
||||
}
|
||||
|
||||
unset($method);
|
||||
|
||||
$classLineCoverage = $class['executableLines'] ? ($class['executedLines'] / $class['executableLines']) * 100 : 100;
|
||||
$classBranchCoverage = $class['executableBranches'] ? ($class['executedBranches'] / $class['executableBranches']) * 100 : 0;
|
||||
$classPathCoverage = $class['executablePaths'] ? ($class['executedPaths'] / $class['executablePaths']) * 100 : 0;
|
||||
$classLineCoverage = $class['executableLines'] > 0 ? ($class['executedLines'] / $class['executableLines']) * 100 : 100;
|
||||
$classBranchCoverage = $class['executableBranches'] > 0 ? ($class['executedBranches'] / $class['executableBranches']) * 100 : 0;
|
||||
$classPathCoverage = $class['executablePaths'] > 0 ? ($class['executedPaths'] / $class['executablePaths']) * 100 : 0;
|
||||
|
||||
$class['coverage'] = $classBranchCoverage ?: $classLineCoverage;
|
||||
$class['crap'] = (new CrapIndex($class['ccn'], $classPathCoverage ?: $classLineCoverage))->asString();
|
||||
$class['coverage'] = $classBranchCoverage > 0 ? $classBranchCoverage : $classLineCoverage;
|
||||
$class['crap'] = (new CrapIndex($class['ccn'], $classPathCoverage > 0 ? $classPathCoverage : $classLineCoverage))->asString();
|
||||
|
||||
if ($class['executableLines'] > 0 && $class['coverage'] === 100) {
|
||||
$this->numTestedClasses++;
|
||||
@@ -453,12 +459,12 @@ final class File extends AbstractNode
|
||||
unset($class);
|
||||
|
||||
foreach ($this->functions as &$function) {
|
||||
$functionLineCoverage = $function['executableLines'] ? ($function['executedLines'] / $function['executableLines']) * 100 : 100;
|
||||
$functionBranchCoverage = $function['executableBranches'] ? ($function['executedBranches'] / $function['executableBranches']) * 100 : 0;
|
||||
$functionPathCoverage = $function['executablePaths'] ? ($function['executedPaths'] / $function['executablePaths']) * 100 : 0;
|
||||
$functionLineCoverage = $function['executableLines'] > 0 ? ($function['executedLines'] / $function['executableLines']) * 100 : 100;
|
||||
$functionBranchCoverage = $function['executableBranches'] > 0 ? ($function['executedBranches'] / $function['executableBranches']) * 100 : 0;
|
||||
$functionPathCoverage = $function['executablePaths'] > 0 ? ($function['executedPaths'] / $function['executablePaths']) * 100 : 0;
|
||||
|
||||
$function['coverage'] = $functionBranchCoverage ?: $functionLineCoverage;
|
||||
$function['crap'] = (new CrapIndex($function['ccn'], $functionPathCoverage ?: $functionLineCoverage))->asString();
|
||||
$function['coverage'] = $functionBranchCoverage > 0 ? $functionBranchCoverage : $functionLineCoverage;
|
||||
$function['crap'] = (new CrapIndex($function['ccn'], $functionPathCoverage > 0 ? $functionPathCoverage : $functionLineCoverage))->asString();
|
||||
|
||||
if ($function['coverage'] === 100) {
|
||||
$this->numTestedFunctions++;
|
||||
@@ -467,7 +473,7 @@ final class File extends AbstractNode
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, CodeUnitClassType> $classes
|
||||
* @param array<string, Class_> $classes
|
||||
*/
|
||||
private function processClasses(array $classes): void
|
||||
{
|
||||
@@ -476,9 +482,9 @@ final class File extends AbstractNode
|
||||
foreach ($classes as $className => $class) {
|
||||
$this->classes[$className] = [
|
||||
'className' => $className,
|
||||
'namespace' => $class['namespace'],
|
||||
'namespace' => $class->namespace(),
|
||||
'methods' => [],
|
||||
'startLine' => $class['startLine'],
|
||||
'startLine' => $class->startLine(),
|
||||
'executableLines' => 0,
|
||||
'executedLines' => 0,
|
||||
'executableBranches' => 0,
|
||||
@@ -488,11 +494,11 @@ final class File extends AbstractNode
|
||||
'ccn' => 0,
|
||||
'coverage' => 0,
|
||||
'crap' => 0,
|
||||
'link' => $link . $class['startLine'],
|
||||
'link' => $link . $class->startLine(),
|
||||
];
|
||||
|
||||
foreach ($class['methods'] as $methodName => $method) {
|
||||
$methodData = $this->newMethod($className, $methodName, $method, $link);
|
||||
foreach ($class->methods() as $methodName => $method) {
|
||||
$methodData = $this->newMethod($className, $method, $link);
|
||||
$this->classes[$className]['methods'][$methodName] = $methodData;
|
||||
|
||||
$this->classes[$className]['executableBranches'] += $methodData['executableBranches'];
|
||||
@@ -505,7 +511,7 @@ final class File extends AbstractNode
|
||||
$this->numExecutablePaths += $methodData['executablePaths'];
|
||||
$this->numExecutedPaths += $methodData['executedPaths'];
|
||||
|
||||
foreach (range($method['startLine'], $method['endLine']) as $lineNumber) {
|
||||
foreach (range($method->startLine(), $method->endLine()) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [
|
||||
&$this->classes[$className],
|
||||
&$this->classes[$className]['methods'][$methodName],
|
||||
@@ -516,7 +522,7 @@ final class File extends AbstractNode
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, CodeUnitTraitType> $traits
|
||||
* @param array<string, Trait_> $traits
|
||||
*/
|
||||
private function processTraits(array $traits): void
|
||||
{
|
||||
@@ -525,9 +531,9 @@ final class File extends AbstractNode
|
||||
foreach ($traits as $traitName => $trait) {
|
||||
$this->traits[$traitName] = [
|
||||
'traitName' => $traitName,
|
||||
'namespace' => $trait['namespace'],
|
||||
'namespace' => $trait->namespace(),
|
||||
'methods' => [],
|
||||
'startLine' => $trait['startLine'],
|
||||
'startLine' => $trait->startLine(),
|
||||
'executableLines' => 0,
|
||||
'executedLines' => 0,
|
||||
'executableBranches' => 0,
|
||||
@@ -537,11 +543,11 @@ final class File extends AbstractNode
|
||||
'ccn' => 0,
|
||||
'coverage' => 0,
|
||||
'crap' => 0,
|
||||
'link' => $link . $trait['startLine'],
|
||||
'link' => $link . $trait->startLine(),
|
||||
];
|
||||
|
||||
foreach ($trait['methods'] as $methodName => $method) {
|
||||
$methodData = $this->newMethod($traitName, $methodName, $method, $link);
|
||||
foreach ($trait->methods() as $methodName => $method) {
|
||||
$methodData = $this->newMethod($traitName, $method, $link);
|
||||
$this->traits[$traitName]['methods'][$methodName] = $methodData;
|
||||
|
||||
$this->traits[$traitName]['executableBranches'] += $methodData['executableBranches'];
|
||||
@@ -554,7 +560,7 @@ final class File extends AbstractNode
|
||||
$this->numExecutablePaths += $methodData['executablePaths'];
|
||||
$this->numExecutedPaths += $methodData['executedPaths'];
|
||||
|
||||
foreach (range($method['startLine'], $method['endLine']) as $lineNumber) {
|
||||
foreach (range($method->startLine(), $method->endLine()) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [
|
||||
&$this->traits[$traitName],
|
||||
&$this->traits[$traitName]['methods'][$methodName],
|
||||
@@ -565,7 +571,7 @@ final class File extends AbstractNode
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, CodeUnitFunctionType> $functions
|
||||
* @param array<string, Function_> $functions
|
||||
*/
|
||||
private function processFunctions(array $functions): void
|
||||
{
|
||||
@@ -574,23 +580,23 @@ final class File extends AbstractNode
|
||||
foreach ($functions as $functionName => $function) {
|
||||
$this->functions[$functionName] = [
|
||||
'functionName' => $functionName,
|
||||
'namespace' => $function['namespace'],
|
||||
'signature' => $function['signature'],
|
||||
'startLine' => $function['startLine'],
|
||||
'endLine' => $function['endLine'],
|
||||
'namespace' => $function->namespace(),
|
||||
'signature' => $function->signature(),
|
||||
'startLine' => $function->startLine(),
|
||||
'endLine' => $function->endLine(),
|
||||
'executableLines' => 0,
|
||||
'executedLines' => 0,
|
||||
'executableBranches' => 0,
|
||||
'executedBranches' => 0,
|
||||
'executablePaths' => 0,
|
||||
'executedPaths' => 0,
|
||||
'ccn' => $function['ccn'],
|
||||
'ccn' => $function->cyclomaticComplexity(),
|
||||
'coverage' => 0,
|
||||
'crap' => 0,
|
||||
'link' => $link . $function['startLine'],
|
||||
'link' => $link . $function->startLine(),
|
||||
];
|
||||
|
||||
foreach (range($function['startLine'], $function['endLine']) as $lineNumber) {
|
||||
foreach (range($function->startLine(), $function->endLine()) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [&$this->functions[$functionName]];
|
||||
}
|
||||
|
||||
@@ -634,31 +640,29 @@ final class File extends AbstractNode
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CodeUnitMethodType $method
|
||||
*
|
||||
* @return ProcessedMethodType
|
||||
*/
|
||||
private function newMethod(string $className, string $methodName, array $method, string $link): array
|
||||
private function newMethod(string $className, Method $method, string $link): array
|
||||
{
|
||||
$methodData = [
|
||||
'methodName' => $methodName,
|
||||
'visibility' => $method['visibility'],
|
||||
'signature' => $method['signature'],
|
||||
'startLine' => $method['startLine'],
|
||||
'endLine' => $method['endLine'],
|
||||
'methodName' => $method->name(),
|
||||
'visibility' => $method->visibility()->value,
|
||||
'signature' => $method->signature(),
|
||||
'startLine' => $method->startLine(),
|
||||
'endLine' => $method->endLine(),
|
||||
'executableLines' => 0,
|
||||
'executedLines' => 0,
|
||||
'executableBranches' => 0,
|
||||
'executedBranches' => 0,
|
||||
'executablePaths' => 0,
|
||||
'executedPaths' => 0,
|
||||
'ccn' => $method['ccn'],
|
||||
'ccn' => $method->cyclomaticComplexity(),
|
||||
'coverage' => 0,
|
||||
'crap' => 0,
|
||||
'link' => $link . $method['startLine'],
|
||||
'link' => $link . $method->startLine(),
|
||||
];
|
||||
|
||||
$key = $className . '->' . $methodName;
|
||||
$key = $className . '->' . $method->name();
|
||||
|
||||
if (isset($this->functionCoverageData[$key]['branches'])) {
|
||||
$methodData['executableBranches'] = count(
|
||||
|
||||
+5
-21
@@ -9,10 +9,13 @@
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Node;
|
||||
|
||||
use function assert;
|
||||
use function count;
|
||||
use RecursiveIterator;
|
||||
|
||||
/**
|
||||
* @template-implements RecursiveIterator<int, AbstractNode>
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Iterator implements RecursiveIterator
|
||||
@@ -29,57 +32,38 @@ final class Iterator implements RecursiveIterator
|
||||
$this->nodes = $node->children();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewinds the Iterator to the first element.
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is a current element after calls to rewind() or next().
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return $this->position < count($this->nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key of the current element.
|
||||
*/
|
||||
public function key(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current element.
|
||||
*/
|
||||
public function current(): ?AbstractNode
|
||||
{
|
||||
return $this->valid() ? $this->nodes[$this->position] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves forward to next element.
|
||||
*/
|
||||
public function next(): void
|
||||
{
|
||||
$this->position++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sub iterator for the current element.
|
||||
*/
|
||||
public function getChildren(): self
|
||||
{
|
||||
assert($this->nodes[$this->position] instanceof Directory);
|
||||
|
||||
return new self($this->nodes[$this->position]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current element has children.
|
||||
*/
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
return $this->nodes[$this->position] instanceof Directory;
|
||||
|
||||
+14
-18
@@ -10,31 +10,31 @@
|
||||
namespace SebastianBergmann\CodeCoverage\Report;
|
||||
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function file_put_contents;
|
||||
use function is_string;
|
||||
use function ksort;
|
||||
use function max;
|
||||
use function range;
|
||||
use function str_contains;
|
||||
use function time;
|
||||
use DOMDocument;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
|
||||
use SebastianBergmann\CodeCoverage\Node\File;
|
||||
use SebastianBergmann\CodeCoverage\Util\Filesystem;
|
||||
use SebastianBergmann\CodeCoverage\Util\Xml;
|
||||
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
|
||||
|
||||
final class Clover
|
||||
{
|
||||
/**
|
||||
* @param null|non-empty-string $target
|
||||
* @param null|non-empty-string $name
|
||||
*
|
||||
* @throws WriteOperationFailedException
|
||||
*/
|
||||
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
|
||||
{
|
||||
$time = (string) time();
|
||||
|
||||
$xmlDocument = new DOMDocument('1.0', 'UTF-8');
|
||||
$xmlDocument->formatOutput = true;
|
||||
$xmlDocument = new DOMDocument('1.0', 'UTF-8');
|
||||
|
||||
$xmlCoverage = $xmlDocument->createElement('coverage');
|
||||
$xmlCoverage->setAttribute('generated', $time);
|
||||
@@ -79,6 +79,7 @@ final class Clover
|
||||
}
|
||||
|
||||
foreach ($class['methods'] as $methodName => $method) {
|
||||
/** @phpstan-ignore equal.notAllowed */
|
||||
if ($method['executableLines'] == 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -87,6 +88,7 @@ final class Clover
|
||||
$classStatements += $method['executableLines'];
|
||||
$coveredClassStatements += $method['executedLines'];
|
||||
|
||||
/** @phpstan-ignore equal.notAllowed */
|
||||
if ($method['coverage'] == 100) {
|
||||
$coveredMethods++;
|
||||
}
|
||||
@@ -168,8 +170,8 @@ final class Clover
|
||||
$linesOfCode = $item->linesOfCode();
|
||||
|
||||
$xmlMetrics = $xmlDocument->createElement('metrics');
|
||||
$xmlMetrics->setAttribute('loc', (string) $linesOfCode['linesOfCode']);
|
||||
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode['nonCommentLinesOfCode']);
|
||||
$xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode());
|
||||
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode());
|
||||
$xmlMetrics->setAttribute('classes', (string) $item->numberOfClassesAndTraits());
|
||||
$xmlMetrics->setAttribute('methods', (string) $item->numberOfMethods());
|
||||
$xmlMetrics->setAttribute('coveredmethods', (string) $item->numberOfTestedMethods());
|
||||
@@ -201,8 +203,8 @@ final class Clover
|
||||
|
||||
$xmlMetrics = $xmlDocument->createElement('metrics');
|
||||
$xmlMetrics->setAttribute('files', (string) count($report));
|
||||
$xmlMetrics->setAttribute('loc', (string) $linesOfCode['linesOfCode']);
|
||||
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode['nonCommentLinesOfCode']);
|
||||
$xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode());
|
||||
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode());
|
||||
$xmlMetrics->setAttribute('classes', (string) $report->numberOfClassesAndTraits());
|
||||
$xmlMetrics->setAttribute('methods', (string) $report->numberOfMethods());
|
||||
$xmlMetrics->setAttribute('coveredmethods', (string) $report->numberOfTestedMethods());
|
||||
@@ -214,16 +216,10 @@ final class Clover
|
||||
$xmlMetrics->setAttribute('coveredelements', (string) ($report->numberOfTestedMethods() + $report->numberOfExecutedLines() + $report->numberOfExecutedBranches()));
|
||||
$xmlProject->appendChild($xmlMetrics);
|
||||
|
||||
$buffer = $xmlDocument->saveXML();
|
||||
$buffer = Xml::asString($xmlDocument);
|
||||
|
||||
if ($target !== null) {
|
||||
if (!str_contains($target, '://')) {
|
||||
Filesystem::createDirectory(dirname($target));
|
||||
}
|
||||
|
||||
if (@file_put_contents($target, $buffer) === false) {
|
||||
throw new WriteOperationFailedException($target);
|
||||
}
|
||||
Filesystem::write($target, $buffer);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
|
||||
+11
-18
@@ -12,22 +12,22 @@ namespace SebastianBergmann\CodeCoverage\Report;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use function basename;
|
||||
use function count;
|
||||
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 time;
|
||||
use DOMImplementation;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
|
||||
use SebastianBergmann\CodeCoverage\Node\File;
|
||||
use SebastianBergmann\CodeCoverage\Util\Filesystem;
|
||||
use SebastianBergmann\CodeCoverage\Util\Xml;
|
||||
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
|
||||
|
||||
final class Cobertura
|
||||
{
|
||||
/**
|
||||
* @param null|non-empty-string $target
|
||||
*
|
||||
* @throws WriteOperationFailedException
|
||||
*/
|
||||
public function process(CodeCoverage $coverage, ?string $target = null): string
|
||||
@@ -44,10 +44,9 @@ final class Cobertura
|
||||
'http://cobertura.sourceforge.net/xml/coverage-04.dtd',
|
||||
);
|
||||
|
||||
$document = $implementation->createDocument('', '', $documentType);
|
||||
$document->xmlVersion = '1.0';
|
||||
$document->encoding = 'UTF-8';
|
||||
$document->formatOutput = true;
|
||||
$document = $implementation->createDocument('', '', $documentType);
|
||||
$document->xmlVersion = '1.0';
|
||||
$document->encoding = 'UTF-8';
|
||||
|
||||
$coverageElement = $document->createElement('coverage');
|
||||
|
||||
@@ -153,7 +152,7 @@ final class Cobertura
|
||||
|
||||
$linesValid = $method['executableLines'];
|
||||
$linesCovered = $method['executedLines'];
|
||||
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
|
||||
$lineRate = $linesCovered / $linesValid;
|
||||
|
||||
$branchesValid = $method['executableBranches'];
|
||||
$branchesCovered = $method['executedBranches'];
|
||||
@@ -228,7 +227,7 @@ final class Cobertura
|
||||
|
||||
$linesValid = $function['executableLines'];
|
||||
$linesCovered = $function['executedLines'];
|
||||
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
|
||||
$lineRate = $linesCovered / $linesValid;
|
||||
|
||||
$functionsLinesValid += $linesValid;
|
||||
$functionsLinesCovered += $linesCovered;
|
||||
@@ -289,16 +288,10 @@ final class Cobertura
|
||||
|
||||
$coverageElement->setAttribute('complexity', (string) $complexity);
|
||||
|
||||
$buffer = $document->saveXML();
|
||||
$buffer = Xml::asString($document);
|
||||
|
||||
if ($target !== null) {
|
||||
if (!str_contains($target, '://')) {
|
||||
Filesystem::createDirectory(dirname($target));
|
||||
}
|
||||
|
||||
if (@file_put_contents($target, $buffer) === false) {
|
||||
throw new WriteOperationFailedException($target);
|
||||
}
|
||||
Filesystem::write($target, $buffer);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
|
||||
+11
-17
@@ -10,21 +10,19 @@
|
||||
namespace SebastianBergmann\CodeCoverage\Report;
|
||||
|
||||
use function date;
|
||||
use function dirname;
|
||||
use function file_put_contents;
|
||||
use function htmlspecialchars;
|
||||
use function is_string;
|
||||
use function round;
|
||||
use function str_contains;
|
||||
use DOMDocument;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
|
||||
use SebastianBergmann\CodeCoverage\Node\File;
|
||||
use SebastianBergmann\CodeCoverage\Util\Filesystem;
|
||||
use SebastianBergmann\CodeCoverage\Util\Xml;
|
||||
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
|
||||
|
||||
final class Crap4j
|
||||
final readonly class Crap4j
|
||||
{
|
||||
private readonly int $threshold;
|
||||
private int $threshold;
|
||||
|
||||
public function __construct(int $threshold = 30)
|
||||
{
|
||||
@@ -32,12 +30,14 @@ final class Crap4j
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|non-empty-string $target
|
||||
* @param null|non-empty-string $name
|
||||
*
|
||||
* @throws WriteOperationFailedException
|
||||
*/
|
||||
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
|
||||
{
|
||||
$document = new DOMDocument('1.0', 'UTF-8');
|
||||
$document->formatOutput = true;
|
||||
$document = new DOMDocument('1.0', 'UTF-8');
|
||||
|
||||
$root = $document->createElement('crap_result');
|
||||
$document->appendChild($root);
|
||||
@@ -83,7 +83,7 @@ final class Crap4j
|
||||
|
||||
$methodNode = $document->createElement('method');
|
||||
|
||||
if (!empty($class['namespace'])) {
|
||||
if ($class['namespace'] !== '') {
|
||||
$namespace = $class['namespace'];
|
||||
}
|
||||
|
||||
@@ -119,16 +119,10 @@ final class Crap4j
|
||||
$root->appendChild($stats);
|
||||
$root->appendChild($methodsNode);
|
||||
|
||||
$buffer = $document->saveXML();
|
||||
$buffer = Xml::asString($document);
|
||||
|
||||
if ($target !== null) {
|
||||
if (!str_contains($target, '://')) {
|
||||
Filesystem::createDirectory(dirname($target));
|
||||
}
|
||||
|
||||
if (@file_put_contents($target, $buffer) === false) {
|
||||
throw new WriteOperationFailedException($target);
|
||||
}
|
||||
Filesystem::write($target, $buffer);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
|
||||
@@ -12,13 +12,13 @@ namespace SebastianBergmann\CodeCoverage\Report\Html;
|
||||
/**
|
||||
* @immutable
|
||||
*/
|
||||
final class Colors
|
||||
final readonly class Colors
|
||||
{
|
||||
private readonly string $successLow;
|
||||
private readonly string $successMedium;
|
||||
private readonly string $successHigh;
|
||||
private readonly string $warning;
|
||||
private readonly string $danger;
|
||||
private string $successLow;
|
||||
private string $successMedium;
|
||||
private string $successHigh;
|
||||
private string $warning;
|
||||
private string $danger;
|
||||
|
||||
public static function default(): self
|
||||
{
|
||||
|
||||
@@ -15,9 +15,9 @@ use SebastianBergmann\CodeCoverage\InvalidArgumentException;
|
||||
/**
|
||||
* @immutable
|
||||
*/
|
||||
final class CustomCssFile
|
||||
final readonly class CustomCssFile
|
||||
{
|
||||
private readonly string $path;
|
||||
private string $path;
|
||||
|
||||
public static function default(): self
|
||||
{
|
||||
|
||||
+17
-15
@@ -22,13 +22,13 @@ use SebastianBergmann\CodeCoverage\Util\Filesystem;
|
||||
use SebastianBergmann\Template\Exception;
|
||||
use SebastianBergmann\Template\Template;
|
||||
|
||||
final class Facade
|
||||
final readonly class Facade
|
||||
{
|
||||
private readonly string $templatePath;
|
||||
private readonly string $generator;
|
||||
private readonly Colors $colors;
|
||||
private readonly Thresholds $thresholds;
|
||||
private readonly CustomCssFile $customCssFile;
|
||||
private string $templatePath;
|
||||
private string $generator;
|
||||
private Colors $colors;
|
||||
private Thresholds $thresholds;
|
||||
private CustomCssFile $customCssFile;
|
||||
|
||||
public function __construct(string $generator = '', ?Colors $colors = null, ?Thresholds $thresholds = null, ?CustomCssFile $customCssFile = null)
|
||||
{
|
||||
@@ -41,16 +41,17 @@ final class Facade
|
||||
|
||||
public function process(CodeCoverage $coverage, string $target): void
|
||||
{
|
||||
$target = $this->directory($target);
|
||||
$report = $coverage->getReport();
|
||||
$date = date('D M j G:i:s T Y');
|
||||
$target = $this->directory($target);
|
||||
$report = $coverage->getReport();
|
||||
$date = date('D M j G:i:s T Y');
|
||||
$hasBranchCoverage = $coverage->getData(true)->functionCoverage() !== [];
|
||||
|
||||
$dashboard = new Dashboard(
|
||||
$this->templatePath,
|
||||
$this->generator,
|
||||
$date,
|
||||
$this->thresholds,
|
||||
$coverage->collectsBranchAndPathCoverage(),
|
||||
$hasBranchCoverage,
|
||||
);
|
||||
|
||||
$directory = new Directory(
|
||||
@@ -58,7 +59,7 @@ final class Facade
|
||||
$this->generator,
|
||||
$date,
|
||||
$this->thresholds,
|
||||
$coverage->collectsBranchAndPathCoverage(),
|
||||
$hasBranchCoverage,
|
||||
);
|
||||
|
||||
$file = new File(
|
||||
@@ -66,7 +67,7 @@ final class Facade
|
||||
$this->generator,
|
||||
$date,
|
||||
$this->thresholds,
|
||||
$coverage->collectsBranchAndPathCoverage(),
|
||||
$hasBranchCoverage,
|
||||
);
|
||||
|
||||
$directory->render($report, $target . 'index.html');
|
||||
@@ -97,8 +98,8 @@ final class Facade
|
||||
{
|
||||
$dir = $this->directory($target . '_css');
|
||||
|
||||
copy($this->templatePath . 'css/billboard.min.css', $dir . 'billboard.min.css');
|
||||
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->customCssFile->path(), $dir . 'custom.css');
|
||||
copy($this->templatePath . 'css/octicons.css', $dir . 'octicons.css');
|
||||
|
||||
@@ -107,10 +108,9 @@ final class Facade
|
||||
copy($this->templatePath . 'icons/file-directory.svg', $dir . 'file-directory.svg');
|
||||
|
||||
$dir = $this->directory($target . '_js');
|
||||
copy($this->templatePath . 'js/billboard.pkgd.min.js', $dir . 'billboard.pkgd.min.js');
|
||||
copy($this->templatePath . 'js/bootstrap.bundle.min.js', $dir . 'bootstrap.bundle.min.js');
|
||||
copy($this->templatePath . 'js/d3.min.js', $dir . 'd3.min.js');
|
||||
copy($this->templatePath . 'js/jquery.min.js', $dir . 'jquery.min.js');
|
||||
copy($this->templatePath . 'js/nv.d3.min.js', $dir . 'nv.d3.min.js');
|
||||
copy($this->templatePath . 'js/file.js', $dir . 'file.js');
|
||||
}
|
||||
|
||||
@@ -130,12 +130,14 @@ final class Facade
|
||||
|
||||
try {
|
||||
$template->renderTo($this->directory($target . '_css') . 'style.css');
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (Exception $e) {
|
||||
throw new FileCouldNotBeWrittenException(
|
||||
$e->getMessage(),
|
||||
$e->getCode(),
|
||||
$e,
|
||||
);
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@ abstract class Renderer
|
||||
$this->hasBranchCoverage = $hasBranchCoverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<non-empty-string, float|int|string> $data
|
||||
*/
|
||||
protected function renderItemTemplate(Template $template, array $data): string
|
||||
{
|
||||
$numSeparator = ' / ';
|
||||
@@ -171,8 +174,8 @@ abstract class Renderer
|
||||
'version' => $this->version,
|
||||
'runtime' => $this->runtimeString(),
|
||||
'generator' => $this->generator,
|
||||
'low_upper_bound' => $this->thresholds->lowUpperBound(),
|
||||
'high_lower_bound' => $this->thresholds->highLowerBound(),
|
||||
'low_upper_bound' => (string) $this->thresholds->lowUpperBound(),
|
||||
'high_lower_bound' => (string) $this->thresholds->highLowerBound(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+66
-33
@@ -10,21 +10,27 @@
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Html;
|
||||
|
||||
use function array_values;
|
||||
use function arsort;
|
||||
use function asort;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function floor;
|
||||
use function json_encode;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function uasort;
|
||||
use function usort;
|
||||
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
|
||||
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
|
||||
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
|
||||
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
|
||||
use SebastianBergmann\Template\Exception;
|
||||
use SebastianBergmann\Template\Template;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ProcessedClassType from FileNode
|
||||
* @phpstan-import-type ProcessedTraitType from FileNode
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Dashboard extends Renderer
|
||||
@@ -81,7 +87,9 @@ final class Dashboard extends Renderer
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data for the Class/Method Complexity charts.
|
||||
* @param array<string, ProcessedClassType|ProcessedTraitType> $classes
|
||||
*
|
||||
* @return array{class: non-empty-string, method: non-empty-string}
|
||||
*/
|
||||
private function complexity(array $classes, string $baseLink): array
|
||||
{
|
||||
@@ -96,33 +104,39 @@ final class Dashboard extends Renderer
|
||||
$result['method'][] = [
|
||||
$method['coverage'],
|
||||
$method['ccn'],
|
||||
sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
str_replace($baseLink, '', $method['link']),
|
||||
$methodName,
|
||||
),
|
||||
str_replace($baseLink, '', $method['link']),
|
||||
$methodName,
|
||||
$method['crap'],
|
||||
];
|
||||
}
|
||||
|
||||
$result['class'][] = [
|
||||
$class['coverage'],
|
||||
$class['ccn'],
|
||||
sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
str_replace($baseLink, '', $class['link']),
|
||||
$className,
|
||||
),
|
||||
str_replace($baseLink, '', $class['link']),
|
||||
$className,
|
||||
$class['crap'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => json_encode($result['class']),
|
||||
'method' => json_encode($result['method']),
|
||||
];
|
||||
usort($result['class'], static fn (mixed $a, mixed $b) => ($a[0] <=> $b[0]));
|
||||
usort($result['method'], static fn (mixed $a, mixed $b) => ($a[0] <=> $b[0]));
|
||||
|
||||
$class = json_encode($result['class']);
|
||||
|
||||
assert($class !== false);
|
||||
|
||||
$method = json_encode($result['method']);
|
||||
|
||||
assert($method !== false);
|
||||
|
||||
return ['class' => $class, 'method' => $method];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data for the Class / Method Coverage Distribution chart.
|
||||
* @param array<string, ProcessedClassType|ProcessedTraitType> $classes
|
||||
*
|
||||
* @return array{class: non-empty-string, method: non-empty-string}
|
||||
*/
|
||||
private function coverageDistribution(array $classes): array
|
||||
{
|
||||
@@ -181,14 +195,21 @@ final class Dashboard extends Renderer
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'class' => json_encode(array_values($result['class'])),
|
||||
'method' => json_encode(array_values($result['method'])),
|
||||
];
|
||||
$class = json_encode(array_values($result['class']));
|
||||
|
||||
assert($class !== false);
|
||||
|
||||
$method = json_encode(array_values($result['method']));
|
||||
|
||||
assert($method !== false);
|
||||
|
||||
return ['class' => $class, 'method' => $method];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the classes / methods with insufficient coverage.
|
||||
* @param array<string, ProcessedClassType|ProcessedTraitType> $classes
|
||||
*
|
||||
* @return array{class: string, method: string}
|
||||
*/
|
||||
private function insufficientCoverage(array $classes, string $baseLink): array
|
||||
{
|
||||
@@ -242,7 +263,9 @@ final class Dashboard extends Renderer
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the project risks according to the CRAP index.
|
||||
* @param array<string, ProcessedClassType|ProcessedTraitType> $classes
|
||||
*
|
||||
* @return array{class: string, method: string}
|
||||
*/
|
||||
private function projectRisks(array $classes, string $baseLink): array
|
||||
{
|
||||
@@ -259,37 +282,47 @@ final class Dashboard extends Renderer
|
||||
$key = $className . '::' . $methodName;
|
||||
}
|
||||
|
||||
$methodRisks[$key] = $method['crap'];
|
||||
$methodRisks[$key] = $method;
|
||||
}
|
||||
}
|
||||
|
||||
if ($class['coverage'] < $this->thresholds->highLowerBound() &&
|
||||
$class['ccn'] > count($class['methods'])) {
|
||||
$classRisks[$className] = $class['crap'];
|
||||
$classRisks[$className] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
arsort($classRisks);
|
||||
arsort($methodRisks);
|
||||
uasort($classRisks, static function (array $a, array $b)
|
||||
{
|
||||
return ((int) ($a['crap']) <=> (int) ($b['crap'])) * -1;
|
||||
});
|
||||
uasort($methodRisks, static function (array $a, array $b)
|
||||
{
|
||||
return ((int) ($a['crap']) <=> (int) ($b['crap'])) * -1;
|
||||
});
|
||||
|
||||
foreach ($classRisks as $className => $crap) {
|
||||
foreach ($classRisks as $className => $class) {
|
||||
$result['class'] .= sprintf(
|
||||
' <tr><td><a href="%s">%s</a></td><td class="text-right">%d</td></tr>' . "\n",
|
||||
' <tr><td><a href="%s">%s</a></td><td class="text-right">%.1f%%</td><td class="text-right">%d</td><td class="text-right">%d</td></tr>' . "\n",
|
||||
str_replace($baseLink, '', $classes[$className]['link']),
|
||||
$className,
|
||||
$crap,
|
||||
$class['coverage'],
|
||||
$class['ccn'],
|
||||
$class['crap'],
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($methodRisks as $methodName => $crap) {
|
||||
foreach ($methodRisks as $methodName => $methodVals) {
|
||||
[$class, $method] = explode('::', $methodName);
|
||||
|
||||
$result['method'] .= sprintf(
|
||||
' <tr><td><a href="%s"><abbr title="%s">%s</abbr></a></td><td class="text-right">%d</td></tr>' . "\n",
|
||||
' <tr><td><a href="%s"><abbr title="%s">%s</abbr></a></td><td class="text-right">%.1f%%</td><td class="text-right">%d</td><td class="text-right">%d</td></tr>' . "\n",
|
||||
str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']),
|
||||
$methodName,
|
||||
$method,
|
||||
$crap,
|
||||
$methodVals['coverage'],
|
||||
$methodVals['ccn'],
|
||||
$methodVals['crap'],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,11 @@ use SebastianBergmann\Template\Exception;
|
||||
use SebastianBergmann\Template\Template;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ProcessedClassType from FileNode
|
||||
* @phpstan-import-type ProcessedTraitType from FileNode
|
||||
* @phpstan-import-type ProcessedMethodType from FileNode
|
||||
* @phpstan-import-type ProcessedFunctionType from FileNode
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class File extends Renderer
|
||||
@@ -116,7 +121,7 @@ final class File extends Renderer
|
||||
/**
|
||||
* @var array<int,true>
|
||||
*/
|
||||
private const KEYWORD_TOKENS = [
|
||||
private const array KEYWORD_TOKENS = [
|
||||
T_ABSTRACT => true,
|
||||
T_ARRAY => true,
|
||||
T_AS => true,
|
||||
@@ -186,8 +191,13 @@ final class File extends Renderer
|
||||
T_YIELD => true,
|
||||
T_YIELD_FROM => true,
|
||||
];
|
||||
|
||||
private const int HTML_SPECIAL_CHARS_FLAGS = ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE;
|
||||
|
||||
/**
|
||||
* @var array<non-empty-string, list<string>>
|
||||
*/
|
||||
private static array $formattedSourceCache = [];
|
||||
private int $htmlSpecialCharsFlags = ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE;
|
||||
|
||||
public function render(FileNode $node, string $file): void
|
||||
{
|
||||
@@ -315,11 +325,14 @@ final class File extends Renderer
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, ProcessedClassType|ProcessedTraitType> $items
|
||||
*/
|
||||
private function renderTraitOrClassItems(array $items, Template $template, Template $methodItemTemplate): string
|
||||
{
|
||||
$buffer = '';
|
||||
|
||||
if (empty($items)) {
|
||||
if ($items === []) {
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
@@ -419,9 +432,12 @@ final class File extends Renderer
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, ProcessedFunctionType> $functions
|
||||
*/
|
||||
private function renderFunctionItems(array $functions, Template $template): string
|
||||
{
|
||||
if (empty($functions)) {
|
||||
if ($functions === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -437,6 +453,9 @@ final class File extends Renderer
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProcessedFunctionType|ProcessedMethodType $item
|
||||
*/
|
||||
private function renderFunctionOrMethodItem(Template $template, array $item, string $indent = ''): string
|
||||
{
|
||||
$numMethods = 0;
|
||||
@@ -477,7 +496,7 @@ final class File extends Renderer
|
||||
'%s<a href="#%d"><abbr title="%s">%s</abbr></a>',
|
||||
$indent,
|
||||
$item['startLine'],
|
||||
htmlspecialchars($item['signature'], $this->htmlSpecialCharsFlags),
|
||||
htmlspecialchars($item['signature'], self::HTML_SPECIAL_CHARS_FLAGS),
|
||||
$item['functionName'] ?? $item['methodName'],
|
||||
),
|
||||
'numMethods' => $numMethods,
|
||||
@@ -518,7 +537,7 @@ final class File extends Renderer
|
||||
$popoverTitle = '';
|
||||
|
||||
if (array_key_exists($i, $coverageData)) {
|
||||
$numTests = ($coverageData[$i] ? count($coverageData[$i]) : 0);
|
||||
$numTests = ($coverageData[$i] !== null ? count($coverageData[$i]) : 0);
|
||||
|
||||
if ($coverageData[$i] === null) {
|
||||
$trClass = 'warning';
|
||||
@@ -551,11 +570,11 @@ final class File extends Renderer
|
||||
|
||||
$popover = '';
|
||||
|
||||
if (!empty($popoverTitle)) {
|
||||
if ($popoverTitle !== '') {
|
||||
$popover = sprintf(
|
||||
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
|
||||
$popoverTitle,
|
||||
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
|
||||
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -580,7 +599,6 @@ final class File extends Renderer
|
||||
|
||||
$lineData = [];
|
||||
|
||||
/** @var int $line */
|
||||
foreach (array_keys($codeLines) as $line) {
|
||||
$lineData[$line + 1] = [
|
||||
'includedInBranches' => 0,
|
||||
@@ -642,7 +660,7 @@ final class File extends Renderer
|
||||
$popover = sprintf(
|
||||
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
|
||||
$popoverTitle,
|
||||
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
|
||||
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -667,7 +685,6 @@ final class File extends Renderer
|
||||
|
||||
$lineData = [];
|
||||
|
||||
/** @var int $line */
|
||||
foreach (array_keys($codeLines) as $line) {
|
||||
$lineData[$line + 1] = [
|
||||
'includedInPaths' => [],
|
||||
@@ -732,7 +749,7 @@ final class File extends Renderer
|
||||
$popover = sprintf(
|
||||
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
|
||||
$popoverTitle,
|
||||
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
|
||||
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -769,7 +786,7 @@ final class File extends Renderer
|
||||
}
|
||||
|
||||
if ($branchStructure !== '') { // don't show empty branches
|
||||
$branches .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, $this->htmlSpecialCharsFlags) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
|
||||
$branches .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, self::HTML_SPECIAL_CHARS_FLAGS) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
|
||||
$branches .= $branchStructure;
|
||||
}
|
||||
}
|
||||
@@ -779,6 +796,9 @@ final class File extends Renderer
|
||||
return $branchesTemplate->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $codeLines
|
||||
*/
|
||||
private function renderBranchLines(array $branch, array $codeLines, array $testData): string
|
||||
{
|
||||
$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');
|
||||
@@ -826,11 +846,11 @@ final class File extends Renderer
|
||||
|
||||
$popover = '';
|
||||
|
||||
if (!empty($popoverTitle)) {
|
||||
if ($popoverTitle !== '') {
|
||||
$popover = sprintf(
|
||||
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
|
||||
$popoverTitle,
|
||||
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
|
||||
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -875,7 +895,7 @@ final class File extends Renderer
|
||||
}
|
||||
|
||||
if ($pathStructure !== '') {
|
||||
$paths .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, $this->htmlSpecialCharsFlags) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
|
||||
$paths .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, self::HTML_SPECIAL_CHARS_FLAGS) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
|
||||
$paths .= $pathStructure;
|
||||
}
|
||||
}
|
||||
@@ -885,6 +905,9 @@ final class File extends Renderer
|
||||
return $pathsTemplate->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $codeLines
|
||||
*/
|
||||
private function renderPathLines(array $path, array $branches, array $codeLines, array $testData): string
|
||||
{
|
||||
$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');
|
||||
@@ -941,11 +964,11 @@ final class File extends Renderer
|
||||
|
||||
$popover = '';
|
||||
|
||||
if (!empty($popoverTitle)) {
|
||||
if ($popoverTitle !== '') {
|
||||
$popover = sprintf(
|
||||
' data-bs-title="%s" data-bs-content="%s" data-bs-placement="top" data-bs-html="true"',
|
||||
$popoverTitle,
|
||||
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags),
|
||||
htmlspecialchars($popoverContent, self::HTML_SPECIAL_CHARS_FLAGS),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -966,7 +989,7 @@ final class File extends Renderer
|
||||
{
|
||||
$template->setVar(
|
||||
[
|
||||
'lineNumber' => $lineNumber,
|
||||
'lineNumber' => (string) $lineNumber,
|
||||
'lineContent' => $lineContent,
|
||||
'class' => $class,
|
||||
'popover' => $popover,
|
||||
@@ -976,6 +999,11 @@ final class File extends Renderer
|
||||
return $template->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $file
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function loadFile(string $file): array
|
||||
{
|
||||
if (isset(self::$formattedSourceCache[$file])) {
|
||||
@@ -996,14 +1024,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, self::HTML_SPECIAL_CHARS_FLAGS),
|
||||
);
|
||||
|
||||
$stringFlag = !$stringFlag;
|
||||
} else {
|
||||
$result[$i] .= sprintf(
|
||||
'<span class="keyword">%s</span>',
|
||||
htmlspecialchars($token, $this->htmlSpecialCharsFlags),
|
||||
htmlspecialchars($token, self::HTML_SPECIAL_CHARS_FLAGS),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1015,7 +1043,7 @@ final class File extends Renderer
|
||||
$value = str_replace(
|
||||
["\t", ' '],
|
||||
[' ', ' '],
|
||||
htmlspecialchars($value, $this->htmlSpecialCharsFlags),
|
||||
htmlspecialchars($value, self::HTML_SPECIAL_CHARS_FLAGS),
|
||||
);
|
||||
|
||||
if ($value === "\n") {
|
||||
@@ -1114,7 +1142,7 @@ final class File extends Renderer
|
||||
return sprintf(
|
||||
'<li%s>%s</li>',
|
||||
$testCSS,
|
||||
htmlspecialchars($test, $this->htmlSpecialCharsFlags),
|
||||
htmlspecialchars($test, self::HTML_SPECIAL_CHARS_FLAGS),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
.nvd3 .nv-axis{pointer-events:none;opacity:1}.nvd3 .nv-axis path{fill:none;stroke:#000;stroke-opacity:.75;shape-rendering:crispEdges}.nvd3 .nv-axis path.domain{stroke-opacity:.75}.nvd3 .nv-axis.nv-x path.domain{stroke-opacity:0}.nvd3 .nv-axis line{fill:none;stroke:#e5e5e5;shape-rendering:crispEdges}.nvd3 .nv-axis .zero line,.nvd3 .nv-axis line.zero{stroke-opacity:.75}.nvd3 .nv-axis .nv-axisMaxMin text{font-weight:700}.nvd3 .x .nv-axis .nv-axisMaxMin text,.nvd3 .x2 .nv-axis .nv-axisMaxMin text,.nvd3 .x3 .nv-axis .nv-axisMaxMin text{text-anchor:middle}.nvd3 .nv-axis.nv-disabled{opacity:0}.nvd3 .nv-bars rect{fill-opacity:.75;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-bars rect.hover{fill-opacity:1}.nvd3 .nv-bars .hover rect{fill:#add8e6}.nvd3 .nv-bars text{fill:rgba(0,0,0,0)}.nvd3 .nv-bars .hover text{fill:rgba(0,0,0,1)}.nvd3 .nv-multibar .nv-groups rect,.nvd3 .nv-multibarHorizontal .nv-groups rect,.nvd3 .nv-discretebar .nv-groups rect{stroke-opacity:0;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-multibar .nv-groups rect:hover,.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,.nvd3 .nv-candlestickBar .nv-ticks rect:hover,.nvd3 .nv-discretebar .nv-groups rect:hover{fill-opacity:1}.nvd3 .nv-discretebar .nv-groups text,.nvd3 .nv-multibarHorizontal .nv-groups text{font-weight:700;fill:rgba(0,0,0,1);stroke:rgba(0,0,0,0)}.nvd3 .nv-boxplot circle{fill-opacity:.5}.nvd3 .nv-boxplot circle:hover{fill-opacity:1}.nvd3 .nv-boxplot rect:hover{fill-opacity:1}.nvd3 line.nv-boxplot-median{stroke:#000}.nv-boxplot-tick:hover{stroke-width:2.5px}.nvd3.nv-bullet{font:10px sans-serif}.nvd3.nv-bullet .nv-measure{fill-opacity:.8}.nvd3.nv-bullet .nv-measure:hover{fill-opacity:1}.nvd3.nv-bullet .nv-marker{stroke:#000;stroke-width:2px}.nvd3.nv-bullet .nv-markerTriangle{stroke:#000;fill:#fff;stroke-width:1.5px}.nvd3.nv-bullet .nv-tick line{stroke:#666;stroke-width:.5px}.nvd3.nv-bullet .nv-range.nv-s0{fill:#eee}.nvd3.nv-bullet .nv-range.nv-s1{fill:#ddd}.nvd3.nv-bullet .nv-range.nv-s2{fill:#ccc}.nvd3.nv-bullet .nv-title{font-size:14px;font-weight:700}.nvd3.nv-bullet .nv-subtitle{fill:#999}.nvd3.nv-bullet .nv-range{fill:#bababa;fill-opacity:.4}.nvd3.nv-bullet .nv-range:hover{fill-opacity:.7}.nvd3.nv-candlestickBar .nv-ticks .nv-tick{stroke-width:1px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.hover{stroke-width:2px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.positive rect{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.negative rect{stroke:#d62728;fill:#d62728}.with-transitions .nv-candlestickBar .nv-ticks .nv-tick{transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-candlestickBar .nv-ticks line{stroke:#333}.nvd3 .nv-legend .nv-disabled rect{}.nvd3 .nv-check-box .nv-box{fill-opacity:0;stroke-width:2}.nvd3 .nv-check-box .nv-check{fill-opacity:0;stroke-width:4}.nvd3 .nv-series.nv-disabled .nv-check-box .nv-check{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check{opacity:0}.nvd3.nv-linePlusBar .nv-bar rect{fill-opacity:.75}.nvd3.nv-linePlusBar .nv-bar rect:hover{fill-opacity:1}.nvd3 .nv-groups path.nv-line{fill:none}.nvd3 .nv-groups path.nv-area{stroke:none}.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point{fill-opacity:0;stroke-opacity:0}.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point{fill-opacity:.5!important;stroke-opacity:.5!important}.with-transitions .nvd3 .nv-groups .nv-point{transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-moz-transition:stroke-width 250ms linear,stroke-opacity 250ms linear;-webkit-transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-scatter .nv-groups .nv-point.hover,.nvd3 .nv-groups .nv-point.hover{stroke-width:7px;fill-opacity:.95!important;stroke-opacity:.95!important}.nvd3 .nv-point-paths path{stroke:#aaa;stroke-opacity:0;fill:#eee;fill-opacity:0}.nvd3 .nv-indexLine{cursor:ew-resize}svg.nvd3-svg{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-ms-user-select:none;-moz-user-select:none;user-select:none;display:block;width:100%;height:100%}.nvtooltip.with-3d-shadow,.with-3d-shadow .nvtooltip{-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nvd3 text{font:400 12px Arial}.nvd3 .title{font:700 14px Arial}.nvd3 .nv-background{fill:#fff;fill-opacity:0}.nvd3.nv-noData{font-size:18px;font-weight:700}.nv-brush .extent{fill-opacity:.125;shape-rendering:crispEdges}.nv-brush .resize path{fill:#eee;stroke:#666}.nvd3 .nv-legend .nv-series{cursor:pointer}.nvd3 .nv-legend .nv-disabled circle{fill-opacity:0}.nvd3 .nv-brusLine truncated
|
||||
+73
-10
@@ -1,13 +1,76 @@
|
||||
|
||||
:root {
|
||||
--phpunit-breadcrumbs: var(--bs-gray-200);
|
||||
--phpunit-success-bar: #28a745;
|
||||
--phpunit-success-high: {{success-high}};
|
||||
--phpunit-success-medium: {{success-medium}};
|
||||
--phpunit-success-low: {{success-low}};
|
||||
--phpunit-warning: {{warning}};
|
||||
--phpunit-warning-bar: #ffc107;
|
||||
--phpunit-danger: {{danger}};
|
||||
--phpunit-danger-bar: #dc3545;
|
||||
/* Implementing an auto-selection of dark/light theme via: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark */
|
||||
color-scheme: light dark;
|
||||
|
||||
/* PHPUnit ligh/dark colors */
|
||||
--phpunit-breadcrumbs: light-dark(var(--bs-gray-200), var(--bs-gray-800));
|
||||
--phpunit-success-bar: light-dark(#28a745 ,#1f8135);
|
||||
--phpunit-success-high: light-dark(#99cb84, #3d5c4e);
|
||||
--phpunit-success-medium: light-dark(#c3e3b5,#3c6051);
|
||||
--phpunit-success-low: light-dark(#dff0d8, #2d4431);
|
||||
--phpunit-warning: light-dark(#fcf8e3, #3e3408);
|
||||
--phpunit-warning-bar: light-dark(#ffc107 ,#c19406);
|
||||
--phpunit-danger: light-dark(#f2dede, #42221e);
|
||||
--phpunit-danger-bar: light-dark(#dc3545, #a62633);
|
||||
|
||||
/* Bootstrap v5.3 default colors (ligth, dark) */
|
||||
--bs-body-bg-rgb: light-dark((255, 255, 255), (33, 37, 41));
|
||||
--bs-body-bg: light-dark(#fff, #212529);
|
||||
--bs-body-color-rgb: light-dark(33, 37, 41, 222, 226, 230);
|
||||
--bs-body-color: light-dark(#212529, #dee2e6);
|
||||
--bs-border-color-translucent: light-dark(rgba(0, 0, 0, 0.175), rgba(255, 255, 255, 0.15));
|
||||
--bs-border-color: light-dark(#dee2e6, #495057);
|
||||
--bs-code-color: light-dark(#d63384, #e685b5);
|
||||
--bs-danger-bg-subtle: light-dark(#f8d7da, #2c0b0e);
|
||||
--bs-danger-border-subtle: light-dark(#f1aeb5, #842029);
|
||||
--bs-danger-text-emphasis: light-dark(#58151c, #ea868f);
|
||||
--bs-dark-bg-subtle: light-dark(#ced4da, #1a1d20);
|
||||
--bs-dark-border-subtle: light-dark(#adb5bd, #343a40);
|
||||
--bs-dark-text-emphasis: light-dark(#495057, #dee2e6);
|
||||
--bs-emphasis-color-rgb: light-dark((0, 0, 0), (255, 255, 255));
|
||||
--bs-emphasis-color: light-dark(#000, #fff);
|
||||
--bs-form-invalid-border-color: light-dark(#dc3545, #ea868f);
|
||||
--bs-form-invalid-color: light-dark(#dc3545, #ea868f);
|
||||
--bs-form-valid-border-color: light-dark(#198754, #75b798);
|
||||
--bs-form-valid-color: light-dark(#198754, #75b798);
|
||||
--bs-highlight-bg: light-dark(#fff3cd, #664d03);
|
||||
--bs-highlight-color: light-dark(#212529, #dee2e6);
|
||||
--bs-info-bg-subtle: light-dark(#cff4fc, #032830);
|
||||
--bs-info-border-subtle: light-dark(#9eeaf9, #087990);
|
||||
--bs-info-text-emphasis: light-dark(#055160, #6edff6);
|
||||
--bs-light-bg-subtle: light-dark(#fcfcfd, #343a40);
|
||||
--bs-light-border-subtle: light-dark(#e9ecef, #495057);
|
||||
--bs-light-text-emphasis: light-dark(#495057, #f8f9fa);
|
||||
--bs-link-color-rgb: light-dark((13, 110, 253), (110, 168, 254));
|
||||
--bs-link-color: light-dark(#0d6efd, #6ea8fe);
|
||||
--bs-link-hover-color-rgb: light-dark((10, 88, 202), (139, 185, 254));
|
||||
--bs-link-hover-color: light-dark(#0a58ca, #8bb9fe);
|
||||
--bs-primary-bg-subtle: light-dark(#cfe2ff, #031633);
|
||||
--bs-primary-border-subtle: light-dark(#9ec5fe, #084298);
|
||||
--bs-primary-text-emphasis: light-dark(#052c65, #6ea8fe);
|
||||
--bs-secondary-bg-rgb: light-dark((233, 236, 239), (52, 58, 64));
|
||||
--bs-secondary-bg-subtle: light-dark(#e2e3e5, #161719);
|
||||
--bs-secondary-bg: light-dark(#e9ecef, #343a40);
|
||||
--bs-secondary-border-subtle: light-dark(#c4c8cb, #41464b);
|
||||
--bs-secondary-color-rgb: light-dark((33, 37, 41), (222, 226, 230));
|
||||
--bs-secondary-color: light-dark(rgba(33, 37, 41, 0.75), rgba(222, 226, 230, 0.75));
|
||||
--bs-secondary-text-emphasis: light-dark(#2b2f32, #a7acb1);
|
||||
--bs-success-bg-subtle: light-dark(#d1e7dd, #051b11);
|
||||
--bs-success-border-subtle: light-dark(#a3cfbb, #0f5132);
|
||||
--bs-success-text-emphasis: light-dark(#0a3622, #75b798);
|
||||
--bs-tertiary-bg-rgb: light-dark(248, 249, 250, 43, 48, 53);
|
||||
--bs-tertiary-bg: light-dark(#f8f9fa, #2b3035);
|
||||
--bs-tertiary-color-rgb: light-dark((33, 37, 41), (222, 226, 230));
|
||||
--bs-tertiary-color: light-dark(rgba(33, 37, 41, 0.5), rgba(222, 226, 230, 0.5));
|
||||
--bs-warning-bg-subtle: light-dark(#fff3cd, #332701);
|
||||
--bs-warning-border-subtle: light-dark(#ffe69c, #997404);
|
||||
--bs-warning-text-emphasis: light-dark(#664d03, #ffda6a);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* Invert icon's colors on dark mode to improve readability */
|
||||
img.octicon { filter: invert(1); }
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -198,4 +261,4 @@ table#code td:first-of-type a {
|
||||
|
||||
.progress-bar.bg-danger {
|
||||
background-color: var(--phpunit-danger-bar) !important;
|
||||
}
|
||||
}
|
||||
+162
-144
@@ -5,7 +5,7 @@
|
||||
<title>Dashboard for {{full_path}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
|
||||
<link href="{{path_to_root}}_css/nv.d3.min.css?v={{version}}" rel="stylesheet" type="text/css">
|
||||
<link href="{{path_to_root}}_css/billboard.min.css?v={{version}}" rel="stylesheet" type="text/css">
|
||||
<link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
|
||||
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Coverage Distribution</h3>
|
||||
<div id="classCoverageDistribution" style="height: 300px;">
|
||||
<div id="classesCoverageDistribution" style="height: 300px;">
|
||||
<svg></svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Insufficient Coverage</h3>
|
||||
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
|
||||
<div class="scrollbox">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -61,12 +61,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h3>Project Risks</h3>
|
||||
<h3 style="margin-top: 2rem;">Project Risks</h3>
|
||||
<div class="scrollbox">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th class="text-right">Coverage</th>
|
||||
<th class="text-right">Complexity</th>
|
||||
<th class="text-right"><abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -79,13 +81,13 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>Methods</h2>
|
||||
<h2 style="margin-top: 3rem;">Methods</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Coverage Distribution</h3>
|
||||
<div id="methodCoverageDistribution" style="height: 300px;">
|
||||
<div id="methodsCoverageDistribution" style="height: 300px;">
|
||||
<svg></svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,7 +100,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Insufficient Coverage</h3>
|
||||
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
|
||||
<div class="scrollbox">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -114,12 +116,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h3>Project Risks</h3>
|
||||
<h3 style="margin-top: 2rem;">Project Risks</h3>
|
||||
<div class="scrollbox">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th class="text-right">Coverage</th>
|
||||
<th class="text-right">Complexity</th>
|
||||
<th class="text-right"><abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -137,145 +141,159 @@
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="{{path_to_root}}_js/jquery.min.js?v={{version}}" type="text/javascript"></script>
|
||||
<script src="{{path_to_root}}_js/d3.min.js?v={{version}}" type="text/javascript"></script>
|
||||
<script src="{{path_to_root}}_js/nv.d3.min.js?v={{version}}" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
nv.addGraph(function() {
|
||||
var chart = nv.models.multiBarChart();
|
||||
chart.tooltips(false)
|
||||
.showControls(false)
|
||||
.showLegend(false)
|
||||
.reduceXTicks(false)
|
||||
.staggerLabels(true)
|
||||
.yAxis.tickFormat(d3.format('d'));
|
||||
|
||||
d3.select('#classCoverageDistribution svg')
|
||||
.datum(getCoverageDistributionData({{class_coverage_distribution}}, "Class Coverage"))
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
|
||||
return chart;
|
||||
<script src="{{path_to_root}}_js/billboard.pkgd.min.js?v={{version}}" type="text/javascript"></script>
|
||||
<script type="application/json" id="complexity_class">{{complexity_class}}</script>
|
||||
<script type="application/json" id="complexity_method">{{complexity_method}}</script>
|
||||
<script type="text/javascript" defer>
|
||||
const barLabels = [
|
||||
'0%',
|
||||
'0-10%',
|
||||
'10-20%',
|
||||
'20-30%',
|
||||
'30-40%',
|
||||
'40-50%',
|
||||
'50-60%',
|
||||
'60-70%',
|
||||
'70-80%',
|
||||
'80-90%',
|
||||
'90-100%',
|
||||
'100%'
|
||||
];
|
||||
const barConfig = (name, fullName, values) => ({
|
||||
axis: {
|
||||
x: {
|
||||
type: "category",
|
||||
categories: barLabels,
|
||||
},
|
||||
y: {
|
||||
label: {
|
||||
text: `#${name}`,
|
||||
position: 'outer-top',
|
||||
},
|
||||
},
|
||||
},
|
||||
bindto: `#${name}CoverageDistribution`,
|
||||
data: {
|
||||
columns: [
|
||||
[fullName].concat(values),
|
||||
],
|
||||
colors: {
|
||||
[fullName]: "rgba(69, 114, 167, 0.75)",
|
||||
},
|
||||
type: "bar",
|
||||
},
|
||||
bar: {
|
||||
width: {
|
||||
ratio: 0.9,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
focus: {
|
||||
show: false,
|
||||
},
|
||||
x: {
|
||||
show: true,
|
||||
},
|
||||
y: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
contents: function (data) {
|
||||
return `<table class="bb-tooltip"><tbody>
|
||||
<tr><th colspan="2">Coverage ${barLabels[data[0].x]}</th></tr>
|
||||
<tr><td class="value">${data[0].value} ${name}</td></tr>
|
||||
</tbody></table>`;
|
||||
},
|
||||
grouped: false,
|
||||
},
|
||||
});
|
||||
bb.generate(
|
||||
barConfig('classes', "Class Coverage", {{class_coverage_distribution}})
|
||||
);
|
||||
bb.generate(
|
||||
barConfig('methods', "Method Coverage", {{method_coverage_distribution}})
|
||||
);
|
||||
|
||||
nv.addGraph(function() {
|
||||
var chart = nv.models.multiBarChart();
|
||||
chart.tooltips(false)
|
||||
.showControls(false)
|
||||
.showLegend(false)
|
||||
.reduceXTicks(false)
|
||||
.staggerLabels(true)
|
||||
.yAxis.tickFormat(d3.format('d'));
|
||||
|
||||
d3.select('#methodCoverageDistribution svg')
|
||||
.datum(getCoverageDistributionData({{method_coverage_distribution}}, "Method Coverage"))
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
|
||||
return chart;
|
||||
const scatterConfig = (name, complexityData) => ({
|
||||
axis: {
|
||||
x: {
|
||||
label: {
|
||||
text: 'Code Coverage (in percent)',
|
||||
position: 'outer-right',
|
||||
},
|
||||
tick: {
|
||||
values: [0, 20, 40, 60, 80, 100],
|
||||
},
|
||||
},
|
||||
y: {
|
||||
label: {
|
||||
text: 'Cyclomatic Complexity',
|
||||
position: 'outer-top',
|
||||
},
|
||||
min: 0,
|
||||
padding: {
|
||||
bottom: 0,
|
||||
top: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
bindto: `#${name}Complexity`,
|
||||
data: {
|
||||
columns: [
|
||||
["complexity_x"].concat(complexityData.map(d => d[0])),
|
||||
["complexity"].concat(complexityData.map(d => d[1])),
|
||||
],
|
||||
onclick: function(data, element) {
|
||||
window.location = complexityData[data.index][2];
|
||||
},
|
||||
type: "scatter",
|
||||
xs: {
|
||||
"complexity": "complexity_x",
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
focus: {
|
||||
show: true,
|
||||
y: true,
|
||||
},
|
||||
x: {
|
||||
show: true,
|
||||
},
|
||||
y: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
contents: function (data) {
|
||||
const coverage = Math.round(data[0].x);
|
||||
const complexity = data[0].value;
|
||||
const targetName = complexityData[data[0].index][3];
|
||||
const crap = complexityData[data[0].index][4];
|
||||
return `<table class="bb-tooltip"><tbody>
|
||||
<tr><th colspan="2">${targetName}</th></tr>
|
||||
<tr class="bb-tooltip-name-complexity"><td>Coverage</td><td class="name">${coverage}%</td></tr>
|
||||
<tr class="bb-tooltip-name-complexity"><td>Complexity</td><td class="value">${complexity}</td></tr>
|
||||
<tr class="bb-tooltip-name-complexity"><td>Crap</td><td class="value">${crap}</td></tr>
|
||||
</tbody></table>`;
|
||||
},
|
||||
grouped: false,
|
||||
},
|
||||
});
|
||||
|
||||
function getCoverageDistributionData(data, label) {
|
||||
var labels = [
|
||||
'0%',
|
||||
'0-10%',
|
||||
'10-20%',
|
||||
'20-30%',
|
||||
'30-40%',
|
||||
'40-50%',
|
||||
'50-60%',
|
||||
'60-70%',
|
||||
'70-80%',
|
||||
'80-90%',
|
||||
'90-100%',
|
||||
'100%'
|
||||
];
|
||||
var values = [];
|
||||
$.each(labels, function(key) {
|
||||
values.push({x: labels[key], y: data[key]});
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
key: label,
|
||||
values: values,
|
||||
color: "#4572A7"
|
||||
}
|
||||
];
|
||||
}
|
||||
nv.addGraph(function() {
|
||||
var chart = nv.models.scatterChart()
|
||||
.showDistX(true)
|
||||
.showDistY(true)
|
||||
.showLegend(false)
|
||||
.forceX([0, 100]);
|
||||
chart.tooltipContent(function(graph) {
|
||||
return '<p>' + graph.point.class + '</p>';
|
||||
});
|
||||
|
||||
chart.xAxis.axisLabel('Code Coverage (in percent)');
|
||||
chart.yAxis.axisLabel('Cyclomatic Complexity');
|
||||
|
||||
d3.select('#classComplexity svg')
|
||||
.datum(getComplexityData({{complexity_class}}, 'Class Complexity'))
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
|
||||
return chart;
|
||||
});
|
||||
|
||||
nv.addGraph(function() {
|
||||
var chart = nv.models.scatterChart()
|
||||
.showDistX(true)
|
||||
.showDistY(true)
|
||||
.showLegend(false)
|
||||
.forceX([0, 100]);
|
||||
chart.tooltipContent(function(graph) {
|
||||
return '<p>' + graph.point.class + '</p>';
|
||||
});
|
||||
|
||||
chart.xAxis.axisLabel('Code Coverage (in percent)');
|
||||
chart.yAxis.axisLabel('Method Complexity');
|
||||
|
||||
d3.select('#methodComplexity svg')
|
||||
.datum(getComplexityData({{complexity_method}}, 'Method Complexity'))
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
|
||||
return chart;
|
||||
});
|
||||
|
||||
function getComplexityData(data, label) {
|
||||
var values = [];
|
||||
$.each(data, function(key) {
|
||||
var value = Math.round(data[key][0]*100) / 100;
|
||||
values.push({
|
||||
x: value,
|
||||
y: data[key][1],
|
||||
class: data[key][2],
|
||||
size: 0.05,
|
||||
shape: 'diamond'
|
||||
});
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
key: label,
|
||||
values: values,
|
||||
color: "#4572A7"
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
const classComplexityData = JSON.parse(document.getElementById('complexity_class').textContent);
|
||||
bb.generate(
|
||||
scatterConfig("class", classComplexityData)
|
||||
);
|
||||
const methodComplexityData = JSON.parse(document.getElementById('complexity_method').textContent);
|
||||
bb.generate(
|
||||
scatterConfig("method", methodComplexityData),
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+156
-144
@@ -5,7 +5,7 @@
|
||||
<title>Dashboard for {{full_path}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
|
||||
<link href="{{path_to_root}}_css/nv.d3.min.css?v={{version}}" rel="stylesheet" type="text/css">
|
||||
<link href="{{path_to_root}}_css/billboard.min.css?v={{version}}" rel="stylesheet" type="text/css">
|
||||
<link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
|
||||
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Coverage Distribution</h3>
|
||||
<div id="classCoverageDistribution" style="height: 300px;">
|
||||
<div id="classesCoverageDistribution" style="height: 300px;">
|
||||
<svg></svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Insufficient Coverage</h3>
|
||||
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
|
||||
<div class="scrollbox">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h3>Project Risks</h3>
|
||||
<h3 style="margin-top: 2rem;">Project Risks</h3>
|
||||
<div class="scrollbox">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -79,13 +79,13 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>Methods</h2>
|
||||
<h2 style="margin-top: 3rem;">Methods</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Coverage Distribution</h3>
|
||||
<div id="methodCoverageDistribution" style="height: 300px;">
|
||||
<div id="methodsCoverageDistribution" style="height: 300px;">
|
||||
<svg></svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Insufficient Coverage</h3>
|
||||
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
|
||||
<div class="scrollbox">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -114,7 +114,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h3>Project Risks</h3>
|
||||
<h3 style="margin-top: 2rem;">Project Risks</h3>
|
||||
<div class="scrollbox">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -137,145 +137,157 @@
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="{{path_to_root}}_js/jquery.min.js?v={{version}}" type="text/javascript"></script>
|
||||
<script src="{{path_to_root}}_js/d3.min.js?v={{version}}" type="text/javascript"></script>
|
||||
<script src="{{path_to_root}}_js/nv.d3.min.js?v={{version}}" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
nv.addGraph(function() {
|
||||
var chart = nv.models.multiBarChart();
|
||||
chart.tooltips(false)
|
||||
.showControls(false)
|
||||
.showLegend(false)
|
||||
.reduceXTicks(false)
|
||||
.staggerLabels(true)
|
||||
.yAxis.tickFormat(d3.format('d'));
|
||||
|
||||
d3.select('#classCoverageDistribution svg')
|
||||
.datum(getCoverageDistributionData({{class_coverage_distribution}}, "Class Coverage"))
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
|
||||
return chart;
|
||||
<script src="_js/billboard.pkgd.min.js?v={{version}}" type="text/javascript"></script>
|
||||
<script type="application/json" id="complexity_class">{{complexity_class}}</script>
|
||||
<script type="application/json" id="complexity_method">{{complexity_method}}</script>
|
||||
<script type="text/javascript" defer>
|
||||
const barLabels = [
|
||||
'0%',
|
||||
'0-10%',
|
||||
'10-20%',
|
||||
'20-30%',
|
||||
'30-40%',
|
||||
'40-50%',
|
||||
'50-60%',
|
||||
'60-70%',
|
||||
'70-80%',
|
||||
'80-90%',
|
||||
'90-100%',
|
||||
'100%'
|
||||
];
|
||||
const barConfig = (name, fullName, values) => ({
|
||||
axis: {
|
||||
x: {
|
||||
type: "category",
|
||||
categories: barLabels,
|
||||
},
|
||||
y: {
|
||||
label: {
|
||||
text: `#${name}`,
|
||||
position: 'outer-top',
|
||||
},
|
||||
},
|
||||
},
|
||||
bindto: `#${name}CoverageDistribution`,
|
||||
data: {
|
||||
columns: [
|
||||
[fullName].concat(values),
|
||||
],
|
||||
colors: {
|
||||
[fullName]: "rgba(69, 114, 167, 0.75)",
|
||||
},
|
||||
type: "bar",
|
||||
},
|
||||
bar: {
|
||||
width: {
|
||||
ratio: 0.9,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
focus: {
|
||||
show: false,
|
||||
},
|
||||
x: {
|
||||
show: true,
|
||||
},
|
||||
y: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
contents: function (data) {
|
||||
return `<table class="bb-tooltip"><tbody>
|
||||
<tr><th colspan="2">Coverage ${barLabels[data[0].x]}</th></tr>
|
||||
<tr><td class="value">${data[0].value} ${name}</td></tr>
|
||||
</tbody></table>`;
|
||||
},
|
||||
grouped: false,
|
||||
},
|
||||
});
|
||||
bb.generate(
|
||||
barConfig('classes', "Class Coverage", {{class_coverage_distribution}})
|
||||
);
|
||||
bb.generate(
|
||||
barConfig('methods', "Method Coverage", {{method_coverage_distribution}})
|
||||
);
|
||||
|
||||
nv.addGraph(function() {
|
||||
var chart = nv.models.multiBarChart();
|
||||
chart.tooltips(false)
|
||||
.showControls(false)
|
||||
.showLegend(false)
|
||||
.reduceXTicks(false)
|
||||
.staggerLabels(true)
|
||||
.yAxis.tickFormat(d3.format('d'));
|
||||
|
||||
d3.select('#methodCoverageDistribution svg')
|
||||
.datum(getCoverageDistributionData({{method_coverage_distribution}}, "Method Coverage"))
|
||||
.transition().duration(500).call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
|
||||
return chart;
|
||||
const scatterConfig = (name, complexityData) => ({
|
||||
axis: {
|
||||
x: {
|
||||
label: {
|
||||
text: 'Code Coverage (in percent)',
|
||||
position: 'outer-right',
|
||||
},
|
||||
tick: {
|
||||
values: [0, 20, 40, 60, 80, 100],
|
||||
},
|
||||
},
|
||||
y: {
|
||||
label: {
|
||||
text: 'Cyclomatic Complexity',
|
||||
position: 'outer-top',
|
||||
},
|
||||
min: 0,
|
||||
padding: {
|
||||
bottom: 0,
|
||||
top: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
bindto: `#${name}Complexity`,
|
||||
data: {
|
||||
columns: [
|
||||
["complexity_x"].concat(complexityData.map(d => d[0])),
|
||||
["complexity"].concat(complexityData.map(d => d[1])),
|
||||
],
|
||||
onclick: function(data, element) {
|
||||
window.location = complexityData[data.index][2];
|
||||
},
|
||||
type: "scatter",
|
||||
xs: {
|
||||
"complexity": "complexity_x",
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
focus: {
|
||||
show: true,
|
||||
y: true,
|
||||
},
|
||||
x: {
|
||||
show: true,
|
||||
},
|
||||
y: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
contents: function (data) {
|
||||
const coverage = Math.round(data[0].x);
|
||||
const complexity = data[0].value;
|
||||
const targetName = complexityData[data[0].index][3];
|
||||
return `<table class="bb-tooltip"><tbody>
|
||||
<tr><th colspan="2">${targetName}</th></tr>
|
||||
<tr class="bb-tooltip-name-complexity"><td>Coverage</td><td class="name">${coverage}%</td></tr>
|
||||
<tr class="bb-tooltip-name-complexity"><td>Complexity</td><td class="value">${complexity}</td></tr>
|
||||
</tbody></table>`;
|
||||
},
|
||||
grouped: false,
|
||||
},
|
||||
});
|
||||
|
||||
function getCoverageDistributionData(data, label) {
|
||||
var labels = [
|
||||
'0%',
|
||||
'0-10%',
|
||||
'10-20%',
|
||||
'20-30%',
|
||||
'30-40%',
|
||||
'40-50%',
|
||||
'50-60%',
|
||||
'60-70%',
|
||||
'70-80%',
|
||||
'80-90%',
|
||||
'90-100%',
|
||||
'100%'
|
||||
];
|
||||
var values = [];
|
||||
$.each(labels, function(key) {
|
||||
values.push({x: labels[key], y: data[key]});
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
key: label,
|
||||
values: values,
|
||||
color: "#4572A7"
|
||||
}
|
||||
];
|
||||
}
|
||||
nv.addGraph(function() {
|
||||
var chart = nv.models.scatterChart()
|
||||
.showDistX(true)
|
||||
.showDistY(true)
|
||||
.showLegend(false)
|
||||
.forceX([0, 100]);
|
||||
chart.tooltipContent(function(graph) {
|
||||
return '<p>' + graph.point.class + '</p>';
|
||||
});
|
||||
|
||||
chart.xAxis.axisLabel('Code Coverage (in percent)');
|
||||
chart.yAxis.axisLabel('Cyclomatic Complexity');
|
||||
|
||||
d3.select('#classComplexity svg')
|
||||
.datum(getComplexityData({{complexity_class}}, 'Class Complexity'))
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
|
||||
return chart;
|
||||
});
|
||||
|
||||
nv.addGraph(function() {
|
||||
var chart = nv.models.scatterChart()
|
||||
.showDistX(true)
|
||||
.showDistY(true)
|
||||
.showLegend(false)
|
||||
.forceX([0, 100]);
|
||||
chart.tooltipContent(function(graph) {
|
||||
return '<p>' + graph.point.class + '</p>';
|
||||
});
|
||||
|
||||
chart.xAxis.axisLabel('Code Coverage (in percent)');
|
||||
chart.yAxis.axisLabel('Method Complexity');
|
||||
|
||||
d3.select('#methodComplexity svg')
|
||||
.datum(getComplexityData({{complexity_method}}, 'Method Complexity'))
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
|
||||
return chart;
|
||||
});
|
||||
|
||||
function getComplexityData(data, label) {
|
||||
var values = [];
|
||||
$.each(data, function(key) {
|
||||
var value = Math.round(data[key][0]*100) / 100;
|
||||
values.push({
|
||||
x: value,
|
||||
y: data[key][1],
|
||||
class: data[key][2],
|
||||
size: 0.05,
|
||||
shape: 'diamond'
|
||||
});
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
key: label,
|
||||
values: values,
|
||||
color: "#4572A7"
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
const classComplexityData = JSON.parse(document.getElementById('complexity_class').textContent);
|
||||
bb.generate(
|
||||
scatterConfig("class", classComplexityData)
|
||||
);
|
||||
const methodComplexityData = JSON.parse(document.getElementById('complexity_method').textContent);
|
||||
bb.generate(
|
||||
scatterConfig("method", methodComplexityData),
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function i(n){return!isNaN(n)}function u(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)<0?r=u+1:i=u}return r},right:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)>0?i=u:r=u+1}return r}}}function o(n){return n.length}function a(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function f(n){return(n+="")===bo||n[0]===_o?_o+n:n}function s(n){return(n+="")[0]===_o?n.slice(1):n}function h(n){return f(n)in this._}function p(n){return(n=f(n))in this._&&delete this._[n]}function g(){var n=[];for(var t in this._)n.push(s(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function y(){this._=Object.create(null)}function m(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=wo.length;r>e;++e){var i=wo[e]+t;if(i in n)return i}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,i=-1,u=r.length;++i<u;)(t=r[i].on)&&t.apply(this,arguments);return n}var e=[],r=new c;return t.on=function(t,i){var u,o=r.get(t);return arguments.length<2?o&&o.on:(o&&(o.on=null,e=e.slice(0,u=e.indexOf(o)).concat(e.slice(u+1)),r.remove(t)),i&&e.push(r.set(t,{on:i})),n)},t}function S(){ao.event.preventDefault()}function k(){for(var n,t=ao.event;n=t.sourceEvent;)t=n;return t}function N(n){for(var t=new _,e=0,r=arguments.length;++e<r;)t[arguments[e]]=w(t);return t.of=function(e,r){return function(i){try{var u=i.sourceEvent=ao.event;i.target=n,ao.event=i,t[i.type].apply(e,r)}finally{ao.event=u}}},t}function E(n){return ko(n,Co),n}function A(n){return"function"==typeof n?n:function(){return No(n,this)}}function C(n){return"function"==typeof n?n:function(){return Eo(n,this)}}function z(n,t){function e(){this.removeAttribute(n)}function r(){this.removeAttributeNS(n.space,n.local)}function i(){this.setAttribute(n,t)}function u(){this.setAttributeNS(n.space,n.local,t)}function o(){var e=t.apply(this,arguments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function a(){var e=t.apply(this,arguments);null==e?this.removeAttributeNS(n.space,n.local):this.setAttributeNS(n.space,n.local,e)}return n=ao.ns.qualify(n),null==t?n.local?r:e:"function"==typeof t?n.local?a:o:n.local?u:i}function L(n){return n.trim().replace(/\s+/g," ")}function q(n){return new RegExp("(?:^|\\s+)"+ao.requote(n)+"(?:\\s+|$)","g")}function T(n){return(n+"").trim().split(/^|\s+/)}function R(n,t){function e(){for(var e=-1;++e<i;)n[e](this,t)}function r(){for(var e=-1,r=t.apply(this,arguments);++e<i;)n[e](this,r)}n=T(n).map(D);var i=n.length;return"function"==typeof t?r:e}function D(n){var t=q(n);return function(e,r){if(i=e.classList)return r?i.add(n):i.remove(n);var i=e.getAttribute("class")||"";r?(t.lastIndex=0,t.test(i)||e.setAttribute("class",L(i+" "+n))):e.setAttribute("class",L(i.replace(t," ")))}}function P(n,t,e){function r(){this.style.removeProperty(n)}function i(){this.style.setProperty(n,t,e)}function u(){var r=t.apply(this,arguments);null==r?this.style.removeProperty(n):this.style.setProperty(n,r,e)}return null==t?r:"function"==typeof t?u:i}function U(n,t){function e(){delete this[n]}function r(){this[n]=t}function i(){var e=t.apply(this,arguments);null==e?delete this[n]:this[n]=e}return null==t?e:"function"==typeof t?i:r}function j(n){function t(){var t=this.ownerDocument,e=this.namespaceURI;return e===zo&&t.documentElement.namespaceURI===zo?t.createElement(n):t.createElementNS(e,n)}function e(){return this.ownerDocument.createElementNS(n.space,n.local)}return"function"==typeof n?n:(n=ao.ns.qualify(n)).local?e:t}function F(){var n=this.parentNode;n&&n.removeChild(this)}function H(n){return{__data__:n}}function O(n){return function(){return Ao(this,n)}}function I(n){return arguments.length||(n=e),function(t,e){return t&&e?n(t.__data__,e.__data__):!t-!e}}function Y(n,t){for(var e=0,r=n.length;r>e;e++)for(var i,u=n[e],o=0,a=u.length;a>o;o++)(i=u[o])&&t(i,o,e);return n}function Z(n){return ko(n,qo),n}function V(n){var t,e;return function(r,i,u){var o,a=n[u].update,l=a.length;for(u!=e&&(e=u,t=0),i>=t&&(t=i+1);!(o=a[t])&&++t<l;);return o}}function X(n,t,e){function r(){var t=this[o];t&&(this.removeEventListener(n,t,t.$),delete this[o])}function i(){var i=l(t,co(arguments));r.call(this),this.addEventListener(n,this[o]=i,i.$=e),i._=t}function u(){var t,e=new RegExp("^__on([^.]+)"+ao.requote(n)+"$");for(var r in this)if(t=r.match(e)){var i=thLine truncated
|
||||
r(),S.point=c,S.lineEnd=f}function c(n,t){u(s=n,h=t),p=M,g=x,v=b,d=_,y=w,S.point=u}function f(){i(M,x,m,b,_,w,p,g,s,v,d,y,a,t),S.lineEnd=o,o()}var s,h,p,g,v,d,y,m,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function i(t,e,r,a,l,c,f,s,h,p,g,v,d,y){var m=f-t,M=s-e,x=m*m+M*M;if(x>4*u&&d--){var b=a+p,_=l+g,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=xo(xo(w)-1)<Uo||xo(r-h)<Uo?(r+h)/2:Math.atan2(_,b),E=n(N,k),A=E[0],C=E[1],z=A-t,L=C-e,q=M*z-m*L;(q*q/x>u||xo((m*z+M*L)/x-.5)>.3||o>a*p+l*g+c*v)&&(i(t,e,r,a,l,c,A,C,N,b/=S,_/=S,w,d,y),y.point(A,C),i(A,C,N,b,_,w,f,s,h,p,g,v,d,y))}}var u=.5,o=Math.cos(30*Yo),a=16;return t.precision=function(n){return arguments.length?(a=(u=n*n)>0&&16,t):Math.sqrt(u)},t}function re(n){var t=ee(function(t,e){return n([t*Zo,e*Zo])});return function(n){return le(t(n))}}function ie(n){this.stream=n}function ue(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function oe(n){return ae(function(){return n})()}function ae(n){function t(n){return n=a(n[0]*Yo,n[1]*Yo),[n[0]*h+l,c-n[1]*h]}function e(n){return n=a.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Zo,n[1]*Zo]}function r(){a=Ct(o=se(y,M,x),u);var n=u(v,d);return l=p-n[0]*h,c=g+n[1]*h,i()}function i(){return f&&(f.valid=!1,f=null),t}var u,o,a,l,c,f,s=ee(function(n,t){return n=u(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,p=480,g=250,v=0,d=0,y=0,M=0,x=0,b=Fa,_=m,w=null,S=null;return t.stream=function(n){return f&&(f.valid=!1),f=le(b(o,s(_(n)))),f.valid=!0,f},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,Fa):It((w=+n)*Yo),i()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):m,i()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(p=+n[0],g=+n[1],r()):[p,g]},t.center=function(n){return arguments.length?(v=n[0]%360*Yo,d=n[1]%360*Yo,r()):[v*Zo,d*Zo]},t.rotate=function(n){return arguments.length?(y=n[0]%360*Yo,M=n[1]%360*Yo,x=n.length>2?n[2]%360*Yo:0,r()):[y*Zo,M*Zo,x*Zo]},ao.rebind(t,s,"precision"),function(){return u=n.apply(this,arguments),t.invert=u.invert&&e,r()}}function le(n){return ue(n,function(t,e){n.point(t*Yo,e*Yo)})}function ce(n,t){return[n,t]}function fe(n,t){return[n>Fo?n-Ho:-Fo>n?n+Ho:n,t]}function se(n,t,e){return n?t||e?Ct(pe(n),ge(t,e)):pe(n):t||e?ge(t,e):fe}function he(n){return function(t,e){return t+=n,[t>Fo?t-Ho:-Fo>t?t+Ho:t,e]}}function pe(n){var t=he(n);return t.invert=he(-n),t}function ge(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*r+a*i;return[Math.atan2(l*u-f*o,a*r-c*i),tn(f*u+l*o)]}var r=Math.cos(n),i=Math.sin(n),u=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*u-l*o;return[Math.atan2(l*u+c*o,a*r+f*i),tn(f*r-a*i)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(i,u,o,a){var l=o*t;null!=i?(i=de(e,i),u=de(e,u),(o>0?u>i:i>u)&&(i+=o*Ho)):(i=n+o*Ho,u=n-.5*l);for(var c,f=i;o>0?f>u:u>f;f-=l)a.point((c=_t([e,-r*Math.cos(f),-r*Math.sin(f)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Uo)%(2*Math.PI)}function ye(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function me(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var i=Math.cos(t),u=Math.sin(t),o=Math.cos(r),a=Math.sin(r),l=i*Math.cos(n),c=i*Math.sin(n),f=o*Math.cos(e),s=o*Math.sin(e),h=2*Math.asin(Math.sqrt(on(r-t)+i*o*on(e-n))),p=1/Math.sin(h),g=h?function(n){var t=Math.sin(n*=h)*p,e=Math.sin(h-n)*p,r=e*l+t*f,i=e*c+t*s,o=e*u+t*a;return[Math.atan2(i,r)*Zo,Math.atan2(o,Math.sqrt(r*r+i*i))*Zo]}:function(){return[n*Zo,t*Zo]};return g.distance=h,g}function _e(){function n(n,i){var u=Math.sin(i*=Yo),o=Math.cos(i),a=xo((n*=Yo)-t),l=Math.cos(a);Ja+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*u-e*o*l)*a),e*u+r*o*l),t=n,e=u,r=o}var t,e,r;Ga.point=function(i,u){t=i*Yo,e=Math.sin(u*=Yo),r=Math.cos(u),Ga.point=n},Ga.lineEnd=function(){Ga.point=Ga.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),i=Math.cos(e),u=n(r*i);return[u*i*Math.sin(t),u*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),i=t(r),u=Math.sin(i),o=Math.cos(i);return[Math.atan2(n*u,r*o),Math.asin(r&&e*u/r)]},e}function Se(n,t){function e(n,t){o>0?-Io+Uo>t&&(t=-Io+Uo):t>Io-Uo&&(t=Io-Uo);var e=o/Math.pow(i(t),u);return[e*Math.sin(u*n),o-e*Math.cos(u*n)]}var r=Math.cos(n),i=function(n){return Math.tan(Fo/4+n/2)},u=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(i(t)/i(n)),o=r*Math.pow(i(n),u)/u;return u?(e.invert=function(n,tLine truncated
|
||||
return p}:En(r),w=i===u?function(){return g}:En(u);++m<M;)o.call(this,h=t[m],m)?(d.push([p=+x.call(this,h,m),g=+b.call(this,h,m)]),y.push([+_.call(this,h,m),+w.call(this,h,m)])):d.length&&(l(),d=[],y=[]);return d.length&&l(),v.length?v.join(""):null}var e=Ce,r=Ce,i=0,u=ze,o=zt,a=xu,l=a.key,c=a,f="L",s=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return arguments.length?(i=u=n,t):u},t.y0=function(n){return arguments.length?(i=n,t):i},t.y1=function(n){return arguments.length?(u=n,t):u},t.defined=function(n){return arguments.length?(o=n,t):o},t.interpolate=function(n){return arguments.length?(l="function"==typeof n?a=n:(a=Tl.get(n)||xu).key,c=a.reverse||a,f=a.closed?"M":"L",t):l},t.tension=function(n){return arguments.length?(s=n,t):s},t}function Iu(n){return n.radius}function Yu(n){return[n.x,n.y]}function Zu(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]-Io;return[e*Math.cos(r),e*Math.sin(r)]}}function Vu(){return 64}function Xu(){return"circle"}function $u(n){var t=Math.sqrt(n/Fo);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+-t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function Bu(n){return function(){var t,e,r;(t=this[n])&&(r=t[e=t.active])&&(r.timer.c=null,r.timer.t=NaN,--t.count?delete t[e]:delete this[n],t.active+=.5,r.event&&r.event.interrupt.call(this,this.__data__,r.index))}}function Wu(n,t,e){return ko(n,Yl),n.namespace=t,n.id=e,n}function Ju(n,t,e,r){var i=n.id,u=n.namespace;return Y(n,"function"==typeof e?function(n,o,a){n[u][i].tween.set(t,r(e.call(n,n.__data__,o,a)))}:(e=r(e),function(n){n[u][i].tween.set(t,e)}))}function Gu(n){return null==n&&(n=""),function(){this.textContent=n}}function Ku(n){return null==n?"__transition__":"__transition_"+n+"__"}function Qu(n,t,e,r,i){function u(n){var t=v.delay;return f.t=t+l,n>=t?o(n-t):void(f.c=o)}function o(e){var i=g.active,u=g[i];u&&(u.timer.c=null,u.timer.t=NaN,--g.count,delete g[i],u.event&&u.event.interrupt.call(n,n.__data__,u.index));for(var o in g)if(r>+o){var c=g[o];c.timer.c=null,c.timer.t=NaN,--g.count,delete g[o]}f.c=a,qn(function(){return f.c&&a(e||1)&&(f.c=null,f.t=NaN),1},0,l),g.active=r,v.event&&v.event.start.call(n,n.__data__,t),p=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&p.push(r)}),h=v.ease,s=v.duration}function a(i){for(var u=i/s,o=h(u),a=p.length;a>0;)p[--a].call(n,o);return u>=1?(v.event&&v.event.end.call(n,n.__data__,t),--g.count?delete g[r]:delete n[e],1):void 0}var l,f,s,h,p,g=n[e]||(n[e]={active:0,count:0}),v=g[r];v||(l=i.time,f=qn(u,0,l),v=g[r]={tween:new c,time:l,timer:f,delay:i.delay,duration:i.duration,ease:i.ease,index:t},i=null,++g.count)}function no(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function to(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function eo(n){return n.toISOString()}function ro(n,t,e){function r(t){return n(t)}function i(n,e){var r=n[1]-n[0],i=r/e,u=ao.bisect(Kl,i);return u==Kl.length?[t.year,Ki(n.map(function(n){return n/31536e6}),e)[2]]:u?t[i/Kl[u-1]<Kl[u]/i?u-1:u]:[tc,Ki(n,e)[2]]}return r.invert=function(t){return io(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(io)},r.nice=function(n,t){function e(e){return!isNaN(e)&&!n.range(e,io(+e+1),t).length}var u=r.domain(),o=Yi(u),a=null==n?i(o,10):"number"==typeof n&&i(o,n);return a&&(n=a[0],t=a[1]),r.domain(Xi(u,t>1?{floor:function(t){for(;e(t=n.floor(t));)t=io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yi(r.domain()),u=null==n?i(e,10):"number"==typeof n?i(e,n):!n.range&&[{range:n},t];return u&&(n=u[0],t=u[1]),n.range(e[0],io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ro(n.copy(),t,e)},Ji(r,n)}function io(n){return new Date(n)}function uo(n){return JSON.parse(n.responseText)}function oo(n){var t=fo.createRange();return t.selectNode(fo.body),t.createContextualFragment(n.responseText)}var ao={version:"3.5.17"},lo=[].slice,co=function(n){return lo.call(n)},fo=this.document;if(fo)try{co(fo.documentElement.childNodes)[0].nodeType}catch(so){co=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),fo)try{fo.createElement("DIV").style.setProperty("opacity",0,"")}catch(ho){var po=this.Element.prototype,go=po.setAttribute,vo=po.setAttributeNS,yo=this.CSSStyleDeclaration.prototype,mo=yo.setProperty;po.setAttribute=function(n,t){go.call(this,n,t+"")},po.setAttributeNS=function(n,t,e){vo.call(this,n,t,e+"")},yo.setProperty=function(n,t,e){mo.call(this,n,t+"",e)}}ao.ascending=e,ao.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},ao.min=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i<u;)if(null!=(r=n[i])&&r>=r){e=r;break}for(;++i<u;)null!=(r=n[i])&&e>r&&(e=r)}else{for(;++i<uLine truncated
|
||||
shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});ao.format=xa.numberFormat,ao.geo={},ft.prototype={s:0,t:0,add:function(n){st(n,this.t,ba),st(ba.s,this.s,this),this.s?this.t+=ba.t:this.s=ba.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var ba=new ft;ao.geo.stream=function(n,t){n&&_a.hasOwnProperty(n.type)?_a[n.type](n,t):ht(n,t)};var _a={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,i=e.length;++r<i;)ht(e[r].geometry,t)}},wa={Sphere:function(n,t){t.sphere()},Point:function(n,t){n=n.coordinates,t.point(n[0],n[1],n[2])},MultiPoint:function(n,t){for(var e=n.coordinates,r=-1,i=e.length;++r<i;)n=e[r],t.point(n[0],n[1],n[2])},LineString:function(n,t){pt(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coordinates,r=-1,i=e.length;++r<i;)pt(e[r],t,0)},Polygon:function(n,t){gt(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,i=e.length;++r<i;)gt(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,i=e.length;++r<i;)ht(e[r],t)}};ao.geo.area=function(n){return Sa=0,ao.geo.stream(n,Na),Sa};var Sa,ka=new ft,Na={sphere:function(){Sa+=4*Fo},point:b,lineStart:b,lineEnd:b,polygonStart:function(){ka.reset(),Na.lineStart=vt},polygonEnd:function(){var n=2*ka;Sa+=0>n?4*Fo+n:n,Na.lineStart=Na.lineEnd=Na.point=b}};ao.geo.bounds=function(){function n(n,t){M.push(x=[f=n,h=n]),s>t&&(s=t),t>p&&(p=t)}function t(t,e){var r=dt([t*Yo,e*Yo]);if(y){var i=mt(y,r),u=[i[1],-i[0],0],o=mt(u,i);bt(o),o=_t(o);var l=t-g,c=l>0?1:-1,v=o[0]*Zo*c,d=xo(l)>180;if(d^(v>c*g&&c*t>v)){var m=o[1]*Zo;m>p&&(p=m)}else if(v=(v+360)%360-180,d^(v>c*g&&c*t>v)){var m=-o[1]*Zo;s>m&&(s=m)}else s>e&&(s=e),e>p&&(p=e);d?g>t?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t):h>=f?(f>t&&(f=t),t>h&&(h=t)):t>g?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t)}else n(t,e);y=r,g=t}function e(){b.point=t}function r(){x[0]=f,x[1]=h,b.point=n,y=null}function i(n,e){if(y){var r=n-g;m+=xo(r)>180?r+(r>0?360:-360):r}else v=n,d=e;Na.point(n,e),t(n,e)}function u(){Na.lineStart()}function o(){i(v,d),Na.lineEnd(),xo(m)>Uo&&(f=-(h=180)),x[0]=f,x[1]=h,y=null}function a(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:n<t[0]||t[1]<n}var f,s,h,p,g,v,d,y,m,M,x,b={point:n,lineStart:e,lineEnd:r,polygonStart:function(){b.point=i,b.lineStart=u,b.lineEnd=o,m=0,Na.polygonStart()},polygonEnd:function(){Na.polygonEnd(),b.point=n,b.lineStart=e,b.lineEnd=r,0>ka?(f=-(h=180),s=-(p=90)):m>Uo?p=90:-Uo>m&&(s=-90),x[0]=f,x[1]=h}};return function(n){p=h=-(f=s=1/0),M=[],ao.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,i=M[0],u=[i];t>r;++r)e=M[r],c(e[0],i)||c(e[1],i)?(a(i[0],e[1])>a(i[0],i[1])&&(i[1]=e[1]),a(e[0],i[1])>a(i[0],i[1])&&(i[0]=e[0])):u.push(i=e);for(var o,e,g=-(1/0),t=u.length-1,r=0,i=u[t];t>=r;i=e,++r)e=u[r],(o=a(i[1],e[0]))>g&&(g=o,f=e[0],h=i[1])}return M=x=null,f===1/0||s===1/0?[[NaN,NaN],[NaN,NaN]]:[[f,s],[h,p]]}}(),ao.geo.centroid=function(n){Ea=Aa=Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,ja);var t=Da,e=Pa,r=Ua,i=t*t+e*e+r*r;return jo>i&&(t=qa,e=Ta,r=Ra,Uo>Aa&&(t=Ca,e=za,r=La),i=t*t+e*e+r*r,jo>i)?[NaN,NaN]:[Math.atan2(e,t)*Zo,tn(r/Math.sqrt(i))*Zo]};var Ea,Aa,Ca,za,La,qa,Ta,Ra,Da,Pa,Ua,ja={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){ja.lineStart=At},polygonEnd:function(){ja.lineStart=Nt}},Fa=Rt(zt,jt,Ht,[-Fo,-Fo/2]),Ha=1e9;ao.geo.clipExtent=function(){var n,t,e,r,i,u,o={stream:function(n){return i&&(i.valid=!1),i=u(n),i.valid=!0,i},extent:function(a){return arguments.length?(u=Zt(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),i&&(i.valid=!1,i=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(ao.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,ao.geo.albers=function(){return ao.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},ao.geo.albersUsa=function(){function n(n){var u=n[0],o=n[1];return t=null,e(u,o),t||(r(u,o),t)||i(u,o),t}var t,e,r,i,u=ao.geo.albers(),o=ao.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=ao.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=u.scale(),e=u.translate(),r=(n[0]-e[0])/t,i=(n[1]-e[1])/t;return(i>=.12&&.234>i&&r>=-.425&&-.214>r?o:i>=.166&&.234>i&&r>=-.214&&-.115>r?a:u).invert(n)},n.stream=function(n){var t=u.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,i){t.point(n,i),e.point(n,i),r.point(n,i)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:funLine truncated
|
||||
if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++u<o;)i=n[u],i.x=a,i.y=c,i.dy=f,a+=i.dx=Math.min(e.x+e.dx-a,f?l(i.area/f):0);i.z=!0,i.dx+=e.x+e.dx-a,e.y+=f,e.dy-=f}else{for((r||f>e.dx)&&(f=e.dx);++u<o;)i=n[u],i.x=a,i.y=c,i.dx=f,c+=i.dy=Math.min(e.y+e.dy-c,f?l(i.area/f):0);i.z=!1,i.dy+=e.y+e.dy-c,e.x+=f,e.dx-=f}}function u(r){var i=o||a(r),u=i[0];return u.x=u.y=0,u.value?(u.dx=c[0],u.dy=c[1]):u.dx=u.dy=0,o&&a.revalue(u),n([u],u.dx*u.dy/u.value),(o?e:t)(u),h&&(o=i),i}var o,a=ao.layout.hierarchy(),l=Math.round,c=[1,1],f=null,s=Oi,h=!1,p="squarify",g=.5*(1+Math.sqrt(5));return u.size=function(n){return arguments.length?(c=n,u):c},u.padding=function(n){function t(t){var e=n.call(u,t,t.depth);return null==e?Oi(t):Ii(t,"number"==typeof e?[e,e,e,e]:e)}function e(t){return Ii(t,n)}if(!arguments.length)return f;var r;return s=null==(f=n)?Oi:"function"==(r=typeof n)?t:"number"===r?(n=[n,n,n,n],e):e,u},u.round=function(n){return arguments.length?(l=n?Math.round:Number,u):l!=Number},u.sticky=function(n){return arguments.length?(h=n,o=null,u):h},u.ratio=function(n){return arguments.length?(g=n,u):g},u.mode=function(n){return arguments.length?(p=n+"",u):p},ii(u,a)},ao.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,i;do e=2*Math.random()-1,r=2*Math.random()-1,i=e*e+r*r;while(!i||i>1);return n+t*e*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(){var n=ao.random.normal.apply(ao,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=ao.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},ao.scale={};var Sl={floor:m,ceil:m};ao.scale.linear=function(){return Wi([0,1],[0,1],Mr,!1)};var kl={s:1,g:1,p:1,r:1,e:1};ao.scale.log=function(){return ru(ao.scale.linear().domain([0,1]),10,!0,[1,10])};var Nl=ao.format(".0e"),El={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};ao.scale.pow=function(){return iu(ao.scale.linear(),1,[0,1])},ao.scale.sqrt=function(){return ao.scale.pow().exponent(.5)},ao.scale.ordinal=function(){return ou([],{t:"range",a:[[]]})},ao.scale.category10=function(){return ao.scale.ordinal().range(Al)},ao.scale.category20=function(){return ao.scale.ordinal().range(Cl)},ao.scale.category20b=function(){return ao.scale.ordinal().range(zl)},ao.scale.category20c=function(){return ao.scale.ordinal().range(Ll)};var Al=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Cl=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),zl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),Ll=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);ao.scale.quantile=function(){return au([],[])},ao.scale.quantize=function(){return lu(0,1,[0,1])},ao.scale.threshold=function(){return cu([.5],[0,1])},ao.scale.identity=function(){return fu([0,1])},ao.svg={},ao.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),f=o.apply(this,arguments)-Io,s=a.apply(this,arguments)-Io,h=Math.abs(s-f),p=f>s?0:1;if(n>c&&(g=c,c=n,n=g),h>=Oo)return t(c,p)+(n?t(n,1-p):"")+"Z";var g,v,d,y,m,M,x,b,_,w,S,k,N=0,E=0,A=[];if((y=(+l.apply(this,arguments)||0)/2)&&(d=u===ql?Math.sqrt(n*n+c*c):+u.apply(this,arguments),p||(E*=-1),c&&(E=tn(d/c*Math.sin(y))),n&&(N=tn(d/n*Math.sin(y)))),c){m=c*Math.cos(f+E),M=c*Math.sin(f+E),x=c*Math.cos(s-E),b=c*Math.sin(s-E);var C=Math.abs(s-f-2*E)<=Fo?0:1;if(E&&yu(m,M,x,b)===p^C){var z=(f+s)/2;m=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else m=M=0;if(n){_=n*Math.cos(s-N),w=n*Math.sin(s-N),S=n*Math.cos(f+N),k=n*Math.sin(f+N);var L=Math.abs(f-s+2*N)<=Fo?0:1;if(N&&yu(_,w,S,k)===1-p^L){var q=(f+s)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Uo&&(g=Math.min(Math.abs(c-n)/2,+i.apply(this,arguments)))>.001){v=c>n^p?0:1;var T=g,R=g;if(Fo>h){var D=null==S?[_,w]:null==x?[m,M]:Re([m,M],[S,k],[x,b],[_,w]),P=m-D[0],U=M-D[1],j=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*j+U*F)/(Math.sqrt(P*P+U*U)*Math.sqrt(j*j+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(g,(n-O)/(H-1)),T=Math.min(g,(c-O)/(H+1))}if(null!=x){var I=mu(null==S?[_,w]:[S,k],[m,M],c,T,p),Y=mu([x,b],[_,w],c,T,p);g===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-p^yu(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",p," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",m,",",M);if(null!=S){var Z=mu([m,M],[S,k],n,-R,p),V=mu([_,w],null==x?[m,M]:[x,b],n,-R,p);g===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",p^yu(V[1][0],V[1]Line truncated
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
/* nvd3 version 1.8.1 (https://github.com/novus/nvd3) 2015-06-15 */
|
||||
!function(){var a={};a.dev=!1,a.tooltip=a.tooltip||{},a.utils=a.utils||{},a.models=a.models||{},a.charts={},a.logs={},a.dom={},a.dispatch=d3.dispatch("render_start","render_end"),Function.prototype.bind||(Function.prototype.bind=function(a){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var b=Array.prototype.slice.call(arguments,1),c=this,d=function(){},e=function(){return c.apply(this instanceof d&&a?this:a,b.concat(Array.prototype.slice.call(arguments)))};return d.prototype=this.prototype,e.prototype=new d,e}),a.dev&&(a.dispatch.on("render_start",function(){a.logs.startTime=+new Date}),a.dispatch.on("render_end",function(){a.logs.endTime=+new Date,a.logs.totalTime=a.logs.endTime-a.logs.startTime,a.log("total",a.logs.totalTime)})),a.log=function(){if(a.dev&&window.console&&console.log&&console.log.apply)console.log.apply(console,arguments);else if(a.dev&&window.console&&"function"==typeof console.log&&Function.prototype.bind){var b=Function.prototype.bind.call(console.log,console);b.apply(console,arguments)}return arguments[arguments.length-1]},a.deprecated=function(a,b){console&&console.warn&&console.warn("nvd3 warning: `"+a+"` has been deprecated. ",b||"")},a.render=function(b){b=b||1,a.render.active=!0,a.dispatch.render_start();var c=function(){for(var d,e,f=0;b>f&&(e=a.render.queue[f]);f++)d=e.generate(),typeof e.callback==typeof Function&&e.callback(d);a.render.queue.splice(0,f),a.render.queue.length?setTimeout(c):(a.dispatch.render_end(),a.render.active=!1)};setTimeout(c)},a.render.active=!1,a.render.queue=[],a.addGraph=function(b){typeof arguments[0]==typeof Function&&(b={generate:arguments[0],callback:arguments[1]}),a.render.queue.push(b),a.render.active||a.render()},"undefined"!=typeof module&&"undefined"!=typeof exports&&(module.exports=a),"undefined"!=typeof window&&(window.nv=a),a.dom.write=function(a){return void 0!==window.fastdom?fastdom.write(a):a()},a.dom.read=function(a){return void 0!==window.fastdom?fastdom.read(a):a()},a.interactiveGuideline=function(){"use strict";function b(l){l.each(function(l){function m(){var a=d3.mouse(this),d=a[0],e=a[1],i=!0,j=!1;if(k&&(d=d3.event.offsetX,e=d3.event.offsetY,"svg"!==d3.event.target.tagName&&(i=!1),d3.event.target.className.baseVal.match("nv-legend")&&(j=!0)),i&&(d-=f.left,e-=f.top),0>d||0>e||d>o||e>p||d3.event.relatedTarget&&void 0===d3.event.relatedTarget.ownerSVGElement||j){if(k&&d3.event.relatedTarget&&void 0===d3.event.relatedTarget.ownerSVGElement&&(void 0===d3.event.relatedTarget.className||d3.event.relatedTarget.className.match(c.nvPointerEventsClass)))return;return h.elementMouseout({mouseX:d,mouseY:e}),b.renderGuideLine(null),void c.hidden(!0)}c.hidden(!1);var l=g.invert(d);h.elementMousemove({mouseX:d,mouseY:e,pointXValue:l}),"dblclick"===d3.event.type&&h.elementDblclick({mouseX:d,mouseY:e,pointXValue:l}),"click"===d3.event.type&&h.elementClick({mouseX:d,mouseY:e,pointXValue:l})}var n=d3.select(this),o=d||960,p=e||400,q=n.selectAll("g.nv-wrap.nv-interactiveLineLayer").data([l]),r=q.enter().append("g").attr("class"," nv-wrap nv-interactiveLineLayer");r.append("g").attr("class","nv-interactiveGuideLine"),j&&(j.on("touchmove",m).on("mousemove",m,!0).on("mouseout",m,!0).on("dblclick",m).on("click",m),b.guideLine=null,b.renderGuideLine=function(c){i&&(b.guideLine&&b.guideLine.attr("x1")===c||a.dom.write(function(){var b=q.select(".nv-interactiveGuideLine").selectAll("line").data(null!=c?[a.utils.NaNtoZero(c)]:[],String);b.enter().append("line").attr("class","nv-guideline").attr("x1",function(a){return a}).attr("x2",function(a){return a}).attr("y1",p).attr("y2",0),b.exit().remove()}))})})}var c=a.models.tooltip();c.duration(0).hideDelay(0)._isInteractiveLayer(!0).hidden(!1);var d=null,e=null,f={left:0,top:0},g=d3.scale.linear(),h=d3.dispatch("elementMousemove","elementMouseout","elementClick","elementDblclick"),i=!0,j=null,k="ActiveXObject"in window;return b.dispatch=h,b.tooltip=c,b.margin=function(a){return arguments.length?(f.top="undefined"!=typeof a.top?a.top:f.top,f.left="undefined"!=typeof a.left?a.left:f.left,b):f},b.width=function(a){return arguments.length?(d=a,b):d},b.height=function(a){return arguments.length?(e=a,b):e},b.xScale=function(a){return arguments.length?(g=a,b):g},b.showGuideLine=function(a){return arguments.length?(i=a,b):i},b.svgContainer=function(a){return arguments.length?(j=a,b):j},b},a.interactiveBisect=function(a,b,c){"use strict";if(!(a instanceof Array))return null;var d;d="function"!=typeof c?function(a){return a.x}:c;var e=function(a,b){return d(a)-b},f=d3.bisector(e).left,g=d3.max([0,f(a,b)-1]),h=d(a[g]);if("undefined"==typeof h&&(h=g),h===b)return g;var i=d3.min([g+1,a.length-1]),j=d(a[i]);return"undefined"==typeof j&&(j=i),Math.abs(j-b)>=Math.abs(h-b)?g:i},a.nearestValueIndex=function(a,b,c){"use strict";var d=1/0,e=null;return a.forEach(function(a,f){var g=Math.abs(b-a);null!=a&&d>=g&&c>g&&(d=g,e=f)}),e},function(){"use strict";a.modLine truncated
|
||||
x.append("g").attr("class","nv-x nv-axis"),x.append("g").attr("class","nv-y nv-axis").append("g").attr("class","nv-zeroLine").append("line"),x.append("g").attr("class","nv-barsWrap"),z.attr("transform","translate("+h.left+","+h.top+")"),n&&z.select(".nv-y.nv-axis").attr("transform","translate("+t+",0)"),e.width(t).height(u);var A=z.select(".nv-barsWrap").datum(k.filter(function(a){return!a.disabled}));if(A.transition().call(e),y.append("clipPath").attr("id","nv-x-label-clip-"+e.id()).append("rect"),z.select("#nv-x-label-clip-"+e.id()+" rect").attr("width",c.rangeBand()*(o?2:1)).attr("height",16).attr("x",-c.rangeBand()/(o?1:2)),l){f.scale(c).ticks(a.utils.calcTicksX(t/100,k)).tickSize(-u,0),z.select(".nv-x.nv-axis").attr("transform","translate(0,"+d.range()[0]+")"),z.select(".nv-x.nv-axis").call(f);var B=z.select(".nv-x.nv-axis").selectAll("g");o&&B.selectAll("text").attr("transform",function(a,b,c){return"translate(0,"+(c%2==0?"5":"17")+")"})}m&&(g.scale(d).ticks(Math.floor(u/36)).tickSize(-t,0),z.select(".nv-y.nv-axis").call(g)),z.select(".nv-zeroLine line").attr("x1",0).attr("x2",t).attr("y1",d(0)).attr("y2",d(0))}),t.renderEnd("nv-boxplot chart immediate"),b}var c,d,e=a.models.boxPlot(),f=a.models.axis(),g=a.models.axis(),h={top:15,right:10,bottom:50,left:60},i=null,j=null,k=a.utils.getColor(),l=!0,m=!0,n=!1,o=!1,p=a.models.tooltip(),q="No Data Available.",r=d3.dispatch("tooltipShow","tooltipHide","beforeUpdate","renderEnd"),s=250;f.orient("bottom").showMaxMin(!1).tickFormat(function(a){return a}),g.orient(n?"right":"left").tickFormat(d3.format(",.1f")),p.duration(0);var t=a.utils.renderWatch(r,s);return e.dispatch.on("elementMouseover.tooltip",function(a){p.data(a).hidden(!1)}),e.dispatch.on("elementMouseout.tooltip",function(a){p.data(a).hidden(!0)}),e.dispatch.on("elementMousemove.tooltip",function(){p.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.dispatch=r,b.boxplot=e,b.xAxis=f,b.yAxis=g,b.tooltip=p,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return i},set:function(a){i=a}},height:{get:function(){return j},set:function(a){j=a}},staggerLabels:{get:function(){return o},set:function(a){o=a}},showXAxis:{get:function(){return l},set:function(a){l=a}},showYAxis:{get:function(){return m},set:function(a){m=a}},tooltips:{get:function(){return tooltips},set:function(a){tooltips=a}},tooltipContent:{get:function(){return p},set:function(a){p=a}},noData:{get:function(){return q},set:function(a){q=a}},margin:{get:function(){return h},set:function(a){h.top=void 0!==a.top?a.top:h.top,h.right=void 0!==a.right?a.right:h.right,h.bottom=void 0!==a.bottom?a.bottom:h.bottom,h.left=void 0!==a.left?a.left:h.left}},duration:{get:function(){return s},set:function(a){s=a,t.reset(s),e.duration(s),f.duration(s),g.duration(s)}},color:{get:function(){return k},set:function(b){k=a.utils.getColor(b),e.color(k)}},rightAlignYAxis:{get:function(){return n},set:function(a){n=a,g.orient(a?"right":"left")}}}),a.utils.inheritOptions(b,e),a.utils.initOptions(b),b},a.models.bullet=function(){"use strict";function b(d){return d.each(function(b,d){var p=m-c.left-c.right,s=n-c.top-c.bottom;o=d3.select(this),a.utils.initSVG(o);{var t=f.call(this,b,d).slice().sort(d3.descending),u=g.call(this,b,d).slice().sort(d3.descending),v=h.call(this,b,d).slice().sort(d3.descending),w=i.call(this,b,d).slice(),x=j.call(this,b,d).slice(),y=k.call(this,b,d).slice(),z=d3.scale.linear().domain(d3.extent(d3.merge([l,t]))).range(e?[p,0]:[0,p]);this.__chart__||d3.scale.linear().domain([0,1/0]).range(z.range())}this.__chart__=z;var A=d3.min(t),B=d3.max(t),C=t[1],D=o.selectAll("g.nv-wrap.nv-bullet").data([b]),E=D.enter().append("g").attr("class","nvd3 nv-wrap nv-bullet"),F=E.append("g"),G=D.select("g");F.append("rect").attr("class","nv-range nv-rangeMax"),F.append("rect").attr("class","nv-range nv-rangeAvg"),F.append("rect").attr("class","nv-range nv-rangeMin"),F.append("rect").attr("class","nv-measure"),D.attr("transform","translate("+c.left+","+c.top+")");var H=function(a){return Math.abs(z(a)-z(0))},I=function(a){return z(0>a?a:0)};G.select("rect.nv-rangeMax").attr("height",s).attr("width",H(B>0?B:A)).attr("x",I(B>0?B:A)).datum(B>0?B:A),G.select("rect.nv-rangeAvg").attr("height",s).attr("width",H(C)).attr("x",I(C)).datum(C),G.select("rect.nv-rangeMin").attr("height",s).attr("width",H(B)).attr("x",I(B)).attr("width",H(B>0?A:B)).attr("x",I(B>0?A:B)).datum(B>0?A:B),G.select("rect.nv-measure").style("fill",q).attr("height",s/3).attr("y",s/3).attr("width",0>v?z(0)-z(v[0]):z(v[0])-z(0)).attr("x",I(v)).on("mouseover",function(){r.elementMouseover({value:v[0],label:y[0]||"Current",color:d3.select(this).style("fill")})}).on("mousemove",function(){r.elementMousemove({value:v[0],label:y[0]||"Current",color:d3.select(this).style("fill")})}).on("mouseout",function(){r.elementMouseout({value:v[0],label:y[0]||"Current",color:d3.select(this).style("fill")})});var J=s/6,K=u.map(function(a,b){return{value:a,label:x[b]}});F.sLine truncated
|
||||
var z=x.select(".nv-x.nv-axis").selectAll("g");p&&z.selectAll("text").attr("transform",function(a,b,c){return"translate(0,"+(c%2==0?"5":"17")+")"})}n&&(g.scale(d)._ticks(a.utils.calcTicksY(t/36,h)).tickSize(-q,0),x.select(".nv-y.nv-axis").call(g)),x.select(".nv-zeroLine line").attr("x1",0).attr("x2",q).attr("y1",d(0)).attr("y2",d(0))}),t.renderEnd("discreteBar chart immediate"),b}var c,d,e=a.models.discreteBar(),f=a.models.axis(),g=a.models.axis(),h=a.models.tooltip(),i={top:15,right:10,bottom:50,left:60},j=null,k=null,l=a.utils.getColor(),m=!0,n=!0,o=!1,p=!1,q=null,r=d3.dispatch("beforeUpdate","renderEnd"),s=250;f.orient("bottom").showMaxMin(!1).tickFormat(function(a){return a}),g.orient(o?"right":"left").tickFormat(d3.format(",.1f")),h.duration(0).headerEnabled(!1).valueFormatter(function(a,b){return g.tickFormat()(a,b)}).keyFormatter(function(a,b){return f.tickFormat()(a,b)});var t=a.utils.renderWatch(r,s);return e.dispatch.on("elementMouseover.tooltip",function(a){a.series={key:b.x()(a.data),value:b.y()(a.data),color:a.color},h.data(a).hidden(!1)}),e.dispatch.on("elementMouseout.tooltip",function(){h.hidden(!0)}),e.dispatch.on("elementMousemove.tooltip",function(){h.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.dispatch=r,b.discretebar=e,b.xAxis=f,b.yAxis=g,b.tooltip=h,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return j},set:function(a){j=a}},height:{get:function(){return k},set:function(a){k=a}},staggerLabels:{get:function(){return p},set:function(a){p=a}},showXAxis:{get:function(){return m},set:function(a){m=a}},showYAxis:{get:function(){return n},set:function(a){n=a}},noData:{get:function(){return q},set:function(a){q=a}},tooltips:{get:function(){return h.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),h.enabled(!!b)}},tooltipContent:{get:function(){return h.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),h.contentGenerator(b)}},margin:{get:function(){return i},set:function(a){i.top=void 0!==a.top?a.top:i.top,i.right=void 0!==a.right?a.right:i.right,i.bottom=void 0!==a.bottom?a.bottom:i.bottom,i.left=void 0!==a.left?a.left:i.left}},duration:{get:function(){return s},set:function(a){s=a,t.reset(s),e.duration(s),f.duration(s),g.duration(s)}},color:{get:function(){return l},set:function(b){l=a.utils.getColor(b),e.color(l)}},rightAlignYAxis:{get:function(){return o},set:function(a){o=a,g.orient(a?"right":"left")}}}),a.utils.inheritOptions(b,e),a.utils.initOptions(b),b},a.models.distribution=function(){"use strict";function b(k){return m.reset(),k.each(function(b){var k=(e-("x"===g?d.left+d.right:d.top+d.bottom),"x"==g?"y":"x"),l=d3.select(this);a.utils.initSVG(l),c=c||j;var n=l.selectAll("g.nv-distribution").data([b]),o=n.enter().append("g").attr("class","nvd3 nv-distribution"),p=(o.append("g"),n.select("g"));n.attr("transform","translate("+d.left+","+d.top+")");var q=p.selectAll("g.nv-dist").data(function(a){return a},function(a){return a.key});q.enter().append("g"),q.attr("class",function(a,b){return"nv-dist nv-series-"+b}).style("stroke",function(a,b){return i(a,b)});var r=q.selectAll("line.nv-dist"+g).data(function(a){return a.values});r.enter().append("line").attr(g+"1",function(a,b){return c(h(a,b))}).attr(g+"2",function(a,b){return c(h(a,b))}),m.transition(q.exit().selectAll("line.nv-dist"+g),"dist exit").attr(g+"1",function(a,b){return j(h(a,b))}).attr(g+"2",function(a,b){return j(h(a,b))}).style("stroke-opacity",0).remove(),r.attr("class",function(a,b){return"nv-dist"+g+" nv-dist"+g+"-"+b}).attr(k+"1",0).attr(k+"2",f),m.transition(r,"dist").attr(g+"1",function(a,b){return j(h(a,b))}).attr(g+"2",function(a,b){return j(h(a,b))}),c=j.copy()}),m.renderEnd("distribution immediate"),b}var c,d={top:0,right:0,bottom:0,left:0},e=400,f=8,g="x",h=function(a){return a[g]},i=a.utils.defaultColor(),j=d3.scale.linear(),k=250,l=d3.dispatch("renderEnd"),m=a.utils.renderWatch(l,k);return b.options=a.utils.optionsFunc.bind(b),b.dispatch=l,b.margin=function(a){return arguments.length?(d.top="undefined"!=typeof a.top?a.top:d.top,d.right="undefined"!=typeof a.right?a.right:d.right,d.bottom="undefined"!=typeof a.bottom?a.bottom:d.bottom,d.left="undefined"!=typeof a.left?a.left:d.left,b):d},b.width=function(a){return arguments.length?(e=a,b):e},b.axis=function(a){return arguments.length?(g=a,b):g},b.size=function(a){return arguments.length?(f=a,b):f},b.getData=function(a){return arguments.length?(h=d3.functor(a),b):h},b.scale=function(a){return arguments.length?(j=a,b):j},b.color=function(c){return arguments.length?(i=a.utils.getColor(c),b):i},b.duration=function(a){return arguments.length?(k=a,m.reset(k),b):k},b},a.models.furiousLegend=function(){"use strict";function b(p){function q(a,b){return"furious"!=o?"#000":m?a.disengaged?g(a,b):"#fff":m?void 0:a.disabled?g(a,b):"#fff"}function r(a,b){return m&&"furious"==o?a.disengaged?"#fff":g(a,b):a.disabled?"#fLine truncated
|
||||
}if(!(j&&j.length&&j.filter(function(a){return a.values.length}).length))return a.utils.noData(b,v),b;v.selectAll(".nv-noData").remove(),c=e.xScale(),d=e.yScale();var E=v.selectAll("g.nv-wrap.nv-lineChart").data([j]),F=E.enter().append("g").attr("class","nvd3 nv-wrap nv-lineChart").append("g"),G=E.select("g");F.append("rect").style("opacity",0),F.append("g").attr("class","nv-x nv-axis"),F.append("g").attr("class","nv-y nv-axis"),F.append("g").attr("class","nv-linesWrap"),F.append("g").attr("class","nv-legendWrap"),F.append("g").attr("class","nv-interactive"),G.select("rect").attr("width",B).attr("height",C>0?C:0),o&&(h.width(B),G.select(".nv-legendWrap").datum(j).call(h),k.top!=h.height()&&(k.top=h.height(),C=a.utils.availableHeight(n,v,k)),E.select(".nv-legendWrap").attr("transform","translate(0,"+-k.top+")")),E.attr("transform","translate("+k.left+","+k.top+")"),r&&G.select(".nv-y.nv-axis").attr("transform","translate("+B+",0)"),s&&(i.width(B).height(C).margin({left:k.left,top:k.top}).svgContainer(v).xScale(c),E.select(".nv-interactive").call(i)),e.width(B).height(C).color(j.map(function(a,b){return a.color||l(a,b)}).filter(function(a,b){return!j[b].disabled}));var H=G.select(".nv-linesWrap").datum(j.filter(function(a){return!a.disabled}));H.call(e),p&&(f.scale(c)._ticks(a.utils.calcTicksX(B/100,j)).tickSize(-C,0),G.select(".nv-x.nv-axis").attr("transform","translate(0,"+d.range()[0]+")"),G.select(".nv-x.nv-axis").call(f)),q&&(g.scale(d)._ticks(a.utils.calcTicksY(C/36,j)).tickSize(-B,0),G.select(".nv-y.nv-axis").call(g)),h.dispatch.on("stateChange",function(a){for(var c in a)t[c]=a[c];w.stateChange(t),b.update()}),i.dispatch.on("elementMousemove",function(c){e.clearHighlights();var d,h,m,n=[];if(j.filter(function(a,b){return a.seriesIndex=b,!a.disabled}).forEach(function(f,g){h=a.interactiveBisect(f.values,c.pointXValue,b.x());var i=f.values[h],j=b.y()(i,h);null!=j&&e.highlightPoint(g,h,!0),void 0!==i&&(void 0===d&&(d=i),void 0===m&&(m=b.xScale()(b.x()(i,h))),n.push({key:f.key,value:j,color:l(f,f.seriesIndex)}))}),n.length>2){var o=b.yScale().invert(c.mouseY),p=Math.abs(b.yScale().domain()[0]-b.yScale().domain()[1]),q=.03*p,r=a.nearestValueIndex(n.map(function(a){return a.value}),o,q);null!==r&&(n[r].highlight=!0)}var s=f.tickFormat()(b.x()(d,h));i.tooltip.position({left:c.mouseX+k.left,top:c.mouseY+k.top}).chartContainer(y.parentNode).valueFormatter(function(a){return null==a?"N/A":g.tickFormat()(a)}).data({value:s,index:h,series:n})(),i.renderGuideLine(m)}),i.dispatch.on("elementClick",function(c){var d,f=[];j.filter(function(a,b){return a.seriesIndex=b,!a.disabled}).forEach(function(e){var g=a.interactiveBisect(e.values,c.pointXValue,b.x()),h=e.values[g];if("undefined"!=typeof h){"undefined"==typeof d&&(d=b.xScale()(b.x()(h,g)));var i=b.yScale()(b.y()(h,g));f.push({point:h,pointIndex:g,pos:[d,i],seriesIndex:e.seriesIndex,series:e})}}),e.dispatch.elementClick(f)}),i.dispatch.on("elementMouseout",function(){e.clearHighlights()}),w.on("changeState",function(a){"undefined"!=typeof a.disabled&&j.length===a.disabled.length&&(j.forEach(function(b,c){b.disabled=a.disabled[c]}),t.disabled=a.disabled),b.update()})}),y.renderEnd("lineChart immediate"),b}var c,d,e=a.models.line(),f=a.models.axis(),g=a.models.axis(),h=a.models.legend(),i=a.interactiveGuideline(),j=a.models.tooltip(),k={top:30,right:20,bottom:50,left:60},l=a.utils.defaultColor(),m=null,n=null,o=!0,p=!0,q=!0,r=!1,s=!1,t=a.utils.state(),u=null,v=null,w=d3.dispatch("tooltipShow","tooltipHide","stateChange","changeState","renderEnd"),x=250;f.orient("bottom").tickPadding(7),g.orient(r?"right":"left"),j.valueFormatter(function(a,b){return g.tickFormat()(a,b)}).headerFormatter(function(a,b){return f.tickFormat()(a,b)});var y=a.utils.renderWatch(w,x),z=function(a){return function(){return{active:a.map(function(a){return!a.disabled})}}},A=function(a){return function(b){void 0!==b.active&&a.forEach(function(a,c){a.disabled=!b.active[c]})}};return e.dispatch.on("elementMouseover.tooltip",function(a){j.data(a).position(a.pos).hidden(!1)}),e.dispatch.on("elementMouseout.tooltip",function(){j.hidden(!0)}),b.dispatch=w,b.lines=e,b.legend=h,b.xAxis=f,b.yAxis=g,b.interactiveLayer=i,b.tooltip=j,b.dispatch=w,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return m},set:function(a){m=a}},height:{get:function(){return n},set:function(a){n=a}},showLegend:{get:function(){return o},set:function(a){o=a}},showXAxis:{get:function(){return p},set:function(a){p=a}},showYAxis:{get:function(){return q},set:function(a){q=a}},defaultState:{get:function(){return u},set:function(a){u=a}},noData:{get:function(){return v},set:function(a){v=a}},tooltips:{get:function(){return j.enabled()},set:function(b){a.deprecated("tooltips","use chart.tooltip.enabled() instead"),j.enabled(!!b)}},tooltipContent:{get:function(){return j.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),j.contentGeLine truncated
|
||||
var J=z.selectAll("g.nv-wrap.nv-multiBarWithLegend").data([j]),K=J.enter().append("g").attr("class","nvd3 nv-wrap nv-multiBarWithLegend").append("g"),L=J.select("g");if(K.append("g").attr("class","nv-x nv-axis"),K.append("g").attr("class","nv-y nv-axis"),K.append("g").attr("class","nv-barsWrap"),K.append("g").attr("class","nv-legendWrap"),K.append("g").attr("class","nv-controlsWrap"),q&&(h.width(D-B()),L.select(".nv-legendWrap").datum(j).call(h),k.top!=h.height()&&(k.top=h.height(),H=a.utils.availableHeight(m,z,k)),L.select(".nv-legendWrap").attr("transform","translate("+B()+","+-k.top+")")),o){var M=[{key:p.grouped||"Grouped",disabled:e.stacked()},{key:p.stacked||"Stacked",disabled:!e.stacked()}];i.width(B()).color(["#444","#444","#444"]),L.select(".nv-controlsWrap").datum(M).attr("transform","translate(0,"+-k.top+")").call(i)}J.attr("transform","translate("+k.left+","+k.top+")"),t&&L.select(".nv-y.nv-axis").attr("transform","translate("+D+",0)"),e.disabled(j.map(function(a){return a.disabled})).width(D).height(H).color(j.map(function(a,b){return a.color||n(a,b)}).filter(function(a,b){return!j[b].disabled}));var N=L.select(".nv-barsWrap").datum(j.filter(function(a){return!a.disabled}));if(N.call(e),r){f.scale(c)._ticks(a.utils.calcTicksX(D/100,j)).tickSize(-H,0),L.select(".nv-x.nv-axis").attr("transform","translate(0,"+d.range()[0]+")"),L.select(".nv-x.nv-axis").call(f);var O=L.select(".nv-x.nv-axis > g").selectAll("g");if(O.selectAll("line, text").style("opacity",1),v){var P=function(a,b){return"translate("+a+","+b+")"},Q=5,R=17;O.selectAll("text").attr("transform",function(a,b,c){return P(0,c%2==0?Q:R)});var S=d3.selectAll(".nv-x.nv-axis .nv-wrap g g text")[0].length;L.selectAll(".nv-x.nv-axis .nv-axisMaxMin text").attr("transform",function(a,b){return P(0,0===b||S%2!==0?R:Q)})}u&&O.filter(function(a,b){return b%Math.ceil(j[0].values.length/(D/100))!==0}).selectAll("text, line").style("opacity",0),w&&O.selectAll(".tick text").attr("transform","rotate("+w+" 0,0)").style("text-anchor",w>0?"start":"end"),L.select(".nv-x.nv-axis").selectAll("g.nv-axisMaxMin text").style("opacity",1)}s&&(g.scale(d)._ticks(a.utils.calcTicksY(H/36,j)).tickSize(-D,0),L.select(".nv-y.nv-axis").call(g)),h.dispatch.on("stateChange",function(a){for(var c in a)x[c]=a[c];A.stateChange(x),b.update()}),i.dispatch.on("legendClick",function(a){if(a.disabled){switch(M=M.map(function(a){return a.disabled=!0,a}),a.disabled=!1,a.key){case"Grouped":case p.grouped:e.stacked(!1);break;case"Stacked":case p.stacked:e.stacked(!0)}x.stacked=e.stacked(),A.stateChange(x),b.update()}}),A.on("changeState",function(a){"undefined"!=typeof a.disabled&&(j.forEach(function(b,c){b.disabled=a.disabled[c]}),x.disabled=a.disabled),"undefined"!=typeof a.stacked&&(e.stacked(a.stacked),x.stacked=a.stacked,E=a.stacked),b.update()})}),D.renderEnd("multibarchart immediate"),b}var c,d,e=a.models.multiBar(),f=a.models.axis(),g=a.models.axis(),h=a.models.legend(),i=a.models.legend(),j=a.models.tooltip(),k={top:30,right:20,bottom:50,left:60},l=null,m=null,n=a.utils.defaultColor(),o=!0,p={},q=!0,r=!0,s=!0,t=!1,u=!0,v=!1,w=0,x=a.utils.state(),y=null,z=null,A=d3.dispatch("stateChange","changeState","renderEnd"),B=function(){return o?180:0},C=250;x.stacked=!1,e.stacked(!1),f.orient("bottom").tickPadding(7).showMaxMin(!1).tickFormat(function(a){return a}),g.orient(t?"right":"left").tickFormat(d3.format(",.1f")),j.duration(0).valueFormatter(function(a,b){return g.tickFormat()(a,b)}).headerFormatter(function(a,b){return f.tickFormat()(a,b)}),i.updateState(!1);var D=a.utils.renderWatch(A),E=!1,F=function(a){return function(){return{active:a.map(function(a){return!a.disabled}),stacked:E}}},G=function(a){return function(b){void 0!==b.stacked&&(E=b.stacked),void 0!==b.active&&a.forEach(function(a,c){a.disabled=!b.active[c]})}};return e.dispatch.on("elementMouseover.tooltip",function(a){a.value=b.x()(a.data),a.series={key:a.data.key,value:b.y()(a.data),color:a.color},j.data(a).hidden(!1)}),e.dispatch.on("elementMouseout.tooltip",function(){j.hidden(!0)}),e.dispatch.on("elementMousemove.tooltip",function(){j.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.dispatch=A,b.multibar=e,b.legend=h,b.controls=i,b.xAxis=f,b.yAxis=g,b.state=x,b.tooltip=j,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return l},set:function(a){l=a}},height:{get:function(){return m},set:function(a){m=a}},showLegend:{get:function(){return q},set:function(a){q=a}},showControls:{get:function(){return o},set:function(a){o=a}},controlLabels:{get:function(){return p},set:function(a){p=a}},showXAxis:{get:function(){return r},set:function(a){r=a}},showYAxis:{get:function(){return s},set:function(a){s=a}},defaultState:{get:function(){return y},set:function(a){y=a}},noData:{get:function(){return z},set:function(a){z=a}},reduceXTicks:{get:function(){return u},set:function(a){u=a}},rotateLabels:{get:function(){return w},set:function(a){w=a}},staggerLabels:{get:function(){retLine truncated
|
||||
}},color:{get:function(){return x},set:function(b){x=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.parallelCoordinates=function(){"use strict";function b(p){return p.each(function(b){function p(a){return F(h.map(function(b){if(isNaN(a[b])||isNaN(parseFloat(a[b]))){var c=g[b].domain(),d=g[b].range(),e=c[0]-(c[1]-c[0])/9;if(J.indexOf(b)<0){var h=d3.scale.linear().domain([e,c[1]]).range([x-12,d[1]]);g[b].brush.y(h),J.push(b)}return[f(b),g[b](e)]}return J.length>0?(D.style("display","inline"),E.style("display","inline")):(D.style("display","none"),E.style("display","none")),[f(b),g[b](a[b])]}))}function q(){var a=h.filter(function(a){return!g[a].brush.empty()}),b=a.map(function(a){return g[a].brush.extent()});k=[],a.forEach(function(a,c){k[c]={dimension:a,extent:b[c]}}),l=[],M.style("display",function(c){var d=a.every(function(a,d){return isNaN(c[a])&&b[d][0]==g[a].brush.y().domain()[0]?!0:b[d][0]<=c[a]&&c[a]<=b[d][1]});return d&&l.push(c),d?null:"none"}),o.brush({filters:k,active:l})}function r(a){m[a]=this.parentNode.__origin__=f(a),L.attr("visibility","hidden")}function s(a){m[a]=Math.min(w,Math.max(0,this.parentNode.__origin__+=d3.event.x)),M.attr("d",p),h.sort(function(a,b){return u(a)-u(b)}),f.domain(h),N.attr("transform",function(a){return"translate("+u(a)+")"})}function t(a){delete this.parentNode.__origin__,delete m[a],d3.select(this.parentNode).attr("transform","translate("+f(a)+")"),M.attr("d",p),L.attr("d",p).attr("visibility",null)}function u(a){var b=m[a];return null==b?f(a):b}var v=d3.select(this),w=a.utils.availableWidth(d,v,c),x=a.utils.availableHeight(e,v,c);a.utils.initSVG(v),l=b,f.rangePoints([0,w],1).domain(h);var y={};h.forEach(function(a){var c=d3.extent(b,function(b){return+b[a]});return y[a]=!1,void 0===c[0]&&(y[a]=!0,c[0]=0,c[1]=0),c[0]===c[1]&&(c[0]=c[0]-1,c[1]=c[1]+1),g[a]=d3.scale.linear().domain(c).range([.9*(x-12),0]),g[a].brush=d3.svg.brush().y(g[a]).on("brush",q),"name"!=a});var z=v.selectAll("g.nv-wrap.nv-parallelCoordinates").data([b]),A=z.enter().append("g").attr("class","nvd3 nv-wrap nv-parallelCoordinates"),B=A.append("g"),C=z.select("g");B.append("g").attr("class","nv-parallelCoordinates background"),B.append("g").attr("class","nv-parallelCoordinates foreground"),B.append("g").attr("class","nv-parallelCoordinates missingValuesline"),z.attr("transform","translate("+c.left+","+c.top+")");var D,E,F=d3.svg.line().interpolate("cardinal").tension(n),G=d3.svg.axis().orient("left"),H=d3.behavior.drag().on("dragstart",r).on("drag",s).on("dragend",t),I=f.range()[1]-f.range()[0],J=[],K=[0+I/2,x-12,w-I/2,x-12];D=z.select(".missingValuesline").selectAll("line").data([K]),D.enter().append("line"),D.exit().remove(),D.attr("x1",function(a){return a[0]}).attr("y1",function(a){return a[1]}).attr("x2",function(a){return a[2]}).attr("y2",function(a){return a[3]}),E=z.select(".missingValuesline").selectAll("text").data(["undefined values"]),E.append("text").data(["undefined values"]),E.enter().append("text"),E.exit().remove(),E.attr("y",x).attr("x",w-92-I/2).text(function(a){return a});var L=z.select(".background").selectAll("path").data(b);L.enter().append("path"),L.exit().remove(),L.attr("d",p);var M=z.select(".foreground").selectAll("path").data(b);M.enter().append("path"),M.exit().remove(),M.attr("d",p).attr("stroke",j),M.on("mouseover",function(a,b){d3.select(this).classed("hover",!0),o.elementMouseover({label:a.name,data:a.data,index:b,pos:[d3.mouse(this.parentNode)[0],d3.mouse(this.parentNode)[1]]})}),M.on("mouseout",function(a,b){d3.select(this).classed("hover",!1),o.elementMouseout({label:a.name,data:a.data,index:b})});var N=C.selectAll(".dimension").data(h),O=N.enter().append("g").attr("class","nv-parallelCoordinates dimension");O.append("g").attr("class","nv-parallelCoordinates nv-axis"),O.append("g").attr("class","nv-parallelCoordinates-brush"),O.append("text").attr("class","nv-parallelCoordinates nv-label"),N.attr("transform",function(a){return"translate("+f(a)+",0)"}),N.exit().remove(),N.select(".nv-label").style("cursor","move").attr("dy","-1em").attr("text-anchor","middle").text(String).on("mouseover",function(a){o.elementMouseover({dim:a,pos:[d3.mouse(this.parentNode.parentNode)[0],d3.mouse(this.parentNode.parentNode)[1]]})}).on("mouseout",function(a){o.elementMouseout({dim:a})}).call(H),N.select(".nv-axis").each(function(a,b){d3.select(this).call(G.scale(g[a]).tickFormat(d3.format(i[b])))}),N.select(".nv-parallelCoordinates-brush").each(function(a){d3.select(this).call(g[a].brush)}).selectAll("rect").attr("x",-8).attr("width",16)}),b}var c={top:30,right:0,bottom:10,left:0},d=null,e=null,f=d3.scale.ordinal(),g={},h=[],i=[],j=a.utils.defaultColor(),k=[],l=[],m=[],n=1,o=d3.dispatch("brush","elementMouseover","elementMouseout");return b.dispatch=o,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return d},set:function(a){d=a}},height:{get:function(){return e},set:function(a){e=a}},dimensionNames:{get:function(){return h},set:fLine truncated
|
||||
}},tooltipContent:{get:function(){return i.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),i.contentGenerator(b)}},tooltipXContent:{get:function(){return i.contentGenerator()},set:function(){a.deprecated("tooltipContent","This option is removed, put values into main tooltip.")}},tooltipYContent:{get:function(){return i.contentGenerator()},set:function(){a.deprecated("tooltipContent","This option is removed, put values into main tooltip.")}},margin:{get:function(){return j},set:function(a){j.top=void 0!==a.top?a.top:j.top,j.right=void 0!==a.right?a.right:j.right,j.bottom=void 0!==a.bottom?a.bottom:j.bottom,j.left=void 0!==a.left?a.left:j.left}},rightAlignYAxis:{get:function(){return v},set:function(a){v=a,e.orient(a?"right":"left")}},color:{get:function(){return n},set:function(b){n=a.utils.getColor(b),f.color(n),g.color(n),h.color(n)}}}),a.utils.inheritOptions(b,c),a.utils.initOptions(b),b},a.models.sparkline=function(){"use strict";function b(k){return k.each(function(b){var k=h-g.left-g.right,q=i-g.top-g.bottom;j=d3.select(this),a.utils.initSVG(j),l.domain(c||d3.extent(b,n)).range(e||[0,k]),m.domain(d||d3.extent(b,o)).range(f||[q,0]);{var r=j.selectAll("g.nv-wrap.nv-sparkline").data([b]),s=r.enter().append("g").attr("class","nvd3 nv-wrap nv-sparkline");s.append("g"),r.select("g")}r.attr("transform","translate("+g.left+","+g.top+")");var t=r.selectAll("path").data(function(a){return[a]});t.enter().append("path"),t.exit().remove(),t.style("stroke",function(a,b){return a.color||p(a,b)}).attr("d",d3.svg.line().x(function(a,b){return l(n(a,b))}).y(function(a,b){return m(o(a,b))}));var u=r.selectAll("circle.nv-point").data(function(a){function b(b){if(-1!=b){var c=a[b];return c.pointIndex=b,c}return null}var c=a.map(function(a,b){return o(a,b)}),d=b(c.lastIndexOf(m.domain()[1])),e=b(c.indexOf(m.domain()[0])),f=b(c.length-1);return[e,d,f].filter(function(a){return null!=a})});u.enter().append("circle"),u.exit().remove(),u.attr("cx",function(a){return l(n(a,a.pointIndex))}).attr("cy",function(a){return m(o(a,a.pointIndex))}).attr("r",2).attr("class",function(a){return n(a,a.pointIndex)==l.domain()[1]?"nv-point nv-currentValue":o(a,a.pointIndex)==m.domain()[0]?"nv-point nv-minValue":"nv-point nv-maxValue"})}),b}var c,d,e,f,g={top:2,right:0,bottom:2,left:0},h=400,i=32,j=null,k=!0,l=d3.scale.linear(),m=d3.scale.linear(),n=function(a){return a.x},o=function(a){return a.y},p=a.utils.getColor(["#000"]);return b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return h},set:function(a){h=a}},height:{get:function(){return i},set:function(a){i=a}},xDomain:{get:function(){return c},set:function(a){c=a}},yDomain:{get:function(){return d},set:function(a){d=a}},xRange:{get:function(){return e},set:function(a){e=a}},yRange:{get:function(){return f},set:function(a){f=a}},xScale:{get:function(){return l},set:function(a){l=a}},yScale:{get:function(){return m},set:function(a){m=a}},animate:{get:function(){return k},set:function(a){k=a}},x:{get:function(){return n},set:function(a){n=d3.functor(a)}},y:{get:function(){return o},set:function(a){o=d3.functor(a)}},margin:{get:function(){return g},set:function(a){g.top=void 0!==a.top?a.top:g.top,g.right=void 0!==a.right?a.right:g.right,g.bottom=void 0!==a.bottom?a.bottom:g.bottom,g.left=void 0!==a.left?a.left:g.left}},color:{get:function(){return p},set:function(b){p=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.sparklinePlus=function(){"use strict";function b(p){return p.each(function(p){function q(){if(!j){var a=z.selectAll(".nv-hoverValue").data(i),b=a.enter().append("g").attr("class","nv-hoverValue").style("stroke-opacity",0).style("fill-opacity",0);a.exit().transition().duration(250).style("stroke-opacity",0).style("fill-opacity",0).remove(),a.attr("transform",function(a){return"translate("+c(e.x()(p[a],a))+",0)"}).transition().duration(250).style("stroke-opacity",1).style("fill-opacity",1),i.length&&(b.append("line").attr("x1",0).attr("y1",-f.top).attr("x2",0).attr("y2",u),b.append("text").attr("class","nv-xValue").attr("x",-6).attr("y",-f.top).attr("text-anchor","end").attr("dy",".9em"),z.select(".nv-hoverValue .nv-xValue").text(k(e.x()(p[i[0]],i[0]))),b.append("text").attr("class","nv-yValue").attr("x",6).attr("y",-f.top).attr("text-anchor","start").attr("dy",".9em"),z.select(".nv-hoverValue .nv-yValue").text(l(e.y()(p[i[0]],i[0]))))}}function r(){function a(a,b){for(var c=Math.abs(e.x()(a[0],0)-b),d=0,f=0;f<a.length;f++)Math.abs(e.x()(a[f],f)-b)<c&&(c=Math.abs(e.x()(a[f],f)-b),d=f);return d}if(!j){var b=d3.mouse(this)[0]-f.left;i=[a(p,Math.round(c.invert(b)))],q()}}var s=d3.select(this);a.utils.initSVG(s);var t=a.utils.availableWidth(g,s,f),u=a.utils.availableHeight(h,s,f);if(b.update=function(){s.call(b)},b.container=this,!p||!p.length)return a.utils.noData(b,s),b;s.selectAll(".nv-noData").remove();var v=e.y()(p[p.length-1],p.length-1);c=e.xScale(),d=e.yScale();Line truncated
|
||||
+7
-11
@@ -10,16 +10,18 @@
|
||||
namespace SebastianBergmann\CodeCoverage\Report;
|
||||
|
||||
use const PHP_EOL;
|
||||
use function dirname;
|
||||
use function file_put_contents;
|
||||
use function serialize;
|
||||
use function str_contains;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
|
||||
use SebastianBergmann\CodeCoverage\Util\Filesystem;
|
||||
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
|
||||
|
||||
final class PHP
|
||||
{
|
||||
/**
|
||||
* @param null|non-empty-string $target
|
||||
*
|
||||
* @throws WriteOperationFailedException
|
||||
*/
|
||||
public function process(CodeCoverage $coverage, ?string $target = null): string
|
||||
{
|
||||
$coverage->clearCache();
|
||||
@@ -28,13 +30,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 (!str_contains($target, '://')) {
|
||||
Filesystem::createDirectory(dirname($target));
|
||||
}
|
||||
|
||||
if (@file_put_contents($target, $buffer) === false) {
|
||||
throw new WriteOperationFailedException($target);
|
||||
}
|
||||
Filesystem::write($target, $buffer);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
|
||||
+9
-25
@@ -23,30 +23,11 @@ use SebastianBergmann\CodeCoverage\Util\Percentage;
|
||||
|
||||
final class Text
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const COLOR_GREEN = "\x1b[30;42m";
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const COLOR_YELLOW = "\x1b[30;43m";
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const COLOR_RED = "\x1b[37;41m";
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const COLOR_HEADER = "\x1b[1;37;40m";
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const COLOR_RESET = "\x1b[0m";
|
||||
private const string COLOR_GREEN = "\x1b[30;42m";
|
||||
private const string COLOR_YELLOW = "\x1b[30;43m";
|
||||
private const string COLOR_RED = "\x1b[37;41m";
|
||||
private const string COLOR_HEADER = "\x1b[1;37;40m";
|
||||
private const string COLOR_RESET = "\x1b[0m";
|
||||
private readonly Thresholds $thresholds;
|
||||
private readonly bool $showUncoveredFiles;
|
||||
private readonly bool $showOnlySummary;
|
||||
@@ -60,7 +41,7 @@ final class Text
|
||||
|
||||
public function process(CodeCoverage $coverage, bool $showColors = false): string
|
||||
{
|
||||
$hasBranchCoverage = !empty($coverage->getData(true)->functionCoverage());
|
||||
$hasBranchCoverage = $coverage->getData(true)->functionCoverage() !== [];
|
||||
|
||||
$output = PHP_EOL . PHP_EOL;
|
||||
$report = $coverage->getReport();
|
||||
@@ -210,6 +191,7 @@ final class Text
|
||||
$classMethods = 0;
|
||||
|
||||
foreach ($class['methods'] as $method) {
|
||||
/** @phpstan-ignore equal.notAllowed */
|
||||
if ($method['executableLines'] == 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -222,6 +204,7 @@ final class Text
|
||||
$classExecutablePaths += $method['executablePaths'];
|
||||
$classExecutedPaths += $method['executedPaths'];
|
||||
|
||||
/** @phpstan-ignore equal.notAllowed */
|
||||
if ($method['coverage'] == 100) {
|
||||
$coveredMethods++;
|
||||
}
|
||||
@@ -251,6 +234,7 @@ final class Text
|
||||
$resetColor = '';
|
||||
|
||||
foreach ($classCoverage as $fullQualifiedPath => $classInfo) {
|
||||
/** @phpstan-ignore notEqual.notAllowed */
|
||||
if ($this->showUncoveredFiles || $classInfo['statementsCovered'] != 0) {
|
||||
if ($showColors) {
|
||||
$methodColor = $this->coverageColor($classInfo['methodsCovered'], $classInfo['methodCount']);
|
||||
|
||||
@@ -14,10 +14,10 @@ use SebastianBergmann\CodeCoverage\InvalidArgumentException;
|
||||
/**
|
||||
* @immutable
|
||||
*/
|
||||
final class Thresholds
|
||||
final readonly class Thresholds
|
||||
{
|
||||
private readonly int $lowUpperBound;
|
||||
private readonly int $highLowerBound;
|
||||
private int $lowUpperBound;
|
||||
private int $highLowerBound;
|
||||
|
||||
public static function default(): self
|
||||
{
|
||||
|
||||
@@ -18,9 +18,9 @@ use SebastianBergmann\Environment\Runtime;
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class BuildInformation
|
||||
final readonly class BuildInformation
|
||||
{
|
||||
private readonly DOMElement $contextNode;
|
||||
private DOMElement $contextNode;
|
||||
|
||||
public function __construct(DOMElement $contextNode)
|
||||
{
|
||||
@@ -66,7 +66,7 @@ final class BuildInformation
|
||||
$name,
|
||||
)->item(0);
|
||||
|
||||
if (!$node) {
|
||||
if ($node === null) {
|
||||
$node = $this->contextNode->appendChild(
|
||||
$this->contextNode->ownerDocument->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
|
||||
@@ -28,7 +28,7 @@ final class Coverage
|
||||
|
||||
$this->writer = new XMLWriter;
|
||||
$this->writer->openMemory();
|
||||
$this->writer->startElementNS(null, $context->nodeName, 'https://schema.phpunit.de/coverage/1.0');
|
||||
$this->writer->startElementNs(null, $context->nodeName, 'https://schema.phpunit.de/coverage/1.0');
|
||||
$this->writer->writeAttribute('nr', $line);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ final class Coverage
|
||||
public function addTest(string $test): void
|
||||
{
|
||||
if ($this->finalized) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new ReportAlreadyFinalizedException;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$this->writer->startElement('covered');
|
||||
|
||||
+27
-41
@@ -10,34 +10,37 @@
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const PHP_EOL;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function is_array;
|
||||
use function is_dir;
|
||||
use function is_file;
|
||||
use function is_writable;
|
||||
use function libxml_clear_errors;
|
||||
use function libxml_get_errors;
|
||||
use function libxml_use_internal_errors;
|
||||
use function sprintf;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use DateTimeImmutable;
|
||||
use DOMDocument;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Driver\PathExistsButIsNotDirectoryException;
|
||||
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
|
||||
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
|
||||
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
|
||||
use SebastianBergmann\CodeCoverage\Node\File;
|
||||
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
|
||||
use SebastianBergmann\CodeCoverage\Util\Filesystem as DirectoryUtil;
|
||||
use SebastianBergmann\CodeCoverage\PathExistsButIsNotDirectoryException;
|
||||
use SebastianBergmann\CodeCoverage\Util\Filesystem;
|
||||
use SebastianBergmann\CodeCoverage\Util\Xml;
|
||||
use SebastianBergmann\CodeCoverage\Version;
|
||||
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
|
||||
use SebastianBergmann\CodeCoverage\XmlException;
|
||||
use SebastianBergmann\Environment\Runtime;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ProcessedClassType from File
|
||||
* @phpstan-import-type ProcessedTraitType from File
|
||||
* @phpstan-import-type ProcessedFunctionType from File
|
||||
* @phpstan-import-type TestType from CodeCoverage
|
||||
*/
|
||||
final class Facade
|
||||
{
|
||||
private string $target;
|
||||
@@ -89,6 +92,7 @@ final class Facade
|
||||
private function initTargetDirectory(string $directory): void
|
||||
{
|
||||
if (is_file($directory)) {
|
||||
// @codeCoverageIgnoreStart
|
||||
if (!is_dir($directory)) {
|
||||
throw new PathExistsButIsNotDirectoryException($directory);
|
||||
}
|
||||
@@ -96,9 +100,10 @@ final class Facade
|
||||
if (!is_writable($directory)) {
|
||||
throw new WriteOperationFailedException($directory);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
DirectoryUtil::createDirectory($directory);
|
||||
Filesystem::createDirectory($directory);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,6 +180,9 @@ final class Facade
|
||||
$this->saveDocument($fileReport->asDom(), $file->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProcessedClassType|ProcessedTraitType $unit
|
||||
*/
|
||||
private function processUnit(array $unit, Report $report): void
|
||||
{
|
||||
if (isset($unit['className'])) {
|
||||
@@ -205,6 +213,9 @@ final class Facade
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProcessedFunctionType $function
|
||||
*/
|
||||
private function processFunction(array $function, Report $report): void
|
||||
{
|
||||
$functionObject = $report->functionObject($function['functionName']);
|
||||
@@ -215,6 +226,9 @@ final class Facade
|
||||
$functionObject->setTotals((string) $function['executableLines'], (string) $function['executedLines'], (string) $function['coverage']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, TestType> $tests
|
||||
*/
|
||||
private function processTests(array $tests): void
|
||||
{
|
||||
$testsObject = $this->project->tests();
|
||||
@@ -229,9 +243,9 @@ final class Facade
|
||||
$loc = $node->linesOfCode();
|
||||
|
||||
$totals->setNumLines(
|
||||
$loc['linesOfCode'],
|
||||
$loc['commentLinesOfCode'],
|
||||
$loc['nonCommentLinesOfCode'],
|
||||
$loc->linesOfCode(),
|
||||
$loc->commentLinesOfCode(),
|
||||
$loc->nonCommentLinesOfCode(),
|
||||
$node->numberOfExecutableLines(),
|
||||
$node->numberOfExecutedLines(),
|
||||
);
|
||||
@@ -269,36 +283,8 @@ final class Facade
|
||||
{
|
||||
$filename = sprintf('%s/%s.xml', $this->targetDirectory(), $name);
|
||||
|
||||
$document->formatOutput = true;
|
||||
$document->preserveWhiteSpace = false;
|
||||
$this->initTargetDirectory(dirname($filename));
|
||||
|
||||
file_put_contents($filename, $this->documentAsString($document));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws XmlException
|
||||
*
|
||||
* @see https://bugs.php.net/bug.php?id=79191
|
||||
*/
|
||||
private function documentAsString(DOMDocument $document): string
|
||||
{
|
||||
$xmlErrorHandling = libxml_use_internal_errors(true);
|
||||
$xml = $document->saveXML();
|
||||
|
||||
if ($xml === false) {
|
||||
$message = 'Unable to generate the XML';
|
||||
|
||||
foreach (libxml_get_errors() as $error) {
|
||||
$message .= PHP_EOL . $error->message;
|
||||
}
|
||||
|
||||
throw new XmlException($message);
|
||||
}
|
||||
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($xmlErrorHandling);
|
||||
|
||||
return $xml;
|
||||
Filesystem::write($filename, Xml::asString($document));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use function assert;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
|
||||
@@ -30,7 +31,7 @@ class File
|
||||
{
|
||||
$totalsContainer = $this->contextNode->firstChild;
|
||||
|
||||
if (!$totalsContainer) {
|
||||
if ($totalsContainer === null) {
|
||||
$totalsContainer = $this->contextNode->appendChild(
|
||||
$this->dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
@@ -39,6 +40,8 @@ class File
|
||||
);
|
||||
}
|
||||
|
||||
assert($totalsContainer instanceof DOMElement);
|
||||
|
||||
return new Totals($totalsContainer);
|
||||
}
|
||||
|
||||
@@ -49,7 +52,7 @@ class File
|
||||
'coverage',
|
||||
)->item(0);
|
||||
|
||||
if (!$coverage) {
|
||||
if ($coverage === null) {
|
||||
$coverage = $this->contextNode->appendChild(
|
||||
$this->dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
@@ -65,6 +68,8 @@ class File
|
||||
),
|
||||
);
|
||||
|
||||
assert($lineNode instanceof DOMElement);
|
||||
|
||||
return new Coverage($lineNode, $line);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ use DOMElement;
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Method
|
||||
final readonly class Method
|
||||
{
|
||||
private readonly DOMElement $contextNode;
|
||||
private DOMElement $contextNode;
|
||||
|
||||
public function __construct(DOMElement $context, string $name)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use function assert;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
|
||||
@@ -34,7 +35,7 @@ abstract class Node
|
||||
{
|
||||
$totalsContainer = $this->contextNode()->firstChild;
|
||||
|
||||
if (!$totalsContainer) {
|
||||
if ($totalsContainer === null) {
|
||||
$totalsContainer = $this->contextNode()->appendChild(
|
||||
$this->dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
@@ -43,6 +44,8 @@ abstract class Node
|
||||
);
|
||||
}
|
||||
|
||||
assert($totalsContainer instanceof DOMElement);
|
||||
|
||||
return new Totals($totalsContainer);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,18 @@
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use function assert;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Project extends Node
|
||||
{
|
||||
/**
|
||||
* @phpstan-ignore constructor.missingParentCall
|
||||
*/
|
||||
public function __construct(string $directory)
|
||||
{
|
||||
$this->init();
|
||||
@@ -34,7 +39,7 @@ final class Project extends Node
|
||||
'build',
|
||||
)->item(0);
|
||||
|
||||
if (!$buildNode) {
|
||||
if ($buildNode === null) {
|
||||
$buildNode = $this->dom()->documentElement->appendChild(
|
||||
$this->dom()->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
@@ -43,6 +48,8 @@ final class Project extends Node
|
||||
);
|
||||
}
|
||||
|
||||
assert($buildNode instanceof DOMElement);
|
||||
|
||||
return new BuildInformation($buildNode);
|
||||
}
|
||||
|
||||
@@ -53,7 +60,7 @@ final class Project extends Node
|
||||
'tests',
|
||||
)->item(0);
|
||||
|
||||
if (!$testsNode) {
|
||||
if ($testsNode === null) {
|
||||
$testsNode = $this->contextNode()->appendChild(
|
||||
$this->dom()->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
@@ -62,6 +69,8 @@ final class Project extends Node
|
||||
);
|
||||
}
|
||||
|
||||
assert($testsNode instanceof DOMElement);
|
||||
|
||||
return new Tests($testsNode);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use function assert;
|
||||
use function basename;
|
||||
use function dirname;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
@@ -38,7 +40,7 @@ final class Report extends File
|
||||
return $this->dom();
|
||||
}
|
||||
|
||||
public function functionObject($name): Method
|
||||
public function functionObject(string $name): Method
|
||||
{
|
||||
$node = $this->contextNode()->appendChild(
|
||||
$this->dom()->createElementNS(
|
||||
@@ -47,15 +49,17 @@ final class Report extends File
|
||||
),
|
||||
);
|
||||
|
||||
assert($node instanceof DOMElement);
|
||||
|
||||
return new Method($node, $name);
|
||||
}
|
||||
|
||||
public function classObject($name): Unit
|
||||
public function classObject(string $name): Unit
|
||||
{
|
||||
return $this->unitObject('class', $name);
|
||||
}
|
||||
|
||||
public function traitObject($name): Unit
|
||||
public function traitObject(string $name): Unit
|
||||
{
|
||||
return $this->unitObject('trait', $name);
|
||||
}
|
||||
@@ -67,7 +71,7 @@ final class Report extends File
|
||||
'source',
|
||||
)->item(0);
|
||||
|
||||
if (!$source) {
|
||||
if ($source === null) {
|
||||
$source = $this->contextNode()->appendChild(
|
||||
$this->dom()->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
@@ -76,6 +80,8 @@ final class Report extends File
|
||||
);
|
||||
}
|
||||
|
||||
assert($source instanceof DOMElement);
|
||||
|
||||
return new Source($source);
|
||||
}
|
||||
|
||||
@@ -85,7 +91,7 @@ final class Report extends File
|
||||
$this->contextNode()->setAttribute('path', dirname($name));
|
||||
}
|
||||
|
||||
private function unitObject(string $tagName, $name): Unit
|
||||
private function unitObject(string $tagName, string $name): Unit
|
||||
{
|
||||
$node = $this->contextNode()->appendChild(
|
||||
$this->dom()->createElementNS(
|
||||
@@ -94,6 +100,8 @@ final class Report extends File
|
||||
),
|
||||
);
|
||||
|
||||
assert($node instanceof DOMElement);
|
||||
|
||||
return new Unit($node, $name);
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,9 @@ use TheSeer\Tokenizer\XMLSerializer;
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Source
|
||||
final readonly class Source
|
||||
{
|
||||
private readonly DOMElement $context;
|
||||
private DOMElement $context;
|
||||
|
||||
public function __construct(DOMElement $context)
|
||||
{
|
||||
|
||||
@@ -11,15 +11,16 @@ namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use function assert;
|
||||
use DOMElement;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type TestType from \SebastianBergmann\CodeCoverage\CodeCoverage
|
||||
* @phpstan-import-type TestType from CodeCoverage
|
||||
*/
|
||||
final class Tests
|
||||
final readonly class Tests
|
||||
{
|
||||
private readonly DOMElement $contextNode;
|
||||
private DOMElement $contextNode;
|
||||
|
||||
public function __construct(DOMElement $context)
|
||||
{
|
||||
|
||||
@@ -11,25 +11,22 @@ namespace SebastianBergmann\CodeCoverage\Report\Xml;
|
||||
|
||||
use function sprintf;
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use SebastianBergmann\CodeCoverage\Util\Percentage;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Totals
|
||||
final readonly class Totals
|
||||
{
|
||||
private readonly DOMNode $container;
|
||||
private readonly DOMElement $linesNode;
|
||||
private readonly DOMElement $methodsNode;
|
||||
private readonly DOMElement $functionsNode;
|
||||
private readonly DOMElement $classesNode;
|
||||
private readonly DOMElement $traitsNode;
|
||||
private DOMElement $linesNode;
|
||||
private DOMElement $methodsNode;
|
||||
private DOMElement $functionsNode;
|
||||
private DOMElement $classesNode;
|
||||
private DOMElement $traitsNode;
|
||||
|
||||
public function __construct(DOMElement $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
$dom = $container->ownerDocument;
|
||||
$dom = $container->ownerDocument;
|
||||
|
||||
$this->linesNode = $dom->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
@@ -63,11 +60,6 @@ final class Totals
|
||||
$container->appendChild($this->traitsNode);
|
||||
}
|
||||
|
||||
public function container(): DOMNode
|
||||
{
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
public function setNumLines(int $loc, int $cloc, int $ncloc, int $executable, int $executed): void
|
||||
{
|
||||
$this->linesNode->setAttribute('total', (string) $loc);
|
||||
|
||||
@@ -15,9 +15,9 @@ use DOMElement;
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Unit
|
||||
final readonly class Unit
|
||||
{
|
||||
private readonly DOMElement $contextNode;
|
||||
private DOMElement $contextNode;
|
||||
|
||||
public function __construct(DOMElement $context, string $name)
|
||||
{
|
||||
@@ -45,7 +45,7 @@ final class Unit
|
||||
'namespace',
|
||||
)->item(0);
|
||||
|
||||
if (!$node) {
|
||||
if ($node === null) {
|
||||
$node = $this->contextNode->appendChild(
|
||||
$this->contextNode->ownerDocument->createElementNS(
|
||||
'https://schema.phpunit.de/coverage/1.0',
|
||||
@@ -68,6 +68,8 @@ final class Unit
|
||||
),
|
||||
);
|
||||
|
||||
assert($node instanceof DOMElement);
|
||||
|
||||
return new Method($node, $name);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,24 +9,36 @@
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
|
||||
|
||||
use function file_get_contents;
|
||||
use SebastianBergmann\CodeCoverage\Filter;
|
||||
|
||||
final class CacheWarmer
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final readonly class CacheWarmer
|
||||
{
|
||||
public function warmCache(string $cacheDirectory, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode, Filter $filter): void
|
||||
/**
|
||||
* @return array{cacheHits: non-negative-int, cacheMisses: non-negative-int}
|
||||
*/
|
||||
public function warmCache(string $cacheDirectory, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode, Filter $filter): array
|
||||
{
|
||||
$analyser = new CachingFileAnalyser(
|
||||
$analyser = new CachingSourceAnalyser(
|
||||
$cacheDirectory,
|
||||
new ParsingFileAnalyser(
|
||||
$useAnnotationsForIgnoringCode,
|
||||
$ignoreDeprecatedCode,
|
||||
),
|
||||
$useAnnotationsForIgnoringCode,
|
||||
$ignoreDeprecatedCode,
|
||||
new ParsingSourceAnalyser,
|
||||
);
|
||||
|
||||
foreach ($filter->files() as $file) {
|
||||
$analyser->process($file);
|
||||
$analyser->analyse(
|
||||
$file,
|
||||
file_get_contents($file),
|
||||
$useAnnotationsForIgnoringCode,
|
||||
$ignoreDeprecatedCode,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'cacheHits' => $analyser->cacheHits(),
|
||||
'cacheMisses' => $analyser->cacheMisses(),
|
||||
];
|
||||
}
|
||||
}
|
||||
-184
@@ -1,184 +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\StaticAnalysis;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function implode;
|
||||
use function is_file;
|
||||
use function md5;
|
||||
use function serialize;
|
||||
use function unserialize;
|
||||
use SebastianBergmann\CodeCoverage\Util\Filesystem;
|
||||
use SebastianBergmann\CodeCoverage\Version;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
* @phpstan-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
*/
|
||||
final class CachingFileAnalyser implements FileAnalyser
|
||||
{
|
||||
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)
|
||||
{
|
||||
Filesystem::createDirectory($directory);
|
||||
|
||||
$this->analyser = $analyser;
|
||||
$this->directory = $directory;
|
||||
$this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
|
||||
$this->ignoreDeprecatedCode = $ignoreDeprecatedCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, CodeUnitClassType>
|
||||
*/
|
||||
public function classesIn(string $filename): array
|
||||
{
|
||||
if (!isset($this->cache[$filename])) {
|
||||
$this->process($filename);
|
||||
}
|
||||
|
||||
return $this->cache[$filename]['classesIn'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, CodeUnitTraitType>
|
||||
*/
|
||||
public function traitsIn(string $filename): array
|
||||
{
|
||||
if (!isset($this->cache[$filename])) {
|
||||
$this->process($filename);
|
||||
}
|
||||
|
||||
return $this->cache[$filename]['traitsIn'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, CodeUnitFunctionType>
|
||||
*/
|
||||
public function functionsIn(string $filename): array
|
||||
{
|
||||
if (!isset($this->cache[$filename])) {
|
||||
$this->process($filename);
|
||||
}
|
||||
|
||||
return $this->cache[$filename]['functionsIn'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinesOfCodeType
|
||||
*/
|
||||
public function linesOfCodeFor(string $filename): array
|
||||
{
|
||||
if (!isset($this->cache[$filename])) {
|
||||
$this->process($filename);
|
||||
}
|
||||
|
||||
return $this->cache[$filename]['linesOfCodeFor'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinesType
|
||||
*/
|
||||
public function executableLinesIn(string $filename): array
|
||||
{
|
||||
if (!isset($this->cache[$filename])) {
|
||||
$this->process($filename);
|
||||
}
|
||||
|
||||
return $this->cache[$filename]['executableLinesIn'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinesType
|
||||
*/
|
||||
public function ignoredLinesFor(string $filename): array
|
||||
{
|
||||
if (!isset($this->cache[$filename])) {
|
||||
$this->process($filename);
|
||||
}
|
||||
|
||||
return $this->cache[$filename]['ignoredLinesFor'];
|
||||
}
|
||||
|
||||
public function process(string $filename): void
|
||||
{
|
||||
$cache = $this->read($filename);
|
||||
|
||||
if ($cache !== false) {
|
||||
$this->cache[$filename] = $cache;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->cache[$filename] = [
|
||||
'classesIn' => $this->analyser->classesIn($filename),
|
||||
'traitsIn' => $this->analyser->traitsIn($filename),
|
||||
'functionsIn' => $this->analyser->functionsIn($filename),
|
||||
'linesOfCodeFor' => $this->analyser->linesOfCodeFor($filename),
|
||||
'ignoredLinesFor' => $this->analyser->ignoredLinesFor($filename),
|
||||
'executableLinesIn' => $this->analyser->executableLinesIn($filename),
|
||||
];
|
||||
|
||||
$this->write($filename, $this->cache[$filename]);
|
||||
}
|
||||
|
||||
private function read(string $filename): array|false
|
||||
{
|
||||
$cacheFile = $this->cacheFile($filename);
|
||||
|
||||
if (!is_file($cacheFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return unserialize(
|
||||
file_get_contents($cacheFile),
|
||||
['allowed_classes' => false],
|
||||
);
|
||||
}
|
||||
|
||||
private function write(string $filename, array $data): void
|
||||
{
|
||||
file_put_contents(
|
||||
$this->cacheFile($filename),
|
||||
serialize($data),
|
||||
);
|
||||
}
|
||||
|
||||
private function cacheFile(string $filename): string
|
||||
{
|
||||
$cacheKey = md5(
|
||||
implode(
|
||||
"\0",
|
||||
[
|
||||
$filename,
|
||||
file_get_contents($filename),
|
||||
Version::id(),
|
||||
$this->useAnnotationsForIgnoringCode,
|
||||
$this->ignoreDeprecatedCode,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return $this->directory . DIRECTORY_SEPARATOR . $cacheKey;
|
||||
}
|
||||
}
|
||||
-360
@@ -1,360 +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\StaticAnalysis;
|
||||
|
||||
use function assert;
|
||||
use function implode;
|
||||
use function rtrim;
|
||||
use function trim;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\ComplexType;
|
||||
use PhpParser\Node\Identifier;
|
||||
use PhpParser\Node\IntersectionType;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\NullableType;
|
||||
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_;
|
||||
use PhpParser\Node\UnionType;
|
||||
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
|
||||
*
|
||||
* @phpstan-type CodeUnitFunctionType = array{
|
||||
* name: string,
|
||||
* namespacedName: string,
|
||||
* namespace: string,
|
||||
* signature: string,
|
||||
* startLine: int,
|
||||
* endLine: int,
|
||||
* ccn: int
|
||||
* }
|
||||
* @phpstan-type CodeUnitMethodType = array{
|
||||
* methodName: string,
|
||||
* signature: string,
|
||||
* visibility: string,
|
||||
* startLine: int,
|
||||
* endLine: int,
|
||||
* ccn: int
|
||||
* }
|
||||
* @phpstan-type CodeUnitClassType = array{
|
||||
* name: string,
|
||||
* namespacedName: string,
|
||||
* namespace: string,
|
||||
* startLine: int,
|
||||
* endLine: int,
|
||||
* methods: array<string, CodeUnitMethodType>
|
||||
* }
|
||||
* @phpstan-type CodeUnitTraitType = array{
|
||||
* name: string,
|
||||
* namespacedName: string,
|
||||
* namespace: string,
|
||||
* startLine: int,
|
||||
* endLine: int,
|
||||
* methods: array<string, CodeUnitMethodType>
|
||||
* }
|
||||
*/
|
||||
final class CodeUnitFindingVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
/**
|
||||
* @var array<string, CodeUnitClassType>
|
||||
*/
|
||||
private array $classes = [];
|
||||
|
||||
/**
|
||||
* @var array<string, CodeUnitTraitType>
|
||||
*/
|
||||
private array $traits = [];
|
||||
|
||||
/**
|
||||
* @var array<string, CodeUnitFunctionType>
|
||||
*/
|
||||
private array $functions = [];
|
||||
|
||||
public function enterNode(Node $node): void
|
||||
{
|
||||
if ($node instanceof Class_) {
|
||||
if ($node->isAnonymous()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processClass($node);
|
||||
}
|
||||
|
||||
if ($node instanceof Trait_) {
|
||||
$this->processTrait($node);
|
||||
}
|
||||
|
||||
if (!$node instanceof ClassMethod && !$node instanceof Function_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof ClassMethod) {
|
||||
$parentNode = $node->getAttribute('parent');
|
||||
|
||||
if ($parentNode instanceof Class_ && $parentNode->isAnonymous()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processMethod($node);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processFunction($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, CodeUnitClassType>
|
||||
*/
|
||||
public function classes(): array
|
||||
{
|
||||
return $this->classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, CodeUnitTraitType>
|
||||
*/
|
||||
public function traits(): array
|
||||
{
|
||||
return $this->traits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, CodeUnitFunctionType>
|
||||
*/
|
||||
public function functions(): array
|
||||
{
|
||||
return $this->functions;
|
||||
}
|
||||
|
||||
private function cyclomaticComplexity(ClassMethod|Function_ $node): int
|
||||
{
|
||||
$nodes = $node->getStmts();
|
||||
|
||||
if ($nodes === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$traverser = new NodeTraverser;
|
||||
|
||||
$cyclomaticComplexityCalculatingVisitor = new CyclomaticComplexityCalculatingVisitor;
|
||||
|
||||
$traverser->addVisitor($cyclomaticComplexityCalculatingVisitor);
|
||||
|
||||
/* @noinspection UnusedFunctionResultInspection */
|
||||
$traverser->traverse($nodes);
|
||||
|
||||
return $cyclomaticComplexityCalculatingVisitor->cyclomaticComplexity();
|
||||
}
|
||||
|
||||
private function signature(ClassMethod|Function_ $node): string
|
||||
{
|
||||
$signature = ($node->returnsByRef() ? '&' : '') . $node->name->toString() . '(';
|
||||
$parameters = [];
|
||||
|
||||
foreach ($node->getParams() as $parameter) {
|
||||
assert(isset($parameter->var->name));
|
||||
|
||||
$parameterAsString = '';
|
||||
|
||||
if ($parameter->type !== null) {
|
||||
$parameterAsString = $this->type($parameter->type) . ' ';
|
||||
}
|
||||
|
||||
$parameterAsString .= '$' . $parameter->var->name;
|
||||
|
||||
/* @todo Handle default values */
|
||||
|
||||
$parameters[] = $parameterAsString;
|
||||
}
|
||||
|
||||
$signature .= implode(', ', $parameters) . ')';
|
||||
|
||||
$returnType = $node->getReturnType();
|
||||
|
||||
if ($returnType !== null) {
|
||||
$signature .= ': ' . $this->type($returnType);
|
||||
}
|
||||
|
||||
return $signature;
|
||||
}
|
||||
|
||||
private function type(ComplexType|Identifier|Name $type): string
|
||||
{
|
||||
if ($type instanceof NullableType) {
|
||||
return '?' . $type->type;
|
||||
}
|
||||
|
||||
if ($type instanceof UnionType) {
|
||||
return $this->unionTypeAsString($type);
|
||||
}
|
||||
|
||||
if ($type instanceof IntersectionType) {
|
||||
return $this->intersectionTypeAsString($type);
|
||||
}
|
||||
|
||||
return $type->toString();
|
||||
}
|
||||
|
||||
private function visibility(ClassMethod $node): string
|
||||
{
|
||||
if ($node->isPrivate()) {
|
||||
return 'private';
|
||||
}
|
||||
|
||||
if ($node->isProtected()) {
|
||||
return 'protected';
|
||||
}
|
||||
|
||||
return 'public';
|
||||
}
|
||||
|
||||
private function processClass(Class_ $node): void
|
||||
{
|
||||
$name = $node->name->toString();
|
||||
$namespacedName = $node->namespacedName->toString();
|
||||
|
||||
$this->classes[$namespacedName] = [
|
||||
'name' => $name,
|
||||
'namespacedName' => $namespacedName,
|
||||
'namespace' => $this->namespace($namespacedName, $name),
|
||||
'startLine' => $node->getStartLine(),
|
||||
'endLine' => $node->getEndLine(),
|
||||
'methods' => [],
|
||||
];
|
||||
}
|
||||
|
||||
private function processTrait(Trait_ $node): void
|
||||
{
|
||||
$name = $node->name->toString();
|
||||
$namespacedName = $node->namespacedName->toString();
|
||||
|
||||
$this->traits[$namespacedName] = [
|
||||
'name' => $name,
|
||||
'namespacedName' => $namespacedName,
|
||||
'namespace' => $this->namespace($namespacedName, $name),
|
||||
'startLine' => $node->getStartLine(),
|
||||
'endLine' => $node->getEndLine(),
|
||||
'methods' => [],
|
||||
];
|
||||
}
|
||||
|
||||
private function processMethod(ClassMethod $node): void
|
||||
{
|
||||
$parentNode = $node->getAttribute('parent');
|
||||
|
||||
if ($parentNode instanceof Interface_) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert($parentNode instanceof Class_ || $parentNode instanceof Trait_ || $parentNode instanceof Enum_);
|
||||
assert(isset($parentNode->name));
|
||||
assert(isset($parentNode->namespacedName));
|
||||
assert($parentNode->namespacedName instanceof Name);
|
||||
|
||||
$parentName = $parentNode->name->toString();
|
||||
$parentNamespacedName = $parentNode->namespacedName->toString();
|
||||
|
||||
if ($parentNode instanceof Class_) {
|
||||
$storage = &$this->classes;
|
||||
} else {
|
||||
$storage = &$this->traits;
|
||||
}
|
||||
|
||||
if (!isset($storage[$parentNamespacedName])) {
|
||||
$storage[$parentNamespacedName] = [
|
||||
'name' => $parentName,
|
||||
'namespacedName' => $parentNamespacedName,
|
||||
'namespace' => $this->namespace($parentNamespacedName, $parentName),
|
||||
'startLine' => $parentNode->getStartLine(),
|
||||
'endLine' => $parentNode->getEndLine(),
|
||||
'methods' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$storage[$parentNamespacedName]['methods'][$node->name->toString()] = [
|
||||
'methodName' => $node->name->toString(),
|
||||
'signature' => $this->signature($node),
|
||||
'visibility' => $this->visibility($node),
|
||||
'startLine' => $node->getStartLine(),
|
||||
'endLine' => $node->getEndLine(),
|
||||
'ccn' => $this->cyclomaticComplexity($node),
|
||||
];
|
||||
}
|
||||
|
||||
private function processFunction(Function_ $node): void
|
||||
{
|
||||
assert(isset($node->name));
|
||||
assert(isset($node->namespacedName));
|
||||
assert($node->namespacedName instanceof Name);
|
||||
|
||||
$name = $node->name->toString();
|
||||
$namespacedName = $node->namespacedName->toString();
|
||||
|
||||
$this->functions[$namespacedName] = [
|
||||
'name' => $name,
|
||||
'namespacedName' => $namespacedName,
|
||||
'namespace' => $this->namespace($namespacedName, $name),
|
||||
'signature' => $this->signature($node),
|
||||
'startLine' => $node->getStartLine(),
|
||||
'endLine' => $node->getEndLine(),
|
||||
'ccn' => $this->cyclomaticComplexity($node),
|
||||
];
|
||||
}
|
||||
|
||||
private function namespace(string $namespacedName, string $name): string
|
||||
{
|
||||
return trim(rtrim($namespacedName, $name), '\\');
|
||||
}
|
||||
|
||||
private function unionTypeAsString(UnionType $node): string
|
||||
{
|
||||
$types = [];
|
||||
|
||||
foreach ($node->types as $type) {
|
||||
if ($type instanceof IntersectionType) {
|
||||
$types[] = '(' . $this->intersectionTypeAsString($type) . ')';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$types[] = $this->typeAsString($type);
|
||||
}
|
||||
|
||||
return implode('|', $types);
|
||||
}
|
||||
|
||||
private function intersectionTypeAsString(IntersectionType $node): string
|
||||
{
|
||||
$types = [];
|
||||
|
||||
foreach ($node->types as $type) {
|
||||
$types[] = $this->typeAsString($type);
|
||||
}
|
||||
|
||||
return implode('&', $types);
|
||||
}
|
||||
|
||||
private function typeAsString(Identifier|Name $node): string
|
||||
{
|
||||
if ($node instanceof Name) {
|
||||
return $node->toCodeString();
|
||||
}
|
||||
|
||||
return $node->toString();
|
||||
}
|
||||
}
|
||||
-398
@@ -1,398 +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\StaticAnalysis;
|
||||
|
||||
use function array_diff_key;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function current;
|
||||
use function end;
|
||||
use function explode;
|
||||
use function max;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function range;
|
||||
use function reset;
|
||||
use function sprintf;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
*/
|
||||
final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
private int $nextBranch = 0;
|
||||
private readonly string $source;
|
||||
|
||||
/**
|
||||
* @var LinesType
|
||||
*/
|
||||
private array $executableLinesGroupedByBranch = [];
|
||||
|
||||
/**
|
||||
* @var array<int, bool>
|
||||
*/
|
||||
private array $unsets = [];
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private array $commentsToCheckForUnset = [];
|
||||
|
||||
public function __construct(string $source)
|
||||
{
|
||||
$this->source = $source;
|
||||
}
|
||||
|
||||
public function enterNode(Node $node): void
|
||||
{
|
||||
foreach ($node->getComments() as $comment) {
|
||||
$commentLine = $comment->getStartLine();
|
||||
|
||||
if (!isset($this->executableLinesGroupedByBranch[$commentLine])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (explode("\n", $comment->getText()) as $text) {
|
||||
$this->commentsToCheckForUnset[$commentLine] = $text;
|
||||
$commentLine++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Scalar\String_ ||
|
||||
$node instanceof Node\Scalar\EncapsedStringPart) {
|
||||
$startLine = $node->getStartLine() + 1;
|
||||
$endLine = $node->getEndLine() - 1;
|
||||
|
||||
if ($startLine <= $endLine) {
|
||||
foreach (range($startLine, $endLine) as $line) {
|
||||
unset($this->executableLinesGroupedByBranch[$line]);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Stmt\Interface_) {
|
||||
foreach (range($node->getStartLine(), $node->getEndLine()) as $line) {
|
||||
$this->unsets[$line] = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Stmt\Declare_ ||
|
||||
$node instanceof Node\Stmt\DeclareDeclare ||
|
||||
$node instanceof Node\Stmt\Else_ ||
|
||||
$node instanceof Node\Stmt\EnumCase ||
|
||||
$node instanceof Node\Stmt\Finally_ ||
|
||||
$node instanceof Node\Stmt\GroupUse ||
|
||||
$node instanceof Node\Stmt\Label ||
|
||||
$node instanceof Node\Stmt\Namespace_ ||
|
||||
$node instanceof Node\Stmt\Nop ||
|
||||
$node instanceof Node\Stmt\Switch_ ||
|
||||
$node instanceof Node\Stmt\TryCatch ||
|
||||
$node instanceof Node\Stmt\Use_ ||
|
||||
$node instanceof Node\Stmt\UseUse ||
|
||||
$node instanceof Node\Expr\ConstFetch ||
|
||||
$node instanceof Node\Expr\Variable ||
|
||||
$node instanceof Node\Expr\Throw_ ||
|
||||
$node instanceof Node\ComplexType ||
|
||||
$node instanceof Node\Const_ ||
|
||||
$node instanceof Node\Identifier ||
|
||||
$node instanceof Node\Name ||
|
||||
$node instanceof Node\Param ||
|
||||
$node instanceof Node\Scalar) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Expr\Match_) {
|
||||
foreach ($node->arms as $arm) {
|
||||
$this->setLineBranch(
|
||||
$arm->body->getStartLine(),
|
||||
$arm->body->getEndLine(),
|
||||
++$this->nextBranch,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Throw_) {
|
||||
$this->setLineBranch($node->expr->expr->getEndLine(), $node->expr->expr->getEndLine(), ++$this->nextBranch);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Stmt\Enum_ ||
|
||||
$node instanceof Node\Stmt\Function_ ||
|
||||
$node instanceof Node\Stmt\Class_ ||
|
||||
$node instanceof Node\Stmt\ClassMethod ||
|
||||
$node instanceof Node\Expr\Closure ||
|
||||
$node instanceof Node\Stmt\Trait_) {
|
||||
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) {
|
||||
foreach ($node->stmts as $stmt) {
|
||||
if ($stmt instanceof Node\Stmt\Nop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (range($stmt->getStartLine(), $stmt->getEndLine()) as $line) {
|
||||
unset($this->executableLinesGroupedByBranch[$line]);
|
||||
|
||||
if (
|
||||
$isConcreteClassLike &&
|
||||
!$stmt instanceof Node\Stmt\ClassMethod
|
||||
) {
|
||||
$this->unsets[$line] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isConcreteClassLike) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hasEmptyBody = [] === $node->stmts ||
|
||||
null === $node->stmts ||
|
||||
(
|
||||
1 === count($node->stmts) &&
|
||||
$node->stmts[0] instanceof Node\Stmt\Nop
|
||||
);
|
||||
|
||||
if ($hasEmptyBody) {
|
||||
if ($node->getEndLine() === $node->getStartLine() && isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setLineBranch($node->getEndLine(), $node->getEndLine(), ++$this->nextBranch);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Expr\ArrowFunction) {
|
||||
$startLine = max(
|
||||
$node->getStartLine() + 1,
|
||||
$node->expr->getStartLine(),
|
||||
);
|
||||
|
||||
$endLine = $node->expr->getEndLine();
|
||||
|
||||
if ($endLine < $startLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setLineBranch($startLine, $endLine, ++$this->nextBranch);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Expr\Ternary) {
|
||||
if (null !== $node->if &&
|
||||
$node->getStartLine() !== $node->if->getEndLine()) {
|
||||
$this->setLineBranch($node->if->getStartLine(), $node->if->getEndLine(), ++$this->nextBranch);
|
||||
}
|
||||
|
||||
if ($node->getStartLine() !== $node->else->getEndLine()) {
|
||||
$this->setLineBranch($node->else->getStartLine(), $node->else->getEndLine(), ++$this->nextBranch);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Expr\BinaryOp\Coalesce) {
|
||||
if ($node->getStartLine() !== $node->getEndLine()) {
|
||||
$this->setLineBranch($node->getEndLine(), $node->getEndLine(), ++$this->nextBranch);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Stmt\If_ ||
|
||||
$node instanceof Node\Stmt\ElseIf_ ||
|
||||
$node instanceof Node\Stmt\Case_) {
|
||||
if (null === $node->cond) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setLineBranch(
|
||||
$node->cond->getStartLine(),
|
||||
$node->cond->getStartLine(),
|
||||
++$this->nextBranch,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Stmt\For_) {
|
||||
$startLine = null;
|
||||
$endLine = null;
|
||||
|
||||
if ([] !== $node->init) {
|
||||
$startLine = $node->init[0]->getStartLine();
|
||||
|
||||
end($node->init);
|
||||
|
||||
$endLine = current($node->init)->getEndLine();
|
||||
|
||||
reset($node->init);
|
||||
}
|
||||
|
||||
if ([] !== $node->cond) {
|
||||
if (null === $startLine) {
|
||||
$startLine = $node->cond[0]->getStartLine();
|
||||
}
|
||||
|
||||
end($node->cond);
|
||||
|
||||
$endLine = current($node->cond)->getEndLine();
|
||||
|
||||
reset($node->cond);
|
||||
}
|
||||
|
||||
if ([] !== $node->loop) {
|
||||
if (null === $startLine) {
|
||||
$startLine = $node->loop[0]->getStartLine();
|
||||
}
|
||||
|
||||
end($node->loop);
|
||||
|
||||
$endLine = current($node->loop)->getEndLine();
|
||||
|
||||
reset($node->loop);
|
||||
}
|
||||
|
||||
if (null === $startLine || null === $endLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setLineBranch(
|
||||
$startLine,
|
||||
$endLine,
|
||||
++$this->nextBranch,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Stmt\Foreach_) {
|
||||
$this->setLineBranch(
|
||||
$node->expr->getStartLine(),
|
||||
$node->valueVar->getEndLine(),
|
||||
++$this->nextBranch,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Stmt\While_ ||
|
||||
$node instanceof Node\Stmt\Do_) {
|
||||
$this->setLineBranch(
|
||||
$node->cond->getStartLine(),
|
||||
$node->cond->getEndLine(),
|
||||
++$this->nextBranch,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Stmt\Catch_) {
|
||||
assert([] !== $node->types);
|
||||
$startLine = $node->types[0]->getStartLine();
|
||||
end($node->types);
|
||||
$endLine = current($node->types)->getEndLine();
|
||||
|
||||
$this->setLineBranch(
|
||||
$startLine,
|
||||
$endLine,
|
||||
++$this->nextBranch,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Expr\CallLike) {
|
||||
if (isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) {
|
||||
$branch = $this->executableLinesGroupedByBranch[$node->getStartLine()];
|
||||
} else {
|
||||
$branch = ++$this->nextBranch;
|
||||
}
|
||||
|
||||
$this->setLineBranch($node->getStartLine(), $node->getEndLine(), $branch);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($this->executableLinesGroupedByBranch[$node->getStartLine()])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setLineBranch($node->getStartLine(), $node->getEndLine(), ++$this->nextBranch);
|
||||
}
|
||||
|
||||
public function afterTraverse(array $nodes): void
|
||||
{
|
||||
$lines = explode("\n", $this->source);
|
||||
|
||||
foreach ($lines as $lineNumber => $line) {
|
||||
$lineNumber++;
|
||||
|
||||
if (1 === preg_match('/^\s*$/', $line) ||
|
||||
(
|
||||
isset($this->commentsToCheckForUnset[$lineNumber]) &&
|
||||
1 === preg_match(sprintf('/^\s*%s\s*$/', preg_quote($this->commentsToCheckForUnset[$lineNumber], '/')), $line)
|
||||
)) {
|
||||
unset($this->executableLinesGroupedByBranch[$lineNumber]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->executableLinesGroupedByBranch = array_diff_key(
|
||||
$this->executableLinesGroupedByBranch,
|
||||
$this->unsets,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinesType
|
||||
*/
|
||||
public function executableLinesGroupedByBranch(): array
|
||||
{
|
||||
return $this->executableLinesGroupedByBranch;
|
||||
}
|
||||
|
||||
private function setLineBranch(int $start, int $end, int $branch): void
|
||||
{
|
||||
foreach (range($start, $end) as $line) {
|
||||
$this->executableLinesGroupedByBranch[$line] = $branch;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,50 +9,45 @@
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
|
||||
|
||||
use function file_get_contents;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
*
|
||||
* @phpstan-type LinesOfCodeType = array{
|
||||
* linesOfCode: int,
|
||||
* commentLinesOfCode: int,
|
||||
* nonCommentLinesOfCode: int
|
||||
* }
|
||||
* @phpstan-type LinesType = array<int, int>
|
||||
*/
|
||||
interface FileAnalyser
|
||||
final class FileAnalyser
|
||||
{
|
||||
/**
|
||||
* @return array<string, CodeUnitClassType>
|
||||
*/
|
||||
public function classesIn(string $filename): array;
|
||||
private readonly SourceAnalyser $sourceAnalyser;
|
||||
private readonly bool $useAnnotationsForIgnoringCode;
|
||||
private readonly bool $ignoreDeprecatedCode;
|
||||
|
||||
/**
|
||||
* @return array<string, CodeUnitTraitType>
|
||||
* @var array<non-empty-string, AnalysisResult>
|
||||
*/
|
||||
public function traitsIn(string $filename): array;
|
||||
private array $cache = [];
|
||||
|
||||
public function __construct(SourceAnalyser $sourceAnalyser, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode)
|
||||
{
|
||||
$this->sourceAnalyser = $sourceAnalyser;
|
||||
$this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
|
||||
$this->ignoreDeprecatedCode = $ignoreDeprecatedCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, CodeUnitFunctionType>
|
||||
* @param non-empty-string $sourceCodeFile
|
||||
*/
|
||||
public function functionsIn(string $filename): array;
|
||||
public function analyse(string $sourceCodeFile): AnalysisResult
|
||||
{
|
||||
if (isset($this->cache[$sourceCodeFile])) {
|
||||
return $this->cache[$sourceCodeFile];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinesOfCodeType
|
||||
*/
|
||||
public function linesOfCodeFor(string $filename): array;
|
||||
$this->cache[$sourceCodeFile] = $this->sourceAnalyser->analyse(
|
||||
$sourceCodeFile,
|
||||
file_get_contents($sourceCodeFile),
|
||||
$this->useAnnotationsForIgnoringCode,
|
||||
$this->ignoreDeprecatedCode,
|
||||
);
|
||||
|
||||
/**
|
||||
* @return LinesType
|
||||
*/
|
||||
public function executableLinesIn(string $filename): array;
|
||||
|
||||
/**
|
||||
* @return LinesType
|
||||
*/
|
||||
public function ignoredLinesFor(string $filename): array;
|
||||
return $this->cache[$sourceCodeFile];
|
||||
}
|
||||
}
|
||||
-121
@@ -1,121 +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\StaticAnalysis;
|
||||
|
||||
use function assert;
|
||||
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_;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
/**
|
||||
* @var array<int>
|
||||
*/
|
||||
private array $ignoredLines = [];
|
||||
private readonly bool $useAnnotationsForIgnoringCode;
|
||||
private readonly bool $ignoreDeprecated;
|
||||
|
||||
public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecated)
|
||||
{
|
||||
$this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
|
||||
$this->ignoreDeprecated = $ignoreDeprecated;
|
||||
}
|
||||
|
||||
public function enterNode(Node $node): void
|
||||
{
|
||||
if (!$node instanceof Class_ &&
|
||||
!$node instanceof Trait_ &&
|
||||
!$node instanceof Interface_ &&
|
||||
!$node instanceof Enum_ &&
|
||||
!$node instanceof ClassMethod &&
|
||||
!$node instanceof Function_ &&
|
||||
!$node instanceof Attribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Class_ && $node->isAnonymous()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Class_ ||
|
||||
$node instanceof Trait_ ||
|
||||
$node instanceof Interface_ ||
|
||||
$node instanceof Attribute) {
|
||||
$this->ignoredLines[] = $node->getStartLine();
|
||||
|
||||
assert($node->name !== null);
|
||||
|
||||
// Workaround for https://github.com/nikic/PHP-Parser/issues/886
|
||||
$this->ignoredLines[] = $node->name->getStartLine();
|
||||
}
|
||||
|
||||
if (!$this->useAnnotationsForIgnoringCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node instanceof Interface_) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
public function ignoredLines(): array
|
||||
{
|
||||
return $this->ignoredLines;
|
||||
}
|
||||
|
||||
private function processDocComment(Node $node): void
|
||||
{
|
||||
$docComment = $node->getDocComment();
|
||||
|
||||
if ($docComment === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_contains($docComment->getText(), '@codeCoverageIgnore')) {
|
||||
for ($line = $node->getStartLine(); $line <= $node->getEndLine(); $line++) {
|
||||
$this->ignoredLines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->ignoreDeprecated && str_contains($docComment->getText(), '@deprecated')) {
|
||||
for ($line = $node->getStartLine(); $line <= $node->getEndLine(); $line++) {
|
||||
$this->ignoredLines[] = $line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-267
@@ -1,267 +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\StaticAnalysis;
|
||||
|
||||
use const T_COMMENT;
|
||||
use const T_DOC_COMMENT;
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
use function assert;
|
||||
use function file_get_contents;
|
||||
use function is_array;
|
||||
use function max;
|
||||
use function range;
|
||||
use function sort;
|
||||
use function sprintf;
|
||||
use function substr_count;
|
||||
use function token_get_all;
|
||||
use function trim;
|
||||
use PhpParser\Error;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\NodeVisitor\NameResolver;
|
||||
use PhpParser\NodeVisitor\ParentConnectingVisitor;
|
||||
use PhpParser\ParserFactory;
|
||||
use SebastianBergmann\CodeCoverage\ParserException;
|
||||
use SebastianBergmann\LinesOfCode\LineCountingVisitor;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type CodeUnitFunctionType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitMethodType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitClassType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type CodeUnitTraitType from \SebastianBergmann\CodeCoverage\StaticAnalysis\CodeUnitFindingVisitor
|
||||
* @phpstan-import-type LinesOfCodeType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
* @phpstan-import-type LinesType from \SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser
|
||||
*/
|
||||
final class ParsingFileAnalyser implements FileAnalyser
|
||||
{
|
||||
/**
|
||||
* @var array<string, array<string, CodeUnitClassType>>
|
||||
*/
|
||||
private array $classes = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, CodeUnitTraitType>>
|
||||
*/
|
||||
private array $traits = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, CodeUnitFunctionType>>
|
||||
*/
|
||||
private array $functions = [];
|
||||
|
||||
/**
|
||||
* @var array<string, LinesOfCodeType>
|
||||
*/
|
||||
private array $linesOfCode = [];
|
||||
|
||||
/**
|
||||
* @var array<string, LinesType>
|
||||
*/
|
||||
private array $ignoredLines = [];
|
||||
|
||||
/**
|
||||
* @var array<string, LinesType>
|
||||
*/
|
||||
private array $executableLines = [];
|
||||
private readonly bool $useAnnotationsForIgnoringCode;
|
||||
private readonly bool $ignoreDeprecatedCode;
|
||||
|
||||
public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode)
|
||||
{
|
||||
$this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
|
||||
$this->ignoreDeprecatedCode = $ignoreDeprecatedCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, CodeUnitClassType>
|
||||
*/
|
||||
public function classesIn(string $filename): array
|
||||
{
|
||||
$this->analyse($filename);
|
||||
|
||||
return $this->classes[$filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, CodeUnitTraitType>
|
||||
*/
|
||||
public function traitsIn(string $filename): array
|
||||
{
|
||||
$this->analyse($filename);
|
||||
|
||||
return $this->traits[$filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, CodeUnitFunctionType>
|
||||
*/
|
||||
public function functionsIn(string $filename): array
|
||||
{
|
||||
$this->analyse($filename);
|
||||
|
||||
return $this->functions[$filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinesOfCodeType
|
||||
*/
|
||||
public function linesOfCodeFor(string $filename): array
|
||||
{
|
||||
$this->analyse($filename);
|
||||
|
||||
return $this->linesOfCode[$filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinesType
|
||||
*/
|
||||
public function executableLinesIn(string $filename): array
|
||||
{
|
||||
$this->analyse($filename);
|
||||
|
||||
return $this->executableLines[$filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LinesType
|
||||
*/
|
||||
public function ignoredLinesFor(string $filename): array
|
||||
{
|
||||
$this->analyse($filename);
|
||||
|
||||
return $this->ignoredLines[$filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ParserException
|
||||
*/
|
||||
private function analyse(string $filename): void
|
||||
{
|
||||
if (isset($this->classes[$filename])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$source = file_get_contents($filename);
|
||||
$linesOfCode = max(substr_count($source, "\n") + 1, substr_count($source, "\r") + 1);
|
||||
|
||||
if ($linesOfCode === 0 && !empty($source)) {
|
||||
$linesOfCode = 1;
|
||||
}
|
||||
|
||||
assert($linesOfCode > 0);
|
||||
|
||||
$parser = (new ParserFactory)->createForHostVersion();
|
||||
|
||||
try {
|
||||
$nodes = $parser->parse($source);
|
||||
|
||||
assert($nodes !== null);
|
||||
|
||||
$traverser = new NodeTraverser;
|
||||
$codeUnitFindingVisitor = new CodeUnitFindingVisitor;
|
||||
$lineCountingVisitor = new LineCountingVisitor($linesOfCode);
|
||||
$ignoredLinesFindingVisitor = new IgnoredLinesFindingVisitor($this->useAnnotationsForIgnoringCode, $this->ignoreDeprecatedCode);
|
||||
$executableLinesFindingVisitor = new ExecutableLinesFindingVisitor($source);
|
||||
|
||||
$traverser->addVisitor(new NameResolver);
|
||||
$traverser->addVisitor(new ParentConnectingVisitor);
|
||||
$traverser->addVisitor($codeUnitFindingVisitor);
|
||||
$traverser->addVisitor($lineCountingVisitor);
|
||||
$traverser->addVisitor($ignoredLinesFindingVisitor);
|
||||
$traverser->addVisitor($executableLinesFindingVisitor);
|
||||
|
||||
/* @noinspection UnusedFunctionResultInspection */
|
||||
$traverser->traverse($nodes);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (Error $error) {
|
||||
throw new ParserException(
|
||||
sprintf(
|
||||
'Cannot parse %s: %s',
|
||||
$filename,
|
||||
$error->getMessage(),
|
||||
),
|
||||
$error->getCode(),
|
||||
$error,
|
||||
);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$this->classes[$filename] = $codeUnitFindingVisitor->classes();
|
||||
$this->traits[$filename] = $codeUnitFindingVisitor->traits();
|
||||
$this->functions[$filename] = $codeUnitFindingVisitor->functions();
|
||||
$this->executableLines[$filename] = $executableLinesFindingVisitor->executableLinesGroupedByBranch();
|
||||
$this->ignoredLines[$filename] = [];
|
||||
|
||||
$this->findLinesIgnoredByLineBasedAnnotations($filename, $source, $this->useAnnotationsForIgnoringCode);
|
||||
|
||||
$this->ignoredLines[$filename] = array_unique(
|
||||
array_merge(
|
||||
$this->ignoredLines[$filename],
|
||||
$ignoredLinesFindingVisitor->ignoredLines(),
|
||||
),
|
||||
);
|
||||
|
||||
sort($this->ignoredLines[$filename]);
|
||||
|
||||
$result = $lineCountingVisitor->result();
|
||||
|
||||
$this->linesOfCode[$filename] = [
|
||||
'linesOfCode' => $result->linesOfCode(),
|
||||
'commentLinesOfCode' => $result->commentLinesOfCode(),
|
||||
'nonCommentLinesOfCode' => $result->nonCommentLinesOfCode(),
|
||||
];
|
||||
}
|
||||
|
||||
private function findLinesIgnoredByLineBasedAnnotations(string $filename, string $source, bool $useAnnotationsForIgnoringCode): void
|
||||
{
|
||||
if (!$useAnnotationsForIgnoringCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$start = false;
|
||||
|
||||
foreach (token_get_all($source) as $token) {
|
||||
if (!is_array($token) ||
|
||||
!(T_COMMENT === $token[0] || T_DOC_COMMENT === $token[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$comment = trim($token[1]);
|
||||
|
||||
if ($comment === '// @codeCoverageIgnore' ||
|
||||
$comment === '//@codeCoverageIgnore') {
|
||||
$this->ignoredLines[$filename][] = $token[2];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($comment === '// @codeCoverageIgnoreStart' ||
|
||||
$comment === '//@codeCoverageIgnoreStart') {
|
||||
$start = $token[2];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($comment === '// @codeCoverageIgnoreEnd' ||
|
||||
$comment === '//@codeCoverageIgnoreEnd') {
|
||||
if (false === $start) {
|
||||
$start = $token[2];
|
||||
}
|
||||
|
||||
$this->ignoredLines[$filename] = array_merge(
|
||||
$this->ignoredLines[$filename],
|
||||
range($start, $token[2]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,13 @@
|
||||
*/
|
||||
namespace SebastianBergmann\CodeCoverage\Util;
|
||||
|
||||
use function dirname;
|
||||
use function file_put_contents;
|
||||
use function is_dir;
|
||||
use function mkdir;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
@@ -34,4 +38,20 @@ final class Filesystem
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $target
|
||||
*
|
||||
* @throws WriteOperationFailedException
|
||||
*/
|
||||
public static function write(string $target, string $buffer): void
|
||||
{
|
||||
if (!str_contains($target, '://')) {
|
||||
self::createDirectory(dirname($target));
|
||||
}
|
||||
|
||||
if (@file_put_contents($target, $buffer) === false) {
|
||||
throw new WriteOperationFailedException($target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,10 @@ use function sprintf;
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Percentage
|
||||
final readonly class Percentage
|
||||
{
|
||||
private readonly float $fraction;
|
||||
private readonly float $total;
|
||||
private float $fraction;
|
||||
private float $total;
|
||||
|
||||
public static function fromFractionAndTotal(float $fraction, float $total): self
|
||||
{
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ final class Version
|
||||
public static function id(): string
|
||||
{
|
||||
if (self::$version === '') {
|
||||
self::$version = (new VersionId('11.0.11', dirname(__DIR__)))->asString();
|
||||
self::$version = (new VersionId('12.4.0', dirname(__DIR__)))->asString();
|
||||
}
|
||||
|
||||
return self::$version;
|
||||
|
||||
Reference in new issue
Block a user