Skip to content

Commit b0951be

Browse files
Initial work on building a map from code units to source files and line numbers
1 parent 0a140e3 commit b0951be

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed

src/Target/MapBuilder.php

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Test\Target;
11+
12+
use function array_keys;
13+
use function array_merge;
14+
use function array_unique;
15+
use function range;
16+
use function sort;
17+
use SebastianBergmann\CodeCoverage\Filter;
18+
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
19+
20+
/**
21+
* @immutable
22+
*
23+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage
24+
*
25+
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
26+
*/
27+
final readonly class MapBuilder
28+
{
29+
/**
30+
* @return array{namespaces: array<non-empty-string, list<positive-int>>, classes: array<non-empty-string, list<positive-int>>, classesThatExtendClass: array<non-empty-string, list<positive-int>>, classesThatImplementInterface: array<non-empty-string, list<positive-int>>, traits: array<non-empty-string, list<positive-int>>, methods: array<non-empty-string, list<positive-int>>, functions: array<non-empty-string, list<positive-int>>}
31+
*/
32+
public function build(Filter $filter, FileAnalyser $analyser): array
33+
{
34+
$namespaces = [];
35+
$classes = [];
36+
$classDetails = [];
37+
$classesThatExtendClass = [];
38+
$classesThatImplementInterface = [];
39+
$traits = [];
40+
$methods = [];
41+
$functions = [];
42+
43+
foreach ($filter->files() as $file) {
44+
foreach ($analyser->interfacesIn($file) as $interface) {
45+
$classesThatImplementInterface[$interface->namespacedName()] = [];
46+
}
47+
48+
foreach ($analyser->classesIn($file) as $class) {
49+
if ($class->isNamespaced()) {
50+
$this->process($namespaces, $class->namespace(), $file, $class->startLine(), $class->endLine());
51+
}
52+
53+
$this->process($classes, $class->namespacedName(), $file, $class->startLine(), $class->endLine());
54+
55+
foreach ($class->methods() as $method) {
56+
$this->process($methods, $class->namespacedName() . '::' . $method->name(), $file, $method->startLine(), $method->endLine());
57+
}
58+
59+
$classesThatExtendClass[$class->namespacedName()] = [];
60+
$classDetails[] = $class;
61+
}
62+
63+
foreach ($analyser->traitsIn($file) as $trait) {
64+
if ($trait->isNamespaced()) {
65+
$this->process($namespaces, $trait->namespace(), $file, $trait->startLine(), $trait->endLine());
66+
}
67+
68+
$this->process($traits, $trait->namespacedName(), $file, $trait->startLine(), $trait->endLine());
69+
70+
foreach ($trait->methods() as $method) {
71+
$this->process($methods, $trait->namespacedName() . '::' . $method->name(), $file, $method->startLine(), $method->endLine());
72+
}
73+
}
74+
75+
foreach ($analyser->functionsIn($file) as $function) {
76+
if ($function->isNamespaced()) {
77+
$this->process($namespaces, $function->namespace(), $file, $function->startLine(), $function->endLine());
78+
}
79+
80+
$this->process($functions, $function->namespacedName(), $file, $function->startLine(), $function->endLine());
81+
}
82+
}
83+
84+
foreach (array_keys($namespaces) as $namespace) {
85+
foreach (array_keys($namespaces[$namespace]) as $file) {
86+
$namespaces[$namespace][$file] = array_unique($namespaces[$namespace][$file]);
87+
88+
sort($namespaces[$namespace][$file]);
89+
}
90+
}
91+
92+
foreach ($classDetails as $class) {
93+
foreach ($class->interfaces() as $interfaceName) {
94+
if (!isset($classesThatImplementInterface[$interfaceName])) {
95+
continue;
96+
}
97+
98+
$this->process($classesThatImplementInterface, $interfaceName, $class->file(), $class->startLine(), $class->endLine());
99+
}
100+
101+
if (!$class->hasParent()) {
102+
continue;
103+
}
104+
105+
if (!isset($classesThatExtendClass[$class->parentClass()])) {
106+
continue;
107+
}
108+
109+
$this->process($classesThatExtendClass, $class->parentClass(), $class->file(), $class->startLine(), $class->endLine());
110+
}
111+
112+
foreach (array_keys($classesThatImplementInterface) as $className) {
113+
if ($classesThatImplementInterface[$className] !== []) {
114+
continue;
115+
}
116+
117+
unset($classesThatImplementInterface[$className]);
118+
}
119+
120+
foreach (array_keys($classesThatExtendClass) as $className) {
121+
if ($classesThatExtendClass[$className] !== []) {
122+
continue;
123+
}
124+
125+
unset($classesThatExtendClass[$className]);
126+
}
127+
128+
return [
129+
'namespaces' => $namespaces,
130+
'classes' => $classes,
131+
'classesThatExtendClass' => $classesThatExtendClass,
132+
'classesThatImplementInterface' => $classesThatImplementInterface,
133+
'traits' => $traits,
134+
'methods' => $methods,
135+
'functions' => $functions,
136+
];
137+
}
138+
139+
/**
140+
* @param-out array $namespaces
141+
*
142+
* @param non-empty-string $unit
143+
* @param non-empty-string $file
144+
* @param positive-int $startLine
145+
* @param positive-int $endLine
146+
*/
147+
private function process(array &$data, string $unit, string $file, int $startLine, int $endLine): void
148+
{
149+
if (!isset($data[$unit])) {
150+
$data[$unit] = [];
151+
}
152+
153+
if (!isset($data[$unit][$file])) {
154+
$data[$unit][$file] = [];
155+
}
156+
157+
$data[$unit][$file] = array_merge(
158+
$data[$unit][$file],
159+
range($startLine, $endLine),
160+
);
161+
}
162+
}

