Skip to content

Commit eeee6ed

Browse files
committed
Added rule for enforcing either interface or abstract class as dependency / method argument
1 parent 6ff2611 commit eeee6ed

15 files changed

+202
-159
lines changed

phpstan-baseline.neon

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: "#^Property Ibexa\\\\Tests\\\\PHPStan\\\\Rules\\\\Fixtures\\\\RequireAbstractionInDependenciesFixture\\:\\:\\$abstractClass is never read, only written\\.$#"
5+
count: 1
6+
path: tests/rules/Fixtures/RequireAbstractionInDependenciesFixture.php
7+
8+
-
9+
message: "#^Property Ibexa\\\\Tests\\\\PHPStan\\\\Rules\\\\Fixtures\\\\RequireAbstractionInDependenciesFixture\\:\\:\\$classWithoutInterface is never read, only written\\.$#"
10+
count: 1
11+
path: tests/rules/Fixtures/RequireAbstractionInDependenciesFixture.php
12+
13+
-
14+
message: "#^Property Ibexa\\\\Tests\\\\PHPStan\\\\Rules\\\\Fixtures\\\\RequireAbstractionInDependenciesFixture\\:\\:\\$concreteClass is never read, only written\\.$#"
15+
count: 1
16+
path: tests/rules/Fixtures/RequireAbstractionInDependenciesFixture.php
17+
18+
-
19+
message: "#^Property Ibexa\\\\Tests\\\\PHPStan\\\\Rules\\\\Fixtures\\\\RequireAbstractionInDependenciesFixture\\:\\:\\$concreteExtendingAbstract is never read, only written\\.$#"
20+
count: 1
21+
path: tests/rules/Fixtures/RequireAbstractionInDependenciesFixture.php
22+
23+
-
24+
message: "#^Property Ibexa\\\\Tests\\\\PHPStan\\\\Rules\\\\Fixtures\\\\RequireAbstractionInDependenciesFixture\\:\\:\\$testInterface is never read, only written\\.$#"
25+
count: 1
26+
path: tests/rules/Fixtures/RequireAbstractionInDependenciesFixture.php

rules/RequireInterfaceInDependenciesRule.php renamed to rules/RequireAbstractionInDependenciesRule.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
/**
1818
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\ClassMethod>
1919
*/
20-
final class RequireInterfaceInDependenciesRule implements Rule
20+
final class RequireAbstractionInDependenciesRule implements Rule
2121
{
2222
public function getNodeType(): string
2323
{
@@ -33,7 +33,6 @@ public function processNode(Node $node, Scope $scope): array
3333
}
3434

3535
foreach ($node->params as $param) {
36-
3736
if (!$param->type instanceof Node\Name) {
3837
continue;
3938
}
@@ -47,17 +46,42 @@ public function processNode(Node $node, Scope $scope): array
4746
continue;
4847
}
4948

50-
if (!interface_exists($typeName) && class_exists($typeName)) {
51-
// Check if this class implements any interface
49+
// Skip interfaces - they are always acceptable
50+
if (interface_exists($typeName)) {
51+
continue;
52+
}
53+
54+
// Check if it's a class
55+
if (class_exists($typeName)) {
56+
$reflection = new \ReflectionClass($typeName);
57+
58+
// Skip abstract classes - they are acceptable
59+
if ($reflection->isAbstract()) {
60+
continue;
61+
}
62+
63+
// This is a concrete class - check if it has interfaces or extends an abstract class
5264
$interfaces = class_implements($typeName);
65+
$parentClass = $reflection->getParentClass();
66+
$hasAbstractParent = $parentClass && $parentClass->isAbstract();
67+
68+
if (!empty($interfaces) || $hasAbstractParent) {
69+
$suggestions = [];
70+
71+
if (!empty($interfaces)) {
72+
$suggestions[] = 'Available interfaces: ' . implode(', ', $interfaces);
73+
}
74+
75+
if ($hasAbstractParent) {
76+
$suggestions[] = 'Abstract parent: ' . $parentClass->getName();
77+
}
5378

54-
if (!empty($interfaces)) {
5579
$errors[] = RuleErrorBuilder::message(
5680
sprintf(
57-
'Parameter $%s uses concrete class %s instead of an interface. Available interfaces: %s',
81+
'Parameter $%s uses concrete class %s instead of an interface or abstract class. %s',
5882
is_string($param->var->name) ? $param->var->name : $param->var->name->getType(),
5983
$typeName,
60-
implode(', ', $interfaces)
84+
implode('. ', $suggestions)
6185
)
6286
)->build();
6387
}

tests/rules/Fixtures/NamingConvention/CorrectNameInterface.php

Lines changed: 0 additions & 13 deletions
This file was deleted.

tests/rules/Fixtures/NamingConvention/CorrectNameTrait.php

Lines changed: 0 additions & 13 deletions
This file was deleted.

tests/rules/Fixtures/NamingConvention/SimpleThing.php

Lines changed: 0 additions & 13 deletions
This file was deleted.

tests/rules/Fixtures/NamingConvention/WrongName.php

Lines changed: 0 additions & 13 deletions
This file was deleted.

tests/rules/Fixtures/NamingConvention/SimpleClass.php renamed to tests/rules/Fixtures/RequireAbstractionInDependencies/AbstractClass.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
*/
77
declare(strict_types=1);
88

9-
namespace Ibexa\Tests\PHPStan\Rules\Fixtures\NamingConvention;
9+
namespace Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies;
1010

11-
abstract class SimpleClass
11+
abstract class AbstractClass
1212
{
13+
abstract public function doSomethingAbstract(): void;
1314
}

tests/rules/Fixtures/NamingConvention/AbstractCorrectClass.php renamed to tests/rules/Fixtures/RequireAbstractionInDependencies/ClassWithoutInterface.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66
*/
77
declare(strict_types=1);
88

9-
namespace Ibexa\Tests\PHPStan\Rules\Fixtures\NamingConvention;
9+
namespace Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies;
1010

11-
abstract class AbstractCorrectClass
11+
class ClassWithoutInterface
1212
{
13+
public function doNothing(): void
14+
{
15+
// Implementation
16+
}
1317
}

tests/rules/Fixtures/RequireInterfaceInDependencies/ConcreteClass.php renamed to tests/rules/Fixtures/RequireAbstractionInDependencies/ConcreteClass.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
declare(strict_types=1);
88

9-
namespace Ibexa\Tests\PHPStan\Rules\Fixtures\RequireInterfaceInDependencies;
9+
namespace Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies;
1010

1111
class ConcreteClass implements TestInterface
1212
{
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Tests\PHPStan\Rules\Fixtures\RequireAbstractionInDependencies;
10+
11+
class ConcreteClassExtendingAbstract extends AbstractClass
12+
{
13+
public function doSomethingAbstract(): void
14+
{
15+
// Implementation
16+
}
17+
}

0 commit comments

Comments
 (0)