Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions src/CodeBlockAnalyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
use ShipMonk\CoverageGuard\Report\ReportedError;
use ShipMonk\CoverageGuard\Rule\CoverageRule;
use ShipMonk\CoverageGuard\Rule\InspectionContext;
use function array_pop;
use function assert;
use function end;
use function range;

final class CodeBlockAnalyser extends NodeVisitorAbstract
Expand All @@ -23,7 +25,10 @@ final class CodeBlockAnalyser extends NodeVisitorAbstract

private ?string $currentMethod = null;

private bool $inAnonymousClass = false;
/**
* @var list<string|null> FQN of each entered class-like, null for anonymous class
*/
private array $classStack = [];

/**
* @var list<ReportedError>
Expand Down Expand Up @@ -56,20 +61,22 @@ public function enterNode(Node $node): ?int
{
if ($node instanceof ClassLike) {
if ($node->name === null) {
$this->inAnonymousClass = true;
$this->classStack[] = null;
} else {
assert($node->namespacedName !== null); // using NameResolver
$this->currentClass = $node->namespacedName->toString();
$this->updateContext();
$this->classStack[] = $node->namespacedName->toString();
}
}

if ($node instanceof ClassMethod && $node->stmts !== null) {
if ($this->inAnonymousClass) {
return null; // ClassMethodBlock is emitted only for real methods
if ($this->classStack === []) {
throw new LogicException('Found class method without a class, should never happen');
}

$this->currentClass = end($this->classStack);

if ($this->currentClass === null) {
throw new LogicException('Found class method without a class, should never happen');
return null; // ClassMethodBlock is emitted only for real methods
}

$startLine = $node->name->getStartLine();
Expand Down Expand Up @@ -106,11 +113,7 @@ public function enterNode(Node $node): ?int
public function leaveNode(Node $node): mixed
{
if ($node instanceof ClassLike) {
if ($node->name !== null) {
$this->currentClass = null;
} else {
$this->inAnonymousClass = false;
}
array_pop($this->classStack);
}

return null;
Expand Down
57 changes: 57 additions & 0 deletions tests/CodeBlockAnalyserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,43 @@ public function testAnalysesClassWithAnonymousClass(): void
self::assertSame(['methodWithAnonymousClass', 'regularMethod'], $methodsByClass['ClassWithAnonymousClass']);
}

public function testAnalysesClassWithNestedAnonymousClasses(): void
{
$filePath = __DIR__ . '/_fixtures/CodeBlockAnalyser/ClassWithNestedAnonymousClasses.php';

$rule = $this->createContextCapturingRule();
$analyser = $this->createAnalyser($filePath, [$rule]);

$this->traverseFile($filePath, $analyser);

self::assertSame(
[
'ClassWithNestedAnonymousClasses::methodWithAnonymousClass',
'ClassWithNestedAnonymousClasses::regularMethod',
],
$this->getAnalysedMethods($rule->capturedContexts),
);
}

public function testAnalysesClassDeclaredInMethod(): void
{
$filePath = __DIR__ . '/_fixtures/CodeBlockAnalyser/ClassWithClassDeclaredInMethod.php';

$rule = $this->createContextCapturingRule();
$analyser = $this->createAnalyser($filePath, [$rule]);

$this->traverseFile($filePath, $analyser);

self::assertSame(
[
'ClassWithClassDeclaredInMethod::methodDeclaringClass',
'ClassDeclaredInMethod::innerMethod',
'ClassWithClassDeclaredInMethod::regularMethod',
],
$this->getAnalysedMethods($rule->capturedContexts),
);
}

public function testAnalysesTrait(): void
{
$filePath = __DIR__ . '/_fixtures/CodeBlockAnalyser/TraitWithMethod.php';
Expand Down Expand Up @@ -263,6 +300,26 @@ private function traverseFile(
$traverser->traverse($filePath, $this->getFileLines($filePath), ...$visitors);
}

/**
* @param list<InspectionContext> $contexts
* @return list<string>
*/
private function getAnalysedMethods(array $contexts): array
{
$methods = [];
foreach ($contexts as $context) {
$className = $context->getClassName();
$methodName = $context->getMethodName();

self::assertNotNull($className);
self::assertNotNull($methodName);

$methods[] = "{$className}::{$methodName}";
}

return $methods;
}

/**
* @return CoverageRule&object{capturedContexts: list<InspectionContext>}
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types = 1);

class ClassWithClassDeclaredInMethod
{

public function methodDeclaringClass(): void
{
class ClassDeclaredInMethod
{
public function innerMethod(): void
{
echo 'Hi';
}
}
}

public function regularMethod(): void
{
echo 'Hello';
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

class ClassWithNestedAnonymousClasses
{

public function methodWithAnonymousClass(): object
{
return new class {
public function methodWithInnerAnonymousClass(): object
{
return new class {
};
}

public function methodAfterInnerAnonymousClass(): void
{
echo 'Hi';
}
};
}

public function regularMethod(): void
{
echo 'Hello';
}

}
Loading