From 841d4f442988dd5913b5a90012fec8c3854ca4dc Mon Sep 17 00:00:00 2001 From: orklah Date: Sun, 16 Jan 2022 09:47:05 +0100 Subject: [PATCH] improve support for enum_exists --- dictionaries/CallMap.php | 4 +- dictionaries/CallMap_81_delta.php | 2 +- dictionaries/CallMap_historical.php | 2 +- .../type_syntax/atomic_types.md | 1 + .../type_syntax/scalar_types.md | 4 ++ .../Statements/Expression/AssertionFinder.php | 11 +++++ .../Call/NamedFunctionCallHandler.php | 26 ++++++++++- src/Psalm/Internal/Codebase/Functions.php | 1 + .../Reflector/ExpressionResolver.php | 32 +++++++++++++ src/Psalm/Internal/Type/TypeParser.php | 5 ++- src/Psalm/Internal/Type/TypeTokenizer.php | 1 + src/Psalm/Type/Atomic.php | 11 ++++- src/Psalm/Type/Atomic/TClassString.php | 26 ++++++++--- stubs/Reflection.phpstub | 2 +- tests/FunctionCallTest.php | 45 +++++++++++++++++++ 15 files changed, 160 insertions(+), 13 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 02baede993f..da4d8579020 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -2519,7 +2519,7 @@ 'enchant_dict_store_replacement' => ['void', 'dictionary'=>'resource', 'misspelled'=>'string', 'correct'=>'string'], 'enchant_dict_suggest' => ['array', 'dictionary'=>'resource', 'word'=>'string'], '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::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?Throwable|?Error'], 'Error::__toString' => ['string'], @@ -7504,7 +7504,7 @@ 'MessageFormatter::parseMessage' => ['array|false', 'locale'=>'string', 'pattern'=>'string', 'source'=>'string'], 'MessageFormatter::setPattern' => ['bool', 'pattern'=>'string'], '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_count' => ['int'], 'mhash_get_block_size' => ['int|false', 'algo'=>'int'], diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index 81bc19a890f..350368df08a 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -17,7 +17,7 @@ return [ 'added' => [ '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'], 'fdatasync' => ['bool', 'stream' => 'resource'], 'imageavif' => ['bool', 'image'=>'GdImage', 'file='=>'resource|string|null', 'quality='=>'int', 'speed='=>'int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 9b158957567..9610396d54e 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -12998,7 +12998,7 @@ 'memory_get_peak_usage' => ['int', 'real_usage='=>'bool'], 'memory_get_usage' => ['int', 'real_usage='=>'bool'], '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_count' => ['int'], 'mhash_get_block_size' => ['int|false', 'algo'=>'int'], diff --git a/docs/annotating_code/type_syntax/atomic_types.md b/docs/annotating_code/type_syntax/atomic_types.md index 28d31d82918..9d267e3149a 100644 --- a/docs/annotating_code/type_syntax/atomic_types.md +++ b/docs/annotating_code/type_syntax/atomic_types.md @@ -10,6 +10,7 @@ Atomic types are the basic building block of all type information used in Psalm. - [string](scalar_types.md) - [class-string and class-string<Foo>](scalar_types.md#class-string-interface-string) - [trait-string](scalar_types.md#trait-string) +- [enum-string](scalar_types.md#enum-string) - [callable-string](scalar_types.md#callable-string) - [numeric-string](scalar_types.md#numeric-string) - [literal-string](scalar_types.md#literal-string) diff --git a/docs/annotating_code/type_syntax/scalar_types.md b/docs/annotating_code/type_syntax/scalar_types.md index c326001db8c..cb9f9cd7a9b 100644 --- a/docs/annotating_code/type_syntax/scalar_types.md +++ b/docs/annotating_code/type_syntax/scalar_types.md @@ -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. +### enum-string + +Psalm also supports a `enum-string` annotation denote an enum that exists. + ### callable-string `callable-string` denotes a string value that has passed an `is_callable` check. diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 6257fb934da..828cf7f06e8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -790,6 +790,12 @@ public static function processFunctionCall( $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)) { if ($first_var_name) { $class_string = new TClassString(); @@ -1994,6 +2000,11 @@ protected static function hasTraitExistsCheck(PhpParser\Node\Expr\FuncCall $stmt 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 { return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->parts[0]) === 'interface_exists'; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php index 6882a4d9067..06d6f10c17b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php @@ -132,7 +132,9 @@ public static function handle( if ($function_id === 'interface_exists') { if ($first_arg) { 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 && $first_arg->value->class instanceof PhpParser\Node\Name && $first_arg->value->name instanceof PhpParser\Node\Identifier @@ -149,6 +151,28 @@ public static function handle( 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) { $var_id = ExpressionIdentifier::getArrayVarId($first_arg->value, null); diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index a31bebd6954..c29ce9815fd 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -459,6 +459,7 @@ public function isCallMapFunctionPure( 'wincache_ucache_delete', 'wincache_ucache_set', 'wincache_ucache_inc', 'class_alias', 'class_exists', // impure by virtue of triggering autoloader + 'enum_exists', // impure by virtue of triggering autoloader // php environment 'ini_set', 'sleep', 'usleep', 'register_shutdown_function', diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php index 2d80914b502..65a78a746c9 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php @@ -448,6 +448,38 @@ private static function functionEvaluatesToTrue( 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; } } diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 2506fc2dce8..7795373a05b 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -617,7 +617,10 @@ private static function getTypeFromGenericTree( 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]; if (isset($template_type_map[$class_name])) { diff --git a/src/Psalm/Internal/Type/TypeTokenizer.php b/src/Psalm/Internal/Type/TypeTokenizer.php index a3e96874d3c..9caa1b21716 100644 --- a/src/Psalm/Internal/Type/TypeTokenizer.php +++ b/src/Psalm/Internal/Type/TypeTokenizer.php @@ -47,6 +47,7 @@ class TypeTokenizer 'numeric-string' => true, 'class-string' => true, 'interface-string' => true, + 'enum-string' => true, 'trait-string' => true, 'callable-string' => true, 'callable-array' => true, diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index e3d18b20e4a..9754dbcbe16 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -251,9 +251,18 @@ public static function create( return new TObjectWithProperties([], ['__tostring' => 'string']); case 'class-string': - case 'interface-string': 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': return new TTraitString(); diff --git a/src/Psalm/Type/Atomic/TClassString.php b/src/Psalm/Type/Atomic/TClassString.php index f34c5e603a4..f4dd549a74f 100644 --- a/src/Psalm/Type/Atomic/TClassString.php +++ b/src/Psalm/Type/Atomic/TClassString.php @@ -39,6 +39,9 @@ class TClassString extends TString /** @var bool */ public $is_interface = false; + /** @var bool */ + public $is_enum = false; + public function __construct(string $as = 'object', ?TNamedObject $as_type = null) { $this->as = $as; @@ -47,8 +50,15 @@ public function __construct(string $as = 'object', ?TNamedObject $as_type = null public function getKey(bool $include_extra = true): string { - return ($this->is_interface ? 'interface' : 'class') - . '-string' . ($this->as === 'object' ? '' : '<' . $this->as_type . '>'); + if ($this->is_interface) { + $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 @@ -58,9 +68,15 @@ public function __toString(): string public function getId(bool $nested = false): string { - return ($this->is_loaded ? 'loaded-' : '') - . ($this->is_interface ? 'interface' : 'class') - . '-string' . ($this->as === 'object' ? '' : '<' . $this->as_type . '>'); + if ($this->is_interface) { + $key = 'interface-string'; + } 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 diff --git a/stubs/Reflection.phpstub b/stubs/Reflection.phpstub index 15df6c68d3b..b573c8d17df 100644 --- a/stubs/Reflection.phpstub +++ b/stubs/Reflection.phpstub @@ -13,7 +13,7 @@ class ReflectionClass implements Reflector { public $name; /** - * @param T|class-string|interface-string|trait-string $argument + * @param T|class-string|interface-string|trait-string|enum-string $argument */ public function __construct($argument) {} diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 84c268fc1a2..48193fe1d83 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1800,6 +1800,51 @@ function sayHello(string $needle): void { '$a===' => 'float(10.36)', ], ], + 'allowConstructorAfterEnumExists' => [ + 'code' => ' [], + 'error_levels' => ['MixedMethodCall'], + 'php_version' => '8.1', + ], + 'refineWithEnumExists' => [ + 'code' => ' [], + 'error_levels' => [], + 'php_version' => '8.1', + ], + 'refineWithClassExistsOrEnumExists' => [ + 'code' => ' [], + 'error_levels' => [], + 'php_version' => '8.1', + ], ]; }