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

Allow conditional types to reference class constants

This commit is contained in:
Matthew Brown 2020-03-29 13:37:37 -04:00
parent 9055c4a79b
commit 194f02507d
11 changed files with 237 additions and 93 deletions

View File

@ -8,7 +8,11 @@ Conditional types have the form:
All conditional types must be wrapped inside brackets e.g. `(...)`
Conditional types are dependent on [template parameters](../templated_annotations.md), so you can only use them in a function where template parameters are defined:
Conditional types are dependent on [template parameters](../templated_annotations.md), so you can only use them in a function where template parameters are defined.
## Example application
Let's suppose we want to make a userland implementation of PHP's numeric addition (but please never do this). You could type this with a conditional return type:
```php
<?php
@ -40,3 +44,41 @@ Calling `add(1, 2.1)` means `T` would instead be inferred as `int|float`, which
`(int|float is int ? int : float)`
The union `int|float` is clearly not an `int`, so the expression is simplified to `(false ? int : float)`, which simplifies to `float`.
## Nested conditionals
You can also nest conditionals just as you could ternary expressions:
```php
<?php
class A {
const TYPE_STRING = 0;
const TYPE_INT = 1;
/**
* @template T as int
* @param T $i
* @psalm-return (
* T is self::TYPE_STRING
* ? string
* : (T is self::TYPE_INT ? int : bool)
* )
*/
public static function getDifferentType(int $i) {
if ($i === self::TYPE_STRING) {
return "hello";
}
if ($i === self::TYPE_INT) {
return 5;
}
return true;
}
}
```
Calling `getDifferentType(0)` will

View File

@ -372,6 +372,7 @@ class ReturnTypeAnalyzer
$self_fq_class_name,
$parent_class,
true,
true,
$function_like_storage instanceof MethodStorage && $function_like_storage->final
);

View File

@ -1724,6 +1724,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
$this->getFQCLN(),
$this->getParentFQCLN(),
true,
true,
$final
);

View File

@ -676,6 +676,7 @@ class MethodComparator
? $implementer_classlike_storage->parent_class
: $guide_classlike_storage->parent_class,
true,
true,
$implementer_method_storage->final
);
@ -768,6 +769,7 @@ class MethodComparator
: $guide_classlike_storage->name,
$guide_classlike_storage->parent_class,
true,
true,
$implementer_method_storage->final
);

View File

@ -907,6 +907,14 @@ class FunctionCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expressio
$return_type = clone $function_storage->return_type;
if ($template_result->generic_params && $function_storage->template_types) {
$return_type = ExpressionAnalyzer::fleshOutType(
$codebase,
$return_type,
null,
null,
null
);
$return_type->replaceTemplateTypesWithArgTypes(
$template_result->generic_params,
$codebase

View File

@ -148,6 +148,14 @@ class MethodCallReturnTypeFetcher
}
if ($template_result->generic_params) {
$return_type_candidate = ExpressionAnalyzer::fleshOutType(
$codebase,
$return_type_candidate,
null,
null,
null
);
$return_type_candidate->replaceTemplateTypesWithArgTypes(
$template_result->generic_params,
$codebase

View File

@ -923,13 +923,6 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
}
}
if ($template_result->generic_params) {
$return_type_candidate->replaceTemplateTypesWithArgTypes(
$template_result->generic_params,
$codebase
);
}
if ($lhs_type_part instanceof Type\Atomic\TTemplateParam) {
$static_type = $lhs_type_part;
} elseif ($lhs_type_part instanceof Type\Atomic\TTemplateParamClass) {
@ -944,6 +937,21 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
$static_type = $fq_class_name;
}
if ($template_result->generic_params) {
$return_type_candidate = ExpressionAnalyzer::fleshOutType(
$codebase,
$return_type_candidate,
null,
null,
null
);
$return_type_candidate->replaceTemplateTypesWithArgTypes(
$template_result->generic_params,
$codebase
);
}
$return_type_candidate = ExpressionAnalyzer::fleshOutType(
$codebase,
$return_type_candidate,

View File

@ -1969,6 +1969,7 @@ class CallAnalyzer
$static_classlike_storage ? $static_classlike_storage->name : null,
$parent_class,
true,
false,
$static_classlike_storage ? $static_classlike_storage->final : false
);

View File

@ -1193,7 +1193,8 @@ class ExpressionAnalyzer
?string $self_class,
$static_class_type,
?string $parent_class,
bool $evaluate = true,
bool $evaluate_class_constants = true,
bool $evaluate_conditional_types = false,
bool $final = false
) {
$return_type = clone $return_type;
@ -1207,7 +1208,8 @@ class ExpressionAnalyzer
$self_class,
$static_class_type,
$parent_class,
$evaluate,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
@ -1246,7 +1248,8 @@ class ExpressionAnalyzer
?string $self_class,
$static_class_type,
?string $parent_class,
bool $evaluate = true,
bool $evaluate_class_constants = true,
bool $evaluate_conditional_types = false,
bool $final = false
) {
if ($return_type instanceof TNamedObject
@ -1262,7 +1265,8 @@ class ExpressionAnalyzer
$self_class,
$static_class_type,
$parent_class,
$evaluate
$evaluate_class_constants,
$evaluate_conditional_types
);
if ($extra_type instanceof TNamedObject && $extra_type->extra_types) {
@ -1282,13 +1286,7 @@ class ExpressionAnalyzer
if ($return_type instanceof TNamedObject) {
$return_type_lc = strtolower($return_type->value);
if ($return_type_lc === 'static' || $return_type_lc === '$this') {
if (!$static_class_type) {
throw new \UnexpectedValueException(
'Cannot handle ' . $return_type->value . ' when $static_class is empty'
);
}
if ($static_class_type && ($return_type_lc === 'static' || $return_type_lc === '$this')) {
if (is_string($static_class_type)) {
$return_type->value = $static_class_type;
} else {
@ -1322,21 +1320,9 @@ class ExpressionAnalyzer
$return_type->extra_types[$extra_static_type->getKey()] = clone $extra_static_type;
}
}
} elseif ($return_type_lc === 'self') {
if (!$self_class) {
throw new \UnexpectedValueException(
'Cannot handle ' . $return_type->value . ' when $self_class is empty'
);
}
} elseif ($self_class && $return_type_lc === 'self') {
$return_type->value = $self_class;
} elseif ($return_type_lc === 'parent') {
if (!$parent_class) {
throw new \UnexpectedValueException(
'Cannot handle ' . $return_type->value . ' when $parent_class is empty'
);
}
} elseif ($parent_class && $return_type_lc === 'parent') {
$return_type->value = $parent_class;
} else {
$return_type->value = $codebase->classlikes->getUnAliasedName($return_type->value);
@ -1355,7 +1341,8 @@ class ExpressionAnalyzer
$self_class,
$static_class_type,
$parent_class,
$evaluate,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
@ -1370,7 +1357,7 @@ class ExpressionAnalyzer
$return_type->fq_classlike_name = $self_class;
}
if ($evaluate && $codebase->classOrInterfaceExists($return_type->fq_classlike_name)) {
if ($evaluate_class_constants && $codebase->classOrInterfaceExists($return_type->fq_classlike_name)) {
if (strtolower($return_type->const_name) === 'class') {
return new Type\Atomic\TLiteralClassString($return_type->fq_classlike_name);
}
@ -1435,7 +1422,7 @@ class ExpressionAnalyzer
$return_type->fq_classlike_name = $self_class;
}
if ($evaluate && $codebase->classOrInterfaceExists($return_type->fq_classlike_name)) {
if ($evaluate_class_constants && $codebase->classOrInterfaceExists($return_type->fq_classlike_name)) {
try {
$class_constant_type = $codebase->classlikes->getConstantForClass(
$return_type->fq_classlike_name,
@ -1479,7 +1466,8 @@ class ExpressionAnalyzer
$self_class,
$static_class_type,
$parent_class,
$evaluate,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
}
@ -1491,7 +1479,8 @@ class ExpressionAnalyzer
$self_class,
$static_class_type,
$parent_class,
$evaluate,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
}
@ -1502,7 +1491,8 @@ class ExpressionAnalyzer
$self_class,
$static_class_type,
$parent_class,
$evaluate,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
}
@ -1517,7 +1507,8 @@ class ExpressionAnalyzer
$self_class,
$static_class_type,
$parent_class,
$evaluate,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
}
@ -1530,67 +1521,105 @@ class ExpressionAnalyzer
$self_class,
$static_class_type,
$parent_class,
$evaluate,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
}
}
if ($return_type instanceof Type\Atomic\TConditional && $evaluate) {
$all_conditional_return_types = [];
if ($return_type instanceof Type\Atomic\TConditional) {
if ($evaluate_conditional_types) {
$all_conditional_return_types = [];
foreach ($return_type->if_type->getAtomicTypes() as $if_atomic_type) {
$candidate = self::fleshOutAtomicType(
$codebase,
$if_atomic_type,
$self_class,
$static_class_type,
$parent_class,
$evaluate,
$final
);
if (is_array($candidate)) {
$all_conditional_return_types = array_merge(
$all_conditional_return_types,
$candidate
foreach ($return_type->if_type->getAtomicTypes() as $if_atomic_type) {
$candidate = self::fleshOutAtomicType(
$codebase,
$if_atomic_type,
$self_class,
$static_class_type,
$parent_class,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
} else {
$all_conditional_return_types[] = $candidate;
if (is_array($candidate)) {
$all_conditional_return_types = array_merge(
$all_conditional_return_types,
$candidate
);
} else {
$all_conditional_return_types[] = $candidate;
}
}
}
foreach ($return_type->else_type->getAtomicTypes() as $else_atomic_type) {
$candidate = self::fleshOutAtomicType(
$codebase,
$else_atomic_type,
$self_class,
$static_class_type,
$parent_class,
$evaluate,
$final
);
if (is_array($candidate)) {
$all_conditional_return_types = array_merge(
$all_conditional_return_types,
$candidate
foreach ($return_type->else_type->getAtomicTypes() as $else_atomic_type) {
$candidate = self::fleshOutAtomicType(
$codebase,
$else_atomic_type,
$self_class,
$static_class_type,
$parent_class,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
} else {
$all_conditional_return_types[] = $candidate;
if (is_array($candidate)) {
$all_conditional_return_types = array_merge(
$all_conditional_return_types,
$candidate
);
} else {
$all_conditional_return_types[] = $candidate;
}
}
foreach ($all_conditional_return_types as $i => $conditional_return_type) {
if ($conditional_return_type instanceof Type\Atomic\TVoid
&& count($all_conditional_return_types) > 1
) {
$all_conditional_return_types[$i] = new Type\Atomic\TNull();
$all_conditional_return_types[$i]->from_docblock = true;
}
}
return $all_conditional_return_types;
}
foreach ($all_conditional_return_types as $i => $conditional_return_type) {
if ($conditional_return_type instanceof Type\Atomic\TVoid
&& count($all_conditional_return_types) > 1
) {
$all_conditional_return_types[$i] = new Type\Atomic\TNull();
$all_conditional_return_types[$i]->from_docblock = true;
}
}
$return_type->conditional_type = self::fleshOutType(
$codebase,
$return_type->conditional_type,
$self_class,
$static_class_type,
$parent_class,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
return $all_conditional_return_types;
$return_type->if_type = self::fleshOutType(
$codebase,
$return_type->if_type,
$self_class,
$static_class_type,
$parent_class,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
$return_type->else_type = self::fleshOutType(
$codebase,
$return_type->else_type,
$self_class,
$static_class_type,
$parent_class,
$evaluate_class_constants,
$evaluate_conditional_types,
$final
);
}
return $return_type;

View File

@ -72,6 +72,13 @@ class TConditional extends \Psalm\Type\Atomic
. ')';
}
public function __clone()
{
$this->conditional_type = clone $this->conditional_type;
$this->if_type = clone $this->if_type;
$this->else_type = clone $this->else_type;
}
/**
* @return string
*/

View File

@ -1251,6 +1251,49 @@ class AnnotationTest extends TestCase
'$float3' => 'float',
]
],
'nestedClassConstantConditionalComparison' => [
'<?php
class A {
const TYPE_STRING = 0;
const TYPE_INT = 1;
/**
* @template T as int
* @param T $i
* @psalm-return (
* T is self::TYPE_STRING
* ? string
* : (T is self::TYPE_INT ? int : bool)
* )
*/
public static function getDifferentType(int $i) {
if ($i === self::TYPE_STRING) {
return "hello";
}
if ($i === self::TYPE_INT) {
return 5;
}
return true;
}
}
$string = A::getDifferentType(0);
$int = A::getDifferentType(1);
$bool = A::getDifferentType(4);
$string2 = (new A)->getDifferentType(0);
$int2 = (new A)->getDifferentType(1);
$bool2 = (new A)->getDifferentType(4);',
[
'$string' => 'string',
'$int' => 'int',
'$bool' => 'bool',
'$string2' => 'string',
'$int2' => 'int',
'$bool2' => 'bool',
]
],
];
}
@ -1518,12 +1561,6 @@ class AnnotationTest extends TestCase
}',
'error_message' => 'PossiblyInvalidMethodCall',
],
'badStaticVar' => [
'<?php
/** @var static */
$a = new stdClass();',
'error_message' => 'InvalidDocblock',
],
'doubleBar' => [
'<?php
/** @param PDO||Closure|numeric $a */