Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rule call constructor #1208

Merged
merged 9 commits into from Apr 17, 2022
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');
}

}