diff --git a/src/Target/MapBuilder.php b/src/Target/MapBuilder.php new file mode 100644 index 000000000..50af85bd4 --- /dev/null +++ b/src/Target/MapBuilder.php @@ -0,0 +1,162 @@ + + * + * 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>, classes: array>, classesThatExtendClass: array>, classesThatImplementInterface: array>, traits: array>, methods: array>, functions: array>} + */ + 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), + ); + } +} diff --git a/tests/tests/Target/MapBuilderTest.php b/tests/tests/Target/MapBuilderTest.php new file mode 100644 index 000000000..54b6baecf --- /dev/null +++ b/tests/tests/Target/MapBuilderTest.php @@ -0,0 +1,109 @@ + + * + * 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>, classes: array>, classesThatExtendClass: array>, classesThatImplementInterface: array>, traits: array>, methods: array>, functions: array>} + */ + private function map(array $files): array + { + $filter = new Filter; + + $filter->includeFiles($files); + + return (new MapBuilder)->build($filter, new ParsingFileAnalyser(false, false)); + } +}