diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index e080c998ea..78cb4c9e47 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -80,6 +80,7 @@ use PHPStan\Type\IterableType; use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; +use PHPStan\Type\NewObjectType; use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\NonexistentParentClassType; use PHPStan\Type\NullType; @@ -755,6 +756,13 @@ static function (string $variance): TemplateTypeVariance { return TypeCombinator::union(...$result); } + return new ErrorType(); + } elseif ($mainTypeName === 'new') { + if (count($genericTypes) === 1) { + $type = new NewObjectType($genericTypes[0]); + return $type->isResolvable() ? $type->resolve() : $type; + } + return new ErrorType(); } diff --git a/src/Type/NewObjectType.php b/src/Type/NewObjectType.php new file mode 100644 index 0000000000..57b932a398 --- /dev/null +++ b/src/Type/NewObjectType.php @@ -0,0 +1,104 @@ +type; + } + + public function getReferencedClasses(): array + { + return $this->type->getReferencedClasses(); + } + + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array + { + return $this->type->getReferencedTemplateTypes($positionVariance); + } + + public function equals(Type $type): bool + { + return $type instanceof self + && $this->type->equals($type->type); + } + + public function describe(VerbosityLevel $level): string + { + return sprintf('new<%s>', $this->type->describe($level)); + } + + public function isResolvable(): bool + { + return !TypeUtils::containsTemplateType($this->type); + } + + protected function getResult(): Type + { + return $this->type->getObjectTypeOrClassStringObjectType(); + } + + /** + * @param callable(Type): Type $cb + */ + public function traverse(callable $cb): Type + { + $type = $cb($this->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + if (!$right instanceof self) { + return $this; + } + + $type = $cb($this->type, $right->type); + + if ($this->type === $type) { + return $this; + } + + return new self($type); + } + + public function toPhpDocNode(): TypeNode + { + return new GenericTypeNode(new IdentifierTypeNode('new'), [$this->type->toPhpDocNode()]); + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): Type + { + return new self( + $properties['type'], + ); + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 13b5e0b62c..a5f1c378b1 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -773,6 +773,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4357.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10863.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5817.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9704.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-chunk.php'); if (PHP_VERSION_ID >= 80000) { diff --git a/tests/PHPStan/Analyser/data/bug-9704.php b/tests/PHPStan/Analyser/data/bug-9704.php new file mode 100644 index 0000000000..1d435746a5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9704.php @@ -0,0 +1,54 @@ + + */ + private const TYPES = [ + 'foo' => DateTime::class, + 'bar' => DateTimeImmutable::class, + ]; + + /** + * @template M of self::TYPES + * @template T of key-of + * @param T $type + * + * @return new + */ + public static function get(string $type) : object + { + $class = self::TYPES[$type]; + + return new $class('now'); + } + + /** + * @template T of key-of + * @param T $type + * + * @return new + */ + public static function get2(string $type) : object + { + $class = self::TYPES[$type]; + + return new $class('now'); + } +} + +assertType(DateTime::class, Foo::get('foo')); +assertType(DateTimeImmutable::class, Foo::get('bar')); + +assertType(DateTime::class, Foo::get2('foo')); +assertType(DateTimeImmutable::class, Foo::get2('bar')); + +