Skip to content

Commit 101e438

Browse files
Merge pull request #16 from DaveLiddament/feature/override
Add Override attribute
2 parents 911700f + 741a419 commit 101e438

File tree

10 files changed

+818
-659
lines changed

10 files changed

+818
-659
lines changed

composer.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
"type": "phpstan-extension",
66
"require": {
77
"php": ">=8.0 <8.3",
8-
"phpstan/phpstan": "^1.9.14",
9-
"dave-liddament/php-language-extensions": "^0.4.0"
8+
"phpstan/phpstan": "^1.10.34",
9+
"dave-liddament/php-language-extensions": "^0.5.0"
1010
},
1111
"require-dev": {
12-
"phpunit/phpunit": "^9.5.28",
13-
"friendsofphp/php-cs-fixer": "^3.13.2",
14-
"php-parallel-lint/php-parallel-lint": "^1.3.2"
12+
"phpunit/phpunit": "^9.6.12",
13+
"friendsofphp/php-cs-fixer": "^3.26.1",
14+
"php-parallel-lint/php-parallel-lint": "^1.3.2",
15+
"dave-liddament/phpstan-rule-test-helper": "^0.1.0"
1516
},
1617
"license": "MIT",
1718
"autoload": {

composer.lock

Lines changed: 453 additions & 654 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ services:
7676
tags:
7777
- phpstan.rules.rule
7878

79+
-
80+
class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\OverrideRule
81+
tags:
82+
- phpstan.rules.rule
83+
7984

8085
parametersSchema:
8186
phpLanguageExtensions: structure([

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ parameters:
33
paths:
44
- src
55
- tests
6+
excludePaths:
7+
- tests/Rules/data

src/Rules/OverrideRule.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DaveLiddament\PhpstanPhpLanguageExtensions\Rules;
6+
7+
use DaveLiddament\PhpLanguageExtensions\Override;
8+
use DaveLiddament\PhpstanPhpLanguageExtensions\AttributeValueReaders\AttributeFinder;
9+
use PhpParser\Node;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Node\InClassNode;
12+
use PHPStan\Reflection\ClassReflection;
13+
use PHPStan\Rules\Rule;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
16+
/**
17+
* @implements Rule<InClassNode>
18+
*/
19+
final class OverrideRule implements Rule
20+
{
21+
public function getNodeType(): string
22+
{
23+
return InClassNode::class;
24+
}
25+
26+
/** @param InClassNode $node */
27+
public function processNode(Node $node, Scope $scope): array
28+
{
29+
$methods = $node->getOriginalNode()->getMethods();
30+
31+
$classReflection = $node->getClassReflection();
32+
33+
$errors = [];
34+
foreach ($methods as $method) {
35+
$methodName = $method->name->toLowerString();
36+
37+
if (!AttributeFinder::hasAttributeOnMethod(
38+
$classReflection->getNativeReflection(),
39+
$methodName,
40+
Override::class,
41+
)) {
42+
continue;
43+
}
44+
45+
if ($this->isMethodInAncestor($classReflection, $methodName, $scope)) {
46+
continue;
47+
}
48+
49+
$message = "Method {$methodName} has the Override attribute, but no matching parent method exists";
50+
$errors[] = RuleErrorBuilder::message($message)->line($method->getLine())->build();
51+
}
52+
53+
return $errors;
54+
}
55+
56+
private function isMethodInAncestor(ClassReflection $classReflection, string $methodName, Scope $scope): bool
57+
{
58+
foreach ($classReflection->getAncestors() as $ancestor) {
59+
if ($ancestor === $classReflection) {
60+
continue;
61+
}
62+
63+
if ($ancestor->hasMethod($methodName)) {
64+
$method = $ancestor->getMethod($methodName, $scope);
65+
66+
if ($method->isPrivate()) {
67+
continue;
68+
}
69+
70+
return true;
71+
}
72+
}
73+
74+
return false;
75+
}
76+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace DaveLiddament\PhpstanPhpLanguageExtensions\Tests\Rules;
4+
5+
use DaveLiddament\PhpstanRuleTestHelper\ErrorMessageFormatter;
6+
7+
final class OverrideErrorFormatter extends ErrorMessageFormatter
8+
{
9+
public function getErrorMessage(string $errorContext): string
10+
{
11+
return "Method {$errorContext} has the Override attribute, but no matching parent method exists";
12+
}
13+
}

tests/Rules/OverrideTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DaveLiddament\PhpstanPhpLanguageExtensions\Tests\Rules;
6+
7+
use DaveLiddament\PhpstanPhpLanguageExtensions\Rules\OverrideRule;
8+
use DaveLiddament\PhpstanRuleTestHelper\AbstractRuleTestCase;
9+
use DaveLiddament\PhpstanRuleTestHelper\ErrorMessageFormatter;
10+
use PHPStan\Rules\Rule;
11+
12+
/** @extends AbstractRuleTestCase<OverrideRule> */
13+
final class OverrideTest extends AbstractRuleTestCase
14+
{
15+
protected function getRule(): Rule
16+
{
17+
return new OverrideRule();
18+
}
19+
20+
public function testOverrideRuleOnClass(): void
21+
{
22+
$this->assertIssuesReported(
23+
__DIR__.'/data/override/overrideOnClass.php',
24+
);
25+
}
26+
27+
public function testOverrideRuleOnInterface(): void
28+
{
29+
$this->assertIssuesReported(
30+
__DIR__.'/data/override/overrideOnInterface.php',
31+
);
32+
}
33+
34+
public function testOverrideRuleRfcExamples(): void
35+
{
36+
$this->assertIssuesReported(
37+
__DIR__.'/data/override/overrideRfcExamples.php',
38+
);
39+
}
40+
41+
public function getErrorFormatter(): ErrorMessageFormatter
42+
{
43+
return new OverrideErrorFormatter();
44+
}
45+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OverrideOnClass {
6+
7+
8+
use DaveLiddament\PhpLanguageExtensions\Override;
9+
10+
abstract class BaseClass {
11+
12+
abstract public function method1(): void;
13+
14+
public function method2(): void {}
15+
16+
public function anotherMethod(): void {}
17+
18+
}
19+
20+
21+
class Class1 extends BaseClass {
22+
23+
#[Override] public function method1(): void
24+
{
25+
}
26+
27+
#[Override] public function method2(): void
28+
{
29+
}
30+
31+
#[Override] public function method4(): void // ERROR method4
32+
{
33+
}
34+
}
35+
36+
37+
new class extends BaseClass {
38+
#[Override] public function method1(): void
39+
{
40+
}
41+
42+
#[Override] public function method2(): void
43+
{
44+
}
45+
46+
#[Override] public function method3(): void // ERROR method3
47+
{
48+
}
49+
};
50+
51+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OverrideOnInterface {
6+
7+
8+
use DaveLiddament\PhpLanguageExtensions\Override;
9+
10+
abstract class AnInterface {
11+
12+
abstract public function method1(): void;
13+
14+
public function method2(): void {}
15+
16+
public function anotherMethod(): void {}
17+
18+
}
19+
20+
21+
class Class1 extends AnInterface {
22+
23+
#[Override] public function method1(): void
24+
{
25+
}
26+
27+
#[Override] public function method2(): void
28+
{
29+
}
30+
31+
#[Override] public function method4(): void // ERROR method4
32+
{
33+
}
34+
}
35+
36+
37+
new class extends AnInterface {
38+
#[Override] public function method1(): void
39+
{
40+
}
41+
42+
#[Override] public function method2(): void
43+
{
44+
}
45+
46+
#[Override] public function method3(): void // ERROR method3
47+
{
48+
}
49+
};
50+
51+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace overrideRfcExample1 {
6+
7+
8+
use DaveLiddament\PhpLanguageExtensions\Override;
9+
10+
class P {
11+
protected function p(): void {}
12+
}
13+
14+
class C extends P {
15+
#[Override]
16+
public function p(): void {}
17+
}
18+
}
19+
20+
namespace overrideRfcExample2 {
21+
22+
use DaveLiddament\PhpLanguageExtensions\Override;
23+
24+
class Foo implements \IteratorAggregate
25+
{
26+
#[Override]
27+
public function getIterator(): \Traversable
28+
{
29+
yield from [];
30+
}
31+
}
32+
}
33+
34+
35+
namespace overrideRfcExample5 {
36+
37+
use DaveLiddament\PhpLanguageExtensions\Override;
38+
39+
interface I {
40+
public function i(): void;
41+
}
42+
43+
interface II extends I {
44+
#[Override]
45+
public function i(): void;
46+
}
47+
48+
class P {
49+
public function p1(): void {}
50+
public function p2(): void {}
51+
public function p3(): void {}
52+
public function p4(): void {}
53+
}
54+
55+
class PP extends P {
56+
#[Override]
57+
public function p1(): void {}
58+
public function p2(): void {}
59+
#[Override]
60+
public function p3(): void {}
61+
}
62+
63+
class C extends PP implements I {
64+
#[Override]
65+
public function i(): void {}
66+
#[Override]
67+
public function p1(): void {}
68+
#[Override]
69+
public function p2(): void {}
70+
public function p3(): void {}
71+
#[Override]
72+
public function p4(): void {}
73+
public function c(): void {}
74+
}
75+
}
76+
77+
namespace overrideRfcExample6 {
78+
79+
use DaveLiddament\PhpLanguageExtensions\Override;
80+
81+
class C
82+
{
83+
#[Override] public function c(): void {} // ERROR c
84+
}
85+
}
86+
87+
namespace overrideRfcExample7 {
88+
89+
use DaveLiddament\PhpLanguageExtensions\Override;
90+
91+
interface I {
92+
public function i(): void;
93+
}
94+
95+
class P {
96+
#[Override] public function i(): void {} // ERROR i
97+
}
98+
99+
class C extends P implements I {}
100+
}
101+
102+
103+
104+
namespace overrideRfcExample9 {
105+
106+
use DaveLiddament\PhpLanguageExtensions\Override;
107+
108+
class P {
109+
private function p(): void {}
110+
}
111+
112+
class C extends P {
113+
#[Override] public function p(): void {} // ERROR p
114+
}
115+
}
116+

0 commit comments

Comments
 (0)