mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
improve support for enum_exists
This commit is contained in:
parent
070a1cc7a3
commit
841d4f4429
@ -2519,7 +2519,7 @@ return [
|
|||||||
'enchant_dict_store_replacement' => ['void', 'dictionary'=>'resource', 'misspelled'=>'string', 'correct'=>'string'],
|
'enchant_dict_store_replacement' => ['void', 'dictionary'=>'resource', 'misspelled'=>'string', 'correct'=>'string'],
|
||||||
'enchant_dict_suggest' => ['array', 'dictionary'=>'resource', 'word'=>'string'],
|
'enchant_dict_suggest' => ['array', 'dictionary'=>'resource', 'word'=>'string'],
|
||||||
'end' => ['mixed|false', '&r_array'=>'array|object'],
|
'end' => ['mixed|false', '&r_array'=>'array|object'],
|
||||||
'enum_exists' => ['bool', 'class' => 'class-string', 'autoload=' => 'bool'],
|
'enum_exists' => ['bool', 'class' => 'string', 'autoload=' => 'bool'],
|
||||||
'Error::__clone' => ['void'],
|
'Error::__clone' => ['void'],
|
||||||
'Error::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable|?Error'],
|
'Error::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable|?Error'],
|
||||||
'Error::__toString' => ['string'],
|
'Error::__toString' => ['string'],
|
||||||
@ -7504,7 +7504,7 @@ return [
|
|||||||
'MessageFormatter::parseMessage' => ['array|false', 'locale'=>'string', 'pattern'=>'string', 'source'=>'string'],
|
'MessageFormatter::parseMessage' => ['array|false', 'locale'=>'string', 'pattern'=>'string', 'source'=>'string'],
|
||||||
'MessageFormatter::setPattern' => ['bool', 'pattern'=>'string'],
|
'MessageFormatter::setPattern' => ['bool', 'pattern'=>'string'],
|
||||||
'metaphone' => ['string|false', 'string'=>'string', 'max_phonemes='=>'int'],
|
'metaphone' => ['string|false', 'string'=>'string', 'max_phonemes='=>'int'],
|
||||||
'method_exists' => ['bool', 'object_or_class'=>'object|class-string|interface-string', 'method'=>'string'],
|
'method_exists' => ['bool', 'object_or_class'=>'object|class-string|interface-string|enum-string', 'method'=>'string'],
|
||||||
'mhash' => ['string', 'algo'=>'int', 'data'=>'string', 'key='=>'string'],
|
'mhash' => ['string', 'algo'=>'int', 'data'=>'string', 'key='=>'string'],
|
||||||
'mhash_count' => ['int'],
|
'mhash_count' => ['int'],
|
||||||
'mhash_get_block_size' => ['int|false', 'algo'=>'int'],
|
'mhash_get_block_size' => ['int|false', 'algo'=>'int'],
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
return [
|
return [
|
||||||
'added' => [
|
'added' => [
|
||||||
'array_is_list' => ['bool', 'array' => 'array'],
|
'array_is_list' => ['bool', 'array' => 'array'],
|
||||||
'enum_exists' => ['bool', 'class' => 'class-string', 'autoload=' => 'bool'],
|
'enum_exists' => ['bool', 'class' => 'string', 'autoload=' => 'bool'],
|
||||||
'fsync' => ['bool', 'stream' => 'resource'],
|
'fsync' => ['bool', 'stream' => 'resource'],
|
||||||
'fdatasync' => ['bool', 'stream' => 'resource'],
|
'fdatasync' => ['bool', 'stream' => 'resource'],
|
||||||
'imageavif' => ['bool', 'image'=>'GdImage', 'file='=>'resource|string|null', 'quality='=>'int', 'speed='=>'int'],
|
'imageavif' => ['bool', 'image'=>'GdImage', 'file='=>'resource|string|null', 'quality='=>'int', 'speed='=>'int'],
|
||||||
|
@ -12998,7 +12998,7 @@ return [
|
|||||||
'memory_get_peak_usage' => ['int', 'real_usage='=>'bool'],
|
'memory_get_peak_usage' => ['int', 'real_usage='=>'bool'],
|
||||||
'memory_get_usage' => ['int', 'real_usage='=>'bool'],
|
'memory_get_usage' => ['int', 'real_usage='=>'bool'],
|
||||||
'metaphone' => ['string|false', 'string'=>'string', 'max_phonemes='=>'int'],
|
'metaphone' => ['string|false', 'string'=>'string', 'max_phonemes='=>'int'],
|
||||||
'method_exists' => ['bool', 'object_or_class'=>'object|class-string|interface-string', 'method'=>'string'],
|
'method_exists' => ['bool', 'object_or_class'=>'object|class-string|interface-string|enum-string', 'method'=>'string'],
|
||||||
'mhash' => ['string', 'algo'=>'int', 'data'=>'string', 'key='=>'string'],
|
'mhash' => ['string', 'algo'=>'int', 'data'=>'string', 'key='=>'string'],
|
||||||
'mhash_count' => ['int'],
|
'mhash_count' => ['int'],
|
||||||
'mhash_get_block_size' => ['int|false', 'algo'=>'int'],
|
'mhash_get_block_size' => ['int|false', 'algo'=>'int'],
|
||||||
|
@ -10,6 +10,7 @@ Atomic types are the basic building block of all type information used in Psalm.
|
|||||||
- [string](scalar_types.md)
|
- [string](scalar_types.md)
|
||||||
- [class-string and class-string<Foo>](scalar_types.md#class-string-interface-string)
|
- [class-string and class-string<Foo>](scalar_types.md#class-string-interface-string)
|
||||||
- [trait-string](scalar_types.md#trait-string)
|
- [trait-string](scalar_types.md#trait-string)
|
||||||
|
- [enum-string](scalar_types.md#enum-string)
|
||||||
- [callable-string](scalar_types.md#callable-string)
|
- [callable-string](scalar_types.md#callable-string)
|
||||||
- [numeric-string](scalar_types.md#numeric-string)
|
- [numeric-string](scalar_types.md#numeric-string)
|
||||||
- [literal-string](scalar_types.md#literal-string)
|
- [literal-string](scalar_types.md#literal-string)
|
||||||
|
@ -42,6 +42,10 @@ You can also parameterize `class-string` with an object name e.g. [`class-string
|
|||||||
|
|
||||||
Psalm also supports a `trait-string` annotation denote a trait that exists.
|
Psalm also supports a `trait-string` annotation denote a trait that exists.
|
||||||
|
|
||||||
|
### enum-string
|
||||||
|
|
||||||
|
Psalm also supports a `enum-string` annotation denote an enum that exists.
|
||||||
|
|
||||||
### callable-string
|
### callable-string
|
||||||
|
|
||||||
`callable-string` denotes a string value that has passed an `is_callable` check.
|
`callable-string` denotes a string value that has passed an `is_callable` check.
|
||||||
|
@ -790,6 +790,12 @@ class AssertionFinder
|
|||||||
$if_types[$first_var_name] = [[new IsIdentical(new TTraitString())]];
|
$if_types[$first_var_name] = [[new IsIdentical(new TTraitString())]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} elseif (self::hasEnumExistsCheck($expr)) {
|
||||||
|
if ($first_var_name) {
|
||||||
|
$class_string = new TClassString();
|
||||||
|
$class_string->is_enum = true;
|
||||||
|
$if_types[$first_var_name] = [[new IsType($class_string)]];
|
||||||
|
}
|
||||||
} elseif (self::hasInterfaceExistsCheck($expr)) {
|
} elseif (self::hasInterfaceExistsCheck($expr)) {
|
||||||
if ($first_var_name) {
|
if ($first_var_name) {
|
||||||
$class_string = new TClassString();
|
$class_string = new TClassString();
|
||||||
@ -1994,6 +2000,11 @@ class AssertionFinder
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function hasEnumExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
|
||||||
|
{
|
||||||
|
return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->parts[0]) === 'enum_exists';
|
||||||
|
}
|
||||||
|
|
||||||
protected static function hasInterfaceExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
|
protected static function hasInterfaceExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
|
||||||
{
|
{
|
||||||
return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->parts[0]) === 'interface_exists';
|
return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->parts[0]) === 'interface_exists';
|
||||||
|
@ -132,7 +132,9 @@ class NamedFunctionCallHandler
|
|||||||
if ($function_id === 'interface_exists') {
|
if ($function_id === 'interface_exists') {
|
||||||
if ($first_arg) {
|
if ($first_arg) {
|
||||||
if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) {
|
if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) {
|
||||||
$context->phantom_classes[strtolower($first_arg->value->value)] = true;
|
if (!$codebase->classlikes->interfaceExists($first_arg->value->value)) {
|
||||||
|
$context->phantom_classes[strtolower($first_arg->value->value)] = true;
|
||||||
|
}
|
||||||
} elseif ($first_arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
} elseif ($first_arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
||||||
&& $first_arg->value->class instanceof PhpParser\Node\Name
|
&& $first_arg->value->class instanceof PhpParser\Node\Name
|
||||||
&& $first_arg->value->name instanceof PhpParser\Node\Identifier
|
&& $first_arg->value->name instanceof PhpParser\Node\Identifier
|
||||||
@ -149,6 +151,28 @@ class NamedFunctionCallHandler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($function_id === 'enum_exists') {
|
||||||
|
if ($first_arg) {
|
||||||
|
if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) {
|
||||||
|
if (!$codebase->classlikes->enumExists($first_arg->value->value)) {
|
||||||
|
$context->phantom_classes[strtolower($first_arg->value->value)] = true;
|
||||||
|
}
|
||||||
|
} elseif ($first_arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
||||||
|
&& $first_arg->value->class instanceof PhpParser\Node\Name
|
||||||
|
&& $first_arg->value->name instanceof PhpParser\Node\Identifier
|
||||||
|
&& $first_arg->value->name->name === 'class'
|
||||||
|
) {
|
||||||
|
$resolved_name = (string) $first_arg->value->class->getAttribute('resolvedName');
|
||||||
|
|
||||||
|
if (!$codebase->classlikes->enumExists($resolved_name)) {
|
||||||
|
$context->phantom_classes[strtolower($resolved_name)] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (in_array($function_id, ['is_file', 'file_exists']) && $first_arg) {
|
if (in_array($function_id, ['is_file', 'file_exists']) && $first_arg) {
|
||||||
$var_id = ExpressionIdentifier::getArrayVarId($first_arg->value, null);
|
$var_id = ExpressionIdentifier::getArrayVarId($first_arg->value, null);
|
||||||
|
|
||||||
|
@ -459,6 +459,7 @@ class Functions
|
|||||||
'wincache_ucache_delete', 'wincache_ucache_set', 'wincache_ucache_inc',
|
'wincache_ucache_delete', 'wincache_ucache_set', 'wincache_ucache_inc',
|
||||||
'class_alias',
|
'class_alias',
|
||||||
'class_exists', // impure by virtue of triggering autoloader
|
'class_exists', // impure by virtue of triggering autoloader
|
||||||
|
'enum_exists', // impure by virtue of triggering autoloader
|
||||||
|
|
||||||
// php environment
|
// php environment
|
||||||
'ini_set', 'sleep', 'usleep', 'register_shutdown_function',
|
'ini_set', 'sleep', 'usleep', 'register_shutdown_function',
|
||||||
|
@ -448,6 +448,38 @@ class ExpressionResolver
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($function->name->parts === ['enum_exists']
|
||||||
|
&& isset($function->getArgs()[0])
|
||||||
|
) {
|
||||||
|
$string_value = null;
|
||||||
|
|
||||||
|
if ($function->getArgs()[0]->value instanceof PhpParser\Node\Scalar\String_) {
|
||||||
|
$string_value = $function->getArgs()[0]->value->value;
|
||||||
|
} elseif ($function->getArgs()[0]->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
||||||
|
&& $function->getArgs()[0]->value->class instanceof PhpParser\Node\Name
|
||||||
|
&& $function->getArgs()[0]->value->name instanceof PhpParser\Node\Identifier
|
||||||
|
&& strtolower($function->getArgs()[0]->value->name->name) === 'class'
|
||||||
|
) {
|
||||||
|
$string_value = (string) $function->getArgs()[0]->value->class->getAttribute('resolvedName');
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're using class_exists here because enum_exists doesn't exist on old versions of PHP
|
||||||
|
// Not sure what happens if we try to autoload or reflect on an enum on an old version of PHP though...
|
||||||
|
if ($string_value && class_exists($string_value)) {
|
||||||
|
$reflection_class = new ReflectionClass($string_value);
|
||||||
|
|
||||||
|
if ($reflection_class->getFileName() !== $file_path) {
|
||||||
|
$codebase->scanner->queueClassLikeForScanning(
|
||||||
|
$string_value
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -617,7 +617,10 @@ class TypeParser
|
|||||||
return new TNonEmptyList($generic_params[0]);
|
return new TNonEmptyList($generic_params[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($generic_type_value === 'class-string' || $generic_type_value === 'interface-string') {
|
if ($generic_type_value === 'class-string'
|
||||||
|
|| $generic_type_value === 'interface-string'
|
||||||
|
|| $generic_type_value === 'enum-string'
|
||||||
|
) {
|
||||||
$class_name = (string)$generic_params[0];
|
$class_name = (string)$generic_params[0];
|
||||||
|
|
||||||
if (isset($template_type_map[$class_name])) {
|
if (isset($template_type_map[$class_name])) {
|
||||||
|
@ -47,6 +47,7 @@ class TypeTokenizer
|
|||||||
'numeric-string' => true,
|
'numeric-string' => true,
|
||||||
'class-string' => true,
|
'class-string' => true,
|
||||||
'interface-string' => true,
|
'interface-string' => true,
|
||||||
|
'enum-string' => true,
|
||||||
'trait-string' => true,
|
'trait-string' => true,
|
||||||
'callable-string' => true,
|
'callable-string' => true,
|
||||||
'callable-array' => true,
|
'callable-array' => true,
|
||||||
|
@ -251,9 +251,18 @@ abstract class Atomic implements TypeNode
|
|||||||
return new TObjectWithProperties([], ['__tostring' => 'string']);
|
return new TObjectWithProperties([], ['__tostring' => 'string']);
|
||||||
|
|
||||||
case 'class-string':
|
case 'class-string':
|
||||||
case 'interface-string':
|
|
||||||
return new TClassString();
|
return new TClassString();
|
||||||
|
|
||||||
|
case 'interface-string':
|
||||||
|
$type = new TClassString();
|
||||||
|
$type->is_interface = true;
|
||||||
|
return $type;
|
||||||
|
|
||||||
|
case 'enum-string':
|
||||||
|
$type = new TClassString();
|
||||||
|
$type->is_enum = true;
|
||||||
|
return $type;
|
||||||
|
|
||||||
case 'trait-string':
|
case 'trait-string':
|
||||||
return new TTraitString();
|
return new TTraitString();
|
||||||
|
|
||||||
|
@ -39,6 +39,9 @@ class TClassString extends TString
|
|||||||
/** @var bool */
|
/** @var bool */
|
||||||
public $is_interface = false;
|
public $is_interface = false;
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
public $is_enum = false;
|
||||||
|
|
||||||
public function __construct(string $as = 'object', ?TNamedObject $as_type = null)
|
public function __construct(string $as = 'object', ?TNamedObject $as_type = null)
|
||||||
{
|
{
|
||||||
$this->as = $as;
|
$this->as = $as;
|
||||||
@ -47,8 +50,15 @@ class TClassString extends TString
|
|||||||
|
|
||||||
public function getKey(bool $include_extra = true): string
|
public function getKey(bool $include_extra = true): string
|
||||||
{
|
{
|
||||||
return ($this->is_interface ? 'interface' : 'class')
|
if ($this->is_interface) {
|
||||||
. '-string' . ($this->as === 'object' ? '' : '<' . $this->as_type . '>');
|
$key = 'interface-string';
|
||||||
|
} elseif ($this->is_enum) {
|
||||||
|
$key = 'enum-string';
|
||||||
|
} else {
|
||||||
|
$key = 'class-string';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $key . ($this->as === 'object' ? '' : '<' . $this->as_type . '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __toString(): string
|
public function __toString(): string
|
||||||
@ -58,9 +68,15 @@ class TClassString extends TString
|
|||||||
|
|
||||||
public function getId(bool $nested = false): string
|
public function getId(bool $nested = false): string
|
||||||
{
|
{
|
||||||
return ($this->is_loaded ? 'loaded-' : '')
|
if ($this->is_interface) {
|
||||||
. ($this->is_interface ? 'interface' : 'class')
|
$key = 'interface-string';
|
||||||
. '-string' . ($this->as === 'object' ? '' : '<' . $this->as_type . '>');
|
} elseif ($this->is_enum) {
|
||||||
|
$key = 'enum-string';
|
||||||
|
} else {
|
||||||
|
$key = 'class-string';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($this->is_loaded ? 'loaded-' : '') . $key . ($this->as === 'object' ? '' : '<' . $this->as_type . '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAssertionString(): string
|
public function getAssertionString(): string
|
||||||
|
@ -13,7 +13,7 @@ class ReflectionClass implements Reflector {
|
|||||||
public $name;
|
public $name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param T|class-string<T>|interface-string<T>|trait-string $argument
|
* @param T|class-string<T>|interface-string<T>|trait-string|enum-string<T> $argument
|
||||||
*/
|
*/
|
||||||
public function __construct($argument) {}
|
public function __construct($argument) {}
|
||||||
|
|
||||||
|
@ -1800,6 +1800,51 @@ class FunctionCallTest extends TestCase
|
|||||||
'$a===' => 'float(10.36)',
|
'$a===' => 'float(10.36)',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'allowConstructorAfterEnumExists' => [
|
||||||
|
'code' => '<?php
|
||||||
|
function foo(string $s) : void {
|
||||||
|
if (enum_exists($s)) {
|
||||||
|
new $s();
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
'assertions' => [],
|
||||||
|
'error_levels' => ['MixedMethodCall'],
|
||||||
|
'php_version' => '8.1',
|
||||||
|
],
|
||||||
|
'refineWithEnumExists' => [
|
||||||
|
'code' => '<?php
|
||||||
|
function foo(string $s) : void {
|
||||||
|
if (enum_exists($s)) {
|
||||||
|
new ReflectionClass($s);
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
'assertions' => [],
|
||||||
|
'error_levels' => [],
|
||||||
|
'php_version' => '8.1',
|
||||||
|
],
|
||||||
|
'refineWithClassExistsOrEnumExists' => [
|
||||||
|
'code' => '<?php
|
||||||
|
function foo(string $s) : void {
|
||||||
|
if (trait_exists($s) || enum_exists($s)) {
|
||||||
|
new ReflectionClass($s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bar(string $s) : void {
|
||||||
|
if (enum_exists($s) || trait_exists($s)) {
|
||||||
|
new ReflectionClass($s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function baz(string $s) : void {
|
||||||
|
if (enum_exists($s) || interface_exists($s) || trait_exists($s)) {
|
||||||
|
new ReflectionClass($s);
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
'assertions' => [],
|
||||||
|
'error_levels' => [],
|
||||||
|
'php_version' => '8.1',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user