1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 13:21:22 +01:00

improve support for enum_exists

This commit is contained in:
orklah 2022-01-16 09:47:05 +01:00
parent 070a1cc7a3
commit 841d4f4429
15 changed files with 160 additions and 13 deletions

View File

@ -2519,7 +2519,7 @@ return [
'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 @@ return [
'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'],

View File

@ -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'],

View File

@ -12998,7 +12998,7 @@ return [
'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'],

View File

@ -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)

View File

@ -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.

View File

@ -790,6 +790,12 @@ class AssertionFinder
$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 @@ class AssertionFinder
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';

View File

@ -132,7 +132,9 @@ class NamedFunctionCallHandler
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 @@ class NamedFunctionCallHandler
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);

View File

@ -459,6 +459,7 @@ class Functions
'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',

View File

@ -448,6 +448,38 @@ class ExpressionResolver
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;
}
}

View File

@ -617,7 +617,10 @@ class TypeParser
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])) {

View File

@ -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,

View File

@ -251,9 +251,18 @@ abstract class Atomic implements TypeNode
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();

View File

@ -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 @@ class TClassString extends TString
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 @@ class TClassString extends TString
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

View File

@ -13,7 +13,7 @@ class ReflectionClass implements Reflector {
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) {}

View File

@ -1800,6 +1800,51 @@ class FunctionCallTest extends TestCase
'$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',
],
];
}