Skip to content
Merged
2 changes: 1 addition & 1 deletion src/Analyzer/DocblockTypesResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function enterNode(Node $node): void

private function resolvePropertyTypes(Node $node): void
{
if (!($node instanceof Stmt\Property)) {
if (!$node instanceof Stmt\Property) {
return;
}

Expand Down
42 changes: 21 additions & 21 deletions src/Analyzer/FileVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public function leaveNode(Node $node): void

private function handleClassNode(Node $node): void
{
if (!($node instanceof Node\Stmt\Class_)) {
if (!$node instanceof Node\Stmt\Class_) {
return;
}

Expand Down Expand Up @@ -140,7 +140,7 @@ private function handleClassNode(Node $node): void

private function handleAnonClassNode(Node $node): void
{
if (!($node instanceof Node\Stmt\Class_)) {
if (!$node instanceof Node\Stmt\Class_) {
return;
}

Expand All @@ -161,7 +161,7 @@ private function handleAnonClassNode(Node $node): void

private function handleEnumNode(Node $node): void
{
if (!($node instanceof Node\Stmt\Enum_)) {
if (!$node instanceof Node\Stmt\Enum_) {
return;
}

Expand All @@ -180,11 +180,11 @@ private function handleEnumNode(Node $node): void

private function handleStaticClassConstantNode(Node $node): void
{
if (!($node instanceof Node\Expr\ClassConstFetch)) {
if (!$node instanceof Node\Expr\ClassConstFetch) {
return;
}

if (!($node->class instanceof Node\Name\FullyQualified)) {
if (!$node->class instanceof Node\Name\FullyQualified) {
return;
}

Expand All @@ -194,11 +194,11 @@ private function handleStaticClassConstantNode(Node $node): void

private function handleStaticClassCallsNode(Node $node): void
{
if (!($node instanceof Node\Expr\StaticCall)) {
if (!$node instanceof Node\Expr\StaticCall) {
return;
}

if (!($node->class instanceof Node\Name\FullyQualified)) {
if (!$node->class instanceof Node\Name\FullyQualified) {
return;
}

Expand All @@ -208,11 +208,11 @@ private function handleStaticClassCallsNode(Node $node): void

private function handleInstanceOf(Node $node): void
{
if (!($node instanceof Node\Expr\Instanceof_)) {
if (!$node instanceof Node\Expr\Instanceof_) {
return;
}

if (!($node->class instanceof Node\Name\FullyQualified)) {
if (!$node->class instanceof Node\Name\FullyQualified) {
return;
}

Expand All @@ -222,11 +222,11 @@ private function handleInstanceOf(Node $node): void

private function handleNewExpression(Node $node): void
{
if (!($node instanceof Node\Expr\New_)) {
if (!$node instanceof Node\Expr\New_) {
return;
}

if (!($node->class instanceof Node\Name\FullyQualified)) {
if (!$node->class instanceof Node\Name\FullyQualified) {
return;
}

Expand All @@ -236,7 +236,7 @@ private function handleNewExpression(Node $node): void

private function handleTypedProperty(Node $node): void
{
if (!($node instanceof Node\Stmt\Property)) {
if (!$node instanceof Node\Stmt\Property) {
return;
}

Expand All @@ -246,7 +246,7 @@ private function handleTypedProperty(Node $node): void

$type = $node->type instanceof NullableType ? $node->type->type : $node->type;

if (!($type instanceof Node\Name\FullyQualified)) {
if (!$type instanceof Node\Name\FullyQualified) {
return;
}

Expand Down Expand Up @@ -274,7 +274,7 @@ private function handleParamDependency(Node $node): void

private function handleInterfaceNode(Node $node): void
{
if (!($node instanceof Node\Stmt\Interface_)) {
if (!$node instanceof Node\Stmt\Interface_) {
return;
}

Expand All @@ -293,7 +293,7 @@ private function handleInterfaceNode(Node $node): void

private function handleTraitNode(Node $node): void
{
if (!($node instanceof Node\Stmt\Trait_)) {
if (!$node instanceof Node\Stmt\Trait_) {
return;
}

Expand All @@ -307,13 +307,13 @@ private function handleTraitNode(Node $node): void

private function handleReturnTypeDependency(Node $node): void
{
if (!($node instanceof Node\Stmt\ClassMethod)) {
if (!$node instanceof Node\Stmt\ClassMethod) {
return;
}

$returnType = $node->returnType;

if (!($returnType instanceof Node\Name\FullyQualified)) {
if (!$returnType instanceof Node\Name\FullyQualified) {
return;
}

Expand All @@ -323,13 +323,13 @@ private function handleReturnTypeDependency(Node $node): void

private function handleAttributeNode(Node $node): void
{
if (!($node instanceof Node\Attribute)) {
if (!$node instanceof Node\Attribute) {
return;
}

$nodeName = $node->name;

if (!($nodeName instanceof Node\Name\FullyQualified)) {
if (!$nodeName instanceof Node\Name\FullyQualified) {
return;
}

Expand All @@ -345,7 +345,7 @@ private function addParamDependency(Node\Param $node): void

$type = $node->type instanceof NullableType ? $node->type->type : $node->type;

if (!($type instanceof Node\Name\FullyQualified)) {
if (!$type instanceof Node\Name\FullyQualified) {
return;
}

Expand All @@ -355,7 +355,7 @@ private function addParamDependency(Node\Param $node): void

private function handlePropertyHookNode(Node $node): void
{
if (!($node instanceof Node\PropertyHook)) {
if (!$node instanceof Node\PropertyHook) {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Expression/ForClasses/Implement.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function evaluate(ClassDescription $theClass, Violations $violations, str

$interface = $this->interface;
$interfaces = $theClass->getInterfaces();
$implements = function (FullyQualifiedClassName $FQCN) use ($interface): bool {
$implements = static function (FullyQualifiedClassName $FQCN) use ($interface): bool {
return $FQCN->matches($interface);
};

Expand Down
2 changes: 1 addition & 1 deletion src/Expression/ForClasses/IsAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function describe(ClassDescription $theClass, string $because): Descripti

public function appliesTo(ClassDescription $theClass): bool
{
return !($theClass->isInterface() || $theClass->isTrait() || $theClass->isEnum() || $theClass->isFinal());
return !($theClass->isInterface() || $theClass->isTrait() || $theClass->isEnum());
}

public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
Expand Down
2 changes: 1 addition & 1 deletion src/Expression/ForClasses/IsFinal.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function describe(ClassDescription $theClass, string $because): Descripti

public function appliesTo(ClassDescription $theClass): bool
{
return !($theClass->isInterface() || $theClass->isTrait() || $theClass->isEnum() || $theClass->isAbstract());
return !($theClass->isInterface() || $theClass->isTrait() || $theClass->isEnum());
}

public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
Expand Down
2 changes: 1 addition & 1 deletion src/Expression/ForClasses/IsNotAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function describe(ClassDescription $theClass, string $because): Descripti

public function appliesTo(ClassDescription $theClass): bool
{
return !($theClass->isInterface() || $theClass->isTrait() || $theClass->isEnum() || $theClass->isFinal());
return !($theClass->isInterface() || $theClass->isTrait() || $theClass->isEnum());
}

public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
Expand Down
2 changes: 1 addition & 1 deletion src/Expression/ForClasses/IsNotFinal.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function describe(ClassDescription $theClass, string $because): Descripti

public function appliesTo(ClassDescription $theClass): bool
{
return !($theClass->isInterface() || $theClass->isTrait() || $theClass->isEnum() || $theClass->isAbstract());
return !($theClass->isInterface() || $theClass->isTrait() || $theClass->isEnum());
}

public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function describe(ClassDescription $theClass, string $because): Descripti
public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
{
$namespace = $this->namespace;
$depends = function (ClassDependency $dependency) use ($namespace): bool {
$depends = static function (ClassDependency $dependency) use ($namespace): bool {
return !$dependency->getFQCN()->matches($namespace);
};

Expand Down
2 changes: 1 addition & 1 deletion src/Expression/ForClasses/NotImplement.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function evaluate(ClassDescription $theClass, Violations $violations, str

$interface = $this->interface;
$interfaces = $theClass->getInterfaces();
$implements = function (FullyQualifiedClassName $FQCN) use ($interface): bool {
$implements = static function (FullyQualifiedClassName $FQCN) use ($interface): bool {
return $FQCN->matches($interface);
};

Expand Down
4 changes: 2 additions & 2 deletions src/Rules/Violations.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static function fromJson(string $json): self

$instance = new self();

$instance->violations = array_map(function (array $json): Violation {
$instance->violations = array_map(static function (array $json): Violation {
return Violation::fromJson($json);
}, $json['violations']);

Expand Down Expand Up @@ -67,7 +67,7 @@ public function count(): int

public function groupedByFqcn(): array
{
return array_reduce($this->violations, function (array $accumulator, Violation $element) {
return array_reduce($this->violations, static function (array $accumulator, Violation $element) {
$accumulator[$element->getFqcn()][] = $element;

return $accumulator;
Expand Down
89 changes: 89 additions & 0 deletions tests/Integration/IsAbstractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,95 @@ public function test_is_not_abstract_in_should_should_consider_final_traits_enum
self::assertEquals('App\MyAbstract', $runner->getViolations()->get(0)->getFqcn());
}

public function test_is_not_abstract_in_that_should_include_final_classes(): void
{
$structure = [
'App' => [
'Test' => [
// Final class with correct name - should pass
'GoodEndToEndTest.php' => '<?php namespace App\Test; final class GoodEndToEndTest {} ',

// Abstract class - should be filtered out by IsNotAbstract
'AbstractTestCase.php' => '<?php namespace App\Test; abstract class AbstractTestCase {} ',

// Final class with wrong name - should generate violation
'BadE2ETest.php' => '<?php namespace App\Test; final class BadE2ETest {} ',

// Normal class with wrong name - should also generate violation
'AnotherBadTest.php' => '<?php namespace App\Test; class AnotherBadTest {} ',
],
],
];

$runner = TestRunner::create('8.4');

// This replicates the use case from PR 560:
// Filter for non-abstract classes (including final), then check naming convention
$rule = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Test'))
->andThat(new IsNotAbstract())
->should(new HaveNameMatching('*EndToEndTest'))
->because('all E2E tests must follow naming convention');

$runner->run(vfsStream::setup('root', null, $structure)->url(), $rule);

// Should find 2 violations: BadE2ETest and AnotherBadTest
// AbstractTestCase should be filtered out by IsNotAbstract
// GoodEndToEndTest should pass
self::assertCount(2, $runner->getViolations());
self::assertEquals('App\Test\AnotherBadTest', $runner->getViolations()->get(0)->getFqcn());
self::assertEquals('App\Test\BadE2ETest', $runner->getViolations()->get(1)->getFqcn());
self::assertCount(0, $runner->getParsingErrors());
}

public function test_is_not_abstract_in_should_validates_final_classes_correctly(): void
{
$structure = [
'App' => [
'Service' => [
// Abstract class - should generate violation
'AbstractService.php' => '<?php namespace App\Service; abstract class AbstractService {} ',

// Final class - should NOT generate violation (final is non-abstract)
'FinalService.php' => '<?php namespace App\Service; final class FinalService {} ',

// Normal class - should NOT generate violation (normal is non-abstract)
'NormalService.php' => '<?php namespace App\Service; class NormalService {} ',

// Interface - should NOT generate violation (isAbstract() returns false)
'ServiceInterface.php' => '<?php namespace App\Service; interface ServiceInterface {} ',

// Trait - should NOT generate violation (isAbstract() returns false)
'ServiceTrait.php' => '<?php namespace App\Service; trait ServiceTrait {} ',

// Enum - should NOT generate violation (isAbstract() returns false)
'ServiceEnum.php' => '<?php namespace App\Service; enum ServiceEnum {} ',
],
],
];

$runner = TestRunner::create('8.4');

// When IsNotAbstract is used in should() (not that()), it validates ALL classes
// In PHP, isAbstract() returns true only for classes with 'abstract' keyword
// It does NOT return true for interface/trait/enum
$rule = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Service'))
->should(new IsNotAbstract())
->because('services should be concrete implementations');

$runner->run(vfsStream::setup('root', null, $structure)->url(), $rule);

// Should find violation only for: AbstractService
// Should NOT violate: FinalService, NormalService, ServiceInterface, ServiceTrait, ServiceEnum
// This test verifies that:
// 1. Final classes are correctly recognized as non-abstract
// 2. Interface/trait/enum don't trigger violations (isAbstract() = false for them)
self::assertCount(1, $runner->getViolations());
self::assertEquals('App\Service\AbstractService', $runner->getViolations()->get(0)->getFqcn());
self::assertCount(0, $runner->getParsingErrors());
}

public function test_it_can_check_multiple_class_properties(): void
{
$structure = [
Expand Down
4 changes: 2 additions & 2 deletions tests/Unit/Analyzer/FileParser/CanParsePropertyHooksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class User {
self::assertInstanceOf(ClassDescription::class, $cd[0]);

$dependencies = $cd[0]->getDependencies();
$dependencyNames = array_map(fn ($dep) => $dep->getFQCN()->toString(), $dependencies);
$dependencyNames = array_map(static fn ($dep) => $dep->getFQCN()->toString(), $dependencies);

self::assertContains('App\Services\Formatter', $dependencyNames);
self::assertContains('App\Services\Validator', $dependencyNames);
Expand Down Expand Up @@ -107,7 +107,7 @@ class User {
self::assertInstanceOf(ClassDescription::class, $cd[0]);

$dependencies = $cd[0]->getDependencies();
$dependencyNames = array_map(fn ($dep) => $dep->getFQCN()->toString(), $dependencies);
$dependencyNames = array_map(static fn ($dep) => $dep->getFQCN()->toString(), $dependencies);

self::assertContains('App\ValueObjects\Name', $dependencyNames);
}
Expand Down
6 changes: 3 additions & 3 deletions tests/Unit/ClassSetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function test_can_exclude_files_or_directories_from_multiple_dir_class_se
$path.'/Model/Products.php',
$path.'/Model/User.php',
];
$actual = array_values(array_map(function ($item) {
$actual = array_values(array_map(static function ($item) {
/** @var \SplFileInfo $item */
return $item->getPathname();
}, iterator_to_array($set)));
Expand All @@ -54,7 +54,7 @@ public function test_can_exclude_files_or_directories(): void
'View/UserView.php',
];

$actual = array_values(array_map(function ($item) {
$actual = array_values(array_map(static function ($item) {
return $item->getRelativePathname();
}, iterator_to_array($set)));

Expand Down Expand Up @@ -83,7 +83,7 @@ public function test_can_exclude_glob_patterns(): void
'View/UserView.php',
];

$actual = array_values(array_map(function ($item) {
$actual = array_values(array_map(static function ($item) {
return $item->getRelativePathname();
}, iterator_to_array($set)));

Expand Down
Loading