Skip to content

Commit

Permalink
improve support for enum_exists
Browse files Browse the repository at this point in the history
  • Loading branch information
orklah committed Jan 22, 2022
1 parent 070a1cc commit 536d872
Show file tree
Hide file tree
Showing 14 changed files with 153 additions and 7 deletions.
4 changes: 2 additions & 2 deletions dictionaries/CallMap.php
Expand Up @@ -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'],
Expand Down Expand Up @@ -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'],
Expand Down
2 changes: 1 addition & 1 deletion dictionaries/CallMap_81_delta.php
Expand Up @@ -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'],
Expand Down
2 changes: 1 addition & 1 deletion dictionaries/CallMap_historical.php
Expand Up @@ -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'],
Expand Down
1 change: 1 addition & 0 deletions docs/annotating_code/type_syntax/atomic_types.md
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions docs/annotating_code/type_syntax/scalar_types.md
Expand Up @@ -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.
Expand Down
Expand Up @@ -790,6 +790,14 @@ public static function processFunctionCall(
$if_types[$first_var_name] = [[new IsIdentical(new TTraitString())]];
}
}
} elseif ($class_exists_check_type = self::hasEnumExistsCheck($expr)) {
if ($first_var_name) {
if ($class_exists_check_type === 2) {
$if_types[$first_var_name] = [['enum-string']];
} else {
$if_types[$first_var_name] = [['=enum-string']];
}
}
} elseif (self::hasInterfaceExistsCheck($expr)) {
if ($first_var_name) {
$class_string = new TClassString();
Expand Down Expand Up @@ -1994,6 +2002,32 @@ protected static function hasTraitExistsCheck(PhpParser\Node\Expr\FuncCall $stmt
return 0;
}

/**
* @return 0|1|2
*/
protected static function hasEnumExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): int
{
if ($stmt->name instanceof PhpParser\Node\Name
&& strtolower($stmt->name->parts[0]) === 'enum_exists'
) {
if (!isset($stmt->getArgs()[1])) {
return 2;
}

$second_arg = $stmt->getArgs()[1]->value;

if ($second_arg instanceof PhpParser\Node\Expr\ConstFetch
&& strtolower($second_arg->name->parts[0]) === 'true'
) {
return 2;
}

return 1;
}

return 0;
}

protected static function hasInterfaceExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
{
return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->parts[0]) === 'interface_exists';
Expand Down
Expand Up @@ -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
Expand All @@ -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);

Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Internal/Codebase/Functions.php
Expand Up @@ -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',
Expand Down
32 changes: 32 additions & 0 deletions src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php
Expand Up @@ -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;
}
}
5 changes: 4 additions & 1 deletion src/Psalm/Internal/Type/TypeParser.php
Expand Up @@ -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])) {
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Internal/Type/TypeTokenizer.php
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Type/Atomic.php
Expand Up @@ -252,6 +252,7 @@ public static function create(

case 'class-string':
case 'interface-string':
case 'enum-string':
return new TClassString();

case 'trait-string':
Expand Down
2 changes: 1 addition & 1 deletion stubs/Reflection.phpstub
Expand Up @@ -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) {}

Expand Down
45 changes: 45 additions & 0 deletions tests/FunctionCallTest.php
Expand Up @@ -1800,6 +1800,51 @@ function sayHello(string $needle): void {
'$a===' => 'float(10.36)',
],
],
'allowConstructorAfterEnumExists' => [
'<?php
function foo(string $s) : void {
if (enum_exists($s)) {
new $s();
}
}',
'assertions' => [],
'error_levels' => ['MixedMethodCall'],
'php_version' => '8.1',
],
'refineWithEnumExists' => [
'<?php
function foo(string $s) : void {
if (enum_exists($s)) {
new ReflectionClass($s);
}
}',
'assertions' => [],
'error_levels' => [],
'php_version' => '8.1',
],
'refineWithClassExistsOrEnumExists' => [
'<?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',
],
];
}

Expand Down

0 comments on commit 536d872

Please sign in to comment.