Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit e193d17

Browse files
RequireParentConstructCallRule moved to strict-rules
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+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /