Skip to content

Commit

Permalink
Initial work on building a map from code units to source files and li…
Browse files Browse the repository at this point in the history
…ne numbers
  • Loading branch information
sebastianbergmann committed Oct 9, 2024
1 parent 0a140e3 commit b0951be
Show file tree
Hide file tree
Showing 2 changed files with 271 additions and 0 deletions.
162 changes: 162 additions & 0 deletions src/Target/MapBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\Target;

use function array_keys;
use function array_merge;
use function array_unique;
use function range;
use function sort;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;

/**
* @immutable
*
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final readonly class MapBuilder
{
/**
* @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>>}
*/
public function build(Filter $filter, FileAnalyser $analyser): array
{
$namespaces = [];
$classes = [];
$classDetails = [];
$classesThatExtendClass = [];
$classesThatImplementInterface = [];
$traits = [];
$methods = [];
$functions = [];

foreach ($filter->files() as $file) {
foreach ($analyser->interfacesIn($file) as $interface) {
$classesThatImplementInterface[$interface->namespacedName()] = [];
}

foreach ($analyser->classesIn($file) as $class) {
if ($class->isNamespaced()) {
$this->process($namespaces, $class->namespace(), $file, $class->startLine(), $class->endLine());
}

$this->process($classes, $class->namespacedName(), $file, $class->startLine(), $class->endLine());

foreach ($class->methods() as $method) {
$this->process($methods, $class->namespacedName() . '::' . $method->name(), $file, $method->startLine(), $method->endLine());
}

$classesThatExtendClass[$class->namespacedName()] = [];
$classDetails[] = $class;
}

foreach ($analyser->traitsIn($file) as $trait) {
if ($trait->isNamespaced()) {
$this->process($namespaces, $trait->namespace(), $file, $trait->startLine(), $trait->endLine());
}

$this->process($traits, $trait->namespacedName(), $file, $trait->startLine(), $trait->endLine());

foreach ($trait->methods() as $method) {
$this->process($methods, $trait->namespacedName() . '::' . $method->name(), $file, $method->startLine(), $method->endLine());
}
}

foreach ($analyser->functionsIn($file) as $function) {
if ($function->isNamespaced()) {
$this->process($namespaces, $function->namespace(), $file, $function->startLine(), $function->endLine());
}

$this->process($functions, $function->namespacedName(), $file, $function->startLine(), $function->endLine());
}
}

foreach (array_keys($namespaces) as $namespace) {
foreach (array_keys($namespaces[$namespace]) as $file) {
$namespaces[$namespace][$file] = array_unique($namespaces[$namespace][$file]);

sort($namespaces[$namespace][$file]);
}
}

foreach ($classDetails as $class) {
foreach ($class->interfaces() as $interfaceName) {
if (!isset($classesThatImplementInterface[$interfaceName])) {
continue;
}

$this->process($classesThatImplementInterface, $interfaceName, $class->file(), $class->startLine(), $class->endLine());
}

if (!$class->hasParent()) {
continue;
}

if (!isset($classesThatExtendClass[$class->parentClass()])) {
continue;
}

$this->process($classesThatExtendClass, $class->parentClass(), $class->file(), $class->startLine(), $class->endLine());
}

foreach (array_keys($classesThatImplementInterface) as $className) {
if ($classesThatImplementInterface[$className] !== []) {
continue;
}

unset($classesThatImplementInterface[$className]);
}

foreach (array_keys($classesThatExtendClass) as $className) {
if ($classesThatExtendClass[$className] !== []) {
continue;
}

unset($classesThatExtendClass[$className]);
}

return [
'namespaces' => $namespaces,
'classes' => $classes,
'classesThatExtendClass' => $classesThatExtendClass,
'classesThatImplementInterface' => $classesThatImplementInterface,
'traits' => $traits,
'methods' => $methods,
'functions' => $functions,
];
}

/**
* @param-out array $namespaces
*
* @param non-empty-string $unit
* @param non-empty-string $file
* @param positive-int $startLine
* @param positive-int $endLine
*/
private function process(array &$data, string $unit, string $file, int $startLine, int $endLine): void
{
if (!isset($data[$unit])) {
$data[$unit] = [];
}

if (!isset($data[$unit][$file])) {
$data[$unit][$file] = [];
}

$data[$unit][$file] = array_merge(
$data[$unit][$file],
range($startLine, $endLine),
);
}
}
109 changes: 109 additions & 0 deletions tests/tests/Target/MapBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Test\Target;

use function array_merge;
use function range;
use function realpath;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\TestCase;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;

#[CoversClass(MapBuilder::class)]
#[Small]
final class MapBuilderTest extends TestCase
{
public function testBuildsMap(): void
{
$this->assertSame(
[
'namespaces' => [
'SebastianBergmann\\CodeCoverage\\StaticAnalysis' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => array_merge(
range(19, 24),
range(26, 31),
range(33, 52),
range(54, 56),
),
],
],
'classes' => [
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ParentClass' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(26, 31),
],
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(33, 52),
],
],
'classesThatExtendClass' => [
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ParentClass' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(33, 52),
],
],
'classesThatImplementInterface' => [
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\A' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(33, 52),
],
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\B' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(33, 52),
],
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\C' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(26, 31),
],
],
'traits' => [
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\T' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(19, 24),
],
],
'methods' => [
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ParentClass::five' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(28, 30),
],
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass::six' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(37, 39),
],
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass::one' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(41, 43),
],
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass::two' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(45, 47),
],
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass::three' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(49, 51),
],
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\T::four' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(21, 23),
],
],
'functions' => [
'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\f' => [
realpath(__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php') => range(54, 56),
],
],
],
$this->map([__DIR__ . '/../../_files/source_with_interfaces_classes_traits_functions.php']),
);
}

/**
* @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>>}
*/
private function map(array $files): array
{
$filter = new Filter;

$filter->includeFiles($files);

return (new MapBuilder)->build($filter, new ParsingFileAnalyser(false, false));
}
}

0 comments on commit b0951be

Please sign in to comment.