Skip to content

Commit

Permalink
Introduce new type
Browse files Browse the repository at this point in the history
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 <rvanvelzen1@gmail.com>
  • Loading branch information
2 people authored and ondrejmirtes committed May 6, 2024
1 parent 1090835 commit 1f95482
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

Expand Down
104 changes: 104 additions & 0 deletions src/Type/NewObjectType.php
@@ -0,0 +1,104 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Traits\LateResolvableTypeTrait;
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
use function sprintf;

/** @api */
class NewObjectType implements CompoundType, LateResolvableType
{

use LateResolvableTypeTrait;
use NonGeneralizableTypeTrait;

public function __construct(private Type $type)
{
}

public function getType(): Type
{
return $this->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'],
);
}

}
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -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) {
Expand Down
54 changes: 54 additions & 0 deletions tests/PHPStan/Analyser/data/bug-9704.php
@@ -0,0 +1,54 @@
<?php

namespace Bug9704;

use DateTime;
use DateTimeImmutable;
use function PHPStan\dumpType;
use function PHPStan\Testing\assertType;

class Foo
{
/**
* @var array<string, class-string>
*/
private const TYPES = [
'foo' => DateTime::class,
'bar' => DateTimeImmutable::class,
];

/**
* @template M of self::TYPES
* @template T of key-of<M>
* @param T $type
*
* @return new<M[T]>
*/
public static function get(string $type) : object
{
$class = self::TYPES[$type];

return new $class('now');
}

/**
* @template T of key-of<self::TYPES>
* @param T $type
*
* @return new<self::TYPES[T]>
*/
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'));


0 comments on commit 1f95482

Please sign in to comment.