Skip to content

Commit ba9588a

Browse files
Initial work on class-oriented HTML report
1 parent 6c99a17 commit ba9588a

72 files changed

Lines changed: 6134 additions & 733 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/Node/File.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ final class File extends AbstractNode
8686
*/
8787
private array $codeUnitsByLine = [];
8888

89+
/**
90+
* @var array<string, Class_>
91+
*/
92+
private readonly array $rawClasses;
93+
94+
/**
95+
* @var array<string, Trait_>
96+
*/
97+
private readonly array $rawTraits;
98+
8999
/**
90100
* @param non-empty-string $sha1
91101
* @param array<int, ?list<non-empty-string>> $lineCoverageData
@@ -103,6 +113,8 @@ public function __construct(string $name, AbstractNode $parent, string $sha1, ar
103113
$this->functionCoverageData = $functionCoverageData;
104114
$this->testData = $testData;
105115
$this->linesOfCode = $linesOfCode;
116+
$this->rawClasses = $classes;
117+
$this->rawTraits = $traits;
106118

107119
$this->calculateStatistics($classes, $traits, $functions);
108120
}
@@ -141,6 +153,22 @@ public function testData(): array
141153
return $this->testData;
142154
}
143155

156+
/**
157+
* @return array<string, Class_>
158+
*/
159+
public function rawClasses(): array
160+
{
161+
return $this->rawClasses;
162+
}
163+
164+
/**
165+
* @return array<string, Trait_>
166+
*/
167+
public function rawTraits(): array
168+
{
169+
return $this->rawTraits;
170+
}
171+
144172
/**
145173
* @return array<string, ProcessedClassType>
146174
*/
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Report\Html\ClassView;
11+
12+
use function array_key_exists;
13+
use function array_keys;
14+
use function count;
15+
use function explode;
16+
use function in_array;
17+
use SebastianBergmann\CodeCoverage\Data\ProcessedClassType;
18+
use SebastianBergmann\CodeCoverage\Data\ProcessedTraitType;
19+
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
20+
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
21+
use SebastianBergmann\CodeCoverage\Report\Html\ClassView\Node\ClassNode;
22+
use SebastianBergmann\CodeCoverage\Report\Html\ClassView\Node\NamespaceNode;
23+
use SebastianBergmann\CodeCoverage\Report\Html\ClassView\Node\ParentSection;
24+
use SebastianBergmann\CodeCoverage\Report\Html\ClassView\Node\TraitSection;
25+
use SebastianBergmann\CodeCoverage\StaticAnalysis\Class_;
26+
use SebastianBergmann\CodeCoverage\StaticAnalysis\Trait_;
27+
28+
/**
29+
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
30+
*
31+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage
32+
*/
33+
final class Builder
34+
{
35+
/**
36+
* @var array<string, array{file: FileNode, raw: Class_, processed: ProcessedClassType}>
37+
*/
38+
private array $classRegistry = [];
39+
40+
/**
41+
* @var array<string, array{file: FileNode, raw: Trait_, processed: ProcessedTraitType}>
42+
*/
43+
private array $traitRegistry = [];
44+
45+
public function build(DirectoryNode $root): NamespaceNode
46+
{
47+
$this->classRegistry = [];
48+
$this->traitRegistry = [];
49+
50+
$this->collectRegistries($root);
51+
52+
$rootNamespace = new NamespaceNode('(Global)', '');
53+
54+
/** @var array<string, NamespaceNode> $namespaceMap */
55+
$namespaceMap = ['' => $rootNamespace];
56+
57+
foreach ($this->classRegistry as $fqcn => $entry) {
58+
$raw = $entry['raw'];
59+
$namespace = $raw->namespace();
60+
61+
$parentNs = $this->ensureNamespaceExists($namespace, $namespaceMap, $rootNamespace);
62+
63+
$traitSections = $this->resolveTraits($raw);
64+
$parentSections = $this->resolveParents($raw);
65+
66+
$classNode = new ClassNode(
67+
$fqcn,
68+
$namespace,
69+
$entry['file']->pathAsString(),
70+
$raw->startLine(),
71+
$raw->endLine(),
72+
$entry['processed'],
73+
$entry['file'],
74+
$traitSections,
75+
$parentSections,
76+
$parentNs,
77+
);
78+
79+
$parentNs->addClass($classNode);
80+
}
81+
82+
return $this->reduceRoot($rootNamespace);
83+
}
84+
85+
private function reduceRoot(NamespaceNode $root): NamespaceNode
86+
{
87+
while (count($root->childNamespaces()) === 1 && count($root->classes()) === 0) {
88+
$root = $root->childNamespaces()[0];
89+
}
90+
91+
$root->promoteToRoot();
92+
93+
return $root;
94+
}
95+
96+
private function collectRegistries(DirectoryNode $directory): void
97+
{
98+
foreach ($directory as $node) {
99+
if ($node instanceof DirectoryNode) {
100+
continue;
101+
}
102+
103+
if (!$node instanceof FileNode) {
104+
continue;
105+
}
106+
107+
foreach ($node->rawClasses() as $className => $rawClass) {
108+
if (array_key_exists($className, $node->classes())) {
109+
$this->classRegistry[$className] = [
110+
'file' => $node,
111+
'raw' => $rawClass,
112+
'processed' => $node->classes()[$className],
113+
];
114+
}
115+
}
116+
117+
foreach ($node->rawTraits() as $traitName => $rawTrait) {
118+
if (array_key_exists($traitName, $node->traits())) {
119+
$this->traitRegistry[$traitName] = [
120+
'file' => $node,
121+
'raw' => $rawTrait,
122+
'processed' => $node->traits()[$traitName],
123+
];
124+
}
125+
}
126+
}
127+
}
128+
129+
/**
130+
* @param array<string, NamespaceNode> $namespaceMap
131+
*/
132+
private function ensureNamespaceExists(string $namespace, array &$namespaceMap, NamespaceNode $rootNamespace): NamespaceNode
133+
{
134+
if (isset($namespaceMap[$namespace])) {
135+
return $namespaceMap[$namespace];
136+
}
137+
138+
$parts = explode('\\', $namespace);
139+
$current = '';
140+
141+
$parentNode = $rootNamespace;
142+
143+
foreach ($parts as $part) {
144+
$current = $current === '' ? $part : $current . '\\' . $part;
145+
146+
if (!isset($namespaceMap[$current])) {
147+
$node = new NamespaceNode($part, $current, $parentNode);
148+
$parentNode->addNamespace($node);
149+
$namespaceMap[$current] = $node;
150+
}
151+
152+
$parentNode = $namespaceMap[$current];
153+
}
154+
155+
return $parentNode;
156+
}
157+
158+
/**
159+
* @return list<TraitSection>
160+
*/
161+
private function resolveTraits(Class_ $class): array
162+
{
163+
$sections = [];
164+
165+
foreach ($class->traits() as $traitName) {
166+
if (!isset($this->traitRegistry[$traitName])) {
167+
continue;
168+
}
169+
170+
$entry = $this->traitRegistry[$traitName];
171+
172+
$sections[] = new TraitSection(
173+
$traitName,
174+
$entry['file']->pathAsString(),
175+
$entry['raw']->startLine(),
176+
$entry['raw']->endLine(),
177+
$entry['processed'],
178+
$entry['file'],
179+
);
180+
}
181+
182+
return $sections;
183+
}
184+
185+
/**
186+
* @return list<ParentSection>
187+
*/
188+
private function resolveParents(Class_ $class): array
189+
{
190+
$sections = [];
191+
$ownMethods = array_keys($class->methods());
192+
$seenMethods = $ownMethods;
193+
$currentClass = $class;
194+
195+
while ($currentClass->hasParent()) {
196+
$parentName = $currentClass->parentClass();
197+
198+
if ($parentName === null || !isset($this->classRegistry[$parentName])) {
199+
break;
200+
}
201+
202+
$parentEntry = $this->classRegistry[$parentName];
203+
$parentRaw = $parentEntry['raw'];
204+
$parentProcessed = $parentEntry['processed'];
205+
206+
$inheritedMethods = [];
207+
208+
foreach ($parentProcessed->methods as $methodName => $method) {
209+
if (!in_array($methodName, $seenMethods, true)) {
210+
$inheritedMethods[$methodName] = $method;
211+
$seenMethods[] = $methodName;
212+
}
213+
}
214+
215+
if ($inheritedMethods !== []) {
216+
$sections[] = new ParentSection(
217+
$parentName,
218+
$parentEntry['file']->pathAsString(),
219+
$inheritedMethods,
220+
$parentEntry['file'],
221+
);
222+
}
223+
224+
$currentClass = $parentRaw;
225+
}
226+
227+
return $sections;
228+
}
229+
}

0 commit comments

Comments
 (0)