Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions src/Rules/Methods/MethodPrototypeFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@ public function __construct(
}

/**
* @return array{ExtendedMethodReflection, ClassReflection, bool}|null
* Finds the prototype method that a class method should be validated against.
* Returns two prototypes with different purposes:
* - Signature prototype: Used for validating method signature (parameters, return type, ...).
* - Inheritance prototype: Used for validating inheritance rules (final keyword, override attribute, ...).
* Also, return a bool to precise if the visibility of the prototype needs to be respected.
*
* @return array{ExtendedMethodReflection, ClassReflection, bool, ExtendedMethodReflection, ClassReflection}|null
*/
public function findPrototype(ClassReflection $classReflection, string $methodName): ?array
{
foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) {
if ($immediateInterface->hasNativeMethod($methodName)) {
$method = $immediateInterface->getNativeMethod($methodName);
return [$method, $method->getDeclaringClass(), true];
return [$method, $method->getDeclaringClass(), true, $method, $method->getDeclaringClass()];
}
}

Expand All @@ -47,15 +53,19 @@ public function findPrototype(ClassReflection $classReflection, string $methodNa
$isAbstract = $methodReflection->isAbstract();
if ($isAbstract) {
$declaringTrait = $trait->getNativeMethod($methodName)->getDeclaringClass();
$prototype = $this->phpClassReflectionExtension->createUserlandMethodReflection(
$trait,
$classReflection,
$methodReflection,
$declaringTrait->getName(),
);

return [
$this->phpClassReflectionExtension->createUserlandMethodReflection(
$trait,
$classReflection,
$methodReflection,
$declaringTrait->getName(),
),
$prototype,
$declaringTrait,
false,
$prototype,
$declaringTrait,
];
}
}
Expand Down Expand Up @@ -94,7 +104,23 @@ public function findPrototype(ClassReflection $classReflection, string $methodNa
}
}

return [$method, $method->getDeclaringClass(), true];
$prototype = $method;
if (strtolower($method->getName()) === '__construct') {
foreach ($parentClass->getInterfaces() as $interface) {
if ($interface->hasNativeMethod($method->getName())) {
$prototype = $interface->getNativeMethod($method->getName());
break;
}
}
}

return [
$prototype,
$prototype->getDeclaringClass(),
true,
$method,
$declaringClass,
];
}

}
24 changes: 15 additions & 9 deletions src/Rules/Methods/OverridingMethodRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,13 @@
return [];
}

[$prototype, $prototypeDeclaringClass, $checkVisibility] = $prototypeData;
[
$prototype,
$prototypeDeclaringClass,
$checkVisibility,
$inheritancePrototype,
$inheritancePrototypeDeclaringClass,
] = $prototypeData;

