Skip to content

Commit 1f95482

Browse files
ruudkrvanvelzen
authored andcommitted
Introduce new type
This makes it possible that a new instance of a class-string will be returned. ```php /** * @var array<string, class-string> */ private const TYPES = [ 'foo' => DateTime::class, 'bar' => DateTimeImmutable::class, ]; /** * @template T of key-of<self::TYPES> * @param T $type * * @return new<self::TYPES[T]> */ public static function get(string $type) : ?object { $class = self::TYPES[$type]; return new $class('now'); } ``` See phpstan/phpstan#9704 The work was done by @rvanvelzen in a gist. I just created the PR for it. Co-Authored-By: Richard van Velzen <[email protected]>
1 parent 1090835 commit 1f95482

File tree

4 files changed

+167
-0
lines changed

4 files changed

+167
-0
lines changed

src/PhpDoc/TypeNodeResolver.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
use PHPStan\Type\IterableType;
8181
use PHPStan\Type\KeyOfType;
8282
use PHPStan\Type\MixedType;
83+
use PHPStan\Type\NewObjectType;
8384
use PHPStan\Type\NonAcceptingNeverType;
8485
use PHPStan\Type\NonexistentParentClassType;
8586
use PHPStan\Type\NullType;
@@ -755,6 +756,13 @@ static function (string $variance): TemplateTypeVariance {
755756
return TypeCombinator::union(...$result);
756757
}
757758

759+
return new ErrorType();
760+
} elseif ($mainTypeName === 'new') {
761+
if (count($genericTypes) === 1) {
762+
$type = new NewObjectType($genericTypes[0]);
763+
return $type->isResolvable() ? $type->resolve() : $type;
764+
}
765+
758766
return new ErrorType();
759767
}
760768

src/Type/NewObjectType.php

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
6+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
7+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
8+
use PHPStan\Type\Generic\TemplateTypeVariance;
9+
use PHPStan\Type\Traits\LateResolvableTypeTrait;
10+
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
11+
use function sprintf;
12+
13+
/** @api */
14+
class NewObjectType implements CompoundType, LateResolvableType
15+
{
16+
17+
use LateResolvableTypeTrait;
18+
use NonGeneralizableTypeTrait;
19+
20+
public function __construct(private Type $type)
21+
{
22+
}
23+
24+
public function getType(): Type
25+
{
26+
return $this->type;
27+
}
28+
29+
public function getReferencedClasses(): array
30+
{
31+
return $this->type->getReferencedClasses();
32+
}
33+
34+
public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
35+
{
36+
return $this->type->getReferencedTemplateTypes($positionVariance);
37+
}
38+
39+
public function equals(Type $type): bool
40+
{
41+
return $type instanceof self
42+
&& $this->type->equals($type->type);
43+
}
44+
45+
public function describe(VerbosityLevel $level): string
46+
{
47+
return sprintf('new<%s>', $this->type->describe($level));
48+
}
49+
50+
public function isResolvable(): bool
51+
{
52+
return !TypeUtils::containsTemplateType($this->type);
53+
}
54+
55+
protected function getResult(): Type
56+
{
57+
return $this->type->getObjectTypeOrClassStringObjectType();
58+
}
59+
60+
/**
61+
* @param callable(Type): Type $cb
62+
*/
63+
public function traverse(callable $cb): Type
64+
{
65+
$type = $cb($this->type);
66+
67+
if ($this->type === $type) {
68+
return $this;
69+
}
70+
71+
return new self($type);
72+
}
73+
74+
public function traverseSimultaneously(Type $right, callable $cb): Type
75+
{
76+
if (!$right instanceof self) {
77+
return $this;
78+
}
79+
80+
$type = $cb($this->type, $right->type);
81+
82+
if ($this->type === $type) {
83+
return $this;
84+
}
85+
86+
return new self($type);
87+
}
88+
89+
public function toPhpDocNode(): TypeNode
90+
{
91+
return new GenericTypeNode(new IdentifierTypeNode('new'), [$this->type->toPhpDocNode()]);
92+
}
93+
94+
/**
95+
* @param mixed[] $properties
96+
*/
97+
public static function __set_state(array $properties): Type
98+
{
99+
return new self(
100+
$properties['type'],
101+
);
102+
}
103+
104+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,7 @@ public function dataFileAsserts(): iterable
773773
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4357.php');
774774
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10863.php');
775775
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5817.php');
776+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9704.php');
776777

777778
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-chunk.php');
778779
if (PHP_VERSION_ID >= 80000) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Bug9704;
4+
5+
use DateTime;
6+
use DateTimeImmutable;
7+
use function PHPStan\dumpType;
8+
use function PHPStan\Testing\assertType;
9+
10+
class Foo
11+
{
12+
/**
13+
* @var array<string, class-string>
14+
*/
15+
private const TYPES = [
16+
'foo' => DateTime::class,
17+
'bar' => DateTimeImmutable::class,
18+
];
19+
20+
/**
21+
* @template M of self::TYPES
22+
* @template T of key-of<M>
23+
* @param T $type
24+
*
25+
* @return new<M[T]>
26+
*/
27+
public static function get(string $type) : object
28+
{
29+
$class = self::TYPES[$type];
30+
31+
return new $class('now');
32+
}
33+
34+
/**
35+
* @template T of key-of<self::TYPES>
36+
* @param T $type
37+
*
38+
* @return new<self::TYPES[T]>
39+
*/
40+
public static function get2(string $type) : object
41+
{
42+
$class = self::TYPES[$type];
43+
44+
return new $class('now');
45+
}
46+
}
47+
48+
assertType(DateTime::class, Foo::get('foo'));
49+
assertType(DateTimeImmutable::class, Foo::get('bar'));
50+
51+
assertType(DateTime::class, Foo::get2('foo'));
52+
assertType(DateTimeImmutable::class, Foo::get2('bar'));
53+
54+

0 commit comments

Comments
 (0)