Skip to content

Commit e193d17

Browse files
committedJul 8, 2019
RequireParentConstructCallRule moved to strict-rules
·
2.0.60.12.0
1 parent 491540d commit e193d17

File tree

6 files changed

+421
-0
lines changed

6 files changed

+421
-0
lines changed
 

‎README.md

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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
Lines changed: 147 additions & 0 deletions
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+
}
Lines changed: 48 additions & 0 deletions
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+
}
Lines changed: 31 additions & 0 deletions
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+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<?php
2+
3+
class FooCallToParentConstructor
4+
{
5+
6+
public function __construct()
7+
{
8+
9+
}
10+
11+
}
12+
13+
class BarCallToParentConstructor extends FooCallToParentConstructor
14+
{
15+
16+
public function __construct()
17+
{
18+
parent::__construct();
19+
}
20+
21+
}
22+
23+
class LoremCallToParentConstructor
24+
{
25+
26+
}
27+
28+
class IpsumCallToParentConstructor extends LoremCallToParentConstructor
29+
{
30+
31+
public function __construct()
32+
{
33+
parent::__construct();
34+
}
35+
36+
}
37+
38+
class ACallToParentConstructor
39+
{
40+
41+
public function __construct()
42+
{
43+
44+
}
45+
46+
}
47+
48+
class BCallToParentConstructor extends ACallToParentConstructor
49+
{
50+
51+
public function __construct()
52+
{
53+
54+
}
55+
56+
}
57+
58+
class CCallToParentConstructor
59+
{
60+
61+
public function __construct()
62+
{
63+
parent::__construct();
64+
}
65+
66+
}
67+
68+
class DCallToParentConstructor
69+
{
70+
71+
public function __construct()
72+
{
73+
74+
}
75+
76+
}
77+
78+
class ECallToParentConstructor extends DCallToParentConstructor
79+
{
80+
81+
}
82+
83+
class FCallToParentConstructor extends ECallToParentConstructor
84+
{
85+
86+
public function __construct()
87+
{
88+
89+
}
90+
91+
}
92+
93+
interface FooBarCallToParentConstructor
94+
{
95+
96+
public function __construct();
97+
98+
}
99+
100+
class NestedCallToParentConstruct extends FooCallToParentConstructor
101+
{
102+
103+
public function __construct()
104+
{
105+
if ($bar) {
106+
test();
107+
}
108+
if ($foo) {
109+
parent::__construct();
110+
}
111+
}
112+
113+
}
114+
115+
class FooSoapClient extends \SoapClient
116+
{
117+
118+
public function __construct()
119+
{
120+
parent::__construct();
121+
}
122+
123+
124+
}
125+
126+
class BarSoapClient extends \SoapClient
127+
{
128+
129+
public function __construct()
130+
{
131+
132+
}
133+
134+
135+
}
136+
137+
class StaticCallOnAVariable extends FooCallToParentConstructor
138+
{
139+
140+
public function __construct()
141+
{
142+
$thisClass = __CLASS__;
143+
$thisClass::myMethod();
144+
}
145+
146+
}
147+
148+
abstract class AbstractClassWithAbstractConstructor
149+
{
150+
151+
abstract public function __construct();
152+
153+
}
154+
155+
class ClassThatExtendsAbstractClassWithAbstractConstructor extends AbstractClassWithAbstractConstructor
156+
{
157+
158+
public function __construct()
159+
{
160+
161+
}
162+
163+
}
164+
165+
class BarCallToMutedParentConstructor extends FooCallToParentConstructor
166+
{
167+
168+
public function __construct()
169+
{
170+
@parent::__construct();
171+
}
172+
173+
}
174+
175+
class PrivateConstructor
176+
{
177+
178+
private function __construct()
179+
{
180+
181+
}
182+
183+
}
184+
185+
class ExtendsPrivateConstructor extends PrivateConstructor
186+
{
187+
188+
public function __construct()
189+
{
190+
// cannot call parent
191+
}
192+
193+
}

0 commit comments

Comments
 (0)
Please sign in to comment.