Skip to content

Commit e193d17

Browse files
committed
RequireParentConstructCallRule moved to strict-rules
1 parent 491540d commit e193d17

6 files changed

+421
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* Contravariance for parameter types and covariance for return types in inherited methods (also known as Liskov substitution principle - LSP)
2929
* Check LSP even for static methods
3030
* Check missing typehint in anonymous function when a native one could be added
31+
* Require calling parent constructor
3132

3233
Additional rules are coming in subsequent releases!
3334

rules.neon

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ rules:
1818
- PHPStan\Rules\BooleansInConditions\BooleanInIfConditionRule
1919
- PHPStan\Rules\BooleansInConditions\BooleanInTernaryOperatorRule
2020
- PHPStan\Rules\Cast\UselessCastRule
21+
- PHPStan\Rules\Classes\RequireParentConstructCallRule
2122
- PHPStan\Rules\DisallowedConstructs\DisallowedEmptyRule
2223
- PHPStan\Rules\DisallowedConstructs\DisallowedImplicitArrayCreationRule
2324
- PHPStan\Rules\ForeachLoop\OverwriteVariablesWithForeachRule
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Name;
7+
use PhpParser\Node\Stmt\ClassMethod;
8+
use PHPStan\Analyser\Scope;
9+
10+
class RequireParentConstructCallRule implements \PHPStan\Rules\Rule
11+
{
12+
13+
public function getNodeType(): string
14+
{
15+
return ClassMethod::class;
16+
}
17+
18+
/**
19+
* @param \PhpParser\Node\Stmt\ClassMethod $node
20+
* @param \PHPStan\Analyser\Scope $scope
21+
* @return string[]
22+
*/
23+
public function processNode(Node $node, Scope $scope): array
24+
{
25+
if (!$scope->isInClass()) {
26+
throw new \PHPStan\ShouldNotHappenException();
27+
}
28+
29+
if ($scope->isInTrait()) {
30+
return [];
31+
}
32+
33+
if ($node->name->name !== '__construct') {
34+
return [];
35+
}
36+
37+
$classReflection = $scope->getClassReflection()->getNativeReflection();
38+
if ($classReflection->isInterface() || $classReflection->isAnonymous()) {
39+
return [];
40+
}
41+
42+
if ($this->callsParentConstruct($node)) {
43+
if ($classReflection->getParentClass() === false) {
44+
return [
45+
sprintf(
46+
'%s::__construct() calls parent constructor but does not extend any class.',
47+
$classReflection->getName()
48+
),
49+
];
50+
}
51+
52+
if ($this->getParentConstructorClass($classReflection) === false) {
53+
return [
54+
sprintf(
55+
'%s::__construct() calls parent constructor but parent does not have one.',
56+
$classReflection->getName()
57+
),
58+
];
59+
}
60+
} else {
61+
$parentClass = $this->getParentConstructorClass($classReflection);
62+
if ($parentClass !== false) {
63+
return [
64+
sprintf(
65+
'%s::__construct() does not call parent constructor from %s.',
66+
$classReflection->getName(),
67+
$parentClass->getName()
68+
),
69+
];
70+
}
71+
}
72+
73+
return [];
74+
}
75+
76+
private function callsParentConstruct(Node $parserNode): bool
77+
{
78+
if (!isset($parserNode->stmts)) {
79+
return false;
80+
}
81+
82+
foreach ($parserNode->stmts as $statement) {
83+
if ($statement instanceof Node\Stmt\Expression) {
84+
$statement = $statement->expr;
85+
}
86+
87+
$statement = $this->ignoreErrorSuppression($statement);
88+
if ($statement instanceof \PhpParser\Node\Expr\StaticCall) {
89+
if (
90+
$statement->class instanceof Name
91+
&& ((string) $statement->class === 'parent')
92+
&& $statement->name instanceof Node\Identifier
93+
&& $statement->name->name === '__construct'
94+
) {
95+
return true;
96+
}
97+
} else {
98+
if ($this->callsParentConstruct($statement)) {
99+
return true;
100+
}
101+
}
102+
}
103+
104+
return false;
105+
}
106+
107+
/**
108+
* @param \ReflectionClass $classReflection
109+
* @return \ReflectionClass|false
110+
*/
111+
private function getParentConstructorClass(\ReflectionClass $classReflection)
112+
{
113+
while ($classReflection->getParentClass() !== false) {
114+
$constructor = $classReflection->getParentClass()->hasMethod('__construct') ? $classReflection->getParentClass()->getMethod('__construct') : null;
115+
$constructorWithClassName = $classReflection->getParentClass()->hasMethod($classReflection->getParentClass()->getName()) ? $classReflection->getParentClass()->getMethod($classReflection->getParentClass()->getName()) : null;
116+
if (
117+
(
118+
$constructor !== null
119+
&& $constructor->getDeclaringClass()->getName() === $classReflection->getParentClass()->getName()
120+
&& !$constructor->isAbstract()
121+
&& !$constructor->isPrivate()
122+
) || (
123+
$constructorWithClassName !== null
124+
&& $constructorWithClassName->getDeclaringClass()->getName() === $classReflection->getParentClass()->getName()
125+
&& !$constructorWithClassName->isAbstract()
126+
)
127+
) {
128+
return $classReflection->getParentClass();
129+
}
130+
131+
$classReflection = $classReflection->getParentClass();
132+
}
133+
134+
return false;
135+
}
136+
137+
private function ignoreErrorSuppression(Node $statement): Node
138+
{
139+
if ($statement instanceof Node\Expr\ErrorSuppress) {
140+
141+
return $statement->expr;
142+
}
143+
144+
return $statement;
145+
}
146+
147+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
class RequireParentConstructCallRuleTest extends \PHPStan\Testing\RuleTestCase
6+
{
7+
8+
protected function getRule(): \PHPStan\Rules\Rule
9+
{
10+
return new RequireParentConstructCallRule();
11+
}
12+
13+
public function testCallToParentConstructor(): void
14+
{
15+
$this->analyse([__DIR__ . '/data/call-to-parent-constructor.php'], [
16+
[
17+
'IpsumCallToParentConstructor::__construct() calls parent constructor but parent does not have one.',
18+
31,
19+
],
20+
[
21+
'BCallToParentConstructor::__construct() does not call parent constructor from ACallToParentConstructor.',
22+
51,
23+
],
24+
[
25+
'CCallToParentConstructor::__construct() calls parent constructor but does not extend any class.',
26+
61,
27+
],
28+
[
29+
'FCallToParentConstructor::__construct() does not call parent constructor from DCallToParentConstructor.',
30+
86,
31+
],
32+
[
33+
'BarSoapClient::__construct() does not call parent constructor from SoapClient.',
34+
129,
35+
],
36+
[
37+
'StaticCallOnAVariable::__construct() does not call parent constructor from FooCallToParentConstructor.',
38+
140,
39+
],
40+
]);
41+
}
42+
43+
public function testCheckInTraits(): void
44+
{
45+
$this->analyse([__DIR__ . '/data/call-to-parent-constructor-in-trait.php'], []);
46+
}
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace CallToParentConstructorInTrait;
4+
5+
trait AcmeTrait
6+
{
7+
public function __construct()
8+
{
9+
}
10+
}
11+
12+
class BaseAcme
13+
{
14+
public function __construct()
15+
{
16+
}
17+
}
18+
19+
class Acme extends BaseAcme
20+
{
21+
use AcmeTrait {
22+
AcmeTrait::__construct as private __acmeConstruct;
23+
}
24+
25+
public function __construct()
26+
{
27+
$this->__acmeConstruct();
28+
29+
parent::__construct();
30+
}
31+
}

0 commit comments

Comments
 (0)