Skip to content

Commit 17d6b29

Browse files
herndlmondrejmirtes
authored andcommitted
Enforce safe constructor overrides with @phpstan-consistent-constructor
1 parent 317a997 commit 17d6b29

File tree

9 files changed

+111
-29
lines changed

9 files changed

+111
-29
lines changed

conf/config.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,9 @@ services:
986986
-
987987
class: PHPStan\Rules\Methods\MethodParameterComparisonHelper
988988

989+
-
990+
class: PHPStan\Rules\Methods\MethodVisibilityComparisonHelper
991+
989992
-
990993
class: PHPStan\Rules\MissingTypehintCheck
991994
arguments:

src/PhpDoc/StubValidator.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
use PHPStan\Rules\Methods\ExistingClassesInTypehintsRule;
6969
use PHPStan\Rules\Methods\MethodParameterComparisonHelper;
7070
use PHPStan\Rules\Methods\MethodSignatureRule;
71+
use PHPStan\Rules\Methods\MethodVisibilityComparisonHelper;
7172
use PHPStan\Rules\Methods\MissingMethodParameterTypehintRule;
7273
use PHPStan\Rules\Methods\MissingMethodReturnTypehintRule;
7374
use PHPStan\Rules\Methods\MissingMethodSelfOutTypeRule;
@@ -209,7 +210,15 @@ private function getRuleRegistry(Container $container): RuleRegistry
209210
new ExistingClassesInTypehintsRule($functionDefinitionCheck),
210211
new \PHPStan\Rules\Functions\ExistingClassesInTypehintsRule($functionDefinitionCheck),
211212
new ExistingClassesInPropertiesRule($reflectionProvider, $classNameCheck, $unresolvableTypeHelper, $phpVersion, true, false),
212-
new OverridingMethodRule($phpVersion, new MethodSignatureRule($phpClassReflectionExtension, true, true), true, new MethodParameterComparisonHelper($phpVersion), $phpClassReflectionExtension, $container->getParameter('checkMissingOverrideMethodAttribute')),
213+
new OverridingMethodRule(
214+
$phpVersion,
215+
new MethodSignatureRule($phpClassReflectionExtension, true, true),
216+
true,
217+
new MethodParameterComparisonHelper($phpVersion),
218+
new MethodVisibilityComparisonHelper(),
219+
$phpClassReflectionExtension,
220+
$container->getParameter('checkMissingOverrideMethodAttribute'),
221+
),
213222
new DuplicateDeclarationRule(),
214223
new LocalTypeAliasesRule($localTypeAliasesCheck),
215224
new LocalTypeTraitAliasesRule($localTypeAliasesCheck, $reflectionProvider),

src/Rules/Methods/ConsistentConstructorRule.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPStan\Node\InClassMethodNode;
88
use PHPStan\Reflection\Dummy\DummyConstructorReflection;
99
use PHPStan\Rules\Rule;
10+
use function array_merge;
1011
use function strtolower;
1112

1213
/** @implements Rule<InClassMethodNode> */
@@ -15,6 +16,7 @@ final class ConsistentConstructorRule implements Rule
1516

1617
public function __construct(
1718
private MethodParameterComparisonHelper $methodParameterComparisonHelper,
19+
private MethodVisibilityComparisonHelper $methodVisibilityComparisonHelper,
1820
)
1921
{
2022
}
@@ -47,7 +49,10 @@ public function processNode(Node $node, Scope $scope): array
4749
return [];
4850
}
4951

50-
return $this->methodParameterComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method, true);
52+
return array_merge(
53+
$this->methodParameterComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method, true),
54+
$this->methodVisibilityComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method),
55+
);
5156
}
5257

5358
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Methods;
4+
5+
use PHPStan\Reflection\ClassReflection;
6+
use PHPStan\Reflection\ExtendedMethodReflection;
7+
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
8+
use PHPStan\Rules\IdentifierRuleError;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use function sprintf;
11+
12+
final class MethodVisibilityComparisonHelper
13+
{
14+
15+
/** @return list<IdentifierRuleError> */
16+
public function compare(ExtendedMethodReflection $prototype, ClassReflection $prototypeDeclaringClass, PhpMethodFromParserNodeReflection $method): array
17+
{
18+
/** @var list<IdentifierRuleError> $messages */
19+
$messages = [];
20+
21+
if ($prototype->isPublic()) {
22+
if (!$method->isPublic()) {
23+
$messages[] = RuleErrorBuilder::message(sprintf(
24+
'%s method %s::%s() overriding public method %s::%s() should also be public.',
25+
$method->isPrivate() ? 'Private' : 'Protected',
26+
$method->getDeclaringClass()->getDisplayName(),
27+
$method->getName(),
28+
$prototypeDeclaringClass->getDisplayName(true),
29+
$prototype->getName(),
30+
))
31+
->nonIgnorable()
32+
->identifier('method.visibility')
33+
->build();
34+
}
35+
} elseif ($method->isPrivate()) {
36+
$messages[] = RuleErrorBuilder::message(sprintf(
37+
'Private method %s::%s() overriding protected method %s::%s() should be protected or public.',
38+
$method->getDeclaringClass()->getDisplayName(),
39+
$method->getName(),
40+
$prototypeDeclaringClass->getDisplayName(true),
41+
$prototype->getName(),
42+
))
43+
->nonIgnorable()
44+
->identifier('method.visibility')
45+
->build();
46+
}
47+
48+
return $messages;
49+
}
50+
51+
}