$messages = [];
if (
Expand All @@ -119,8 +125,8 @@
'Method %s::%s() overrides method %s::%s() but is missing the #[\Override] attribute.',
$method->getDeclaringClass()->getDisplayName(),
$method->getName(),
$prototypeDeclaringClass->getDisplayName(true),
$prototype->getName(),
$inheritancePrototypeDeclaringClass->getDisplayName(true),
$inheritancePrototype->getName(),
))
->identifier('method.missingOverride')
->fixNode($node->getOriginalNode(), static function (Node\Stmt\ClassMethod $method) {
Expand All @@ -132,24 +138,24 @@
})
->build();
}
if ($prototype->isFinalByKeyword()->yes()) {
if ($inheritancePrototype->isFinalByKeyword()->yes()) {

Check warning on line 141 in src/Rules/Methods/OverridingMethodRule.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ }) ->build(); } - if ($inheritancePrototype->isFinalByKeyword()->yes()) { + if (!$inheritancePrototype->isFinalByKeyword()->no()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides final method %s::%s().', $method->getDeclaringClass()->getDisplayName(),

Check warning on line 141 in src/Rules/Methods/OverridingMethodRule.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ }) ->build(); } - if ($inheritancePrototype->isFinalByKeyword()->yes()) { + if (!$inheritancePrototype->isFinalByKeyword()->no()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides final method %s::%s().', $method->getDeclaringClass()->getDisplayName(),
$messages[] = RuleErrorBuilder::message(sprintf(
'Method %s::%s() overrides final method %s::%s().',
$method->getDeclaringClass()->getDisplayName(),
$method->getName(),
$prototypeDeclaringClass->getDisplayName(true),
$prototype->getName(),
$inheritancePrototypeDeclaringClass->getDisplayName(true),
$inheritancePrototype->getName(),
))
->nonIgnorable()
->identifier('method.parentMethodFinal')
->build();
} elseif ($prototype->isFinal()->yes()) {
} elseif ($inheritancePrototype->isFinal()->yes()) {

Check warning on line 152 in src/Rules/Methods/OverridingMethodRule.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ ->nonIgnorable() ->identifier('method.parentMethodFinal') ->build(); - } elseif ($inheritancePrototype->isFinal()->yes()) { + } elseif (!$inheritancePrototype->isFinal()->no()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides @Final method %s::%s().', $method->getDeclaringClass()->getDisplayName(),

Check warning on line 152 in src/Rules/Methods/OverridingMethodRule.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ ->nonIgnorable() ->identifier('method.parentMethodFinal') ->build(); - } elseif ($inheritancePrototype->isFinal()->yes()) { + } elseif (!$inheritancePrototype->isFinal()->no()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides @Final method %s::%s().', $method->getDeclaringClass()->getDisplayName(),
$messages[] = RuleErrorBuilder::message(sprintf(
'Method %s::%s() overrides @final method %s::%s().',
$method->getDeclaringClass()->getDisplayName(),
$method->getName(),
$prototypeDeclaringClass->getDisplayName(true),
$prototype->getName(),
$inheritancePrototypeDeclaringClass->getDisplayName(true),
$inheritancePrototype->getName(),
))->identifier('method.parentMethodFinalByPhpDoc')
->build();
}
Expand Down
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,29 @@ public function testBug10165(): void
$this->analyse([__DIR__ . '/data/bug-10165.php'], []);
}

public function testBug11067(): void
{
$this->phpVersionId = PHP_VERSION_ID;
$this->analyse([__DIR__ . '/data/bug-11067.php'], [
[
'Method Bug11067\BooleanBuilder2::__construct() overrides final method Bug11067\BaseBuilder2::__construct().',
41,
],
]);
}

public function testBug12272(): void
{
$this->phpVersionId = PHP_VERSION_ID;
$this->analyse([__DIR__ . '/data/bug-12272.php'], []);
}

public function testBug12830(): void
{
$this->phpVersionId = PHP_VERSION_ID;
$this->analyse([__DIR__ . '/data/bug-12830.php'], []);
}

public function testBug9524(): void
{
$this->phpVersionId = PHP_VERSION_ID;
Expand Down
46 changes: 46 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-11067.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types = 1);

namespace Bug11067;

interface BuilderInterface
{
public function __construct(string $field);
}

class BaseBuilder implements BuilderInterface
{
public function __construct(
protected string $field,
bool $checkType = true,
) {
var_dump($field, $checkType);
}
}

class BooleanBuilder extends BaseBuilder
{
public function __construct(string $field)
{
parent::__construct($field, false);

}
}

class BaseBuilder2 implements BuilderInterface
{
final public function __construct(
protected string $field,
bool $checkType = true,
) {
var_dump($field, $checkType);
}
}

class BooleanBuilder2 extends BaseBuilder2
{
public function __construct(string $field)
{
parent::__construct($field, false);

}
}
32 changes: 32 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-12272.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Bug12272;

interface ExceptionContract
{
public function __construct(string $message);
}

class BaseException extends Exception implements ExceptionContract
{
public function __construct(string $message, ?Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
}
}

class SomeException extends BaseException
{
}

class SpecificException extends SomeException
{
public function __construct(string $message, int $code = 0)
{
if ($code) {
echo 1;
}

parent::__construct($message);
}
}
36 changes: 36 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-12830.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types=1);

namespace Bug12830;

interface I
{
public function __construct(string $mustBeString);
}

class A implements I
{
public string $MustBeString;
public int $CanBeInt;

public function __construct(string $mustBeString, int $canBeInt = -1)
{
$this->MustBeString = $mustBeString;
$this->CanBeInt = $canBeInt;
}
}

class B extends A
{
public bool $CanBeBool;

public function __construct(string $mustBeString, bool $canBeBool = false)
{
$this->MustBeString = $mustBeString;
$this->CanBeBool = $canBeBool;
}
}

var_dump([
new A('A', 1),
new B('B', true),
]);
Loading