-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RequireParentConstructCallRule moved to strict-rules
- Loading branch information
1 parent
491540d
commit e193d17
Showing
6 changed files
with
421 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Classes; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Name; | ||
use PhpParser\Node\Stmt\ClassMethod; | ||
use PHPStan\Analyser\Scope; | ||
|
||
class RequireParentConstructCallRule implements \PHPStan\Rules\Rule | ||
{ | ||
|
||
public function getNodeType(): string | ||
{ | ||
return ClassMethod::class; | ||
} | ||
|
||
/** | ||
* @param \PhpParser\Node\Stmt\ClassMethod $node | ||
* @param \PHPStan\Analyser\Scope $scope | ||
* @return string[] | ||
*/ | ||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (!$scope->isInClass()) { | ||
throw new \PHPStan\ShouldNotHappenException(); | ||
} | ||
|
||
if ($scope->isInTrait()) { | ||
return []; | ||
} | ||
|
||
if ($node->name->name !== '__construct') { | ||
return []; | ||
} | ||
|
||
$classReflection = $scope->getClassReflection()->getNativeReflection(); | ||
if ($classReflection->isInterface() || $classReflection->isAnonymous()) { | ||
return []; | ||
} | ||
|
||
if ($this->callsParentConstruct($node)) { | ||
if ($classReflection->getParentClass() === false) { | ||
return [ | ||
sprintf( | ||
'%s::__construct() calls parent constructor but does not extend any class.', | ||
$classReflection->getName() | ||
), | ||
]; | ||
} | ||
|
||
if ($this->getParentConstructorClass($classReflection) === false) { | ||
return [ | ||
sprintf( | ||
'%s::__construct() calls parent constructor but parent does not have one.', | ||
$classReflection->getName() | ||
), | ||
]; | ||
} | ||
} else { | ||
$parentClass = $this->getParentConstructorClass($classReflection); | ||
if ($parentClass !== false) { | ||
return [ | ||
sprintf( | ||
'%s::__construct() does not call parent constructor from %s.', | ||
$classReflection->getName(), | ||
$parentClass->getName() | ||
), | ||
]; | ||
} | ||
} | ||
|
||
return []; | ||
} | ||
|
||
private function callsParentConstruct(Node $parserNode): bool | ||
{ | ||
if (!isset($parserNode->stmts)) { | ||
return false; | ||
} | ||
|
||
foreach ($parserNode->stmts as $statement) { | ||
if ($statement instanceof Node\Stmt\Expression) { | ||
$statement = $statement->expr; | ||
} | ||
|
||
$statement = $this->ignoreErrorSuppression($statement); | ||
if ($statement instanceof \PhpParser\Node\Expr\StaticCall) { | ||
if ( | ||
$statement->class instanceof Name | ||
&& ((string) $statement->class === 'parent') | ||
&& $statement->name instanceof Node\Identifier | ||
&& $statement->name->name === '__construct' | ||
) { | ||
return true; | ||
} | ||
} else { | ||
if ($this->callsParentConstruct($statement)) { | ||
return true; | ||
} | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* @param \ReflectionClass $classReflection | ||
* @return \ReflectionClass|false | ||
*/ | ||
private function getParentConstructorClass(\ReflectionClass $classReflection) | ||
{ | ||
while ($classReflection->getParentClass() !== false) { | ||
$constructor = $classReflection->getParentClass()->hasMethod('__construct') ? $classReflection->getParentClass()->getMethod('__construct') : null; | ||
$constructorWithClassName = $classReflection->getParentClass()->hasMethod($classReflection->getParentClass()->getName()) ? $classReflection->getParentClass()->getMethod($classReflection->getParentClass()->getName()) : null; | ||
if ( | ||
( | ||
$constructor !== null | ||
&& $constructor->getDeclaringClass()->getName() === $classReflection->getParentClass()->getName() | ||
&& !$constructor->isAbstract() | ||
&& !$constructor->isPrivate() | ||
) || ( | ||
$constructorWithClassName !== null | ||
&& $constructorWithClassName->getDeclaringClass()->getName() === $classReflection->getParentClass()->getName() | ||
&& !$constructorWithClassName->isAbstract() | ||
) | ||
) { | ||
return $classReflection->getParentClass(); | ||
} | ||
|
||
$classReflection = $classReflection->getParentClass(); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private function ignoreErrorSuppression(Node $statement): Node | ||
{ | ||
if ($statement instanceof Node\Expr\ErrorSuppress) { | ||
|
||
return $statement->expr; | ||
} | ||
|
||
return $statement; | ||
} | ||
|
||
} |
48 changes: 48 additions & 0 deletions
48
tests/Rules/Classes/RequireParentConstructCallRuleTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Classes; | ||
|
||
class RequireParentConstructCallRuleTest extends \PHPStan\Testing\RuleTestCase | ||
{ | ||
|
||
protected function getRule(): \PHPStan\Rules\Rule | ||
{ | ||
return new RequireParentConstructCallRule(); | ||
} | ||
|
||
public function testCallToParentConstructor(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/call-to-parent-constructor.php'], [ | ||
[ | ||
'IpsumCallToParentConstructor::__construct() calls parent constructor but parent does not have one.', | ||
31, | ||
], | ||
[ | ||
'BCallToParentConstructor::__construct() does not call parent constructor from ACallToParentConstructor.', | ||
51, | ||
], | ||
[ | ||
'CCallToParentConstructor::__construct() calls parent constructor but does not extend any class.', | ||
61, | ||
], | ||
[ | ||
'FCallToParentConstructor::__construct() does not call parent constructor from DCallToParentConstructor.', | ||
86, | ||
], | ||
[ | ||
'BarSoapClient::__construct() does not call parent constructor from SoapClient.', | ||
129, | ||
], | ||
[ | ||
'StaticCallOnAVariable::__construct() does not call parent constructor from FooCallToParentConstructor.', | ||
140, | ||
], | ||
]); | ||
} | ||
|
||
public function testCheckInTraits(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/call-to-parent-constructor-in-trait.php'], []); | ||
} | ||
|
||
} |
31 changes: 31 additions & 0 deletions
31
tests/Rules/Classes/data/call-to-parent-constructor-in-trait.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php | ||
|
||
namespace CallToParentConstructorInTrait; | ||
|
||
trait AcmeTrait | ||
{ | ||
public function __construct() | ||
{ | ||
} | ||
} | ||
|
||
class BaseAcme | ||
{ | ||
public function __construct() | ||
{ | ||
} | ||
} | ||
|
||
class Acme extends BaseAcme | ||
{ | ||
use AcmeTrait { | ||
AcmeTrait::__construct as private __acmeConstruct; | ||
} | ||
|
||
public function __construct() | ||
{ | ||
$this->__acmeConstruct(); | ||
|
||
parent::__construct(); | ||
} | ||
} |
Oops, something went wrong.