src/Rules/Methods/OverridingMethodRule.php

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public function __construct(
3535
private MethodSignatureRule $methodSignatureRule,
3636
private bool $checkPhpDocMethodSignatures,
3737
private MethodParameterComparisonHelper $methodParameterComparisonHelper,
38+
private MethodVisibilityComparisonHelper $methodVisibilityComparisonHelper,
3839
private PhpClassReflectionExtension $phpClassReflectionExtension,
3940
private bool $checkMissingOverrideMethodAttribute,
4041
)
@@ -165,32 +166,7 @@ public function processNode(Node $node, Scope $scope): array
165166
}
166167

167168
if ($checkVisibility) {
168-
if ($prototype->isPublic()) {
169-
if (!$method->isPublic()) {
170-
$messages[] = RuleErrorBuilder::message(sprintf(
171-
'%s method %s::%s() overriding public method %s::%s() should also be public.',
172-
$method->isPrivate() ? 'Private' : 'Protected',
173-
$method->getDeclaringClass()->getDisplayName(),
174-
$method->getName(),
175-
$prototypeDeclaringClass->getDisplayName(true),
176-
$prototype->getName(),
177-
))
178-
->nonIgnorable()
179-
->identifier('method.visibility')
180-
->build();
181-
}
182-
} elseif ($method->isPrivate()) {
183-
$messages[] = RuleErrorBuilder::message(sprintf(
184-
'Private method %s::%s() overriding protected method %s::%s() should be protected or public.',
185-
$method->getDeclaringClass()->getDisplayName(),
186-
$method->getName(),
187-
$prototypeDeclaringClass->getDisplayName(true),
188-
$prototype->getName(),
189-
))
190-
->nonIgnorable()
191-
->identifier('method.visibility')
192-
->build();
193-
}
169+
$messages = array_merge($messages, $this->methodVisibilityComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method));
194170
}
195171

196172
$prototypeVariants = $prototype->getVariants();

tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ class ConsistentConstructorRuleTest extends RuleTestCase
1212

1313
protected function getRule(): Rule
1414
{
15-
return new ConsistentConstructorRule(self::getContainer()->getByType(MethodParameterComparisonHelper::class));
15+
return new ConsistentConstructorRule(
16+
self::getContainer()->getByType(MethodParameterComparisonHelper::class),
17+
self::getContainer()->getByType(MethodVisibilityComparisonHelper::class),
18+
);
1619
}
1720

1821
public function testRule(): void
@@ -42,4 +45,14 @@ public function testRuleNoErrors(): void
4245
$this->analyse([__DIR__ . '/data/consistent-constructor-no-errors.php'], []);
4346
}
4447

48+
public function testBug12137(): void
49+
{
50+
$this->analyse([__DIR__ . '/data/bug-12137.php'], [
51+
[
52+
'Private method Bug12137\ChildClass::__construct() overriding protected method Bug12137\ParentClass::__construct() should be protected or public.',
53+
20,
54+
],
55+
]);
56+
}
57+
4558
}

tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ protected function getRule(): Rule
2929
new MethodSignatureRule($phpClassReflectionExtension, $this->reportMaybes, $this->reportStatic),
3030
true,
3131
new MethodParameterComparisonHelper($phpVersion),
32+
new MethodVisibilityComparisonHelper(),
3233
$phpClassReflectionExtension,
3334
false,
3435
);

tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ protected function getRule(): Rule
3131
new MethodSignatureRule($phpClassReflectionExtension, true, true),
3232
false,
3333
new MethodParameterComparisonHelper($phpVersion),
34+
new MethodVisibilityComparisonHelper(),
3435
$phpClassReflectionExtension,
3536
$this->checkMissingOverrideMethodAttribute,
3637
);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug12137;
4+
5+
/** @phpstan-consistent-constructor */
6+
abstract class ParentClass
7+
{
8+
protected function __construct()
9+
{
10+
}
11+
12+
public static function create(): static
13+
{
14+
return new static();
15+
}
16+
}
17+
18+
class ChildClass extends ParentClass
19+
{
20+
private function __construct()
21+
{
22+
}
23+
}

0 commit comments

Comments
 (0)