1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Add prototype for conditional return type

This commit is contained in:
Matthew Brown 2020-03-22 10:44:48 -04:00
parent 26694345d6
commit 6058725256
9 changed files with 498 additions and 59 deletions

View File

@ -24,6 +24,7 @@ use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\GetClassT;
use Psalm\Type\Atomic\GetTypeT;
use Psalm\Type\Atomic\TConditional;
use Psalm\Type\Atomic\THtmlEscapedString;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIterable;
@ -913,6 +914,28 @@ class TypeAnalyzer
return false;
}
if ($container_type_part instanceof TConditional) {
$atomic_types = array_merge(
array_values($container_type_part->if_type->getAtomicTypes()),
array_values($container_type_part->else_type->getAtomicTypes())
);
foreach ($atomic_types as $container_as_type_part) {
if (self::isAtomicContainedBy(
$codebase,
$input_type_part,
$container_as_type_part,
$allow_interface_equality,
$allow_float_int_equality,
$atomic_comparison_result
)) {
return true;
}
}
return false;
}
if ($input_type_part instanceof TTemplateParam) {
if ($input_type_part->extra_types) {
foreach ($input_type_part->extra_types as $extra_type) {

View File

@ -317,6 +317,19 @@ class ParseTree
break;
}
while ($current_parent instanceof ParseTree\UnionTree
&& $current_leaf->parent
) {
$current_leaf = $current_leaf->parent;
$current_parent = $current_leaf->parent;
}
if ($current_parent && $current_parent instanceof ParseTree\ConditionalTree) {
$current_leaf = $current_parent;
$current_parent = $current_parent->parent;
break;
}
if (!$current_parent) {
throw new TypeParseTreeException('Cannot process colon without parent');
}
@ -372,22 +385,44 @@ class ParseTree
case '?':
if ($next_token === null || $next_token[0] !== ':') {
$new_parent = !$current_leaf instanceof ParseTree\Root ? $current_leaf : null;
$new_leaf = new ParseTree\NullableTree(
$new_parent
);
if ($current_leaf instanceof ParseTree\Root) {
$current_leaf = $parse_tree = $new_leaf;
break;
while (($current_leaf instanceof ParseTree\Value
|| $current_leaf instanceof ParseTree\UnionTree)
&& $current_leaf->parent
) {
$current_leaf = $current_leaf->parent;
}
if ($new_leaf->parent) {
$new_leaf->parent->children[] = $new_leaf;
}
if ($current_leaf instanceof ParseTree\TemplateIsTree && $current_leaf->parent) {
$current_parent = $current_leaf->parent;
$current_leaf = $new_leaf;
$new_leaf = new ParseTree\ConditionalTree(
$current_leaf,
$current_leaf->parent
);
$current_leaf->parent = $new_leaf;
array_pop($current_parent->children);
$current_parent->children[] = $new_leaf;
$current_leaf = $new_leaf;
} else {
$new_parent = !$current_leaf instanceof ParseTree\Root ? $current_leaf : null;
$new_leaf = new ParseTree\NullableTree(
$new_parent
);
if ($current_leaf instanceof ParseTree\Root) {
$current_leaf = $parse_tree = $new_leaf;
break;
}
if ($new_leaf->parent) {
$new_leaf->parent->children[] = $new_leaf;
}
$current_leaf = $new_leaf;
}
}
break;
@ -423,9 +458,16 @@ class ParseTree
$current_parent = $current_leaf->parent;
}
$new_parent_leaf = new ParseTree\UnionTree($current_parent);
$new_parent_leaf->children = [$current_leaf];
$current_leaf->parent = $new_parent_leaf;
if ($current_parent instanceof ParseTree\TemplateIsTree) {
$new_parent_leaf = new ParseTree\UnionTree($current_leaf);
$new_parent_leaf->children = [$current_leaf];
$new_parent_leaf->parent = $current_parent;
$current_leaf->parent = $new_parent_leaf;
} else {
$new_parent_leaf = new ParseTree\UnionTree($current_parent);
$new_parent_leaf->children = [$current_leaf];
$current_leaf->parent = $new_parent_leaf;
}
if ($current_parent) {
array_pop($current_parent->children);
@ -472,32 +514,46 @@ class ParseTree
break;
case 'is':
case 'as':
if ($i > 0) {
$current_parent = $current_leaf->parent;
if (!$current_leaf instanceof ParseTree\Value
|| !$current_parent instanceof ParseTree\GenericTree
|| !$next_token
) {
throw new TypeParseTreeException('Unexpected token ' . $type_token[0]);
if ($current_parent) {
array_pop($current_parent->children);
}
array_pop($current_parent->children);
if ($type_token[0] === 'as') {
if (!$current_leaf instanceof ParseTree\Value
|| !$current_parent instanceof ParseTree\GenericTree
|| !$next_token
) {
throw new TypeParseTreeException('Unexpected token ' . $type_token[0]);
}
$current_leaf = new ParseTree\TemplateAsTree(
$current_leaf->value,
$next_token[0],
$current_parent
);
$current_leaf = new ParseTree\TemplateAsTree(
$current_leaf->value,
$next_token[0],
$current_parent
);
$current_parent->children[] = $current_leaf;
++$i;
$current_parent->children[] = $current_leaf;
++$i;
} elseif ($current_leaf instanceof ParseTree\Value) {
$current_leaf = new ParseTree\TemplateIsTree(
$current_leaf->value,
$current_parent
);
if ($current_parent) {
$current_parent->children[] = $current_leaf;
}
}
break;
}
// falling through for methods named 'as'
// falling through for methods named 'as' or 'is'
default:
$new_parent = !$current_leaf instanceof ParseTree\Root ? $current_leaf : null;

View File

@ -0,0 +1,19 @@
<?php
namespace Psalm\Internal\Type\ParseTree;
/**
* @internal
*/
class ConditionalTree extends \Psalm\Internal\Type\ParseTree
{
/**
* @var TemplateIsTree
*/
public $condition;
public function __construct(TemplateIsTree $condition, ?\Psalm\Internal\Type\ParseTree $parent = null)
{
$this->condition = $condition;
$this->parent = $parent;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Psalm\Internal\Type\ParseTree;
/**
* @internal
*/
class TemplateIsTree extends \Psalm\Internal\Type\ParseTree
{
/**
* @var string
*/
public $param_name;
public function __construct(string $param_name, ?\Psalm\Internal\Type\ParseTree $parent = null)
{
$this->param_name = $param_name;
$this->parent = $parent;
}
}

View File

@ -735,6 +735,53 @@ abstract class Type
);
}
if ($parse_tree instanceof ParseTree\ConditionalTree) {
$template_param_name = $parse_tree->condition->param_name;
if (isset($template_type_map[$template_param_name])) {
$first_class = array_keys($template_type_map[$template_param_name])[0];
$conditional_type = self::getTypeFromTree(
$parse_tree->condition->children[0],
null,
$template_type_map
);
$if_type = self::getTypeFromTree(
$parse_tree->children[0],
null,
$template_type_map
);
$else_type = self::getTypeFromTree(
$parse_tree->children[1],
null,
$template_type_map
);
if ($conditional_type instanceof Type\Atomic) {
$conditional_type = new Type\Union([$conditional_type]);
}
if ($if_type instanceof Type\Atomic) {
$if_type = new Type\Union([$if_type]);
}
if ($else_type instanceof Type\Atomic) {
$else_type = new Type\Union([$else_type]);
}
return new Atomic\TConditional(
$template_param_name,
$first_class,
$template_type_map[$template_param_name][$first_class][0],
$conditional_type,
$if_type,
$else_type
);
}
}
if (!$parse_tree instanceof ParseTree\Value) {
throw new \InvalidArgumentException('Unrecognised parse tree type ' . get_class($parse_tree));
}
@ -905,11 +952,11 @@ abstract class Type
$type_tokens[++$rtc] = [' ', $i - 1];
$type_tokens[++$rtc] = ['', $i];
} elseif ($was_space
&& $char === 'a'
&& ($char === 'a' || $char === 'i')
&& ($chars[$i + 1] ?? null) === 's'
&& ($chars[$i + 2] ?? null) === ' '
) {
$type_tokens[++$rtc] = ['as', $i - 1];
$type_tokens[++$rtc] = [$char . 's', $i - 1];
$type_tokens[++$rtc] = ['', ++$i];
continue;
} elseif ($was_char) {
@ -1079,7 +1126,7 @@ abstract class Type
if (in_array(
$string_type_token[0],
[
'<', '>', '|', '?', ',', '{', '}', ':', '::', '[', ']', '(', ')', '&', '=', '...', 'as',
'<', '>', '|', '?', ',', '{', '}', ':', '::', '[', ']', '(', ')', '&', '=', '...', 'as', 'is',
],
true
)) {

View File

@ -0,0 +1,161 @@
<?php
namespace Psalm\Type\Atomic;
use function implode;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Union;
use Psalm\Storage\MethodStorage;
use function array_map;
use function strtolower;
class TConditional extends \Psalm\Type\Atomic
{
/**
* @var string
*/
public $param_name;
/**
* @var string
*/
public $defining_class;
/**
* @var Union
*/
public $as_type;
/**
* @var Union
*/
public $conditional_type;
/**
* @var Union
*/
public $if_type;
/**
* @var Union
*/
public $else_type;
/**
* @param string $defining_class
*/
public function __construct(
string $param_name,
string $defining_class,
Union $as_type,
Union $conditional_type,
Union $if_type,
Union $else_type
) {
$this->param_name = $param_name;
$this->defining_class = $defining_class;
$this->as_type = $as_type;
$this->conditional_type = $conditional_type;
$this->if_type = $if_type;
$this->else_type = $else_type;
}
public function __toString()
{
return '('
. $this->param_name
. ' is ' . $this->conditional_type
. ' ? ' . $this->if_type
. ' : ' . $this->else_type
. ')';
}
/**
* @return string
*/
public function getKey(bool $include_extra = true)
{
return $this->__toString();
}
/**
* @return string
*/
public function getAssertionString()
{
return '';
}
public function getId(bool $nested = false)
{
return '('
. $this->param_name . ':' . $this->defining_class
. ' is ' . $this->conditional_type->getId()
. ' ? ' . $this->if_type->getId()
. ' : ' . $this->else_type->getId()
. ')';
}
/**
* @param string|null $namespace
* @param array<string> $aliased_classes
* @param string|null $this_class
* @param int $php_major_version
* @param int $php_minor_version
*
* @return null
*/
public function toPhpString(
$namespace,
array $aliased_classes,
$this_class,
$php_major_version,
$php_minor_version
) {
return null;
}
/**
* @param string|null $namespace
* @param array<string, string> $aliased_classes
* @param string|null $this_class
* @param bool $use_phpdoc_format
*
* @return string
*/
public function toNamespacedString(
?string $namespace,
array $aliased_classes,
?string $this_class,
bool $use_phpdoc_format
) {
return '';
}
public function getChildNodes() : array
{
return [$this->conditional_type, $this->if_type, $this->else_type];
}
/**
* @return bool
*/
public function canBeFullyExpressedInPhp()
{
return false;
}
/**
* @param array<string, array<string, array{Type\Union, 1?:int}>> $template_types
*
* @return void
*/
public function replaceTemplateTypesWithArgTypes(array $template_types, ?Codebase $codebase)
{
$this->conditional_type->replaceTemplateTypesWithArgTypes($template_types, $codebase);
$this->if_type->replaceTemplateTypesWithArgTypes($template_types, $codebase);
$this->else_type->replaceTemplateTypesWithArgTypes($template_types, $codebase);
}
}

View File

@ -1267,6 +1267,50 @@ class Union implements TypeNode
} else {
$new_types[$key] = new Type\Atomic\TMixed();
}
} elseif ($atomic_type instanceof Type\Atomic\TConditional
&& $codebase
) {
$template_type = isset($template_types[$atomic_type->param_name][$atomic_type->defining_class])
? clone $template_types[$atomic_type->param_name][$atomic_type->defining_class][0]
: null;
$class_template_type = null;
if ($template_type) {
if (TypeAnalyzer::isContainedBy(
$codebase,
$template_type,
$atomic_type->conditional_type
)) {
$class_template_type = clone $atomic_type->if_type;
} elseif (TypeAnalyzer::isContainedBy(
$codebase,
$template_type,
$atomic_type->as_type
)
&& !TypeAnalyzer::isContainedBy(
$codebase,
$atomic_type->as_type,
$template_type
)
) {
$class_template_type = clone $atomic_type->else_type;
}
}
if (!$class_template_type) {
$class_template_type = Type::combineUnionTypes(
$atomic_type->if_type,
$atomic_type->else_type,
$codebase
);
}
$keys_to_unset[] = $key;
foreach ($class_template_type->getAtomicTypes() as $class_template_atomic_type) {
$new_types[$class_template_atomic_type->getKey()] = $class_template_atomic_type;
}
}
}

View File

@ -71,16 +71,15 @@ class AnnotationTest extends TestCase
'<?php
/** @psalm-suppress MissingConstructor */
class Foo {
/** @var \stdClass[]|\ArrayObject */
public $bar;
/** @var \stdClass[]|\ArrayObject */
public $bar;
/**
* @return \stdClass[]|\ArrayObject
*/
public function getBar(): \ArrayObject
{
return $this->bar;
}
/**
* @return \stdClass[]|\ArrayObject
*/
public function getBar(): \ArrayObject {
return $this->bar;
}
}'
);
@ -565,12 +564,11 @@ class AnnotationTest extends TestCase
],
'builtInClassInAShape' => [
'<?php
/**
* @return array{d:Exception}
* @psalm-suppress InvalidReturnType
*/
function f() {}
'
/**
* @return array{d:Exception}
* @psalm-suppress InvalidReturnType
*/
function f() {}'
],
'slashAfter?' => [
'<?php
@ -786,10 +784,10 @@ class AnnotationTest extends TestCase
}
/**
* @psalm-type _A=array{elt:int}
* @param _A $p
* @return _A
*/
* @psalm-type _A=array{elt:int}
* @param _A $p
* @return _A
*/
function f($p) {
/** @var _A */
$r = $p;
@ -1010,7 +1008,7 @@ class AnnotationTest extends TestCase
* } $foo
*/
function foo(array $foo) : int {
return count($foo);
return count($foo);
}
/**
@ -1075,7 +1073,7 @@ class AnnotationTest extends TestCase
'possiblyUndefinedObjectProperty' => [
'<?php
function consume(string $value): void {
echo $value;
echo $value;
}
/** @var object{value?: string} $data */
@ -1090,9 +1088,8 @@ class AnnotationTest extends TestCase
/**
* @throws self
*/
public static function create(): void
{
throw new self();
public static function create(): void {
throw new self();
}
}'
],
@ -1122,6 +1119,38 @@ class AnnotationTest extends TestCase
return false;
}'
],
'conditionalReturnType' => [
'<?php
class A {
/** @var array<string, string> */
private array $itemAttr = [];
/**
* @template T as ?string
* @param T $name
* @return string|string[]
* @psalm-return (T is string ? string : array<string, string>)
*/
public function getAttribute(?string $name, string $default = "")
{
if (null === $name) {
return $this->itemAttr;
}
return isset($this->itemAttr[$name]) ? $this->itemAttr[$name] : $default;
}
}
$a = (new A)->getAttribute("colour", "red"); // typed as string
$b = (new A)->getAttribute(null); // typed as array<string, string>
/** @psalm-suppress MixedArgument */
$c = (new A)->getAttribute($_GET["foo"]); // typed as string|array<string, string>',
[
'$a' => 'string',
'$b' => 'array<string, string>',
'$c' => 'array<string, string>|string'
]
],
];
}
@ -1233,7 +1262,7 @@ class AnnotationTest extends TestCase
* @return \?string
*/
function foo() {
return rand(0, 1) ? "hello" : null;
return rand(0, 1) ? "hello" : null;
}',
'error_message' => 'InvalidDocblock',
],
@ -1336,7 +1365,7 @@ class AnnotationTest extends TestCase
* @psalm-suppress MismatchingDocblockReturnType
*/
function foo(): B {
return new A;
return new A;
}',
'error_message' => 'UndefinedClass',
],

View File

@ -551,6 +551,47 @@ class TypeParseTest extends TestCase
);
}
/**
* @return void
*/
public function testConditionalTypeWithSpaces()
{
$this->assertSame(
'(T is string ? string : int)',
(string) Type::parseString('(T is string ? string : int)', null, ['T' => ['' => [Type::getArray()]]])
);
}
/**
* @return void
*/
public function testConditionalTypeWithUnion()
{
$this->assertSame(
'(T is string|true ? int|string : int)',
(string) Type::parseString('(T is "hello"|true ? string|int : int)', null, ['T' => ['' => [Type::getArray()]]])
);
}
/**
* @return void
*/
public function testConditionalTypeWithoutSpaces()
{
$this->assertSame(
'(T is string ? string : int)',
(string) Type::parseString('(T is string?string:int)', null, ['T' => ['' => [Type::getArray()]]])
);
}
public function testConditionalTypeWithGenerics() : void
{
$this->assertSame(
'(T is string ? string : array<string, string>)',
(string) Type::parseString('(T is string ? string : array<string, string>)', null, ['T' => ['' => [Type::getArray()]]])
);
}
/**
* @return void
*/