Skip to content

Commit

Permalink
Rules for checking direct calls to __construct()
Browse files Browse the repository at this point in the history
  • Loading branch information
muno92 committed Apr 17, 2022
1 parent 0bbc591 commit 58d7df3
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 0 deletions.
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Expand Up @@ -8,3 +8,4 @@ parameters:
arrayUnpacking: true
nodeConnectingVisitorCompatibility: false
disableRuntimeReflectionProvider: true
illegalConstructorMethodCall: true
10 changes: 10 additions & 0 deletions conf/config.level2.neon
Expand Up @@ -40,6 +40,12 @@ rules:
- PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule
- PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule

conditionalTags:
PHPStan\Rules\Methods\IllegalConstructorMethodCallRule:
phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall%
PHPStan\Rules\Methods\IllegalConstructorStaticCallRule:
phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall%

services:
-
class: PHPStan\Rules\Classes\MixinRule
Expand All @@ -53,6 +59,10 @@ services:
reportMaybes: %reportMaybes%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Methods\IllegalConstructorMethodCallRule
-
class: PHPStan\Rules\Methods\IllegalConstructorStaticCallRule
-
class: PHPStan\Rules\PhpDoc\InvalidPhpDocVarTagTypeRule
arguments:
Expand Down
2 changes: 2 additions & 0 deletions conf/config.neon
Expand Up @@ -32,6 +32,7 @@ parameters:
arrayFilter: false
arrayUnpacking: false
nodeConnectingVisitorCompatibility: true
illegalConstructorMethodCall: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -219,6 +220,7 @@ parametersSchema:
arrayFilter: bool(),
arrayUnpacking: bool(),
nodeConnectingVisitorCompatibility: bool(),
illegalConstructorMethodCall: bool(),
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
33 changes: 33 additions & 0 deletions src/Rules/Methods/IllegalConstructorMethodCallRule.php
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Methods;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<Node\Expr\MethodCall>
*/
class IllegalConstructorMethodCallRule implements Rule
{

public function getNodeType(): string
{
return Node\Expr\MethodCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== '__construct') {
return [];
}

return [
RuleErrorBuilder::message('Call to __construct() on an existing object is not allowed.')
->build(),
];
}

}
58 changes: 58 additions & 0 deletions src/Rules/Methods/IllegalConstructorStaticCallRule.php
@@ -0,0 +1,58 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Methods;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<Node\Expr\StaticCall>
*/
class IllegalConstructorStaticCallRule implements Rule
{

public function getNodeType(): string
{
return Node\Expr\StaticCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Node\Identifier || $node->name->toLowerString() !== '__construct') {
return [];
}

if ($this->isCollectCallingConstructor($node, $scope)) {
return [];
}

return [
RuleErrorBuilder::message('Static call to __construct() is only allowed on a parent class in the constructor.')
->build(),
];
}

private function isCollectCallingConstructor(Node $node, Scope $scope): bool
{
if (!$node instanceof Node\Expr\StaticCall) {
return true;
}
// __construct should be called from inside constructor
if ($scope->getFunction() !== null && $scope->getFunction()->getName() !== '__construct') {
return false;
}

if (!$scope->isInClass()) {
return false;
}

if (!$node->class instanceof Node\Name) {
return false;
}

return $node->class->toLowerString() === 'parent';
}

}
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Methods;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<IllegalConstructorMethodCallRule>
*/
class IllegalConstructorMethodCallRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new IllegalConstructorMethodCallRule();
}

public function testMethods(): void
{
$this->analyse([__DIR__ . '/data/illegal-constructor-call-rule-test.php'], [
[
'Call to __construct() on an existing object is not allowed.',
13,
],
[
'Call to __construct() on an existing object is not allowed.',
18,
],
[
'Call to __construct() on an existing object is not allowed.',
60,
],
]);
}

}
@@ -0,0 +1,45 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Methods;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<IllegalConstructorStaticCallRule>
*/
class IllegalConstructorStaticCallRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new IllegalConstructorStaticCallRule();
}

public function testMethods(): void
{
$this->analyse([__DIR__ . '/data/illegal-constructor-call-rule-test.php'], [
[
'Static call to __construct() is only allowed on a parent class in the constructor.',
31,
],
[
'Static call to __construct() is only allowed on a parent class in the constructor.',
43,
],
[
'Static call to __construct() is only allowed on a parent class in the constructor.',
44,
],
[
'Static call to __construct() is only allowed on a parent class in the constructor.',
49,
],
[
'Static call to __construct() is only allowed on a parent class in the constructor.',
50,
],
]);
}

}
@@ -0,0 +1,63 @@
<?php

namespace IllegalConstructorMethodCall;

class ExtendedDateTimeWithMethodCall extends \DateTimeImmutable
{
public function __construct(string $datetime = "now", ?\DateTimeZone $timezone = null)
{
// Avoid infinite loop
if (count(debug_backtrace()) > 1) {
return;
}
$this->__construct($datetime, $timezone);
}

public function mutate(string $datetime = "now", ?\DateTimeZone $timezone = null): void
{
$this->__construct($datetime, $timezone);
}
}

class ExtendedDateTimeWithParentCall extends \DateTimeImmutable
{
public function __construct(string $datetime = "now", ?\DateTimeZone $timezone = null)
{
parent::__construct($datetime, $timezone);
}

public function mutate(string $datetime = "now", ?\DateTimeZone $timezone = null): void
{
parent::__construct($datetime, $timezone);
}
}

class ExtendedDateTimeWithSelfCall extends \DateTimeImmutable
{
public function __construct(string $datetime = "now", ?\DateTimeZone $timezone = null)
{
// Avoid infinite loop
if (count(debug_backtrace()) > 1) {
return;
}
self::__construct($datetime, $timezone);
ExtendedDateTimeWithSelfCall::__construct($datetime, $timezone);
}

public function mutate(string $datetime = "now", ?\DateTimeZone $timezone = null): void
{
self::__construct($datetime, $timezone);
ExtendedDateTimeWithSelfCall::__construct($datetime, $timezone);
}
}

class Foo
{

public function doFoo()
{
$extendedDateTime = new ExtendedDateTimeWithMethodCall('2022/04/12');
$extendedDateTime->__construct('2022/04/13');
}

}

0 comments on commit 58d7df3

Please sign in to comment.