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:
parent
9055c4a79b
commit
194f02507d
@ -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
|
||||
|
@ -372,6 +372,7 @@ class ReturnTypeAnalyzer
|
||||
$self_fq_class_name,
|
||||
$parent_class,
|
||||
true,
|
||||
true,
|
||||
$function_like_storage instanceof MethodStorage && $function_like_storage->final
|
||||
);
|
||||
|
||||
|
@ -1724,6 +1724,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
$this->getFQCLN(),
|
||||
$this->getParentFQCLN(),
|
||||
true,
|
||||
true,
|
||||
$final
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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 */
|
||||
|
Loading…
Reference in New Issue
Block a user