Skip to content

Commit

Permalink
Merge pull request #16 from DaveLiddament/feature/override
Browse files Browse the repository at this point in the history
Add Override attribute
  • Loading branch information
DaveLiddament authored Sep 15, 2023
2 parents 911700f + 741a419 commit 101e438
Show file tree
Hide file tree
Showing 10 changed files with 818 additions and 659 deletions.
11 changes: 6 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
"type": "phpstan-extension",
"require": {
"php": ">=8.0 <8.3",
"phpstan/phpstan": "^1.9.14",
"dave-liddament/php-language-extensions": "^0.4.0"
"phpstan/phpstan": "^1.10.34",
"dave-liddament/php-language-extensions": "^0.5.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5.28",
"friendsofphp/php-cs-fixer": "^3.13.2",
"php-parallel-lint/php-parallel-lint": "^1.3.2"
"phpunit/phpunit": "^9.6.12",
"friendsofphp/php-cs-fixer": "^3.26.1",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"dave-liddament/phpstan-rule-test-helper": "^0.1.0"
},
"license": "MIT",
"autoload": {
Expand Down
1,107 changes: 453 additions & 654 deletions composer.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ services:
tags:
- phpstan.rules.rule

-
class: DaveLiddament\PhpstanPhpLanguageExtensions\Rules\OverrideRule
tags:
- phpstan.rules.rule


parametersSchema:
phpLanguageExtensions: structure([
Expand Down
2 changes: 2 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ parameters:
paths:
- src
- tests
excludePaths:
- tests/Rules/data
76 changes: 76 additions & 0 deletions src/Rules/OverrideRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace DaveLiddament\PhpstanPhpLanguageExtensions\Rules;

use DaveLiddament\PhpLanguageExtensions\Override;
use DaveLiddament\PhpstanPhpLanguageExtensions\AttributeValueReaders\AttributeFinder;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<InClassNode>
*/
final class OverrideRule implements Rule
{
public function getNodeType(): string
{
return InClassNode::class;
}

/** @param InClassNode $node */
public function processNode(Node $node, Scope $scope): array
{
$methods = $node->getOriginalNode()->getMethods();

$classReflection = $node->getClassReflection();

$errors = [];
foreach ($methods as $method) {
$methodName = $method->name->toLowerString();

if (!AttributeFinder::hasAttributeOnMethod(
$classReflection->getNativeReflection(),
$methodName,
Override::class,
)) {
continue;
}

if ($this->isMethodInAncestor($classReflection, $methodName, $scope)) {
continue;
}

$message = "Method {$methodName} has the Override attribute, but no matching parent method exists";
$errors[] = RuleErrorBuilder::message($message)->line($method->getLine())->build();
}

return $errors;
}

private function isMethodInAncestor(ClassReflection $classReflection, string $methodName, Scope $scope): bool
{
foreach ($classReflection->getAncestors() as $ancestor) {
if ($ancestor === $classReflection) {
continue;
}

if ($ancestor->hasMethod($methodName)) {
$method = $ancestor->getMethod($methodName, $scope);

if ($method->isPrivate()) {
continue;
}

return true;
}
}

return false;
}
}
13 changes: 13 additions & 0 deletions tests/Rules/OverrideErrorFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace DaveLiddament\PhpstanPhpLanguageExtensions\Tests\Rules;

use DaveLiddament\PhpstanRuleTestHelper\ErrorMessageFormatter;

final class OverrideErrorFormatter extends ErrorMessageFormatter
{
public function getErrorMessage(string $errorContext): string
{
return "Method {$errorContext} has the Override attribute, but no matching parent method exists";
}
}
45 changes: 45 additions & 0 deletions tests/Rules/OverrideTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace DaveLiddament\PhpstanPhpLanguageExtensions\Tests\Rules;

use DaveLiddament\PhpstanPhpLanguageExtensions\Rules\OverrideRule;
use DaveLiddament\PhpstanRuleTestHelper\AbstractRuleTestCase;
use DaveLiddament\PhpstanRuleTestHelper\ErrorMessageFormatter;
use PHPStan\Rules\Rule;

/** @extends AbstractRuleTestCase<OverrideRule> */
final class OverrideTest extends AbstractRuleTestCase
{
protected function getRule(): Rule
{
return new OverrideRule();
}

public function testOverrideRuleOnClass(): void
{
$this->assertIssuesReported(
__DIR__.'/data/override/overrideOnClass.php',
);
}

public function testOverrideRuleOnInterface(): void
{
$this->assertIssuesReported(
__DIR__.'/data/override/overrideOnInterface.php',
);
}

public function testOverrideRuleRfcExamples(): void
{
$this->assertIssuesReported(
__DIR__.'/data/override/overrideRfcExamples.php',
);
}

public function getErrorFormatter(): ErrorMessageFormatter
{
return new OverrideErrorFormatter();
}
}
51 changes: 51 additions & 0 deletions tests/Rules/data/override/overrideOnClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace OverrideOnClass {


use DaveLiddament\PhpLanguageExtensions\Override;

abstract class BaseClass {

abstract public function method1(): void;

public function method2(): void {}

public function anotherMethod(): void {}

}


class Class1 extends BaseClass {

#[Override] public function method1(): void
{
}

#[Override] public function method2(): void
{
}

#[Override] public function method4(): void // ERROR method4
{
}
}


new class extends BaseClass {
#[Override] public function method1(): void
{
}

#[Override] public function method2(): void
{
}

#[Override] public function method3(): void // ERROR method3
{
}
};

}
51 changes: 51 additions & 0 deletions tests/Rules/data/override/overrideOnInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace OverrideOnInterface {


use DaveLiddament\PhpLanguageExtensions\Override;

abstract class AnInterface {

abstract public function method1(): void;

public function method2(): void {}

public function anotherMethod(): void {}

}


class Class1 extends AnInterface {

#[Override] public function method1(): void
{
}

#[Override] public function method2(): void
{
}

#[Override] public function method4(): void // ERROR method4
{
}
}


new class extends AnInterface {
#[Override] public function method1(): void
{
}

#[Override] public function method2(): void
{
}

#[Override] public function method3(): void // ERROR method3
{
}
};

}
116 changes: 116 additions & 0 deletions tests/Rules/data/override/overrideRfcExamples.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

declare(strict_types=1);

namespace overrideRfcExample1 {


use DaveLiddament\PhpLanguageExtensions\Override;

class P {
protected function p(): void {}
}

class C extends P {
#[Override]
public function p(): void {}
}
}

namespace overrideRfcExample2 {

use DaveLiddament\PhpLanguageExtensions\Override;

class Foo implements \IteratorAggregate
{
#[Override]
public function getIterator(): \Traversable
{
yield from [];
}
}
}


namespace overrideRfcExample5 {

use DaveLiddament\PhpLanguageExtensions\Override;

interface I {
public function i(): void;
}

interface II extends I {
#[Override]
public function i(): void;
}

class P {
public function p1(): void {}
public function p2(): void {}
public function p3(): void {}
public function p4(): void {}
}

class PP extends P {
#[Override]
public function p1(): void {}
public function p2(): void {}
#[Override]
public function p3(): void {}
}

class C extends PP implements I {
#[Override]
public function i(): void {}
#[Override]
public function p1(): void {}
#[Override]
public function p2(): void {}
public function p3(): void {}
#[Override]
public function p4(): void {}
public function c(): void {}
}
}

namespace overrideRfcExample6 {

use DaveLiddament\PhpLanguageExtensions\Override;

class C
{
#[Override] public function c(): void {} // ERROR c
}
}

namespace overrideRfcExample7 {

use DaveLiddament\PhpLanguageExtensions\Override;

interface I {
public function i(): void;
}

class P {
#[Override] public function i(): void {} // ERROR i
}

class C extends P implements I {}
}



namespace overrideRfcExample9 {

use DaveLiddament\PhpLanguageExtensions\Override;

class P {
private function p(): void {}
}

class C extends P {
#[Override] public function p(): void {} // ERROR p
}
}

0 comments on commit 101e438

Please sign in to comment.