diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index e92e18a03d..059b041527 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -61,8 +61,8 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $errors = []; - foreach ($this->getClassNames($node, $scope) as [$class, $isName]) { - $errors = array_merge($errors, $this->checkClassName($class, $isName, $node, $scope)); + foreach ($this->getClassNames($node, $scope) as [$class, $isName, $isFromClassString]) { + $errors = array_merge($errors, $this->checkClassName($class, $isName, $isFromClassString, $node, $scope)); } return $errors; } @@ -71,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array * @param Node\Expr\New_ $node * @return list */ - private function checkClassName(string $class, bool $isName, Node $node, Scope $scope): array + private function checkClassName(string $class, bool $isName, bool $isFromClassString, Node $node, Scope $scope): array { $lowercasedClass = strtolower($class); $messages = []; @@ -180,7 +180,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ ]; } - if (!$isStatic && $classReflection->isAbstract() && $isName) { + if (!$isStatic && $classReflection->isAbstract() && $isName && !$isFromClassString) { return [ RuleErrorBuilder::message( sprintf('Instantiated class %s is abstract.', $classReflection->getDisplayName()), @@ -274,12 +274,12 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ /** * @param Node\Expr\New_ $node - * @return array + * @return array */ private function getClassNames(Node $node, Scope $scope): array { if ($node->class instanceof Node\Name) { - return [[(string) $node->class, true]]; + return [[(string) $node->class, true, false]]; } if ($node->class instanceof Node\Stmt\Class_) { @@ -289,22 +289,22 @@ private function getClassNames(Node $node, Scope $scope): array } return array_map( - static fn (string $className) => [$className, true], + static fn (string $className) => [$className, true, false], $classNames, ); } $type = $scope->getType($node->class); - if ($type->isClassString()->yes()) { + if ($type->isClassString()->yes() && count($type->getConstantStrings()) === 0) { $concretes = array_filter( $type->getClassStringObjectType()->getObjectClassReflections(), - static fn (ClassReflection $classReflection): bool => !$classReflection->isAbstract() && !$classReflection->isInterface(), + static fn (ClassReflection $classReflection): bool => !$classReflection->isInterface(), ); if (count($concretes) > 0) { return array_map( - static fn (ClassReflection $classReflection): array => [$classReflection->getName(), true], + static fn (ClassReflection $classReflection): array => [$classReflection->getName(), true, true], $concretes, ); } @@ -312,11 +312,11 @@ private function getClassNames(Node $node, Scope $scope): array return array_merge( array_map( - static fn (ConstantStringType $type): array => [$type->getValue(), true], + static fn (ConstantStringType $type): array => [$type->getValue(), true, false], $type->getConstantStrings(), ), array_map( - static fn (string $name): array => [$name, false], + static fn (string $name): array => [$name, false, false], $type->getObjectClassNames(), ), ); diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 8558296e14..ba7d6168e3 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -507,6 +507,18 @@ public function testClassString(): void 'Parameter #1 $i of class ClassString\A constructor expects int, string given.', 67, ], + [ + 'Parameter #1 $i of class ClassString\B constructor expects int, string given.', + 70, + ], + [ + 'Parameter #1 $i of class ClassString\B constructor expects int, string given.', + 71, + ], + [ + 'Parameter #1 $i of class ClassString\B constructor expects int, string given.', + 72, + ], [ 'Parameter #1 $i of class ClassString\C constructor expects int, string given.', 75, @@ -534,6 +546,16 @@ public function testClassString(): void ]); } + public function testBug14102(): void + { + $this->analyse([__DIR__ . '/data/bug-14102.php'], [ + [ + 'Class Bug14102\HelloWorld constructor invoked with 0 parameters, 2 required.', + 24, + ], + ]); + } + public function testInternalConstructor(): void { $this->analyse([__DIR__ . '/data/internal-constructor.php'], [ diff --git a/tests/PHPStan/Rules/Classes/data/bug-14102.php b/tests/PHPStan/Rules/Classes/data/bug-14102.php new file mode 100644 index 0000000000..b3b945e38b --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-14102.php @@ -0,0 +1,28 @@ +c = $a + $b; + } +} + +class ChildWorld extends HelloWorld +{} + +class HelloWorldFactory +{ + /** + * @param class-string $className + */ + public function create(string $className): HelloWorld + { + return new $className(); + } +} + +$a = (new HelloWorldFactory())->create(ChildWorld::class);