Skip to content
Draft
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
62 changes: 41 additions & 21 deletions src/Rules/Methods/MethodPrototypeFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,19 @@
}

/**
* @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, ...).
*
* @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 +52,19 @@
$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 All @@ -76,25 +85,36 @@
}

$declaringClass = $method->getDeclaringClass();
if ($declaringClass->hasConstructor()) {
if ($method->getName() === $declaringClass->getConstructor()->getName()) {
$prototype = $method->getPrototype();
if ($prototype instanceof PhpMethodReflection || $prototype instanceof MethodPrototypeReflection || $prototype instanceof NativeMethodReflection) {
$abstract = $prototype->isAbstract();
if (is_bool($abstract)) {
if (!$abstract) {
return null;
}
} elseif (!$abstract->yes()) {
if ($declaringClass->hasConstructor() && $method->getName() === $declaringClass->getConstructor()->getName()) {
$prototype = $method->getPrototype();
if ($prototype instanceof PhpMethodReflection || $prototype instanceof MethodPrototypeReflection || $prototype instanceof NativeMethodReflection) {
$abstract = $prototype->isAbstract();
if (is_bool($abstract)) {
if (!$abstract) {
return null;
}
} elseif (!$abstract->yes()) {

Check warning on line 96 in src/Rules/Methods/MethodPrototypeFinder.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if (!$abstract) { return null; } - } elseif (!$abstract->yes()) { + } elseif ($abstract->no()) { return null; } }

Check warning on line 96 in src/Rules/Methods/MethodPrototypeFinder.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if (!$abstract) { return null; } - } elseif (!$abstract->yes()) { + } elseif ($abstract->no()) { return null; } }
return null;
}
}
}

$prototype = $method;
if (strtolower($method->getName()) === '__construct') {
foreach ($parentClass->getInterfaces() as $interface) {
if ($interface->hasNativeMethod($method->getName())) {
$prototype = $interface->getNativeMethod($method->getName());
}
} elseif (strtolower($methodName) === '__construct') {
return null;
}
}

return [$method, $method->getDeclaringClass(), true];
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.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(),

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(),
$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.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(),

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(),
$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
11 changes: 11 additions & 0 deletions tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,17 @@ 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 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);

}
}
Loading