From a0adc211d8109686e29f500498a804e903173eb4 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 14 Dec 2024 17:06:19 +0100 Subject: [PATCH] Closes #1055 --- src/StaticAnalysis/CodeUnitFindingVisitor.php | 63 ++++++++++---- src/StaticAnalysis/Value/Trait_.php | 32 ++++++- src/Target/MapBuilder.php | 84 +++++++++++++++---- .../Target/ClassUsingTraitUsingTrait.php | 11 +++ tests/_files/Target/TraitOne.php | 9 ++ tests/_files/Target/TraitTwo.php | 11 +++ .../tests/StaticAnalysis/Value/TraitTest.php | 19 ++++- tests/tests/Target/MapBuilderTest.php | 19 +++-- tests/tests/Target/MapperTest.php | 21 ++++- 9 files changed, 220 insertions(+), 49 deletions(-) create mode 100644 tests/_files/Target/ClassUsingTraitUsingTrait.php create mode 100644 tests/_files/Target/TraitOne.php create mode 100644 tests/_files/Target/TraitTwo.php diff --git a/src/StaticAnalysis/CodeUnitFindingVisitor.php b/src/StaticAnalysis/CodeUnitFindingVisitor.php index 8d54b41ad..08fd009eb 100644 --- a/src/StaticAnalysis/CodeUnitFindingVisitor.php +++ b/src/StaticAnalysis/CodeUnitFindingVisitor.php @@ -96,11 +96,11 @@ public function enterNode(Node $node): null public function leaveNode(Node $node): null { - if (!$node instanceof Class_) { + if ($node instanceof Class_ && $node->isAnonymous()) { return null; } - if ($node->isAnonymous()) { + if (!$node instanceof Class_ && !$node instanceof Trait_) { return null; } @@ -116,22 +116,7 @@ public function leaveNode(Node $node): null return null; } - $namespacedClassName = $node->namespacedName->toString(); - - assert(isset($this->classes[$namespacedClassName])); - - $this->classes[$namespacedClassName] = new \SebastianBergmann\CodeCoverage\StaticAnalysis\Class_( - $this->classes[$namespacedClassName]->name(), - $this->classes[$namespacedClassName]->namespacedName(), - $this->classes[$namespacedClassName]->namespace(), - $this->classes[$namespacedClassName]->file(), - $this->classes[$namespacedClassName]->startLine(), - $this->classes[$namespacedClassName]->endLine(), - $this->classes[$namespacedClassName]->parentClass(), - $this->classes[$namespacedClassName]->interfaces(), - $traits, - $this->classes[$namespacedClassName]->methods(), - ); + $this->postProcessClassOrTrait($node, $traits); return null; } @@ -308,8 +293,10 @@ private function processTrait(Trait_ $node): void $name, $namespacedName, $this->namespace($namespacedName, $name), + $this->file, $node->getStartLine(), $node->getEndLine(), + [], $this->processMethods($node->getMethods()), ); } @@ -398,4 +385,44 @@ private function typeAsString(Identifier|Name $node): string return $node->toString(); } + + /** + * @param list $traits + */ + private function postProcessClassOrTrait(Class_|Trait_ $node, array $traits): void + { + $name = $node->namespacedName->toString(); + + if ($node instanceof Class_) { + assert(isset($this->classes[$name])); + + $this->classes[$name] = new \SebastianBergmann\CodeCoverage\StaticAnalysis\Class_( + $this->classes[$name]->name(), + $this->classes[$name]->namespacedName(), + $this->classes[$name]->namespace(), + $this->classes[$name]->file(), + $this->classes[$name]->startLine(), + $this->classes[$name]->endLine(), + $this->classes[$name]->parentClass(), + $this->classes[$name]->interfaces(), + $traits, + $this->classes[$name]->methods(), + ); + + return; + } + + assert(isset($this->traits[$name])); + + $this->traits[$name] = new \SebastianBergmann\CodeCoverage\StaticAnalysis\Trait_( + $this->traits[$name]->name(), + $this->traits[$name]->namespacedName(), + $this->traits[$name]->namespace(), + $this->traits[$name]->file(), + $this->traits[$name]->startLine(), + $this->traits[$name]->endLine(), + $traits, + $this->traits[$name]->methods(), + ); + } } diff --git a/src/StaticAnalysis/Value/Trait_.php b/src/StaticAnalysis/Value/Trait_.php index 19c63aace..7bf1084cd 100644 --- a/src/StaticAnalysis/Value/Trait_.php +++ b/src/StaticAnalysis/Value/Trait_.php @@ -25,6 +25,11 @@ private string $namespacedName; private string $namespace; + /** + * @var non-empty-string + */ + private string $file; + /** * @var non-negative-int */ @@ -35,6 +40,11 @@ */ private int $endLine; + /** + * @var list + */ + private array $traits; + /** * @var array */ @@ -43,17 +53,21 @@ /** * @param non-empty-string $name * @param non-empty-string $namespacedName + * @param non-empty-string $file * @param non-negative-int $startLine * @param non-negative-int $endLine + * @param list $traits * @param array $methods */ - public function __construct(string $name, string $namespacedName, string $namespace, int $startLine, int $endLine, array $methods) + public function __construct(string $name, string $namespacedName, string $namespace, string $file, int $startLine, int $endLine, array $traits, array $methods) { $this->name = $name; $this->namespacedName = $namespacedName; $this->namespace = $namespace; + $this->file = $file; $this->startLine = $startLine; $this->endLine = $endLine; + $this->traits = $traits; $this->methods = $methods; } @@ -83,6 +97,14 @@ public function namespace(): string return $this->namespace; } + /** + * @return non-empty-string + */ + public function file(): string + { + return $this->file; + } + /** * @return non-negative-int */ @@ -99,6 +121,14 @@ public function endLine(): int return $this->endLine; } + /** + * @return list + */ + public function traits(): array + { + return $this->traits; + } + /** * @return array */ diff --git a/src/Target/MapBuilder.php b/src/Target/MapBuilder.php index ae0235b58..7fc02c8bd 100644 --- a/src/Target/MapBuilder.php +++ b/src/Target/MapBuilder.php @@ -44,19 +44,15 @@ public function build(Filter $filter, FileAnalyser $analyser): array $reverseLookup = []; 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()); + foreach ($analyser->traitsIn($file) as $trait) { + if ($trait->isNamespaced()) { + $this->process($namespaces, $trait->namespace(), $file, $trait->startLine(), $trait->endLine()); } - $this->process($classes, $class->namespacedName(), $file, $class->startLine(), $class->endLine()); + $this->process($traits, $trait->namespacedName(), $file, $trait->startLine(), $trait->endLine()); - foreach ($class->methods() as $method) { - $methodName = $class->namespacedName() . '::' . $method->name(); + foreach ($trait->methods() as $method) { + $methodName = $trait->namespacedName() . '::' . $method->name(); $this->process($methods, $methodName, $file, $method->startLine(), $method->endLine()); @@ -64,20 +60,69 @@ public function build(Filter $filter, FileAnalyser $analyser): array $reverseLookup[$file . ':' . $line] = $methodName; } } - - $classesThatExtendClass[$class->namespacedName()] = []; - $classDetails[] = $class; } + } + foreach ($filter->files() as $file) { foreach ($analyser->traitsIn($file) as $trait) { - if ($trait->isNamespaced()) { - $this->process($namespaces, $trait->namespace(), $file, $trait->startLine(), $trait->endLine()); + foreach ($trait->traits() as $traitName) { + if (!isset($traits[$traitName])) { + continue; + } + + $file = array_keys($traits[$traitName])[0]; + + if (!isset($traits[$trait->namespacedName()][$file])) { + $traits[$trait->namespacedName()][$file] = $traits[$traitName][$file]; + + continue; + } + + $traits[$trait->namespacedName()][$file] = array_unique( + array_merge( + $traits[$trait->namespacedName()][$file], + $traits[$traitName][$file], + ), + ); } + } + } - $this->process($traits, $trait->namespacedName(), $file, $trait->startLine(), $trait->endLine()); + foreach ($filter->files() as $file) { + foreach ($analyser->interfacesIn($file) as $interface) { + $classesThatImplementInterface[$interface->namespacedName()] = []; + } - foreach ($trait->methods() as $method) { - $methodName = $trait->namespacedName() . '::' . $method->name(); + 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->traits() as $traitName) { + if (!isset($traits[$traitName])) { + continue; + } + + foreach ($traits[$traitName] as $file => $lines) { + if (!isset($classes[$class->namespacedName()][$file])) { + $classes[$class->namespacedName()][$file] = $lines; + + continue; + } + + $classes[$class->namespacedName()][$file] = array_unique( + array_merge( + $classes[$class->namespacedName()][$file], + $lines, + ), + ); + } + } + + foreach ($class->methods() as $method) { + $methodName = $class->namespacedName() . '::' . $method->name(); $this->process($methods, $methodName, $file, $method->startLine(), $method->endLine()); @@ -85,6 +130,9 @@ public function build(Filter $filter, FileAnalyser $analyser): array $reverseLookup[$file . ':' . $line] = $methodName; } } + + $classesThatExtendClass[$class->namespacedName()] = []; + $classDetails[] = $class; } foreach ($analyser->functionsIn($file) as $function) { diff --git a/tests/_files/Target/ClassUsingTraitUsingTrait.php b/tests/_files/Target/ClassUsingTraitUsingTrait.php new file mode 100644 index 000000000..15a6c9ffe --- /dev/null +++ b/tests/_files/Target/ClassUsingTraitUsingTrait.php @@ -0,0 +1,11 @@ +assertSame('example', $this->trait()->namespace()); } + public function testHasFile(): void + { + $this->assertSame('file.php', $this->trait()->file()); + } + public function testHasStartLine(): void { $this->assertSame(1, $this->trait()->startLine()); @@ -42,6 +47,13 @@ public function testHasEndLine(): void $this->assertSame(2, $this->trait()->endLine()); } + public function testMayHaveTraits(): void + { + $traits = ['trait']; + + $this->assertSame($traits, $this->trait(traits: $traits)->traits()); + } + public function testMayHaveMethods(): void { $methods = [ @@ -55,20 +67,23 @@ public function testMayHaveMethods(): void ), ]; - $this->assertSame($methods, $this->trait($methods)->methods()); + $this->assertSame($methods, $this->trait(methods: $methods)->methods()); } /** + * @param list $traits * @param array $methods */ - private function trait(array $methods = []): Trait_ + private function trait(array $traits = [], array $methods = []): Trait_ { return new Trait_( 'Example', 'example\Example', 'example', + 'file.php', 1, 2, + $traits, $methods, ); } diff --git a/tests/tests/Target/MapBuilderTest.php b/tests/tests/Target/MapBuilderTest.php index 142fa1877..e41cbb644 100644 --- a/tests/tests/Target/MapBuilderTest.php +++ b/tests/tests/Target/MapBuilderTest.php @@ -34,9 +34,9 @@ public function testBuildsMap(): void 'namespaces' => [ 'SebastianBergmann\\CodeCoverage\\StaticAnalysis' => [ $file => array_merge( + range(19, 24), range(26, 31), range(33, 52), - range(19, 24), range(54, 56), ), ], @@ -46,7 +46,10 @@ public function testBuildsMap(): void $file => range(26, 31), ], 'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass' => [ - $file => range(33, 52), + $file => array_merge( + range(33, 52), + range(19, 24), + ), ], ], 'classesThatExtendClass' => [ @@ -71,6 +74,9 @@ public function testBuildsMap(): void ], ], 'methods' => [ + 'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\T::four' => [ + $file => range(21, 23), + ], 'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ParentClass::five' => [ $file => range(28, 30), ], @@ -86,9 +92,6 @@ public function testBuildsMap(): void 'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\ChildClass::three' => [ $file => range(49, 51), ], - 'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\T::four' => [ - $file => range(21, 23), - ], ], 'functions' => [ 'SebastianBergmann\\CodeCoverage\\StaticAnalysis\\f' => [ @@ -96,6 +99,9 @@ public function testBuildsMap(): void ], ], 'reverseLookup' => [ + $file . ':21' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\T::four', + $file . ':22' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\T::four', + $file . ':23' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\T::four', $file . ':28' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\ParentClass::five', $file . ':29' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\ParentClass::five', $file . ':30' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\ParentClass::five', @@ -111,9 +117,6 @@ public function testBuildsMap(): void $file . ':49' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\ChildClass::three', $file . ':50' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\ChildClass::three', $file . ':51' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\ChildClass::three', - $file . ':21' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\T::four', - $file . ':22' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\T::four', - $file . ':23' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\T::four', $file . ':54' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\f', $file . ':55' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\f', $file . ':56' => 'SebastianBergmann\CodeCoverage\StaticAnalysis\f', diff --git a/tests/tests/Target/MapperTest.php b/tests/tests/Target/MapperTest.php index dca0345d9..967e5e1ab 100644 --- a/tests/tests/Target/MapperTest.php +++ b/tests/tests/Target/MapperTest.php @@ -38,7 +38,10 @@ public static function provider(): array return [ 'class' => [ [ - $file => range(33, 52), + $file => array_merge( + range(33, 52), + range(19, 24), + ), ], TargetCollection::fromArray( [ @@ -46,6 +49,20 @@ public static function provider(): array ], ), ], + + 'class (which uses traits)' => [ + [ + realpath(__DIR__ . '/../../_files/Target/ClassUsingTraitUsingTrait.php') => range(4, 11), + realpath(__DIR__ . '/../../_files/Target/TraitTwo.php') => range(4, 11), + realpath(__DIR__ . '/../../_files/Target/TraitOne.php') => range(4, 9), + ], + TargetCollection::fromArray( + [ + Target::forClass('SebastianBergmann\\CodeCoverage\\TestFixture\\Target\\ClassUsingTraitUsingTrait'), + ], + ), + ], + 'classes that extend class' => [ [ $file => range(33, 52), @@ -100,9 +117,9 @@ public static function provider(): array 'namespace' => [ [ $file => array_merge( + range(19, 24), range(26, 31), range(33, 52), - range(19, 24), range(54, 56), ), ],