Skip to content

Commit

Permalink
Support conditional return types by union-ing their branches
Browse files Browse the repository at this point in the history
First baby step for phpstan/phpstan#3853
  • Loading branch information
rvanvelzen authored and ondrejmirtes committed Mar 28, 2022
1 parent 82716a6 commit c7e3eed
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 0 deletions.
11 changes: 11 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Expand Up @@ -20,6 +20,8 @@
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
Expand Down Expand Up @@ -121,6 +123,9 @@ public function resolve(TypeNode $typeNode, NameScope $nameScope): Type
} elseif ($typeNode instanceof IntersectionTypeNode) {
return $this->resolveIntersectionTypeNode($typeNode, $nameScope);

} elseif ($typeNode instanceof ConditionalTypeNode || $typeNode instanceof ConditionalTypeForParameterNode) {
return $this->resolveConditionalTypeNode($typeNode, $nameScope);

} elseif ($typeNode instanceof ArrayTypeNode) {
return $this->resolveArrayTypeNode($typeNode, $nameScope);

Expand Down Expand Up @@ -438,6 +443,12 @@ private function resolveIntersectionTypeNode(IntersectionTypeNode $typeNode, Nam
return TypeCombinator::intersect(...$types);
}

private function resolveConditionalTypeNode(ConditionalTypeNode|ConditionalTypeForParameterNode $typeNode, NameScope $nameScope): Type
{
$types = $this->resolveMultiple([$typeNode->if, $typeNode->else], $nameScope);
return TypeCombinator::union(...$types);
}

private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameScope): Type
{
$itemType = $this->resolve($typeNode->type, $nameScope);
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -830,6 +830,7 @@ public function dataFileAsserts(): iterable
}

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6917.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3853.php');
}

/**
Expand Down
53 changes: 53 additions & 0 deletions tests/PHPStan/Analyser/data/bug-3853.php
@@ -0,0 +1,53 @@
<?php

namespace Bug3853;

use function PHPStan\Testing\assertType;

abstract class Test
{
/**
* @template TKey as array-key

This comment has been minimized.

Copy link
@ZebulanStanphill

ZebulanStanphill Mar 28, 2022

Contributor

What does the "as" mean? Is it just a synonym for "of", or does it behave differently? Or is this just a typo that slipped through?

This comment has been minimized.

Copy link
@rvanvelzen

rvanvelzen Mar 28, 2022

Author Contributor

"as" and "of" mean the same thing. I copied some of this from the Psalm stubs, which use "as" predominantly.

This comment has been minimized.

Copy link
@ZebulanStanphill

ZebulanStanphill Mar 28, 2022

Contributor

Ah, okay. In TypeScript, as is used as for type assertions (similar to @var in PHPStan/Psalm), so this was a bit confusing to me. (And in case anyone is wondering, the TypeScript equivalent of of is extends.)

* @template TArray as array<TKey, mixed>
*
* @param TArray $array
*
* @return (TArray is non-empty-array ? non-empty-list<TKey> : list<TKey>)
*/
abstract public function arrayKeys(array $array);

/**
* @param array $array
* @param non-empty-array $nonEmptyArray
*
* @param array<int, int> $intArray
* @param non-empty-array<int, int> $nonEmptyIntArray
*/
public function testArrayKeys(array $array, array $nonEmptyArray, array $intArray, array $nonEmptyIntArray): void
{
assertType('array<int, (int|string)>', $this->arrayKeys($array));
assertType('array<int, int>', $this->arrayKeys($intArray));

// TODO resolve correctly
//assertType('non-empty-array<int, (int|string)>', $this->arrayKeys($nonEmptyArray));
//assertType('non-empty-array<int, int>', $this->arrayKeys($nonEmptyIntArray));

assertType('array<int, (int|string)>', $this->arrayKeys($nonEmptyArray));
assertType('array<int, int>', $this->arrayKeys($nonEmptyIntArray));
}

/**
* @return ($as_float is true ? float : string)
*/
abstract public function microtime(bool $as_float = false);

public function testMicrotime(): void
{
// TODO resolve correctly
//assertType('float', $this->microtime(true));
//assertType('string', $this->microtime(false));

assertType('float|string', $this->microtime(true));
assertType('float|string', $this->microtime(false));
}
}

0 comments on commit c7e3eed

Please sign in to comment.