diff --git a/src/Rules/Methods/IllegalConstructorStaticCallRule.php b/src/Rules/Methods/IllegalConstructorStaticCallRule.php index ac3e0fa52a..b24aa87b66 100644 --- a/src/Rules/Methods/IllegalConstructorStaticCallRule.php +++ b/src/Rules/Methods/IllegalConstructorStaticCallRule.php @@ -6,8 +6,10 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function array_key_exists; use function array_map; use function in_array; +use function sprintf; use function strtolower; /** @@ -37,16 +39,19 @@ public function processNode(Node $node, Scope $scope): array ]; } - private function isCollectCallingConstructor(Node $node, Scope $scope): bool + private function isCollectCallingConstructor(Node\Expr\StaticCall $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') { + if ($scope->getFunction() === null) { return false; } + if ($scope->getFunction()->getName() !== '__construct') { + if (!$this->isInRenamedTraitConstructor($scope)) { + return false; + } + } + if (!$scope->isInClass()) { return false; } @@ -60,4 +65,27 @@ private function isCollectCallingConstructor(Node $node, Scope $scope): bool return in_array(strtolower($scope->resolveName($node->class)), $parentClasses, true); } + private function isInRenamedTraitConstructor(Scope $scope): bool + { + if (!$scope->isInClass()) { + return false; + } + + if (!$scope->isInTrait()) { + return false; + } + + if ($scope->getFunction() === null) { + return false; + } + + $traitAliases = $scope->getClassReflection()->getNativeReflection()->getTraitAliases(); + $functionName = $scope->getFunction()->getName(); + if (!array_key_exists($functionName, $traitAliases)) { + return false; + } + + return $traitAliases[$functionName] === sprintf('%s::%s', $scope->getTraitReflection()->getName(), '__construct'); + } + } diff --git a/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php b/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php index 2654c0d658..065a5785bf 100644 --- a/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php +++ b/tests/PHPStan/Rules/Methods/IllegalConstructorStaticCallRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -46,4 +47,13 @@ public function testMethods(): void ]); } + public function testBug9577(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-9577.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-9577.php b/tests/PHPStan/Rules/Methods/data/bug-9577.php new file mode 100644 index 0000000000..2214d45b33 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9577.php @@ -0,0 +1,40 @@ += 8.1 + +namespace Bug9577IllegalConstructorStaticCall; + +trait StringableMessageTrait +{ + public function __construct( + private readonly \Stringable $StringableMessage, + int $code = 0, + ?\Throwable $previous = null, + ) { + parent::__construct((string) $StringableMessage, $code, $previous); + } + + public function getStringableMessage(): \Stringable + { + return $this->StringableMessage; + } +} + +class SpecializedException extends \RuntimeException +{ + use StringableMessageTrait { + StringableMessageTrait::__construct as __traitConstruct; + } + + public function __construct( + private readonly object $aService, + \Stringable $StringableMessage, + int $code = 0, + ?\Throwable $previous = null, + ) { + $this->__traitConstruct($StringableMessage, $code, $previous); + } + + public function getService(): object + { + return $this->aService; + } +}