diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 61d5429b49..5495a500ed 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -8,3 +8,4 @@ parameters: arrayUnpacking: true nodeConnectingVisitorCompatibility: false disableRuntimeReflectionProvider: true + illegalConstructorMethodCall: true diff --git a/conf/config.level2.neon b/conf/config.level2.neon index becc7b5b02..8beb42e5f0 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -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 @@ -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: diff --git a/conf/config.neon b/conf/config.neon index 1210d69efc..144dbf1210 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -32,6 +32,7 @@ parameters: arrayFilter: false arrayUnpacking: false nodeConnectingVisitorCompatibility: true + illegalConstructorMethodCall: false fileExtensions: - php checkAdvancedIsset: false @@ -219,6 +220,7 @@ parametersSchema: arrayFilter: bool(), arrayUnpacking: bool(), nodeConnectingVisitorCompatibility: bool(), + illegalConstructorMethodCall: bool(), ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/Methods/IllegalConstructorMethodCallRule.php b/src/Rules/Methods/IllegalConstructorMethodCallRule.php new file mode 100644 index 0000000000..94eb726ab3 --- /dev/null +++ b/src/Rules/Methods/IllegalConstructorMethodCallRule.php @@ -0,0 +1,33 @@ + + */ +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(), + ]; + } + +} diff --git a/src/Rules/Methods/IllegalConstructorStaticCallRule.php b/src/Rules/Methods/IllegalConstructorStaticCallRule.php new file mode 100644 index 0000000000..42647f1de8 --- /dev/null +++ b/src/Rules/Methods/IllegalConstructorStaticCallRule.php @@ -0,0 +1,58 @@ + + */ +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'; + } + +} diff --git a/tests/PHPStan/Rules/Methods/IllegalConstructorMethodCallRuleTest.php b/tests/PHPStan/Rules/Methods/IllegalConstructorMethodCallRuleTest.php new file mode 100644 index 0000000000..8b0f957e85 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/IllegalConstructorMethodCallRuleTest.php @@ -0,0 +1,37 @@ + + */ +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, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php b/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php new file mode 100644 index 0000000000..922c497fa0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php @@ -0,0 +1,45 @@ + + */ +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, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/illegal-constructor-call-rule-test.php b/tests/PHPStan/Rules/Methods/data/illegal-constructor-call-rule-test.php new file mode 100644 index 0000000000..f59fbde165 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/illegal-constructor-call-rule-test.php @@ -0,0 +1,63 @@ + 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'); + } + +}