Repository: vimeo/psalm (PHP)

Issue:
Assumed filter_var return type can be incorrect
Psalm gets a bit confused about the return value from `filter_var` when called with `FILTER_VALIDATE_INT`, a numeric value and some options.

https://psalm.dev/r/c1579d2079


Discussion: I found these snippets:
<details>
<summary>https://psalm.dev/r/c1579d2079</summary>

```php
<?php
// validate(50) returns 50
// validate(9000) returns NULL
function validate(mixed $c): ?int {
    // This is_numeric check makes psalm confused about $ret
    if (is_numeric($c))
    {
        $ret = filter_var($c, FILTER_VALIDATE_INT, [
            'options' => [
                'default'   => null,
                'min_range' => 1,
                'max_range' => 100,
            ],
        ]);
        if ($ret !== null) {
            return $ret;
        }
    }
    return null;
}
```

```
Psalm output (using commit 64868c5):

ERROR: TypeDoesNotContainType - 15:13 - Type null for $ret is always !null

ERROR: NoValue - 16:13 - All possible types for this return were invalidated - This may be dead code
```
</details>

This bug is introduced in https://github.com/vimeo/psalm/pull/10498 and the PR author indicates:

>  I only handled the most commonly used basic ones. This is just a starting point and can be easily improved with additional types once errors/issues are reported.

So here we are, reporting issues and gently pinging @kkmuffme to ask if they would have the time to look into it :)

Downgrading to 5.18.0 is the current workaround if one doesn't want to add ignore comments.



Fix (patch):
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php
index e0de55bba04..5c32126de0e 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php
@@ -33,6 +33,7 @@
 use Psalm\Type\Atomic\TNull;
 use Psalm\Type\Atomic\TNumeric;
 use Psalm\Type\Atomic\TNumericString;
+use Psalm\Type\Atomic\TScalar;
 use Psalm\Type\Atomic\TString;
 use Psalm\Type\Atomic\TTrue;
 use Psalm\Type\Union;
@@ -42,6 +43,7 @@
 use function array_keys;
 use function array_merge;
 use function filter_var;
+use function get_class;
 use function implode;
 use function in_array;
 use function preg_match;
@@ -919,7 +921,11 @@ public static function getReturnType(
                         $filter_types[] = new TFloat();
                     }
 
-                    if ($atomic_type instanceof TMixed) {
+                    // only these specific classes, not any class that extends either
+                    // to avoid matching already better handled cases from above, e.g. float is numeric and scalar
+                    if ($atomic_type instanceof TMixed
+                        || get_class($atomic_type) === TNumeric::class
+                        || get_class($atomic_type) === TScalar::class) {
                         $filter_types[] = new TFloat();
                     }
 
@@ -967,7 +973,9 @@ public static function getReturnType(
                     if ($atomic_type instanceof TMixed
                         || $atomic_type instanceof TString
                         || $atomic_type instanceof TInt
-                        || $atomic_type instanceof TFloat) {
+                        || $atomic_type instanceof TFloat
+                        || $atomic_type instanceof TNumeric
+                        || $atomic_type instanceof TScalar) {
                         $filter_types[] = new TBool();
                     }
 
@@ -994,6 +1002,7 @@ public static function getReturnType(
                 } else {
                     $int_type = new TInt();
                 }
+
                 foreach ($input_type->getAtomicTypes() as $atomic_type) {
                     if ($atomic_type instanceof TLiteralInt) {
                         if ($min_range !== null && $min_range > $atomic_type->value) {
@@ -1108,7 +1117,9 @@ public static function getReturnType(
                         $filter_types[] = $int_type;
                     }
 
-                    if ($atomic_type instanceof TMixed) {
+                    if ($atomic_type instanceof TMixed
+                        || get_class($atomic_type) === TNumeric::class
+                        || get_class($atomic_type) === TScalar::class) {
                         $filter_types[] = $int_type;
                     }
 
@@ -1129,9 +1140,7 @@ public static function getReturnType(
                         $filter_types[] = $atomic_type;
                     } elseif ($atomic_type instanceof TString) {
                         $filter_types[] = new TNonFalsyString();
-                    }
-
-                    if ($atomic_type instanceof TMixed) {
+                    } elseif ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) {
                         $filter_types[] = new TNonFalsyString();
                     }
 
@@ -1159,6 +1168,7 @@ public static function getReturnType(
                         || $atomic_type instanceof TInt
                         || $atomic_type instanceof TFloat
                         || $atomic_type instanceof TNumeric
+                        || $atomic_type instanceof TScalar
                         || $atomic_type instanceof TMixed) {
                         $filter_types[] = new TString();
                     }
@@ -1183,11 +1193,10 @@ public static function getReturnType(
                         } else {
                             $filter_types[] = $atomic_type;
                         }
-                    }
-
-                    if ($atomic_type instanceof TMixed
+                    } elseif ($atomic_type instanceof TMixed
                         || $atomic_type instanceof TInt
-                        || $atomic_type instanceof TFloat) {
+                        || $atomic_type instanceof TFloat
+                        || $atomic_type instanceof TScalar) {
                         $filter_types[] = $string_type;
                     }
 
@@ -1230,7 +1239,7 @@ public static function getReturnType(
                         continue;
                     }
 
-                    if ($atomic_type instanceof TMixed) {
+                    if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) {
                         $filter_types[] = new TString();
                     }
 
@@ -1310,7 +1319,7 @@ public static function getReturnType(
                         continue;
                     }
 
-                    if ($atomic_type instanceof TMixed) {
+                    if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) {
                         $filter_types[] = new TString();
                     }
 
@@ -1329,7 +1338,7 @@ public static function getReturnType(
                         continue;
                     }
 
-                    if ($atomic_type instanceof TMixed) {
+                    if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) {
                         $filter_types[] = new TString();
                     }
 
@@ -1386,7 +1395,7 @@ public static function getReturnType(
                         continue;
                     }
 
-                    if ($atomic_type instanceof TMixed) {
+                    if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) {
                         $filter_types[] = new TNumericString();
                         $filter_types[] = Type::getAtomicStringFromLiteral('');
                     }