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:
parent
070a1cc7a3
commit
841d4f4429
@ -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'],
|
||||
|
@ -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'],
|
||||
|
@ -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'],
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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])) {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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
|
||||
|
@ -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) {}
|
||||
|
||||
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user