Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 33 additions & 19 deletions build/scripts/generate-expectations.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,12 @@ function formatMatches(string $format, string $subject): bool
$provider = new CoverageFixtureProvider;

$scenarios = [
'CoverageForBankAccount' => $provider->lineCoverageForBankAccount(...),
'PathCoverageForBankAccount' => $provider->pathCoverageForBankAccount(...),
'PathCoverageForSourceWithoutNamespace' => $provider->pathCoverageForSourceWithoutNamespace(...),
'CoverageForFileWithIgnoredLines' => $provider->coverageForFileWithIgnoredLines(...),
'CoverageForClassWithAnonymousFunction' => $provider->coverageForClassWithAnonymousFunction(...),
'CoverageForBankAccount' => $provider->lineCoverageForBankAccount(...),
'PathCoverageForBankAccount' => $provider->pathCoverageForBankAccount(...),
'PathCoverageForSourceWithoutNamespace' => $provider->pathCoverageForSourceWithoutNamespace(...),
'CoverageForFileWithIgnoredLines' => $provider->coverageForFileWithIgnoredLines(...),
'CoverageForClassWithAnonymousFunction' => $provider->coverageForClassWithAnonymousFunction(...),
'CoverageForClassesWithTraitsAndInheritance' => $provider->coverageForClassesWithTraitsAndInheritance(...),
];

$expectationPath = TEST_FILES_PATH . 'Report' . DIRECTORY_SEPARATOR . 'HTML' . DIRECTORY_SEPARATOR;
Expand All @@ -133,33 +134,46 @@ function formatMatches(string $format, string $subject): bool

$expectationDirectory = $expectationPath . $expectationDirectoryName;

if (is_dir($expectationDirectory)) {
foreach (glob($expectationDirectory . DIRECTORY_SEPARATOR . '*') as $staleFile) {
unlink($staleFile);
}
} else {
mkdir($expectationDirectory, 0o777, true);
}
removeDirectory($expectationDirectory);
mkdir($expectationDirectory, 0o777, true);

$generatedFiles = new RegexIterator(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($temporaryPath, RecursiveDirectoryIterator::SKIP_DOTS),
),
'/\.html$/',
);

$numberOfFiles = 0;

foreach (glob($temporaryPath . DIRECTORY_SEPARATOR . '*.html') as $generatedFile) {
$name = basename($generatedFile);
$generated = str_replace(PHP_EOL, "\n", file_get_contents($generatedFile));
$content = withPlaceholders($generated);
foreach ($generatedFiles as $generatedFile) {
$relativePath = substr($generatedFile->getPathname(), strlen($temporaryPath) + 1);
$generated = str_replace(PHP_EOL, "\n", file_get_contents($generatedFile->getPathname()));
$content = withPlaceholders($generated);

if (!formatMatches($content, $generated)) {
$content = collapseSourceListing($content);
}

if (!formatMatches($content, $generated)) {
print 'Format description for ' . $expectationDirectoryName . '/' . $name . ' does not match the generated file' . PHP_EOL;
print 'Format description for ' . $expectationDirectoryName . '/' . $relativePath . ' does not match the generated file' . PHP_EOL;

exit(1);
}

file_put_contents($expectationDirectory . DIRECTORY_SEPARATOR . $name, $content);
$targetFile = $expectationDirectory . DIRECTORY_SEPARATOR . $relativePath;
$targetDirectory = dirname($targetFile);

if (!is_dir($targetDirectory)) {
mkdir($targetDirectory, 0o777, true);
}

file_put_contents($targetFile, $content);

$numberOfFiles++;
}

print $expectationDirectoryName . ': ' . count(glob($expectationDirectory . DIRECTORY_SEPARATOR . '*.html')) . ' files' . PHP_EOL;
print $expectationDirectoryName . ': ' . $numberOfFiles . ' files' . PHP_EOL;
}

removeDirectory($temporaryPath);
28 changes: 28 additions & 0 deletions src/Node/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ final class File extends AbstractNode
*/
private array $codeUnitsByLine = [];

/**
* @var array<string, Class_>
*/
private readonly array $rawClasses;

/**
* @var array<string, Trait_>
*/
private readonly array $rawTraits;

/**
* @param non-empty-string $sha1
* @param array<positive-int, ?list<non-empty-string>> $lineCoverageData
Expand All @@ -111,6 +121,8 @@ public function __construct(string $name, AbstractNode $parent, string $sha1, ar
$this->testData = $testData;
$this->linesOfCode = $linesOfCode;
$this->hasBranchCoverageData = $hasBranchCoverageData;
$this->rawClasses = $classes;
$this->rawTraits = $traits;

$this->calculateStatistics($classes, $traits, $functions);
}
Expand Down Expand Up @@ -152,6 +164,22 @@ public function testData(): array
return $this->testData;
}

/**
* @return array<string, Class_>
*/
public function rawClasses(): array
{
return $this->rawClasses;
}

/**
* @return array<string, Trait_>
*/
public function rawTraits(): array
{
return $this->rawTraits;
}

/**
* @return array<string, ProcessedClassType>
*/
Expand Down
246 changes: 246 additions & 0 deletions src/Report/Html/ClassView/Builder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Html\ClassView;

