Skip to content

Commit

Permalink
RequireParentConstructCallRule moved to strict-rules
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Jul 8, 2019
1 parent 491540d commit e193d17
Show file tree
Hide file tree
Showing 6 changed files with 421 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* Contravariance for parameter types and covariance for return types in inherited methods (also known as Liskov substitution principle - LSP)
* Check LSP even for static methods
* Check missing typehint in anonymous function when a native one could be added
* Require calling parent constructor

Additional rules are coming in subsequent releases!

Expand Down
1 change: 1 addition & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ rules:
- PHPStan\Rules\BooleansInConditions\BooleanInIfConditionRule
- PHPStan\Rules\BooleansInConditions\BooleanInTernaryOperatorRule
- PHPStan\Rules\Cast\UselessCastRule
- PHPStan\Rules\Classes\RequireParentConstructCallRule
- PHPStan\Rules\DisallowedConstructs\DisallowedEmptyRule
- PHPStan\Rules\DisallowedConstructs\DisallowedImplicitArrayCreationRule
- PHPStan\Rules\ForeachLoop\OverwriteVariablesWithForeachRule
Expand Down
147 changes: 147 additions & 0 deletions src/Rules/Classes/RequireParentConstructCallRule.php
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 tests/Rules/Classes/RequireParentConstructCallRuleTest.php
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 tests/Rules/Classes/data/call-to-parent-constructor-in-trait.php
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();
}
}

0 comments on commit e193d17

Please sign in to comment.