tests/tests/Target/MapBuilderTest.php

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Test\Target;
11+
12+
use function array_merge;
13+
use function range;
14+
use function realpath;
15+
use PHPUnit\Framework\Attributes\CoversClass;
16+
use PHPUnit\Framework\Attributes\Small;
17+
use PHPUnit\Framework\TestCase;
18+
use SebastianBergmann\CodeCoverage\Filter;
19+
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;
20+
21+
#[CoversClass(MapBuilder::class)]
22+
#[Small]
23+
final class MapBuilderTest extends TestCase
24+
{
25+
public function testBuildsMap(): void
26+
{
27+
$this->assertSame(
28+
[
29+
'namespaces' => [
30+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis' => [
31+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => array_merge(
32+
range(19, 24),
33+
range(26, 31),
34+
range(33, 52),
35+
range(54, 56),
36+
),
37+
],
38+
],
39+
'classes' => [
40+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ParentClass' => [
41+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(26, 31),
42+
],
43+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass' => [
44+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(33, 52),
45+
],
46+
],
47+
'classesThatExtendClass' => [
48+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ParentClass' => [
49+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(33, 52),
50+
],
51+
],
52+
'classesThatImplementInterface' => [
53+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\A' => [
54+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(33, 52),
55+
],
56+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\B' => [
57+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(33, 52),
58+
],
59+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\C' => [
60+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(26, 31),
61+
],
62+
],
63+
'traits' => [
64+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\T' => [
65+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(19, 24),
66+
],
67+
],
68+
'methods' => [
69+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ParentClass::five' => [
70+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(28, 30),
71+
],
72+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass::six' => [
73+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(37, 39),
74+
],
75+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass::one' => [
76+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(41, 43),
77+
],
78+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass::two' => [
79+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(45, 47),
80+
],
81+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass::three' => [
82+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(49, 51),
83+
],
84+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\T::four' => [
85+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(21, 23),
86+
],
87+
],
88+
'functions' => [
89+
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\f' => [
90+
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(54, 56),
91+
],
92+
],
93+
],
94+
$this->map([__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php']),
95+
);
96+
}
97+
98+
/**
99+
* @return array{namespaces: array<non-empty-string, list<positive-int>>, classes: array<non-empty-string, list<positive-int>>, classesThatExtendClass: array<non-empty-string, list<positive-int>>, classesThatImplementInterface: array<non-empty-string, list<positive-int>>, traits: array<non-empty-string, list<positive-int>>, methods: array<non-empty-string, list<positive-int>>, functions: array<non-empty-string, list<positive-int>>}
100+
*/
101+
private function map(array $files): array
102+
{
103+
$filter = new Filter;
104+
105+
$filter->includeFiles($files);
106+
107+
return (new MapBuilder)->build($filter, new ParsingFileAnalyser(false, false));
108+
}
109+
}

0 commit comments

Comments
 (0)