Skip to content

Commit 9d87da8

Browse files
committed
Overriding method rule - check final
1 parent 7ae1393 commit 9d87da8

8 files changed

Lines changed: 140 additions & 5 deletions

File tree

conf/config.level0.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ rules:
2929
- PHPStan\Rules\Functions\InnerFunctionRule
3030
- PHPStan\Rules\Functions\PrintfParametersRule
3131
- PHPStan\Rules\Methods\ExistingClassesInTypehintsRule
32+
- PHPStan\Rules\Methods\OverridingMethodRule
3233
- PHPStan\Rules\Properties\AccessPropertiesInAssignRule
3334
- PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule
3435
- PHPStan\Rules\Variables\ThisVariableRule

src/Reflection/MethodPrototypeReflection.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class MethodPrototypeReflection implements ClassMemberReflection
77

88
private \PHPStan\Reflection\ClassReflection $declaringClass;
99

10+
private string $name;
11+
1012
private bool $isStatic;
1113

1214
private bool $isPrivate;
@@ -15,19 +17,30 @@ class MethodPrototypeReflection implements ClassMemberReflection
1517

1618
private bool $isAbstract;
1719

20+
private bool $isFinal;
21+
1822
public function __construct(
23+
string $name,
1924
ClassReflection $declaringClass,
2025
bool $isStatic,
2126
bool $isPrivate,
2227
bool $isPublic,
23-
bool $isAbstract
28+
bool $isAbstract,
29+
bool $isFinal
2430
)
2531
{
32+
$this->name = $name;
2633
$this->declaringClass = $declaringClass;
2734
$this->isStatic = $isStatic;
2835
$this->isPrivate = $isPrivate;
2936
$this->isPublic = $isPublic;
3037
$this->isAbstract = $isAbstract;
38+
$this->isFinal = $isFinal;
39+
}
40+
41+
public function getName(): string
42+
{
43+
return $this->name;
3144
}
3245

3346
public function getDeclaringClass(): ClassReflection
@@ -55,6 +68,11 @@ public function isAbstract(): bool
5568
return $this->isAbstract;
5669
}
5770

71+
public function isFinal(): bool
72+
{
73+
return $this->isFinal;
74+
}
75+
5876
public function getDocComment(): ?string
5977
{
6078
return null;

src/Reflection/Native/NativeMethodReflection.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,13 @@ public function getPrototype(): ClassMemberReflection
8484
$prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName());
8585

8686
return new MethodPrototypeReflection(
87+
$prototypeMethod->getName(),
8788
$prototypeDeclaringClass,
8889
$prototypeMethod->isStatic(),
8990
$prototypeMethod->isPrivate(),
9091
$prototypeMethod->isPublic(),
91-
$prototypeMethod->isAbstract()
92+
$prototypeMethod->isAbstract(),
93+
$prototypeMethod->isFinal()
9294
);
9395
} catch (\ReflectionException $e) {
9496
return $this;
@@ -125,7 +127,7 @@ public function isInternal(): TrinaryLogic
125127

126128
public function isFinal(): TrinaryLogic
127129
{
128-
return TrinaryLogic::createNo();
130+
return TrinaryLogic::createFromBoolean($this->reflection->isFinal());
129131
}
130132

131133
public function getThrowType(): ?Type

src/Reflection/Php/PhpFunctionFromParserNodeReflection.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,11 @@ public function isInternal(): TrinaryLogic
210210

211211
public function isFinal(): TrinaryLogic
212212
{
213-
return TrinaryLogic::createFromBoolean($this->isFinal);
213+
$finalMethod = false;
214+
if ($this->functionLike instanceof ClassMethod) {
215+
$finalMethod = $this->functionLike->isFinal();
216+
}
217+
return TrinaryLogic::createFromBoolean($finalMethod || $this->isFinal);
214218
}
215219

216220
public function getThrowType(): ?Type

src/Reflection/Php/PhpMethodReflection.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,13 @@ public function getPrototype(): ClassMemberReflection
158158
$prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName());
159159

160160
return new MethodPrototypeReflection(
161+
$prototypeMethod->getName(),
161162
$prototypeDeclaringClass,
162163
$prototypeMethod->isStatic(),
163164
$prototypeMethod->isPrivate(),
164165
$prototypeMethod->isPublic(),
165-
$prototypeMethod->isAbstract()
166+
$prototypeMethod->isAbstract(),
167+
$prototypeMethod->isFinal()
166168
);
167169
} catch (\ReflectionException $e) {
168170
return $this;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Methods;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\InClassMethodNode;
8+
use PHPStan\Reflection\MethodPrototypeReflection;
9+
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
10+
use PHPStan\Rules\Rule;
11+
12+
/**
13+
* @implements Rule<InClassMethodNode>
14+
*/
15+
class OverridingMethodRule implements Rule
16+
{
17+
18+
public function getNodeType(): string
19+
{
20+
return InClassMethodNode::class;
21+
}
22+
23+
public function processNode(Node $node, Scope $scope): array
24+
{
25+
$method = $scope->getFunction();
26+
if (!$method instanceof PhpMethodFromParserNodeReflection) {
27+
throw new \PHPStan\ShouldNotHappenException();
28+
}
29+
30+
$prototype = $method->getPrototype();
31+
if ($prototype->getDeclaringClass()->getName() === $method->getDeclaringClass()->getName()) {
32+
return [];
33+
}
34+
if (!$prototype instanceof MethodPrototypeReflection) {
35+
return [];
36+
}
37+
38+
$messages = [];
39+
if ($prototype->isFinal()) {
40+
$messages[] = sprintf(
41+
'Method %s::%s() overrides final method %s::%s().',
42+
$method->getDeclaringClass()->getName(),
43+
$method->getName(),
44+
$prototype->getDeclaringClass()->getName(),
45+
$prototype->getName()
46+
);
47+
}
48+
49+
return $messages;
50+
}
51+
52+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Methods;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<OverridingMethodRule>
10+
*/
11+
class OverridingMethodRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new OverridingMethodRule();
17+
}
18+
19+
public function testOverridingFinalMethod(): void
20+
{
21+
if (!self::$useStaticReflectionProvider) {
22+
$this->markTestSkipped('Test requires static reflection.');
23+
}
24+
25+
$this->analyse([__DIR__ . '/data/overriding-final-method.php'], [
26+
[
27+
'Method OverridingFinalMethod\Bar::doFoo() overrides final method OverridingFinalMethod\Foo::doFoo().',
28+
18,
29+
],
30+
]);
31+
}
32+
33+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace OverridingFinalMethod;
4+
5+
class Foo
6+
{
7+
8+
final public function doFoo()
9+
{
10+
11+
}
12+
13+
}
14+
15+
class Bar extends Foo
16+
{
17+
18+
public function doFoo()
19+
{
20+
21+
}
22+
23+
}

0 commit comments

Comments
 (0)