use function array_key_exists;
use function array_keys;
use function count;
use function explode;
use function in_array;
use SebastianBergmann\CodeCoverage\Data\ProcessedClassType;
use SebastianBergmann\CodeCoverage\Data\ProcessedTraitType;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\Report\Html\ClassView\Node\ClassNode;
use SebastianBergmann\CodeCoverage\Report\Html\ClassView\Node\NamespaceNode;
use SebastianBergmann\CodeCoverage\Report\Html\ClassView\Node\ParentSection;
use SebastianBergmann\CodeCoverage\Report\Html\ClassView\Node\TraitSection;
use SebastianBergmann\CodeCoverage\StaticAnalysis\Class_;
use SebastianBergmann\CodeCoverage\StaticAnalysis\Trait_;

/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Builder
{
/**
* @var array<non-empty-string, array{file: FileNode, raw: Class_, processed: ProcessedClassType}>
*/
private array $classRegistry = [];

/**
* @var array<non-empty-string, array{file: FileNode, raw: Trait_, processed: ProcessedTraitType}>
*/
private array $traitRegistry = [];

public function build(DirectoryNode $root): NamespaceNode
{
$this->classRegistry = [];
$this->traitRegistry = [];

$this->collectRegistries($root);

$rootNamespace = new NamespaceNode('(Global)', '');

/** @var array<string, NamespaceNode> $namespaceMap */
$namespaceMap = ['' => $rootNamespace];

foreach ($this->classRegistry as $fqcn => $entry) {
$raw = $entry['raw'];
$namespace = $raw->namespace();
$filePath = $entry['file']->pathAsString();

if ($filePath === '') {
continue; // @codeCoverageIgnore
}

$parentNs = $this->ensureNamespaceExists($namespace, $namespaceMap, $rootNamespace);

$traitSections = $this->resolveTraits($raw);
$parentSections = $this->resolveParents($raw);

$classNode = new ClassNode(
$fqcn,
$namespace,
$filePath,
$raw->startLine(),
$raw->endLine(),
$entry['processed'],
$entry['file'],
$traitSections,
$parentSections,
$parentNs,
);

$parentNs->addClass($classNode);
}

return $this->reduceRoot($rootNamespace);
}

private function reduceRoot(NamespaceNode $root): NamespaceNode
{
while (count($root->childNamespaces()) === 1 && count($root->classes()) === 0) {
$root = $root->childNamespaces()[0];
}

$root->promoteToRoot();

return $root;
}

private function collectRegistries(DirectoryNode $directory): void
{
foreach ($directory as $node) {
if ($node instanceof DirectoryNode) {
continue;
}

if (!$node instanceof FileNode) {
continue;
}

foreach ($node->rawClasses() as $className => $rawClass) {
if ($className !== '' && array_key_exists($className, $node->classes())) {
$this->classRegistry[$className] = [
'file' => $node,
'raw' => $rawClass,
'processed' => $node->classes()[$className],
];
}
}

foreach ($node->rawTraits() as $traitName => $rawTrait) {
if ($traitName !== '' && array_key_exists($traitName, $node->traits())) {
$this->traitRegistry[$traitName] = [
'file' => $node,
'raw' => $rawTrait,
'processed' => $node->traits()[$traitName],
];
}
}
}
}

/**
* @param array<string, NamespaceNode> $namespaceMap
*/
private function ensureNamespaceExists(string $namespace, array &$namespaceMap, NamespaceNode $rootNamespace): NamespaceNode
{
if (isset($namespaceMap[$namespace])) {
return $namespaceMap[$namespace];
}

$parts = explode('\\', $namespace);
$current = '';

$parentNode = $rootNamespace;

foreach ($parts as $part) {
$current = $current === '' ? $part : $current . '\\' . $part;

if (!isset($namespaceMap[$current])) {
$node = new NamespaceNode($part, $current, $parentNode);
$parentNode->addNamespace($node);
$namespaceMap[$current] = $node;
}

$parentNode = $namespaceMap[$current];
}

return $parentNode;
}

/**
* @return list<TraitSection>
*/
private function resolveTraits(Class_ $class): array
{
$sections = [];

foreach ($class->traits() as $traitName) {
if (!isset($this->traitRegistry[$traitName])) {
continue;
}

$entry = $this->traitRegistry[$traitName];
$filePath = $entry['file']->pathAsString();

if ($filePath === '') {
continue; // @codeCoverageIgnore
}

$sections[] = new TraitSection(
$traitName,
$filePath,
$entry['raw']->startLine(),
$entry['raw']->endLine(),
$entry['processed'],
$entry['file'],
);
}

return $sections;
}

/**
* @return list<ParentSection>
*/
private function resolveParents(Class_ $class): array
{
$sections = [];
$ownMethods = array_keys($class->methods());
$seenMethods = $ownMethods;
$currentClass = $class;

// Statically analysed code does not have to be runnable, it may declare inheritance cycles
$visited = [$class->namespacedName() => true];

while ($currentClass->hasParent()) {
$parentName = $currentClass->parentClass();

if ($parentName === null || isset($visited[$parentName]) || !isset($this->classRegistry[$parentName])) {
break;
}

$visited[$parentName] = true;

$parentEntry = $this->classRegistry[$parentName];
$parentRaw = $parentEntry['raw'];
$parentProcessed = $parentEntry['processed'];

$inheritedMethods = [];

foreach ($parentProcessed->methods as $methodName => $method) {
if (!in_array($methodName, $seenMethods, true)) {
$inheritedMethods[$methodName] = $method;
$seenMethods[] = $methodName;
}
}

$filePath = $parentEntry['file']->pathAsString();

if ($inheritedMethods !== [] && $filePath !== '') {
$sections[] = new ParentSection(
$parentName,
$filePath,
$inheritedMethods,
$parentEntry['file'],
);
}

$currentClass = $parentRaw;
}

return $sections;
}
}
Loading