diff --git a/config.xsd b/config.xsd
index c97e3198ad9..c5dc07ce0de 100644
--- a/config.xsd
+++ b/config.xsd
@@ -420,6 +420,7 @@
+
diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php
index 853b78ed8a0..cec0545f126 100644
--- a/dictionaries/CallMap.php
+++ b/dictionaries/CallMap.php
@@ -2847,11 +2847,11 @@
'FilesystemIterator::setInfoClass' => ['void', 'class='=>'class-string'],
'FilesystemIterator::valid' => ['bool'],
'filetype' => ['string|false', 'filename'=>'string'],
-'filter_has_var' => ['bool', 'input_type'=>'int', 'var_name'=>'string'],
+'filter_has_var' => ['bool', 'input_type'=>'0|1|2|4|5', 'var_name'=>'string'],
'filter_id' => ['int|false', 'name'=>'string'],
-'filter_input' => ['mixed|false', 'type'=>'int', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
-'filter_input_array' => ['array|false|null', 'type'=>'int', 'options='=>'int|array', 'add_empty='=>'bool'],
-'filter_list' => ['array'],
+'filter_input' => ['mixed|false|null', 'type'=>'0|1|2|4|5', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
+'filter_input_array' => ['array|false|null', 'type'=>'0|1|2|4|5', 'options='=>'int|array', 'add_empty='=>'bool'],
+'filter_list' => ['non-empty-list'],
'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'array|int'],
'filter_var_array' => ['array|false|null', 'array'=>'array', 'options='=>'array|int', 'add_empty='=>'bool'],
'FilterIterator::__construct' => ['void', 'iterator'=>'Iterator'],
@@ -3315,7 +3315,7 @@
'getopt' => ['array>|false', 'short_options'=>'string', 'long_options='=>'array', '&w_rest_index='=>'int'],
'getprotobyname' => ['int|false', 'protocol'=>'string'],
'getprotobynumber' => ['string', 'protocol'=>'int'],
-'getrandmax' => ['int'],
+'getrandmax' => ['int<1, max>'],
'getrusage' => ['array', 'mode='=>'int'],
'getservbyname' => ['int|false', 'service'=>'string', 'protocol'=>'string'],
'getservbyport' => ['string|false', 'port'=>'int', 'protocol'=>'string'],
@@ -6625,7 +6625,7 @@
'mb_ereg_search_setpos' => ['bool', 'offset'=>'int'],
'mb_eregi' => ['bool', 'pattern'=>'string', 'string'=>'string', '&w_matches='=>'array|null'],
'mb_eregi_replace' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'options='=>'string|null'],
-'mb_get_info' => ['array|string|int|false', 'type='=>'string'],
+'mb_get_info' => ['array|string|int|false|null', 'type='=>'string'],
'mb_http_input' => ['array|string|false', 'type='=>'string|null'],
'mb_http_output' => ['string|bool', 'encoding='=>'string|null'],
'mb_internal_encoding' => ['string|bool', 'encoding='=>'string|null'],
@@ -7646,7 +7646,7 @@
'msql_query' => ['resource', 'query'=>'string', 'link_identifier='=>'?resource'],
'msql_result' => ['string', 'result'=>'resource', 'row'=>'int', 'field='=>'mixed'],
'msql_select_db' => ['bool', 'database_name'=>'string', 'link_identifier='=>'?resource'],
-'mt_getrandmax' => ['int'],
+'mt_getrandmax' => ['int<1, max>'],
'mt_rand' => ['int', 'min'=>'int', 'max'=>'int'],
'mt_rand\'1' => ['int'],
'mt_srand' => ['void', 'seed='=>'?int', 'mode='=>'int'],
@@ -11141,8 +11141,8 @@
'SessionIdInterface::create_sid' => ['string'],
'SessionUpdateTimestampHandler::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'],
'SessionUpdateTimestampHandler::validateId' => ['char', 'id'=>'string'],
-'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'key'=>'string', 'value'=>'string'],
-'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'key'=>'string'],
+'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'],
+'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'id'=>'string'],
'set_error_handler' => ['null|callable(int,string,string=,int=,array=):bool', 'callback'=>'null|callable(int,string,string=,int=,array=):bool', 'error_levels='=>'int'],
'set_exception_handler' => ['null|callable(Throwable):void', 'callback'=>'null|callable(Throwable):void'],
'set_file_buffer' => ['int', 'stream'=>'resource', 'size'=>'int'],
@@ -14165,7 +14165,7 @@
'unlink' => ['bool', 'filename'=>'string', 'context='=>'resource'],
'unpack' => ['array|false', 'format'=>'string', 'string'=>'string', 'offset='=>'int'],
'unregister_tick_function' => ['void', 'callback'=>'callable'],
-'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:string[]|bool}'],
+'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:class-string[]|bool}'],
'unset' => ['void', 'var='=>'mixed', '...args='=>'mixed'],
'untaint' => ['bool', '&rw_string'=>'string', '&...rw_strings='=>'string'],
'uopz_allow_exit' => ['void', 'allow'=>'bool'],
diff --git a/dictionaries/CallMap_82_delta.php b/dictionaries/CallMap_82_delta.php
index 47ded4fbb19..3064f54ff52 100644
--- a/dictionaries/CallMap_82_delta.php
+++ b/dictionaries/CallMap_82_delta.php
@@ -53,6 +53,10 @@
'old' => ['non-empty-list', 'string'=>'string', 'length='=>'positive-int'],
'new' => ['list', 'string'=>'string', 'length='=>'positive-int'],
],
+ 'mb_get_info' => [
+ 'old' => ['array|string|int|false', 'type='=>'string'],
+ 'new' => ['array|string|int|false|null', 'type='=>'string'],
+ ],
],
'removed' => [
diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php
index 60e798e09cb..61e3db09d55 100644
--- a/dictionaries/CallMap_historical.php
+++ b/dictionaries/CallMap_historical.php
@@ -6583,8 +6583,8 @@
'SessionIdInterface::create_sid' => ['string'],
'SessionUpdateTimestampHandler::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'],
'SessionUpdateTimestampHandler::validateId' => ['char', 'id'=>'string'],
- 'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'key'=>'string', 'value'=>'string'],
- 'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'key'=>'string'],
+ 'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'],
+ 'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'id'=>'string'],
'SimpleXMLElement::__construct' => ['void', 'data'=>'string', 'options='=>'int', 'dataIsURL='=>'bool', 'namespaceOrPrefix='=>'string', 'isPrefix='=>'bool'],
'SimpleXMLElement::__get' => ['SimpleXMLElement', 'name'=>'string'],
'SimpleXMLElement::__toString' => ['string'],
@@ -10434,11 +10434,11 @@
'filepro_rowcount' => ['int'],
'filesize' => ['int|false', 'filename'=>'string'],
'filetype' => ['string|false', 'filename'=>'string'],
- 'filter_has_var' => ['bool', 'input_type'=>'int', 'var_name'=>'string'],
+ 'filter_has_var' => ['bool', 'input_type'=>'0|1|2|4|5', 'var_name'=>'string'],
'filter_id' => ['int|false', 'name'=>'string'],
- 'filter_input' => ['mixed|false', 'type'=>'int', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
- 'filter_input_array' => ['array|false|null', 'type'=>'int', 'options='=>'int|array', 'add_empty='=>'bool'],
- 'filter_list' => ['array'],
+ 'filter_input' => ['mixed|false|null', 'type'=>'0|1|2|4|5', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
+ 'filter_input_array' => ['array|false|null', 'type'=>'0|1|2|4|5', 'options='=>'int|array', 'add_empty='=>'bool'],
+ 'filter_list' => ['non-empty-list'],
'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'array|int'],
'filter_var_array' => ['array|false|null', 'array'=>'array', 'options='=>'array|int', 'add_empty='=>'bool'],
'finfo::__construct' => ['void', 'flags='=>'int', 'magic_database='=>'string'],
@@ -10679,7 +10679,7 @@
'getopt' => ['array>|false', 'short_options'=>'string', 'long_options='=>'array'],
'getprotobyname' => ['int|false', 'protocol'=>'string'],
'getprotobynumber' => ['string', 'protocol'=>'int'],
- 'getrandmax' => ['int'],
+ 'getrandmax' => ['int<1, max>'],
'getrusage' => ['array', 'mode='=>'int'],
'getservbyname' => ['int|false', 'service'=>'string', 'protocol'=>'string'],
'getservbyport' => ['string|false', 'port'=>'int', 'protocol'=>'string'],
@@ -12488,7 +12488,7 @@
'msql_query' => ['resource', 'query'=>'string', 'link_identifier='=>'?resource'],
'msql_result' => ['string', 'result'=>'resource', 'row'=>'int', 'field='=>'mixed'],
'msql_select_db' => ['bool', 'database_name'=>'string', 'link_identifier='=>'?resource'],
- 'mt_getrandmax' => ['int'],
+ 'mt_getrandmax' => ['int<1, max>'],
'mt_rand' => ['int', 'min'=>'int', 'max'=>'int'],
'mt_rand\'1' => ['int'],
'mt_srand' => ['void', 'seed='=>'int', 'mode='=>'int'],
@@ -15197,7 +15197,7 @@
'unlink' => ['bool', 'filename'=>'string', 'context='=>'resource'],
'unpack' => ['array', 'format'=>'string', 'string'=>'string'],
'unregister_tick_function' => ['void', 'callback'=>'callable'],
- 'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:string[]|bool}'],
+ 'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:class-string[]|bool}'],
'unset' => ['void', 'var='=>'mixed', '...args='=>'mixed'],
'untaint' => ['bool', '&rw_string'=>'string', '&...rw_strings='=>'string'],
'uopz_allow_exit' => ['void', 'allow'=>'bool'],
diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md
index 364541ee439..aa94cf66fbc 100644
--- a/docs/running_psalm/issues.md
+++ b/docs/running_psalm/issues.md
@@ -221,6 +221,7 @@
- [RedundantCastGivenDocblockType](issues/RedundantCastGivenDocblockType.md)
- [RedundantCondition](issues/RedundantCondition.md)
- [RedundantConditionGivenDocblockType](issues/RedundantConditionGivenDocblockType.md)
+ - [RedundantFlag](issues/RedundantFlag.md)
- [RedundantFunctionCall](issues/RedundantFunctionCall.md)
- [RedundantFunctionCallGivenDocblockType](issues/RedundantFunctionCallGivenDocblockType.md)
- [RedundantIdentityWithTrue](issues/RedundantIdentityWithTrue.md)
diff --git a/docs/running_psalm/issues/RedundantFlag.md b/docs/running_psalm/issues/RedundantFlag.md
new file mode 100644
index 00000000000..2ec918aefc5
--- /dev/null
+++ b/docs/running_psalm/issues/RedundantFlag.md
@@ -0,0 +1,8 @@
+# RedundantFlag
+
+Emitted when a flag is redundant. e.g. FILTER_NULL_ON_FAILURE won't do anything when the default option is specified
+
+```php
+ array('default' => 'world.com'), 'flags' => FILTER_NULL_ON_FAILURE));
+```
diff --git a/src/Psalm/Aliases.php b/src/Psalm/Aliases.php
index daa39b90a30..00056ed9c20 100644
--- a/src/Psalm/Aliases.php
+++ b/src/Psalm/Aliases.php
@@ -4,8 +4,12 @@
namespace Psalm;
+use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait;
+
final class Aliases
{
+ use UnserializeMemoryUsageSuppressionTrait;
+
/**
* @var array
*/
diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php
index 9c35acfb340..a51d33b9eff 100644
--- a/src/Psalm/CodeLocation.php
+++ b/src/Psalm/CodeLocation.php
@@ -90,6 +90,32 @@ class CodeLocation
public const CATCH_VAR = 6;
public const FUNCTION_PHPDOC_METHOD = 7;
+ private const PROPERTY_KEYS_FOR_UNSERIALIZE = [
+ 'file_path' => 'file_path',
+ 'file_name' => 'file_name',
+ 'raw_line_number' => 'raw_line_number',
+ "\0" . self::class . "\0" . 'end_line_number' => 'end_line_number',
+ 'raw_file_start' => 'raw_file_start',
+ 'raw_file_end' => 'raw_file_end',
+ "\0*\0" . 'file_start' => 'file_start',
+ "\0*\0" . 'file_end' => 'file_end',
+ "\0*\0" . 'single_line' => 'single_line',
+ "\0*\0" . 'preview_start' => 'preview_start',
+ "\0" . self::class . "\0" . 'preview_end' => 'preview_end',
+ "\0" . self::class . "\0" . 'selection_start' => 'selection_start',
+ "\0" . self::class . "\0" . 'selection_end' => 'selection_end',
+ "\0" . self::class . "\0" . 'column_from' => 'column_from',
+ "\0" . self::class . "\0" . 'column_to' => 'column_to',
+ "\0" . self::class . "\0" . 'snippet' => 'snippet',
+ "\0" . self::class . "\0" . 'text' => 'text',
+ 'docblock_start' => 'docblock_start',
+ "\0" . self::class . "\0" . 'docblock_start_line_number' => 'docblock_start_line_number',
+ "\0*\0" . 'docblock_line_number' => 'docblock_line_number',
+ "\0" . self::class . "\0" . 'regex_type' => 'regex_type',
+ "\0" . self::class . "\0" . 'have_recalculated' => 'have_recalculated',
+ 'previous_location' => 'previous_location',
+ ];
+
public function __construct(
FileSource $file_source,
PhpParser\Node $stmt,
@@ -126,6 +152,19 @@ public function __construct(
$this->docblock_line_number = $comment_line;
}
+ /**
+ * Suppresses memory usage when unserializing objects.
+ *
+ * @see \Psalm\Storage\UnserializeMemoryUsageSuppressionTrait
+ */
+ public function __unserialize(array $properties): void
+ {
+ foreach (self::PROPERTY_KEYS_FOR_UNSERIALIZE as $key => $property_name) {
+ /** @psalm-suppress PossiblyUndefinedStringArrayOffset */
+ $this->$property_name = $properties[$key];
+ }
+ }
+
/**
* @psalm-suppress PossiblyUnusedMethod Part of public API
* @return static
diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php
index e3f48f025ed..9385394d25a 100644
--- a/src/Psalm/Codebase.php
+++ b/src/Psalm/Codebase.php
@@ -1377,7 +1377,7 @@ public function getSignatureInformation(
if (InternalCallMapHandler::inCallMap($function_symbol)) {
$callables = InternalCallMapHandler::getCallablesFromCallMap($function_symbol);
- if (!$callables || !$callables[0]->params) {
+ if (!$callables || !isset($callables[0]->params)) {
return null;
}
@@ -1527,7 +1527,7 @@ public function getBeginedLiteralPart(string $file_path, Position $position): st
$offset = $position->toOffset($file_contents);
preg_match('/\$?\w+$/', substr($file_contents, 0, $offset), $matches);
-
+
return $matches[0] ?? '';
}
@@ -1653,7 +1653,7 @@ public function getCompletionItemsForClassishThing(
str_replace('$', '', $property_name),
);
}
-
+
foreach ($class_storage->pseudo_property_set_types as $property_name => $type) {
$pseudo_property_types[$property_name] = new CompletionItem(
str_replace('$', '', $property_name),
@@ -1665,7 +1665,7 @@ public function getCompletionItemsForClassishThing(
str_replace('$', '', $property_name),
);
}
-
+
$completion_items = [...$completion_items, ...array_values($pseudo_property_types)];
}
diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php
index c248e3e0574..af4810c755c 100644
--- a/src/Psalm/Config.php
+++ b/src/Psalm/Config.php
@@ -636,7 +636,7 @@ public static function loadFromXMLFile(string $file_path, string $current_dir):
{
$file_contents = file_get_contents($file_path);
- $base_dir = dirname($file_path) . DIRECTORY_SEPARATOR;
+ $base_dir = dirname($file_path);
if ($file_contents === false) {
throw new InvalidArgumentException('Cannot open ' . $file_path);
@@ -1147,7 +1147,17 @@ private static function fromXmlAndPaths(
// any paths passed via CLI should be added to the projectFiles
// as they're getting analyzed like if they are part of the project
// ProjectAnalyzer::getInstance()->check_paths_files is not populated at this point in time
- $paths_to_check = CliUtils::getPathsToCheck(null);
+
+ $paths_to_check = null;
+
+ global $argv;
+
+ // Hack for Symfonys own argv resolution.
+ // @see https://github.com/vimeo/psalm/issues/10465
+ if (!isset($argv[0]) || basename($argv[0]) !== 'psalm-plugin') {
+ $paths_to_check = CliUtils::getPathsToCheck(null);
+ }
+
if ($paths_to_check !== null) {
$paths_to_add_to_project_files = array();
foreach ($paths_to_check as $path) {
@@ -1307,7 +1317,7 @@ private static function fromXmlAndPaths(
$path = Path::isAbsolute($plugin_file_name)
? $plugin_file_name
- : $config->base_dir . $plugin_file_name;
+ : $config->base_dir . DIRECTORY_SEPARATOR . $plugin_file_name;
$config->addPluginPath($path);
}
diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php
index 57ba7d23cd5..ae55904eab8 100644
--- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php
@@ -350,7 +350,7 @@ public function analyze(
null,
true,
),
- $this->getSuppressedIssues(),
+ $storage->getSuppressedIssuesForTemplateExtendParams() + $this->getSuppressedIssues(),
);
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php
index b3f7a03e5ed..411203ba72f 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php
@@ -257,6 +257,19 @@ public static function analyze(
$has_numeric_and_non_empty = $has_numeric_type && $has_non_empty;
+ $non_falsy_string = $numeric_type->getBuilder()->addType(new TNonFalsyString())->freeze();
+ $left_non_falsy = UnionTypeComparator::isContainedBy(
+ $codebase,
+ $left_type,
+ $non_falsy_string,
+ );
+
+ $right_non_falsy = UnionTypeComparator::isContainedBy(
+ $codebase,
+ $right_type,
+ $non_falsy_string,
+ );
+
$all_literals = $left_type->allLiterals() && $right_type->allLiterals();
if ($has_non_empty) {
@@ -264,9 +277,10 @@ public static function analyze(
$result_type = new Union([new TNonEmptyNonspecificLiteralString]);
} elseif ($all_lowercase) {
$result_type = Type::getNonEmptyLowercaseString();
+ } elseif ($all_non_empty || $has_numeric_and_non_empty || $left_non_falsy || $right_non_falsy) {
+ $result_type = Type::getNonFalsyString();
} else {
- $result_type = $all_non_empty || $has_numeric_and_non_empty ?
- Type::getNonFalsyString() : Type::getNonEmptyString();
+ $result_type = Type::getNonEmptyString();
}
} else {
if ($all_literals) {
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php
index d265e22d0bb..970f3cad650 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php
@@ -4,11 +4,13 @@
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
+use InvalidArgumentException;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\Internal\Analyzer\AttributesAnalyzer;
+use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
@@ -45,6 +47,7 @@
use Psalm\Type\Atomic\TClosure;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TLiteralString;
+use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNonEmptyArray;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
@@ -1252,6 +1255,42 @@ private static function evaluateArbitraryParam(
return null;
}
+ private static function handleByRefReadonlyArg(
+ StatementsAnalyzer $statements_analyzer,
+ Context $context,
+ PhpParser\Node\Expr\PropertyFetch $stmt,
+ string $fq_class_name,
+ string $prop_name
+ ): void {
+ $property_id = $fq_class_name . '::$' . $prop_name;
+
+ $codebase = $statements_analyzer->getCodebase();
+ $declaring_property_class = (string) $codebase->properties->getDeclaringClassForProperty(
+ $property_id,
+ true,
+ $statements_analyzer,
+ );
+
+ try {
+ $declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
+ } catch (InvalidArgumentException $_) {
+ return;
+ }
+
+ if (isset($declaring_class_storage->properties[$prop_name])) {
+ $property_storage = $declaring_class_storage->properties[$prop_name];
+
+ InstancePropertyAssignmentAnalyzer::trackPropertyImpurity(
+ $statements_analyzer,
+ $stmt,
+ $property_id,
+ $property_storage,
+ $declaring_class_storage,
+ $context,
+ );
+ }
+ }
+
/**
* @return false|null
*/
@@ -1273,6 +1312,46 @@ private static function handleByRefFunctionArg(
'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift', 'extract',
];
+ if ($arg->value instanceof PhpParser\Node\Expr\PropertyFetch
+ && $arg->value->name instanceof PhpParser\Node\Identifier) {
+ $prop_name = $arg->value->name->name;
+ if (!empty($statements_analyzer->getFQCLN())) {
+ $fq_class_name = $statements_analyzer->getFQCLN();
+
+ self::handleByRefReadonlyArg(
+ $statements_analyzer,
+ $context,
+ $arg->value,
+ $fq_class_name,
+ $prop_name,
+ );
+ } else {
+ // @todo atm only works for simple fetch, $a->foo, not $a->foo->bar
+ // I guess there's a function to do this, but I couldn't locate it
+ $var_id = ExpressionIdentifier::getVarId(
+ $arg->value->var,
+ $statements_analyzer->getFQCLN(),
+ $statements_analyzer,
+ );
+
+ if ($var_id && isset($context->vars_in_scope[$var_id])) {
+ foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TNamedObject) {
+ $fq_class_name = $atomic_type->value;
+
+ self::handleByRefReadonlyArg(
+ $statements_analyzer,
+ $context,
+ $arg->value,
+ $fq_class_name,
+ $prop_name,
+ );
+ }
+ }
+ }
+ }
+ }
+
if (($var_id && isset($context->vars_in_scope[$var_id]))
|| ($method_id
&& in_array(
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php
index ff627e40f54..0fe60d359f7 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php
@@ -288,7 +288,8 @@ private static function isSupported(FunctionLikeParameter $container_param): boo
}
foreach ($container_param->type->getAtomicTypes() as $a) {
- if (($a instanceof TClosure || $a instanceof TCallable) && !$a->params) {
+ // must check null explicitly, since no params (empty array) would not be handled correctly otherwise
+ if (($a instanceof TClosure || $a instanceof TCallable) && $a->params === null) {
return false;
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php
index c5409880ba3..fc5049f6747 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php
@@ -37,6 +37,8 @@
use Psalm\Type\Atomic\TDependentGetType;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TInt;
+use Psalm\Type\Atomic\TKeyedArray;
+use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLowercaseString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
@@ -49,12 +51,18 @@
use Psalm\Type\Union;
use function array_map;
+use function count;
use function extension_loaded;
use function in_array;
+use function is_numeric;
use function is_string;
+use function preg_match;
use function strpos;
use function strtolower;
+use const EXTR_OVERWRITE;
+use const EXTR_SKIP;
+
/**
* @internal
*/
@@ -263,13 +271,98 @@ public static function handle(
}
if ($function_id === 'extract') {
+ $flag_value = false;
+ if (!isset($stmt->args[1])) {
+ $flag_value = EXTR_OVERWRITE;
+ } elseif (isset($stmt->args[1]->value)
+ && $stmt->args[1]->value instanceof PhpParser\Node\Expr
+ && ($flags_type = $statements_analyzer->node_data->getType($stmt->args[1]->value))
+ && $flags_type->hasLiteralInt() && count($flags_type->getAtomicTypes()) === 1) {
+ $flag_type_value = $flags_type->getSingleIntLiteral()->value;
+ if ($flag_type_value === EXTR_SKIP) {
+ $flag_value = EXTR_SKIP;
+ } elseif ($flag_type_value === EXTR_OVERWRITE) {
+ $flag_value = EXTR_OVERWRITE;
+ }
+ // @todo add support for other flags
+ }
+
+ $is_unsealed = true;
+ $validated_var_ids = [];
+ if ($flag_value !== false && isset($stmt->args[0]->value)
+ && $stmt->args[0]->value instanceof PhpParser\Node\Expr
+ && ($array_type_union = $statements_analyzer->node_data->getType($stmt->args[0]->value))
+ && $array_type_union->isSingle()
+ ) {
+ foreach ($array_type_union->getAtomicTypes() as $array_type) {
+ if ($array_type instanceof TList) {
+ $array_type = $array_type->getKeyedArray();
+ }
+
+ if ($array_type instanceof TKeyedArray) {
+ foreach ($array_type->properties as $key => $type) {
+ // variables must start with letters or underscore
+ if ($key === '' || is_numeric($key) || preg_match('/^[A-Za-z_]/', $key) !== 1) {
+ continue;
+ }
+
+ $var_id = '$' . $key;
+ $validated_var_ids[] = $var_id;
+
+ if (isset($context->vars_in_scope[$var_id]) && $flag_value === EXTR_SKIP) {
+ continue;
+ }
+
+ if (!isset($context->vars_in_scope[$var_id]) && $type->possibly_undefined === true) {
+ $context->possibly_assigned_var_ids[$var_id] = true;
+ } elseif (isset($context->vars_in_scope[$var_id])
+ && $type->possibly_undefined === true
+ && $flag_value === EXTR_OVERWRITE) {
+ $type = Type::combineUnionTypes(
+ $context->vars_in_scope[$var_id],
+ $type,
+ $codebase,
+ false,
+ true,
+ 500,
+ false,
+ );
+ }
+
+ $context->vars_in_scope[$var_id] = $type;
+ $context->assigned_var_ids[$var_id] = (int) $stmt->getAttribute('startFilePos');
+ }
+
+ if (!isset($array_type->fallback_params)) {
+ $is_unsealed = false;
+ }
+ }
+ }
+ }
+
+ if ($flag_value === EXTR_OVERWRITE && $is_unsealed === false) {
+ return;
+ }
+
+ if ($flag_value === EXTR_SKIP && $is_unsealed === false) {
+ return;
+ }
+
$context->check_variables = false;
+ if ($flag_value === EXTR_SKIP) {
+ return;
+ }
+
foreach ($context->vars_in_scope as $var_id => $_) {
if ($var_id === '$this' || strpos($var_id, '[') || strpos($var_id, '>')) {
continue;
}
+ if (in_array($var_id, $validated_var_ids, true)) {
+ continue;
+ }
+
$mixed_type = new Union([new TMixed()], [
'parent_nodes' => $context->vars_in_scope[$var_id]->parent_nodes,
]);
diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php
index 2cb3ffefd88..fdc13f373f2 100644
--- a/src/Psalm/Internal/Codebase/Functions.php
+++ b/src/Psalm/Internal/Codebase/Functions.php
@@ -455,7 +455,7 @@ public function isCallMapFunctionPure(
null,
);
- if (!$function_callable->params
+ if (!isset($function_callable->params)
|| ($args !== null && count($args) === 0)
|| ($function_callable->return_type && $function_callable->return_type->isVoid())
) {
diff --git a/src/Psalm/Internal/MethodIdentifier.php b/src/Psalm/Internal/MethodIdentifier.php
index c41a4b8ba9e..c03d2959703 100644
--- a/src/Psalm/Internal/MethodIdentifier.php
+++ b/src/Psalm/Internal/MethodIdentifier.php
@@ -6,6 +6,7 @@
use InvalidArgumentException;
use Psalm\Storage\ImmutableNonCloneableTrait;
+use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait;
use function explode;
use function is_string;
@@ -20,6 +21,7 @@
final class MethodIdentifier
{
use ImmutableNonCloneableTrait;
+ use UnserializeMemoryUsageSuppressionTrait;
public string $fq_class_name;
/** @var lowercase-string */
diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
index de3a11462b6..9902454f8f3 100644
--- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
@@ -27,6 +27,7 @@
use Psalm\Internal\Provider\ReturnTypeProvider\BasenameReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\DateReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\DirnameReturnTypeProvider;
+use Psalm\Internal\Provider\ReturnTypeProvider\FilterInputReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\FilterVarReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\FirstArgStringReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\GetClassMethodsReturnTypeProvider;
@@ -87,6 +88,7 @@ public function __construct()
$this->registerClass(ArrayReverseReturnTypeProvider::class);
$this->registerClass(ArrayFillReturnTypeProvider::class);
$this->registerClass(ArrayFillKeysReturnTypeProvider::class);
+ $this->registerClass(FilterInputReturnTypeProvider::class);
$this->registerClass(FilterVarReturnTypeProvider::class);
$this->registerClass(IteratorToArrayReturnTypeProvider::class);
$this->registerClass(ParseUrlReturnTypeProvider::class);
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php
index 8e11092326e..487f1dcffb4 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php
@@ -44,7 +44,48 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
);
if ($evaled_path === null) {
- return Type::getString();
+ $union = $statements_source->getNodeTypeProvider()->getType($call_args[0]->value);
+ $generic = false;
+ $non_empty = false;
+ if ($union !== null) {
+ foreach ($union->getAtomicTypes() as $atomic) {
+ if ($atomic instanceof Type\Atomic\TNonFalsyString) {
+ continue;
+ }
+
+ if ($atomic instanceof Type\Atomic\TLiteralString) {
+ if ($atomic->value === '') {
+ $generic = true;
+ break;
+ }
+
+ if ($atomic->value === '0') {
+ $non_empty = true;
+ continue;
+ }
+
+ continue;
+ }
+
+ if ($atomic instanceof Type\Atomic\TNonEmptyString) {
+ $non_empty = true;
+ continue;
+ }
+
+ $generic = true;
+ break;
+ }
+ }
+
+ if ($union === null || $generic) {
+ return Type::getString();
+ }
+
+ if ($non_empty) {
+ return Type::getNonEmptyString();
+ }
+
+ return Type::getNonFalsyString();
}
$basename = basename($evaled_path);
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php
index f6c63c47ac4..ff4fbc54fed 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php
@@ -9,7 +9,6 @@
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TLiteralInt;
-use Psalm\Type\Atomic\TNonEmptyString;
use Psalm\Type\Union;
use function array_values;
@@ -39,6 +38,41 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
$statements_source = $event->getStatementsSource();
$node_type_provider = $statements_source->getNodeTypeProvider();
+ $union = $node_type_provider->getType($call_args[0]->value);
+ $generic = false;
+ if ($union !== null) {
+ foreach ($union->getAtomicTypes() as $atomic) {
+ if ($atomic instanceof Type\Atomic\TNonFalsyString) {
+ continue;
+ }
+
+ if ($atomic instanceof Type\Atomic\TLiteralString) {
+ if ($atomic->value === '') {
+ $generic = true;
+ break;
+ }
+
+ // 0 will be non-falsy too (.)
+ continue;
+ }
+
+ if ($atomic instanceof Type\Atomic\TNonEmptyString
+ || $atomic instanceof Type\Atomic\TEmptyNumeric) {
+ continue;
+ }
+
+ // generic string is the only other possible case of empty string
+ // which would result in a generic string
+ $generic = true;
+ break;
+ }
+ }
+
+ $fallback_type = Type::getNonFalsyString();
+ if ($union === null || $generic) {
+ $fallback_type = Type::getString();
+ }
+
$dir_level = 1;
if (isset($call_args[1])) {
$type = $node_type_provider->getType($call_args[1]->value);
@@ -49,7 +83,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
$atomic_type->value > 0) {
$dir_level = $atomic_type->value;
} else {
- return Type::getString();
+ return $fallback_type;
}
}
}
@@ -63,17 +97,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
);
if ($evaled_path === null) {
- $type = $node_type_provider->getType($call_args[0]->value);
- if ($type !== null && $type->isSingle()) {
- $atomic_type = array_values($type->getAtomicTypes())[0];
- if ($atomic_type instanceof TNonEmptyString) {
- return Type::getNonEmptyString();
- }
- }
- }
-
- if ($evaled_path === null) {
- return Type::getString();
+ return $fallback_type;
}
$path_to_file = dirname($evaled_path, $dir_level);
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php
new file mode 100644
index 00000000000..09b32b2aa65
--- /dev/null
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php
@@ -0,0 +1,251 @@
+
+ */
+ public static function getFunctionIds(): array
+ {
+ return ['filter_input'];
+ }
+
+ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
+ {
+ $statements_analyzer = $event->getStatementsSource();
+ if (! $statements_analyzer instanceof StatementsAnalyzer) {
+ throw new UnexpectedValueException('Expected StatementsAnalyzer not StatementsSource');
+ }
+
+ $call_args = $event->getCallArgs();
+ $function_id = $event->getFunctionId();
+ $code_location = $event->getCodeLocation();
+ $codebase = $statements_analyzer->getCodebase();
+
+ if (! isset($call_args[0]) || ! isset($call_args[1])) {
+ return FilterUtils::missingFirstArg($codebase);
+ }
+
+ $first_arg_type = $statements_analyzer->node_data->getType($call_args[0]->value);
+ if ($first_arg_type && ! $first_arg_type->isInt()) {
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // default option won't be used in this case
+ return Type::getNull();
+ }
+
+ $filter_int_used = FILTER_DEFAULT;
+ if (isset($call_args[2])) {
+ $filter_int_used = FilterUtils::getFilterArgValueOrError(
+ $call_args[2],
+ $statements_analyzer,
+ $codebase,
+ );
+
+ if (!is_int($filter_int_used)) {
+ return $filter_int_used;
+ }
+ }
+
+ $options = null;
+ $flags_int_used = FILTER_FLAG_NONE;
+ if (isset($call_args[3])) {
+ $helper = FilterUtils::getOptionsArgValueOrError(
+ $call_args[3],
+ $statements_analyzer,
+ $codebase,
+ $code_location,
+ $function_id,
+ $filter_int_used,
+ );
+
+ if (!is_array($helper)) {
+ return $helper;
+ }
+
+ $flags_int_used = $helper['flags_int_used'];
+ $options = $helper['options'];
+ }
+
+ // if we reach this point with callback, the callback is missing
+ if ($filter_int_used === FILTER_CALLBACK) {
+ return FilterUtils::missingFilterCallbackCallable(
+ $function_id,
+ $code_location,
+ $statements_analyzer,
+ $codebase,
+ );
+ }
+
+ [$default, $min_range, $max_range, $has_range, $regexp] = FilterUtils::getOptions(
+ $filter_int_used,
+ $flags_int_used,
+ $options,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ );
+
+ // only return now, as we still want to report errors above
+ if (!$first_arg_type) {
+ return null;
+ }
+
+ if (! $first_arg_type->isSingleIntLiteral()) {
+ // eventually complex cases can be handled too, however practically this is irrelevant
+ return null;
+ }
+
+ if (!$default) {
+ [$fails_type, $not_set_type, $fails_or_not_set_type] = FilterUtils::getFailsNotSetType($flags_int_used);
+ } else {
+ $fails_type = $default;
+ $not_set_type = $default;
+ $fails_or_not_set_type = $default;
+ }
+
+ if ($filter_int_used === FILTER_VALIDATE_REGEXP && $regexp === null) {
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // any "array" flags are ignored by this filter!
+ return $fails_or_not_set_type;
+ }
+
+ $possible_types = array(
+ '$_GET' => INPUT_GET,
+ '$_POST' => INPUT_POST,
+ '$_COOKIE' => INPUT_COOKIE,
+ '$_SERVER' => INPUT_SERVER,
+ '$_ENV' => INPUT_ENV,
+ );
+
+ $first_arg_type_type = $first_arg_type->getSingleIntLiteral();
+ $global_name = array_search($first_arg_type_type->value, $possible_types);
+ if (!$global_name) {
+ // invalid
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // the "not set type" is never in an array, even if FILTER_FORCE_ARRAY is set!
+ return $not_set_type;
+ }
+
+ $second_arg_type = $statements_analyzer->node_data->getType($call_args[1]->value);
+ if (!$second_arg_type) {
+ return null;
+ }
+
+ if (! $second_arg_type->hasString()) {
+ // for filter_input there can only be string array keys
+ return $not_set_type;
+ }
+
+ if (! $second_arg_type->isString()) {
+ // already reports an error by default
+ return null;
+ }
+
+ // in all these cases it can fail or be not set, depending on whether the variable is set or not
+ $redundant_error_return_type = FilterUtils::checkRedundantFlags(
+ $filter_int_used,
+ $flags_int_used,
+ $fails_or_not_set_type,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ );
+ if ($redundant_error_return_type !== null) {
+ return $redundant_error_return_type;
+ }
+
+ if (FilterUtils::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)
+ && in_array($first_arg_type_type->value, array(INPUT_COOKIE, INPUT_SERVER, INPUT_ENV), true)) {
+ // these globals can never be an array
+ return $fails_or_not_set_type;
+ }
+
+ // @todo eventually this needs to be changed when we fully support filter_has_var
+ $global_type = VariableFetchAnalyzer::getGlobalType($global_name, $codebase->analysis_php_version_id);
+
+ $input_type = null;
+ if ($global_type->isArray() && $global_type->getArray() instanceof TKeyedArray) {
+ $array_instance = $global_type->getArray();
+ if ($second_arg_type->isSingleStringLiteral()) {
+ $key = $second_arg_type->getSingleStringLiteral()->value;
+
+ if (isset($array_instance->properties[ $key ])) {
+ $input_type = $array_instance->properties[ $key ];
+ }
+ }
+
+ if ($input_type === null) {
+ $input_type = $array_instance->getGenericValueType();
+ $input_type = $input_type->setPossiblyUndefined(true);
+ }
+ } elseif ($global_type->isArray()
+ && ($array_atomic = $global_type->getArray())
+ && $array_atomic instanceof TArray) {
+ [$_, $input_type] = $array_atomic->type_params;
+ $input_type = $input_type->setPossiblyUndefined(true);
+ } else {
+ // this is impossible
+ throw new UnexpectedValueException('This should not happen');
+ }
+
+ return FilterUtils::getReturnType(
+ $filter_int_used,
+ $flags_int_used,
+ $input_type,
+ $fails_type,
+ $not_set_type,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ $has_range,
+ $min_range,
+ $max_range,
+ $regexp,
+ );
+ }
+}
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php
new file mode 100644
index 00000000000..e0de55bba04
--- /dev/null
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php
@@ -0,0 +1,1738 @@
+analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ return Type::getNull();
+ }
+
+ /** @return int|Union|null */
+ public static function getFilterArgValueOrError(
+ Arg $filter_arg,
+ StatementsAnalyzer $statements_analyzer,
+ Codebase $codebase
+ ) {
+ $filter_arg_type = $statements_analyzer->node_data->getType($filter_arg->value);
+ if (!$filter_arg_type) {
+ return null;
+ }
+
+ if (! $filter_arg_type->isInt()) {
+ // invalid
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // will return null independent of FILTER_NULL_ON_FAILURE or default option
+ return Type::getNull();
+ }
+
+ if (! $filter_arg_type->isSingleIntLiteral()) {
+ // too complex for now
+ return null;
+ }
+
+ $all_filters = self::getFilters($codebase);
+ $filter_int_used = $filter_arg_type->getSingleIntLiteral()->value;
+ if (! isset($all_filters[ $filter_int_used ])) {
+ // inconsistently, this will always return false, even when FILTER_NULL_ON_FAILURE
+ // or a default option is set
+ // and will also not use any default set
+ return Type::getFalse();
+ }
+
+ return $filter_int_used;
+ }
+
+ /** @return array{flags_int_used: int, options: TKeyedArray|null}|Union|null */
+ public static function getOptionsArgValueOrError(
+ Arg $options_arg,
+ StatementsAnalyzer $statements_analyzer,
+ Codebase $codebase,
+ CodeLocation $code_location,
+ string $function_id,
+ int $filter_int_used
+ ) {
+ $options_arg_type = $statements_analyzer->node_data->getType($options_arg->value);
+ if (!$options_arg_type) {
+ return null;
+ }
+
+ if ($options_arg_type->isArray()) {
+ $return_null = false;
+ $defaults = array(
+ 'flags_int_used' => FILTER_FLAG_NONE,
+ 'options' => null,
+ );
+
+ $atomic_type = $options_arg_type->getArray();
+ if ($atomic_type instanceof TKeyedArray) {
+ $redundant_keys = array_diff(array_keys($atomic_type->properties), array('flags', 'options'));
+ if ($redundant_keys !== array()) {
+ // reported as it's usually an oversight/misunderstanding of how the function works
+ // it's silently ignored by the function though
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'The options array contains unused keys '
+ . implode(', ', $redundant_keys),
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if (isset($atomic_type->properties['options'])) {
+ if ($filter_int_used === FILTER_CALLBACK) {
+ $only_callables = true;
+ foreach ($atomic_type->properties['options']->getAtomicTypes() as $option_atomic) {
+ if ($option_atomic->isCallableType()) {
+ continue;
+ }
+
+ if (CallableTypeComparator::getCallableFromAtomic(
+ $codebase,
+ $option_atomic,
+ null,
+ $statements_analyzer,
+ )) {
+ continue;
+ }
+
+ $only_callables = false;
+ }
+ if ($atomic_type->properties['options']->possibly_undefined) {
+ $only_callables = false;
+ }
+
+ if (!$only_callables) {
+ return self::missingFilterCallbackCallable(
+ $function_id,
+ $code_location,
+ $statements_analyzer,
+ $codebase,
+ );
+ }
+
+ // eventually can improve it to return the type from the callback
+ // there are no flags or other options/flags, so it can be handled here directly
+ // @todo
+ $return_type = Type::getMixed();
+ return self::addReturnTaint(
+ $statements_analyzer,
+ $code_location,
+ $return_type,
+ $function_id,
+ );
+ }
+
+ if (! $atomic_type->properties['options']->isArray()) {
+ // silently ignored by the function, but this usually indicates a bug
+ IssueBuffer::maybeAdd(
+ new InvalidArgument(
+ 'The "options" key in ' . $function_id
+ . ' must be a an array',
+ $code_location,
+ $function_id,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ } elseif (($options_array = $atomic_type->properties['options']->getArray())
+ && $options_array instanceof TKeyedArray) {
+ $defaults['options'] = $options_array;
+ } else {
+ // cannot infer a 100% correct specific return type
+ $return_null = true;
+ }
+ }
+
+ if (isset($atomic_type->properties['flags'])) {
+ if ($atomic_type->properties['flags']->isSingleIntLiteral()) {
+ $defaults['flags_int_used'] = $atomic_type->properties['flags']->getSingleIntLiteral()->value;
+ } elseif ($atomic_type->properties['flags']->isInt()) {
+ // cannot infer a 100% correct specific return type
+ $return_null = true;
+ } else {
+ // silently ignored by the function, but this usually indicates a bug
+ IssueBuffer::maybeAdd(
+ new InvalidArgument(
+ 'The "flags" key in ' .
+ $function_id . ' must be a valid flag',
+ $code_location,
+ $function_id,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ $defaults['flags_int_used'] = FILTER_FLAG_NONE;
+ }
+ }
+
+ return $return_null ? null : $defaults;
+ }
+
+ // cannot infer a 100% correct specific return type
+ return null;
+ }
+
+ if ($filter_int_used === FILTER_CALLBACK) {
+ return self::missingFilterCallbackCallable(
+ $function_id,
+ $code_location,
+ $statements_analyzer,
+ $codebase,
+ );
+ }
+
+ if ($options_arg_type->isSingleIntLiteral()) {
+ return array(
+ 'flags_int_used' => $options_arg_type->getSingleIntLiteral()->value,
+ 'options' => null,
+ );
+ }
+
+ if ($options_arg_type->isInt()) {
+ // in most cases we cannot infer a 100% correct specific return type though
+ // unless all int are literal
+ // @todo could handle all literal int cases
+ return null;
+ }
+
+ foreach ($options_arg_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TArray) {
+ continue;
+ }
+
+ if ($atomic_type instanceof TInt) {
+ continue;
+ }
+
+ if ($atomic_type instanceof TFloat) {
+ // ignored
+ continue;
+ }
+
+ if ($atomic_type instanceof TBool) {
+ // ignored
+ continue;
+ }
+
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws for the invalid type
+ // for the other types it will still work correctly
+ // however "never" is a bottom type
+ // and will be lost, therefore it's better to return it here
+ // to identify hard to find bugs in the code
+ return Type::getNever();
+ }
+ // before PHP 8, it's ignored but gives a PHP notice
+ }
+
+ // array|int type which is too complex for now
+ // or any other invalid type
+ return null;
+ }
+
+ public static function missingFilterCallbackCallable(
+ string $function_id,
+ CodeLocation $code_location,
+ StatementsAnalyzer $statements_analyzer,
+ Codebase $codebase
+ ): Union {
+ IssueBuffer::maybeAdd(
+ new InvalidArgument(
+ 'The "options" key in ' . $function_id
+ . ' must be a callable for FILTER_CALLBACK',
+ $code_location,
+ $function_id,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // flags are ignored here
+ return Type::getNull();
+ }
+
+ /** @return array{Union, Union, Union} */
+ public static function getFailsNotSetType(int $flags_int_used): array
+ {
+ $fails_type = Type::getFalse();
+ $not_set_type = Type::getNull();
+ if (self::hasFlag($flags_int_used, FILTER_NULL_ON_FAILURE)) {
+ $fails_type = Type::getNull();
+ $not_set_type = Type::getFalse();
+ }
+
+ $fails_or_not_set_type = new Union([new TNull(), new TFalse()]);
+ return array(
+ $fails_type,
+ $not_set_type,
+ $fails_or_not_set_type,
+ );
+ }
+
+ public static function hasFlag(int $flags, int $flag): bool
+ {
+ if ($flags === 0) {
+ return false;
+ }
+
+ if (($flags & $flag) === $flag) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function checkRedundantFlags(
+ int $filter_int_used,
+ int $flags_int_used,
+ Union $fails_type,
+ StatementsAnalyzer $statements_analyzer,
+ CodeLocation $code_location,
+ Codebase $codebase
+ ): ?Union {
+ $all_filters = self::getFilters($codebase);
+ $flags_int_used_rest = $flags_int_used;
+ foreach ($all_filters[ $filter_int_used ]['flags'] as $flag) {
+ if ($flags_int_used_rest === 0) {
+ break;
+ }
+
+ if (self::hasFlag($flags_int_used_rest, $flag)) {
+ $flags_int_used_rest = $flags_int_used_rest ^ $flag;
+ }
+ }
+
+ if ($flags_int_used_rest !== 0) {
+ // invalid flags used
+ // while they are silently ignored
+ // usually it means there's a mistake and the filter doesn't actually do what one expects
+ // as otherwise the flag wouldn't have been provided
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'Not all flags used are supported by the filter used',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if (self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)
+ && self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY)) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'Flag FILTER_FORCE_ARRAY is ignored when using FILTER_REQUIRE_ARRAY',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if ($filter_int_used === FILTER_VALIDATE_REGEXP
+ && (
+ self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)
+ || self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY)
+ || self::hasFlag($flags_int_used, FILTER_REQUIRE_SCALAR))
+ ) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'FILTER_VALIDATE_REGEXP will ignore ' .
+ 'FILTER_REQUIRE_ARRAY/FILTER_FORCE_ARRAY/FILTER_REQUIRE_SCALAR ' .
+ 'as it only works on scalar types',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if (self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_LOW)
+ && self::hasFlag($flags_int_used, FILTER_FLAG_ENCODE_LOW)) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'Using flag FILTER_FLAG_ENCODE_LOW is redundant when using FILTER_FLAG_STRIP_LOW',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if (self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_HIGH)
+ && self::hasFlag($flags_int_used, FILTER_FLAG_ENCODE_HIGH)) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'Using flag FILTER_FLAG_ENCODE_HIGH is redundant when using FILTER_FLAG_STRIP_HIGH',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if (self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)
+ && self::hasFlag($flags_int_used, FILTER_REQUIRE_SCALAR)) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'You cannot use FILTER_REQUIRE_ARRAY together with FILTER_REQUIRE_SCALAR flag',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ // FILTER_REQUIRE_ARRAY will make PHP ignore FILTER_FORCE_ARRAY
+ return $fails_type;
+ }
+
+ return null;
+ }
+
+ /** @return array{Union|null, float|int|null, float|int|null, bool, non-falsy-string|true|null} */
+ public static function getOptions(
+ int $filter_int_used,
+ int $flags_int_used,
+ ?TKeyedArray $options,
+ StatementsAnalyzer $statements_analyzer,
+ CodeLocation $code_location,
+ Codebase $codebase,
+ string $function_id
+ ): array {
+ $default = null;
+ $min_range = null;
+ $max_range = null;
+ $has_range = false;
+ $regexp = null;
+
+ if (!$options) {
+ return [$default, $min_range, $max_range, $has_range, $regexp];
+ }
+
+ $all_filters = self::getFilters($codebase);
+ foreach ($options->properties as $option => $option_value) {
+ if (! isset($all_filters[ $filter_int_used ]['options'][ $option ])) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'The option ' . $option . ' is not valid for the filter used',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ continue;
+ }
+
+ if (! UnionTypeComparator::isContainedBy(
+ $codebase,
+ $option_value,
+ $all_filters[ $filter_int_used ]['options'][ $option ],
+ )) {
+ // silently ignored by the function, but it's a bug in the code
+ // since the filtering/option will not do what you expect
+ IssueBuffer::maybeAdd(
+ new InvalidArgument(
+ 'The option "' . $option . '" of ' . $function_id . ' expects '
+ . $all_filters[ $filter_int_used ]['options'][ $option ]->getId()
+ . ', but ' . $option_value->getId() . ' provided',
+ $code_location,
+ $function_id,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ continue;
+ }
+
+ if ($option === 'default') {
+ $default = $option_value;
+
+ if (self::hasFlag($flags_int_used, FILTER_NULL_ON_FAILURE)) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'Redundant flag FILTER_NULL_ON_FAILURE when using the "default" option',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ continue;
+ }
+
+ // currently only int ranges are supported
+ // must be numeric, otherwise we would have continued above already
+ if ($option === 'min_range' && $option_value->isSingleLiteral()) {
+ if ($filter_int_used === FILTER_VALIDATE_INT) {
+ $min_range = (int) $option_value->getSingleLiteral()->value;
+ } elseif ($filter_int_used === FILTER_VALIDATE_FLOAT) {
+ $min_range = (float) $option_value->getSingleLiteral()->value;
+ }
+ }
+
+ if ($option === 'max_range' && $option_value->isSingleLiteral()) {
+ if ($filter_int_used === FILTER_VALIDATE_INT) {
+ $max_range = (int) $option_value->getSingleLiteral()->value;
+ } elseif ($filter_int_used === FILTER_VALIDATE_FLOAT) {
+ $max_range = (float) $option_value->getSingleLiteral()->value;
+ }
+ }
+
+ if (($filter_int_used === FILTER_VALIDATE_INT || $filter_int_used === FILTER_VALIDATE_FLOAT)
+ && ($option === 'min_range' || $option === 'max_range')
+ ) {
+ $has_range = true;
+ }
+
+ if ($filter_int_used === FILTER_VALIDATE_REGEXP
+ && $option === 'regexp'
+ ) {
+ if ($option_value->isSingleStringLiteral()) {
+ /**
+ * if it's another type, we would have reported an error above already
+ * @var non-falsy-string $regexp
+ */
+ $regexp = $option_value->getSingleStringLiteral()->value;
+ } elseif ($option_value->isString()) {
+ $regexp = true;
+ }
+ }
+ }
+
+ return [$default, $min_range, $max_range, $has_range, $regexp];
+ }
+
+ /**
+ * @param float|int|null $min_range
+ * @param float|int|null $max_range
+ */
+ protected static function isRangeValid(
+ $min_range,
+ $max_range,
+ StatementsAnalyzer $statements_analyzer,
+ CodeLocation $code_location,
+ string $function_id
+ ): bool {
+ if ($min_range !== null && $max_range !== null && $min_range > $max_range) {
+ IssueBuffer::maybeAdd(
+ new InvalidArgument(
+ 'min_range cannot be larger than max_range',
+ $code_location,
+ $function_id,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * can't split this because the switch is complex since there are too many possibilities
+ *
+ * @psalm-suppress ComplexMethod
+ * @param Union|null $not_set_type null if undefined filtered variable will return $fails_type
+ * @param float|int|null $min_range
+ * @param float|int|null $max_range
+ * @param non-falsy-string|true|null $regexp
+ */
+ public static function getReturnType(
+ int $filter_int_used,
+ int $flags_int_used,
+ Union $input_type,
+ Union $fails_type,
+ ?Union $not_set_type,
+ StatementsAnalyzer $statements_analyzer,
+ CodeLocation $code_location,
+ Codebase $codebase,
+ string $function_id,
+ bool $has_range,
+ $min_range,
+ $max_range,
+ $regexp,
+ bool $in_array_recursion = false
+ ): Union {
+ // if we are inside a recursion of e.g. array
+ // it will never fail or change the type, so we can immediately return
+ if ($in_array_recursion && $input_type->isNever()) {
+ return $input_type;
+ }
+
+ $from_array = [];
+ // will only handle arrays correctly if either flag is set, otherwise always error
+ // regexp doesn't work on arrays
+ if ((self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY) || self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY))
+ && $filter_int_used !== FILTER_VALIDATE_REGEXP
+ && !self::hasFlag($flags_int_used, FILTER_REQUIRE_SCALAR)
+ ) {
+ foreach ($input_type->getAtomicTypes() as $key => $atomic_type) {
+ if ($atomic_type instanceof TList) {
+ $atomic_type = $atomic_type->getKeyedArray();
+ }
+
+ if ($atomic_type instanceof TKeyedArray) {
+ $input_type = $input_type->getBuilder();
+ $input_type->removeType($key);
+ $input_type = $input_type->freeze();
+
+ $new = [];
+ foreach ($atomic_type->properties as $k => $property) {
+ if ($property->isNever()) {
+ $new[$k] = $property;
+ continue;
+ }
+
+ $new[$k] = self::getReturnType(
+ $filter_int_used,
+ $flags_int_used,
+ $property,
+ $fails_type,
+ // irrelevant in nested elements
+ null,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ $has_range,
+ $min_range,
+ $max_range,
+ $regexp,
+ true,
+ );
+ }
+
+ // false positive error in psalm when we loop over a non-empty array
+ if ($new === array()) {
+ throw new UnexpectedValueException('This is impossible');
+ }
+
+ $fallback_params = null;
+ if ($atomic_type->fallback_params) {
+ [$keys_union, $values_union] = $atomic_type->fallback_params;
+ $values_union = self::getReturnType(
+ $filter_int_used,
+ $flags_int_used,
+ $values_union,
+ $fails_type,
+ // irrelevant in nested elements
+ null,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ $has_range,
+ $min_range,
+ $max_range,
+ $regexp,
+ true,
+ );
+ $fallback_params = [$keys_union, $values_union];
+ }
+
+ $from_array[] = new TKeyedArray(
+ $new,
+ $atomic_type->class_strings,
+ $fallback_params,
+ $atomic_type->is_list,
+ );
+
+ continue;
+ }
+
+ if ($atomic_type instanceof TArray) {
+ $input_type = $input_type->getBuilder();
+ $input_type->removeType($key);
+ $input_type = $input_type->freeze();
+
+ [$keys_union, $values_union] = $atomic_type->type_params;
+ $values_union = self::getReturnType(
+ $filter_int_used,
+ $flags_int_used,
+ $values_union,
+ $fails_type,
+ // irrelevant in nested elements
+ null,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ $has_range,
+ $min_range,
+ $max_range,
+ $regexp,
+ true,
+ );
+
+ if ($atomic_type instanceof TNonEmptyArray) {
+ $from_array[] = new TNonEmptyArray([$keys_union, $values_union]);
+ } else {
+ $from_array[] = new TArray([$keys_union, $values_union]);
+ }
+
+ continue;
+ }
+
+ // can be an array too
+ if ($atomic_type instanceof TMixed) {
+ $from_array[] = new TArray(
+ [
+ new Union([new TArrayKey]),
+ new Union([new TMixed]),
+ ],
+ );
+ }
+ }
+ }
+
+ $can_fail = false;
+ $filter_types = array();
+ switch ($filter_int_used) {
+ case FILTER_VALIDATE_FLOAT:
+ if (!self::isRangeValid(
+ $min_range,
+ $max_range,
+ $statements_analyzer,
+ $code_location,
+ $function_id,
+ )) {
+ $can_fail = true;
+ break;
+ }
+
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TLiteralFloat) {
+ if ($min_range !== null && $min_range > $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ // float ranges aren't supported yet
+ $filter_types[] = new TFloat();
+ } elseif ($atomic_type instanceof TFloat) {
+ if ($has_range === false) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ // float ranges aren't supported yet
+ $filter_types[] = new TFloat();
+ }
+
+ if ($atomic_type instanceof TLiteralInt) {
+ if ($min_range !== null && $min_range > $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralFloat((float) $atomic_type->value);
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = new TFloat();
+ } elseif ($atomic_type instanceof TInt) {
+ $filter_types[] = new TFloat();
+
+ if ($has_range === false) {
+ continue;
+ }
+ }
+
+ if ($atomic_type instanceof TLiteralString) {
+ if (($string_to_float = filter_var($atomic_type->value, FILTER_VALIDATE_FLOAT)) === false) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null && $min_range > $string_to_float) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $string_to_float) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralFloat($string_to_float);
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = new TFloat();
+ } elseif ($atomic_type instanceof TString) {
+ $filter_types[] = new TFloat();
+ }
+
+ if ($atomic_type instanceof TBool) {
+ if ($min_range !== null && $min_range > 1) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < 1) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($atomic_type instanceof TFalse) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralFloat(1.0);
+
+ if ($atomic_type instanceof TTrue) {
+ continue;
+ }
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = new TFloat();
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TFloat();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_VALIDATE_BOOLEAN:
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TBool) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ if (($atomic_type instanceof TLiteralInt && $atomic_type->value === 1)
+ || ($atomic_type instanceof TLiteralFloat && $atomic_type->value === 1.0)
+ || ($atomic_type instanceof TLiteralString
+ && in_array(strtolower($atomic_type->value), ['1', 'true', 'on', 'yes'], true))
+ ) {
+ $filter_types[] = new TTrue();
+ continue;
+ }
+
+ if (self::hasFlag($flags_int_used, FILTER_NULL_ON_FAILURE)
+ && (
+ ($atomic_type instanceof TLiteralInt && $atomic_type->value === 0)
+ || ($atomic_type instanceof TLiteralFloat && $atomic_type->value === 0.0)
+ || ($atomic_type instanceof TLiteralString
+ && in_array(strtolower($atomic_type->value), ['0', 'false', 'off', 'no', ''], true)
+ )
+ )
+ ) {
+ $filter_types[] = new TFalse();
+ continue;
+ }
+
+ if ($atomic_type instanceof TLiteralInt
+ || $atomic_type instanceof TLiteralFloat
+ || $atomic_type instanceof TLiteralString
+ ) {
+ // all other literals will fail
+ $can_fail = true;
+ continue;
+ }
+
+ if ($atomic_type instanceof TMixed
+ || $atomic_type instanceof TString
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TFloat) {
+ $filter_types[] = new TBool();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_VALIDATE_INT:
+ if (!self::isRangeValid(
+ $min_range,
+ $max_range,
+ $statements_analyzer,
+ $code_location,
+ $function_id,
+ )) {
+ $can_fail = true;
+ break;
+ }
+
+ $min_range = $min_range !== null ? (int) $min_range : null;
+ $max_range = $max_range !== null ? (int) $max_range : null;
+
+ if ($min_range !== null || $max_range !== null) {
+ $int_type = new TIntRange($min_range, $max_range);
+ } 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) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = new TInt();
+ } elseif ($atomic_type instanceof TInt) {
+ if ($has_range === false) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ $filter_types[] = $int_type;
+ }
+
+ if ($atomic_type instanceof TLiteralFloat) {
+ if ((float) (int) $atomic_type->value !== $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null && $min_range > $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralInt((int) $atomic_type->value);
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = $int_type;
+ } elseif ($atomic_type instanceof TFloat) {
+ $filter_types[] = $int_type;
+ }
+
+ if ($atomic_type instanceof TLiteralString) {
+ if (($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) === false) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null && $min_range > $string_to_int) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $string_to_int) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralInt($string_to_int);
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = $int_type;
+ } elseif ($atomic_type instanceof TString) {
+ $filter_types[] = $int_type;
+ }
+
+ if ($atomic_type instanceof TBool) {
+ if ($min_range !== null && $min_range > 1) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < 1) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($atomic_type instanceof TFalse) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralInt(1);
+
+ if ($atomic_type instanceof TTrue) {
+ continue;
+ }
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = $int_type;
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = $int_type;
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_VALIDATE_IP:
+ case FILTER_VALIDATE_MAC:
+ case FILTER_VALIDATE_URL:
+ case FILTER_VALIDATE_EMAIL:
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TNumericString) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($atomic_type instanceof TNonFalsyString) {
+ $filter_types[] = $atomic_type;
+ } elseif ($atomic_type instanceof TString) {
+ $filter_types[] = new TNonFalsyString();
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TNonFalsyString();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_VALIDATE_REGEXP:
+ // the regexp key is mandatory for this filter
+ // it will only fail if the value exists, therefore it's after the checks above
+ // this must be (and is) handled BEFORE calling this function though
+ // since PHP 8+ throws instead of returning the fails case
+ if ($regexp === null) {
+ $can_fail = true;
+ break;
+ }
+
+ // invalid regex
+ if ($regexp !== true && @preg_match($regexp, 'placeholder') === false) {
+ $can_fail = true;
+ break;
+ }
+
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TString
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TFloat
+ || $atomic_type instanceof TNumeric
+ || $atomic_type instanceof TMixed) {
+ $filter_types[] = new TString();
+ }
+
+ $can_fail = true;
+ }
+
+ break;
+ case FILTER_VALIDATE_DOMAIN:
+ if (self::hasFlag($flags_int_used, FILTER_FLAG_HOSTNAME)) {
+ $string_type = new TNonEmptyString();
+ } else {
+ $string_type = new TString();
+ }
+
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TNonEmptyString) {
+ $filter_types[] = $atomic_type;
+ } elseif ($atomic_type instanceof TString) {
+ if (self::hasFlag($flags_int_used, FILTER_FLAG_HOSTNAME)) {
+ $filter_types[] = $string_type;
+ } else {
+ $filter_types[] = $atomic_type;
+ }
+ }
+
+ if ($atomic_type instanceof TMixed
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TFloat) {
+ $filter_types[] = $string_type;
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_SANITIZE_EMAIL:
+ case FILTER_SANITIZE_URL:
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TNumericString) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ if ($atomic_type instanceof TString) {
+ $filter_types[] = new TString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TFloat
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TNumeric) {
+ $filter_types[] = new TNumericString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TTrue) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ continue;
+ }
+
+ if ($atomic_type instanceof TFalse) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TBool) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TString();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_SANITIZE_ENCODED:
+ case FILTER_SANITIZE_ADD_SLASHES:
+ case 521: // 8.0.0 FILTER_SANITIZE_MAGIC_QUOTES has been removed.
+ case FILTER_SANITIZE_SPECIAL_CHARS:
+ case FILTER_SANITIZE_FULL_SPECIAL_CHARS:
+ case FILTER_DEFAULT:
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($filter_int_used === FILTER_DEFAULT
+ && $flags_int_used === 0
+ && $atomic_type instanceof TString
+ ) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ if ($atomic_type instanceof TNumericString) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ if (in_array(
+ $filter_int_used,
+ [FILTER_SANITIZE_ENCODED, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_DEFAULT],
+ true,
+ )
+ && $atomic_type instanceof TNonEmptyString
+ && (self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_LOW)
+ || self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_HIGH)
+ || self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_BACKTICK)
+ )
+ ) {
+ $filter_types[] = new TString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TNonFalsyString) {
+ $filter_types[] = new TNonFalsyString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TNonEmptyString) {
+ $filter_types[] = new TNonEmptyString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TString) {
+ $filter_types[] = new TString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TFloat
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TNumeric) {
+ $filter_types[] = new TNumericString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TTrue) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ continue;
+ }
+
+ if ($atomic_type instanceof TFalse) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TBool) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TString();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case 513: // 8.1.0 FILTER_SANITIZE_STRING and FILTER_SANITIZE_STRIPPED (alias) have been deprecated.
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TBool
+ || $atomic_type instanceof TString
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TFloat
+ || $atomic_type instanceof TNumeric) {
+ // only basic checking since it's deprecated anyway and not worth the time
+ $filter_types[] = new TString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TString();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_SANITIZE_NUMBER_INT:
+ case FILTER_SANITIZE_NUMBER_FLOAT:
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TLiteralString
+ || $atomic_type instanceof TLiteralInt
+ || $atomic_type instanceof TLiteralFloat
+ ) {
+ /** @var string|false $literal */
+ $literal = filter_var($atomic_type->value, $filter_int_used);
+ if ($literal === false) {
+ $can_fail = true;
+ } else {
+ $filter_types[] = Type::getAtomicStringFromLiteral($literal);
+ }
+
+ continue;
+ }
+
+ if ($atomic_type instanceof TFloat
+ || $atomic_type instanceof TNumericString
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TNumeric) {
+ $filter_types[] = new TNumericString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TString) {
+ $filter_types[] = new TNumericString();
+ // for numeric-string it won't collapse since https://github.com/vimeo/psalm/pull/10459
+ // therefore we can add both
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TTrue) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ continue;
+ }
+
+ if ($atomic_type instanceof TFalse) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TBool) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TNumericString();
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ }
+
+ $can_fail = true;
+ }
+ break;
+ }
+
+ if ($input_type->hasMixed()) {
+ // can always fail if we have mixed
+ // only for redundancy in case there's a mistake in the switch above
+ $can_fail = true;
+ }
+
+ // if an array is required, ignore all types we created from non-array on first level
+ if (!$in_array_recursion && self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)) {
+ $filter_types = array();
+ }
+
+ $return_type = $fails_type;
+ if ($filter_types !== array()
+ && ($can_fail === true ||
+ (!$in_array_recursion && !$not_set_type && $input_type->possibly_undefined)
+ )) {
+ $return_type = Type::combineUnionTypes(
+ $return_type,
+ TypeCombiner::combine($filter_types, $codebase),
+ $codebase,
+ );
+ } elseif ($filter_types !== array()) {
+ $return_type = TypeCombiner::combine($filter_types, $codebase);
+ }
+
+ if (!$in_array_recursion
+ && !self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)
+ && self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY)) {
+ $return_type = new Union([new TKeyedArray(
+ [$return_type],
+ null,
+ null,
+ true,
+ )]);
+ }
+
+ if ($from_array !== array()) {
+ $from_array_union = TypeCombiner::combine($from_array, $codebase);
+
+ $return_type = Type::combineUnionTypes(
+ $return_type,
+ $from_array_union,
+ $codebase,
+ );
+ }
+
+ if ($in_array_recursion && $input_type->possibly_undefined) {
+ $return_type = $return_type->setPossiblyUndefined(true);
+ } elseif (!$in_array_recursion && $not_set_type && $input_type->possibly_undefined) {
+ // in case of PHP CLI it will always fail for all filter_input even when they're set
+ // to fix this we would have to add support for environments in Context
+ // e.g. if php_sapi_name() === 'cli'
+ $return_type = Type::combineUnionTypes(
+ $return_type,
+ // the not set type is not coerced into an array when FILTER_FORCE_ARRAY is used
+ $not_set_type,
+ $codebase,
+ );
+ }
+
+ if (!$in_array_recursion) {
+ $return_type = self::addReturnTaint(
+ $statements_analyzer,
+ $code_location,
+ $return_type,
+ $function_id,
+ );
+ }
+
+ return $return_type;
+ }
+
+ private static function addReturnTaint(
+ StatementsAnalyzer $statements_analyzer,
+ CodeLocation $code_location,
+ Union $return_type,
+ string $function_id
+ ): Union {
+ if ($statements_analyzer->data_flow_graph
+ && !in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())
+ ) {
+ $function_return_sink = DataFlowNode::getForMethodReturn(
+ $function_id,
+ $function_id,
+ null,
+ $code_location,
+ );
+
+ $statements_analyzer->data_flow_graph->addNode($function_return_sink);
+
+ $function_param_sink = DataFlowNode::getForMethodArgument(
+ $function_id,
+ $function_id,
+ 0,
+ null,
+ $code_location,
+ );
+
+ $statements_analyzer->data_flow_graph->addNode($function_param_sink);
+
+ $statements_analyzer->data_flow_graph->addPath(
+ $function_param_sink,
+ $function_return_sink,
+ 'arg',
+ );
+
+ $return_type = $return_type->setParentNodes([$function_return_sink->id => $function_return_sink]);
+ }
+
+ return $return_type;
+ }
+
+ /** @return array, options: array}> */
+ public static function getFilters(Codebase $codebase): array
+ {
+ $general_filter_flags = array(
+ FILTER_REQUIRE_SCALAR,
+ FILTER_REQUIRE_ARRAY,
+ FILTER_FORCE_ARRAY,
+ FILTER_FLAG_NONE, // does nothing, default
+ );
+
+ // https://www.php.net/manual/en/filter.filters.sanitize.php
+ $sanitize_filters = array(
+ FILTER_SANITIZE_EMAIL => array(
+ 'flags' => array(),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_ENCODED => array(
+ 'flags' => array(
+ FILTER_FLAG_STRIP_LOW,
+ FILTER_FLAG_STRIP_HIGH,
+ FILTER_FLAG_STRIP_BACKTICK,
+ FILTER_FLAG_ENCODE_LOW,
+ FILTER_FLAG_ENCODE_HIGH,
+ ),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_NUMBER_FLOAT => array(
+ 'flags' => array(
+ FILTER_FLAG_ALLOW_FRACTION,
+ FILTER_FLAG_ALLOW_THOUSAND,
+ FILTER_FLAG_ALLOW_SCIENTIFIC,
+ ),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_NUMBER_INT => array(
+ 'flags' => array(),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_SPECIAL_CHARS => array(
+ 'flags' => array(
+ FILTER_FLAG_STRIP_LOW,
+ FILTER_FLAG_STRIP_HIGH,
+ FILTER_FLAG_STRIP_BACKTICK,
+ FILTER_FLAG_ENCODE_HIGH,
+ ),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_FULL_SPECIAL_CHARS => array(
+ 'flags' => array(
+ FILTER_FLAG_NO_ENCODE_QUOTES,
+ ),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_URL => array(
+ 'flags' => array(),
+ 'options' => array(),
+ ),
+ FILTER_UNSAFE_RAW => array(
+ 'flags' => array(
+ FILTER_FLAG_STRIP_LOW,
+ FILTER_FLAG_STRIP_HIGH,
+ FILTER_FLAG_STRIP_BACKTICK,
+ FILTER_FLAG_ENCODE_LOW,
+ FILTER_FLAG_ENCODE_HIGH,
+ FILTER_FLAG_ENCODE_AMP,
+ ),
+ 'options' => array(),
+ ),
+
+ );
+
+ if ($codebase->analysis_php_version_id <= 7_03_00) {
+ // FILTER_SANITIZE_MAGIC_QUOTES
+ $sanitize_filters[521] = array(
+ 'flags' => array(),
+ 'options' => array(),
+ );
+ }
+
+ if ($codebase->analysis_php_version_id <= 8_01_00) {
+ // FILTER_SANITIZE_STRING
+ $sanitize_filters[513] = array(
+ 'flags' => array(
+ FILTER_FLAG_NO_ENCODE_QUOTES,
+ FILTER_FLAG_STRIP_LOW,
+ FILTER_FLAG_STRIP_HIGH,
+ FILTER_FLAG_STRIP_BACKTICK,
+ FILTER_FLAG_ENCODE_LOW,
+ FILTER_FLAG_ENCODE_HIGH,
+ FILTER_FLAG_ENCODE_AMP,
+ ),
+ 'options' => array(),
+ );
+ }
+
+ if ($codebase->analysis_php_version_id >= 7_03_00) {
+ // was added as a replacement for FILTER_SANITIZE_MAGIC_QUOTES
+ $sanitize_filters[FILTER_SANITIZE_ADD_SLASHES] = array(
+ 'flags' => array(),
+ 'options' => array(),
+ );
+ }
+
+ foreach ($sanitize_filters as $filter_int => $filter_data) {
+ $sanitize_filters[$filter_int]['flags'] = array_merge($filter_data['flags'], $general_filter_flags);
+ }
+
+ // https://www.php.net/manual/en/filter.filters.validate.php
+ // validation filters all match bitmask 0x100
+ // all support FILTER_NULL_ON_FAILURE flag https://www.php.net/manual/en/filter.filters.flags.php
+ $general_filter_flags_validate = array_merge($general_filter_flags, array(FILTER_NULL_ON_FAILURE));
+
+ $validate_filters = array(
+ FILTER_VALIDATE_BOOLEAN => array(
+ 'flags' => array(),
+ 'options' => array(),
+ ),
+ FILTER_VALIDATE_EMAIL => array(
+ 'flags' => array(
+ FILTER_FLAG_EMAIL_UNICODE,
+ ),
+ 'options' => array(),
+ ),
+ FILTER_VALIDATE_FLOAT => array(
+ 'flags' => array(
+ FILTER_FLAG_ALLOW_THOUSAND,
+ ),
+ 'options' => array(
+ 'decimal' => new Union([
+ Type::getAtomicStringFromLiteral('.'),
+ Type::getAtomicStringFromLiteral(','),
+ ]),
+ ),
+ ),
+ FILTER_VALIDATE_INT => array(
+ 'flags' => array(
+ FILTER_FLAG_ALLOW_OCTAL,
+ FILTER_FLAG_ALLOW_HEX,
+ ),
+ 'options' => array(
+ 'min_range' => Type::getNumeric(),
+ 'max_range' => Type::getNumeric(),
+ ),
+ ),
+ FILTER_VALIDATE_IP => array(
+ 'flags' => array(
+ FILTER_FLAG_IPV4,
+ FILTER_FLAG_IPV6,
+ FILTER_FLAG_NO_PRIV_RANGE,
+ FILTER_FLAG_NO_RES_RANGE,
+
+ ),
+ 'options' => array(),
+ ),
+ FILTER_VALIDATE_MAC => array(
+ 'flags' => array(),
+ 'options' => array(),
+ ),
+ FILTER_VALIDATE_REGEXP => array(
+ 'flags' => array(),
+ 'options' => array(
+ 'regexp' => Type::getNonFalsyString(),
+ ),
+ ),
+ FILTER_VALIDATE_URL => array(
+ 'flags' => array(
+ FILTER_FLAG_PATH_REQUIRED,
+ FILTER_FLAG_QUERY_REQUIRED,
+ ),
+ 'options' => array(),
+ ),
+
+ );
+
+ if ($codebase->analysis_php_version_id >= 7_04_00) {
+ $validate_filters[FILTER_VALIDATE_FLOAT]['options']['min_range'] = Type::getNumeric();
+ $validate_filters[FILTER_VALIDATE_FLOAT]['options']['max_range'] = Type::getNumeric();
+ }
+
+ if ($codebase->analysis_php_version_id < 8_00_00) {
+ // phpcs:ignore SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator.RequiredNumericLiteralSeparator
+ $validate_filters[FILTER_VALIDATE_URL]['flags'][] = 65536; // FILTER_FLAG_SCHEME_REQUIRED
+ // phpcs:ignore SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator.RequiredNumericLiteralSeparator
+ $validate_filters[FILTER_VALIDATE_URL]['flags'][] = 131072; // FILTER_FLAG_HOST_REQUIRED
+ }
+
+ if ($codebase->analysis_php_version_id >= 8_02_00) {
+ // phpcs:ignore SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator.RequiredNumericLiteralSeparator
+ $validate_filters[FILTER_VALIDATE_IP]['flags'][] = 268435456; // FILTER_FLAG_GLOBAL_RANGE
+ }
+
+ if ($codebase->analysis_php_version_id >= 7_00_00) {
+ $validate_filters[FILTER_VALIDATE_DOMAIN] = array(
+ 'flags' => array(
+ FILTER_FLAG_HOSTNAME,
+ ),
+ 'options' => array(),
+ );
+ }
+
+ foreach ($validate_filters as $filter_int => $filter_data) {
+ $validate_filters[$filter_int]['flags'] = array_merge(
+ $filter_data['flags'],
+ $general_filter_flags_validate,
+ );
+
+ $default_options = array(
+ 'default' => Type::getMixed(),
+ );
+ $validate_filters[$filter_int]['options'] = array_merge($filter_data['options'], $default_options);
+ }
+
+ // https://www.php.net/manual/en/filter.filters.misc.php
+ $other_filters = array(
+ FILTER_CALLBACK => array(
+ // the docs say that all flags are ignored
+ // however this seems to be incorrect https://github.com/php/doc-en/issues/2708
+ // however they can only be used in the options array, not as a param directly
+ 'flags' => $general_filter_flags_validate,
+ // the options array is required for this filter
+ // and must be a valid callback instead of an array like in other cases
+ 'options' => array(),
+ ),
+ );
+
+ return $sanitize_filters + $validate_filters + $other_filters;
+ }
+}
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php
index 40598d4a1e4..7d56d35cce6 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php
@@ -5,30 +5,19 @@
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
-use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
-use Psalm\Type\Atomic\TFalse;
-use Psalm\Type\Atomic\TKeyedArray;
-use Psalm\Type\Atomic\TLiteralInt;
-use Psalm\Type\Atomic\TNull;
use Psalm\Type\Union;
use UnexpectedValueException;
-use function in_array;
-
-use const FILTER_NULL_ON_FAILURE;
-use const FILTER_SANITIZE_URL;
-use const FILTER_VALIDATE_BOOLEAN;
-use const FILTER_VALIDATE_DOMAIN;
-use const FILTER_VALIDATE_EMAIL;
-use const FILTER_VALIDATE_FLOAT;
-use const FILTER_VALIDATE_INT;
-use const FILTER_VALIDATE_IP;
-use const FILTER_VALIDATE_MAC;
+use function is_array;
+use function is_int;
+
+use const FILTER_CALLBACK;
+use const FILTER_DEFAULT;
+use const FILTER_FLAG_NONE;
use const FILTER_VALIDATE_REGEXP;
-use const FILTER_VALIDATE_URL;
/**
* @internal
@@ -43,135 +32,124 @@ public static function getFunctionIds(): array
return ['filter_var'];
}
- public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Union
+ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
{
- $statements_source = $event->getStatementsSource();
+ $statements_analyzer = $event->getStatementsSource();
+ if (!$statements_analyzer instanceof StatementsAnalyzer) {
+ throw new UnexpectedValueException();
+ }
+
$call_args = $event->getCallArgs();
$function_id = $event->getFunctionId();
$code_location = $event->getCodeLocation();
- if (!$statements_source instanceof StatementsAnalyzer) {
- throw new UnexpectedValueException();
+ $codebase = $statements_analyzer->getCodebase();
+
+ if (! isset($call_args[0])) {
+ return FilterUtils::missingFirstArg($codebase);
}
- $filter_type = null;
-
- if (isset($call_args[1])
- && ($second_arg_type = $statements_source->node_data->getType($call_args[1]->value))
- && $second_arg_type->isSingleIntLiteral()
- ) {
- $filter_type_type = $second_arg_type->getSingleIntLiteral();
-
- switch ($filter_type_type->value) {
- case FILTER_VALIDATE_INT:
- $filter_type = Type::getInt();
- break;
-
- case FILTER_VALIDATE_FLOAT:
- $filter_type = Type::getFloat();
- break;
-
- case FILTER_VALIDATE_BOOLEAN:
- $filter_type = Type::getBool();
-
- break;
-
- case FILTER_VALIDATE_IP:
- case FILTER_VALIDATE_MAC:
- case FILTER_VALIDATE_REGEXP:
- case FILTER_VALIDATE_URL:
- case FILTER_VALIDATE_EMAIL:
- case FILTER_VALIDATE_DOMAIN:
- case FILTER_SANITIZE_URL:
- $filter_type = Type::getString();
- break;
- }
+ $filter_int_used = FILTER_DEFAULT;
+ if (isset($call_args[1])) {
+ $filter_int_used = FilterUtils::getFilterArgValueOrError(
+ $call_args[1],
+ $statements_analyzer,
+ $codebase,
+ );
- $has_object_like = false;
- $filter_null = false;
-
- if (isset($call_args[2])
- && ($third_arg_type = $statements_source->node_data->getType($call_args[2]->value))
- && $filter_type
- ) {
- foreach ($third_arg_type->getAtomicTypes() as $atomic_type) {
- if ($atomic_type instanceof TKeyedArray) {
- $has_object_like = true;
-
- if (isset($atomic_type->properties['options'])
- && $atomic_type->properties['options']->hasArray()
- && ($options_array = $atomic_type->properties['options']->getArray())
- && $options_array instanceof TKeyedArray
- && isset($options_array->properties['default'])
- ) {
- $filter_type = Type::combineUnionTypes(
- $filter_type,
- $options_array->properties['default'],
- );
- } else {
- $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze();
- }
-
- if (isset($atomic_type->properties['flags'])
- && $atomic_type->properties['flags']->isSingleIntLiteral()
- ) {
- $filter_flag_type =
- $atomic_type->properties['flags']->getSingleIntLiteral();
-
- if ($filter_type->hasBool()
- && $filter_flag_type->value === FILTER_NULL_ON_FAILURE
- ) {
- $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze();
- }
- }
- } elseif ($atomic_type instanceof TLiteralInt) {
- if ($atomic_type->value === FILTER_NULL_ON_FAILURE) {
- $filter_null = true;
- $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze();
- }
- }
- }
+ if (!is_int($filter_int_used)) {
+ return $filter_int_used;
}
+ }
+
+ $options = null;
+ $flags_int_used = FILTER_FLAG_NONE;
+ if (isset($call_args[2])) {
+ $helper = FilterUtils::getOptionsArgValueOrError(
+ $call_args[2],
+ $statements_analyzer,
+ $codebase,
+ $code_location,
+ $function_id,
+ $filter_int_used,
+ );
- if (!$has_object_like && !$filter_null && $filter_type) {
- $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze();
+ if (!is_array($helper)) {
+ return $helper;
}
- }
- if (!$filter_type) {
- $filter_type = Type::getMixed();
+ $flags_int_used = $helper['flags_int_used'];
+ $options = $helper['options'];
}
- if ($statements_source->data_flow_graph
- && !in_array('TaintedInput', $statements_source->getSuppressedIssues())
- ) {
- $function_return_sink = DataFlowNode::getForMethodReturn(
- $function_id,
+ // if we reach this point with callback, the callback is missing
+ if ($filter_int_used === FILTER_CALLBACK) {
+ return FilterUtils::missingFilterCallbackCallable(
$function_id,
- null,
$code_location,
+ $statements_analyzer,
+ $codebase,
);
+ }
- $statements_source->data_flow_graph->addNode($function_return_sink);
+ [$default, $min_range, $max_range, $has_range, $regexp] = FilterUtils::getOptions(
+ $filter_int_used,
+ $flags_int_used,
+ $options,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ );
+
+ if (!$default) {
+ [$fails_type] = FilterUtils::getFailsNotSetType($flags_int_used);
+ } else {
+ $fails_type = $default;
+ }
- $function_param_sink = DataFlowNode::getForMethodArgument(
- $function_id,
- $function_id,
- 0,
- null,
- $code_location,
- );
+ if ($filter_int_used === FILTER_VALIDATE_REGEXP && $regexp === null) {
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // any "array" flags are ignored by this filter!
+ return $fails_type;
+ }
- $statements_source->data_flow_graph->addNode($function_param_sink);
+ $input_type = $statements_analyzer->node_data->getType($call_args[0]->value);
- $statements_source->data_flow_graph->addPath(
- $function_param_sink,
- $function_return_sink,
- 'arg',
- );
+ // only return now, as we still want to report errors above
+ if (!$input_type) {
+ return null;
+ }
- return $filter_type->setParentNodes([$function_return_sink->id => $function_return_sink]);
+ $redundant_error_return_type = FilterUtils::checkRedundantFlags(
+ $filter_int_used,
+ $flags_int_used,
+ $fails_type,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ );
+ if ($redundant_error_return_type !== null) {
+ return $redundant_error_return_type;
}
- return $filter_type;
+ return FilterUtils::getReturnType(
+ $filter_int_used,
+ $flags_int_used,
+ $input_type,
+ $fails_type,
+ null,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ $has_range,
+ $min_range,
+ $max_range,
+ $regexp,
+ );
}
}
diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php
index c51c3a2c372..319d6e59b89 100644
--- a/src/Psalm/Internal/Type/TypeParser.php
+++ b/src/Psalm/Internal/Type/TypeParser.php
@@ -642,13 +642,31 @@ private static function getTypeFromGenericTree(
throw new TypeParseTreeException('No generic params provided for type');
}
- if ($generic_type_value === 'array' || $generic_type_value === 'associative-array') {
+ if ($generic_type_value === 'array'
+ || $generic_type_value === 'associative-array'
+ || $generic_type_value === 'non-empty-array'
+ ) {
+ if ($generic_type_value !== 'non-empty-array') {
+ $generic_type_value = 'array';
+ }
+
if ($generic_params[0]->isMixed()) {
$generic_params[0] = Type::getArrayKey($from_docblock);
}
if (count($generic_params) !== 2) {
- throw new TypeParseTreeException('Too many template parameters for array');
+ throw new TypeParseTreeException('Too many template parameters for '.$generic_type_value);
+ }
+
+ if ($type_aliases !== []) {
+ $intersection_types = self::resolveTypeAliases(
+ $codebase,
+ $generic_params[0]->getAtomicTypes(),
+ );
+
+ if ($intersection_types !== []) {
+ $generic_params[0] = $generic_params[0]->setTypes($intersection_types);
+ }
}
foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) {
@@ -672,6 +690,7 @@ private static function getTypeFromGenericTree(
|| $atomic_type instanceof TNever
|| $atomic_type instanceof TTemplateParam
|| $atomic_type instanceof TValueOf
+ || !$from_docblock
) {
continue;
}
@@ -691,7 +710,10 @@ private static function getTypeFromGenericTree(
throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey());
}
- return new TArray($generic_params, $from_docblock);
+ return $generic_type_value === 'array'
+ ? new TArray($generic_params, $from_docblock)
+ : new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock)
+ ;
}
if ($generic_type_value === 'arraylike-object') {
@@ -710,58 +732,6 @@ private static function getTypeFromGenericTree(
);
}
- if ($generic_type_value === 'non-empty-array') {
- if ($generic_params[0]->isMixed()) {
- $generic_params[0] = Type::getArrayKey($from_docblock);
- }
-
- if (count($generic_params) !== 2) {
- throw new TypeParseTreeException('Too many template parameters for non-empty-array');
- }
-
- foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) {
- // PHP 8 values with whitespace after number are counted as numeric
- // and filter_var treats them as such too
- if ($atomic_type instanceof TLiteralString
- && trim($atomic_type->value) === $atomic_type->value
- && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false
- ) {
- $builder = $generic_params[0]->getBuilder();
- $builder->removeType($key);
- $generic_params[0] = $builder->addType(new TLiteralInt($string_to_int, $from_docblock))->freeze();
- continue;
- }
-
- if ($atomic_type instanceof TInt
- || $atomic_type instanceof TString
- || $atomic_type instanceof TArrayKey
- || $atomic_type instanceof TClassConstant // @todo resolve and check types
- || $atomic_type instanceof TMixed
- || $atomic_type instanceof TNever
- || $atomic_type instanceof TTemplateParam
- || $atomic_type instanceof TValueOf
- ) {
- continue;
- }
-
- if ($codebase->register_stub_files || $codebase->register_autoload_files) {
- $builder = $generic_params[0]->getBuilder();
- $builder->removeType($key);
-
- if (count($generic_params[0]->getAtomicTypes()) <= 1) {
- $builder = $builder->addType(new TArrayKey($from_docblock));
- }
-
- $generic_params[0] = $builder->freeze();
- continue;
- }
-
- throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey());
- }
-
- return new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock);
- }
-
if ($generic_type_value === 'iterable') {
if (count($generic_params) > 2) {
throw new TypeParseTreeException('Too many template parameters for iterable');
diff --git a/src/Psalm/Issue/RedundantFlag.php b/src/Psalm/Issue/RedundantFlag.php
new file mode 100644
index 00000000000..d3f0429636d
--- /dev/null
+++ b/src/Psalm/Issue/RedundantFlag.php
@@ -0,0 +1,9 @@
+
*/
diff --git a/src/Psalm/Storage/ClassConstantStorage.php b/src/Psalm/Storage/ClassConstantStorage.php
index 78488412a81..147fcaf709b 100644
--- a/src/Psalm/Storage/ClassConstantStorage.php
+++ b/src/Psalm/Storage/ClassConstantStorage.php
@@ -21,6 +21,7 @@ final class ClassConstantStorage
/** @psalm-suppress MutableDependency Mutable by design */
use CustomMetadataTrait;
use ImmutableNonCloneableTrait;
+ use UnserializeMemoryUsageSuppressionTrait;
public ?CodeLocation $type_location;
diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php
index 8fabc2e3edd..39b02b3ab96 100644
--- a/src/Psalm/Storage/ClassLikeStorage.php
+++ b/src/Psalm/Storage/ClassLikeStorage.php
@@ -12,15 +12,18 @@
use Psalm\Internal\MethodIdentifier;
use Psalm\Internal\Type\TypeAlias\ClassTypeAlias;
use Psalm\Issue\CodeIssue;
+use Psalm\Issue\DeprecatedClass;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
use function array_values;
+use function in_array;
final class ClassLikeStorage implements HasAttributesInterface
{
use CustomMetadataTrait;
+ use UnserializeMemoryUsageSuppressionTrait;
/**
* @var array
@@ -459,4 +462,22 @@ private function hasAttribute(string $fq_class_name): bool
return false;
}
+
+ /**
+ * @return array
+ */
+ public function getSuppressedIssuesForTemplateExtendParams(): array
+ {
+ $allowed_issue_types = [
+ DeprecatedClass::getIssueType(),
+ ];
+ $suppressed_issues_for_template_extend_params = [];
+ foreach ($this->suppressed_issues as $offset => $suppressed_issue) {
+ if (!in_array($suppressed_issue, $allowed_issue_types, true)) {
+ continue;
+ }
+ $suppressed_issues_for_template_extend_params[$offset] = $suppressed_issue;
+ }
+ return $suppressed_issues_for_template_extend_params;
+ }
}
diff --git a/src/Psalm/Storage/EnumCaseStorage.php b/src/Psalm/Storage/EnumCaseStorage.php
index 12cf8b1098e..77870e3cbd8 100644
--- a/src/Psalm/Storage/EnumCaseStorage.php
+++ b/src/Psalm/Storage/EnumCaseStorage.php
@@ -10,6 +10,8 @@
final class EnumCaseStorage
{
+ use UnserializeMemoryUsageSuppressionTrait;
+
public TLiteralString|TLiteralInt|null $value = null;
public CodeLocation $stmt_location;
diff --git a/src/Psalm/Storage/FileStorage.php b/src/Psalm/Storage/FileStorage.php
index ce309152bba..77514caa132 100644
--- a/src/Psalm/Storage/FileStorage.php
+++ b/src/Psalm/Storage/FileStorage.php
@@ -12,6 +12,7 @@
final class FileStorage
{
use CustomMetadataTrait;
+ use UnserializeMemoryUsageSuppressionTrait;
/**
* @var array
diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php
index cdeaee2d3eb..07f335590e5 100644
--- a/src/Psalm/Storage/FunctionLikeParameter.php
+++ b/src/Psalm/Storage/FunctionLikeParameter.php
@@ -14,6 +14,7 @@
final class FunctionLikeParameter implements HasAttributesInterface, TypeNode
{
use CustomMetadataTrait;
+ use UnserializeMemoryUsageSuppressionTrait;
public string $name;
diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php
index 81685917896..f37991fe963 100644
--- a/src/Psalm/Storage/FunctionLikeStorage.php
+++ b/src/Psalm/Storage/FunctionLikeStorage.php
@@ -18,6 +18,7 @@
abstract class FunctionLikeStorage implements HasAttributesInterface
{
use CustomMetadataTrait;
+ use UnserializeMemoryUsageSuppressionTrait;
public ?CodeLocation $location = null;
diff --git a/src/Psalm/Storage/Possibilities.php b/src/Psalm/Storage/Possibilities.php
index 62959661725..09bdad5c23b 100644
--- a/src/Psalm/Storage/Possibilities.php
+++ b/src/Psalm/Storage/Possibilities.php
@@ -14,6 +14,8 @@
final class Possibilities
{
+ use UnserializeMemoryUsageSuppressionTrait;
+
/**
* @var list the rule being asserted
*/
diff --git a/src/Psalm/Storage/PropertyStorage.php b/src/Psalm/Storage/PropertyStorage.php
index 3eb742c281e..5cf159bd2c1 100644
--- a/src/Psalm/Storage/PropertyStorage.php
+++ b/src/Psalm/Storage/PropertyStorage.php
@@ -11,6 +11,7 @@
final class PropertyStorage implements HasAttributesInterface
{
use CustomMetadataTrait;
+ use UnserializeMemoryUsageSuppressionTrait;
public ?bool $is_static = null;
diff --git a/src/Psalm/Storage/UnserializeMemoryUsageSuppressionTrait.php b/src/Psalm/Storage/UnserializeMemoryUsageSuppressionTrait.php
new file mode 100644
index 00000000000..a59b7ce2a02
--- /dev/null
+++ b/src/Psalm/Storage/UnserializeMemoryUsageSuppressionTrait.php
@@ -0,0 +1,24 @@
+ $value) {
+ $this->$key = $value;
+ }
+ }
+}
diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php
index 12f19b3d658..024251cca09 100644
--- a/src/Psalm/Type.php
+++ b/src/Psalm/Type.php
@@ -279,7 +279,7 @@ public static function getString(?string $value = null): Union
return new Union([$value === null ? new TString() : self::getAtomicStringFromLiteral($value)]);
}
- /** @return TLiteralString|TNonEmptyString */
+ /** @return TLiteralString|TNonEmptyString|TNonFalsyString */
public static function getAtomicStringFromLiteral(string $value, bool $from_docblock = false): TString
{
$config = Config::getInstance();
@@ -289,10 +289,12 @@ public static function getAtomicStringFromLiteral(string $value, bool $from_docb
$type = $config->eventDispatcher->dispatchStringInterpreter($event);
if (!$type) {
- if (strlen($value) < $config->max_string_length) {
+ if ($value === '' || strlen($value) < $config->max_string_length) {
$type = new TLiteralString($value, $from_docblock);
- } else {
+ } elseif ($value === '0') {
$type = new TNonEmptyString($from_docblock);
+ } else {
+ $type = new TNonFalsyString($from_docblock);
}
}
diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php
index b80d0af22a0..835bf49d45c 100644
--- a/src/Psalm/Type/Atomic.php
+++ b/src/Psalm/Type/Atomic.php
@@ -13,6 +13,7 @@
use Psalm\Internal\Type\TypeAlias;
use Psalm\Internal\Type\TypeAlias\LinkableTypeAlias;
use Psalm\Internal\TypeVisitor\ClasslikeReplacer;
+use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait;
use Psalm\Type;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TArrayKey;
@@ -81,6 +82,8 @@
*/
abstract class Atomic implements TypeNode
{
+ use UnserializeMemoryUsageSuppressionTrait;
+
public function __construct(bool $from_docblock = false)
{
$this->from_docblock = $from_docblock;
diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php
index f3c10394ad7..0760f088e9d 100644
--- a/src/Psalm/Type/Atomic/TKeyedArray.php
+++ b/src/Psalm/Type/Atomic/TKeyedArray.php
@@ -113,6 +113,8 @@ public function setProperties(array $properties): self
if ($cloned->is_list) {
$last_k = -1;
$had_possibly_undefined = false;
+
+ /** @psalm-suppress InaccessibleProperty */
ksort($cloned->properties);
foreach ($cloned->properties as $k => $v) {
if (is_string($k) || $last_k !== ($k-1) || ($had_possibly_undefined && !$v->possibly_undefined)) {
diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php
index 1d0b7fd0748..ba6b51f3989 100644
--- a/src/Psalm/Type/Union.php
+++ b/src/Psalm/Type/Union.php
@@ -188,6 +188,52 @@ final class Union implements TypeNode
public bool $different = false;
+ private const PROPERTY_KEYS_FOR_UNSERIALIZE = [
+ "\0" . self::class . "\0" . 'types' => 'types',
+ 'from_docblock' => 'from_docblock',
+ 'from_calculation' => 'from_calculation',
+ 'from_property' => 'from_property',
+ 'from_static_property' => 'from_static_property',
+ 'initialized' => 'initialized',
+ 'initialized_class' => 'initialized_class',
+ 'checked' => 'checked',
+ 'failed_reconciliation' => 'failed_reconciliation',
+ 'ignore_nullable_issues' => 'ignore_nullable_issues',
+ 'ignore_falsable_issues' => 'ignore_falsable_issues',
+ 'ignore_isset' => 'ignore_isset',
+ 'possibly_undefined' => 'possibly_undefined',
+ 'possibly_undefined_from_try' => 'possibly_undefined_from_try',
+ 'explicit_never' => 'explicit_never',
+ 'had_template' => 'had_template',
+ 'from_template_default' => 'from_template_default',
+ "\0" . self::class . "\0" . 'literal_string_types' => 'literal_string_types',
+ "\0" . self::class . "\0" . 'typed_class_strings' => 'typed_class_strings',
+ "\0" . self::class . "\0" . 'literal_int_types' => 'literal_int_types',
+ "\0" . self::class . "\0" . 'literal_float_types' => 'literal_float_types',
+ 'by_ref' => 'by_ref',
+ 'reference_free' => 'reference_free',
+ 'allow_mutations' => 'allow_mutations',
+ 'has_mutations' => 'has_mutations',
+ "\0" . self::class . "\0" . 'id' => 'id',
+ "\0" . self::class . "\0" . 'exact_id' => 'exact_id',
+ 'parent_nodes' => 'parent_nodes',
+ 'propagate_parent_nodes' => 'propagate_parent_nodes',
+ 'different' => 'different',
+ ];
+
+ /**
+ * Suppresses memory usage when unserializing objects.
+ *
+ * @see \Psalm\Storage\UnserializeMemoryUsageSuppressionTrait
+ */
+ public function __unserialize(array $properties): void
+ {
+ foreach (self::PROPERTY_KEYS_FOR_UNSERIALIZE as $key => $property_name) {
+ /** @psalm-suppress PossiblyUndefinedStringArrayOffset */
+ $this->$property_name = $properties[$key];
+ }
+ }
+
/**
* @param TProperties $properties
* @return static
diff --git a/stubs/extensions/simplexml.phpstub b/stubs/extensions/simplexml.phpstub
index d2501f62096..66b67530cf1 100644
--- a/stubs/extensions/simplexml.phpstub
+++ b/stubs/extensions/simplexml.phpstub
@@ -69,6 +69,8 @@ class SimpleXMLElement implements Traversable, Countable
public function count(): int {}
public function __get(string $name): SimpleXMLElement|SimpleXMLIterator|null {}
+
+ public function __set(string $name, $value): void {}
}
/**
diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php
index 1c8b15c4969..3e254899342 100644
--- a/tests/AnnotationTest.php
+++ b/tests/AnnotationTest.php
@@ -1365,6 +1365,20 @@ function f(): array
}
EOT,
],
+ 'validArrayKeyAlias' => [
+ 'code' => '
+ */
+ class Foo {}',
+ 'assertions' => [],
+ ],
];
}
diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php
index c2463e43d83..5ecc5533326 100644
--- a/tests/BinaryOperationTest.php
+++ b/tests/BinaryOperationTest.php
@@ -359,6 +359,15 @@ public function providerValidCodeParse(): iterable
'code' => ' [
+ 'code' => ' [
+ '$a===' => 'non-falsy-string',
+ ],
+ 'ignored_issues' => ['InvalidReturnType'],
+ ],
'concatenationWithNumberInWeakMode' => [
'code' => ' [
+ 'code' => '
+ * @psalm-suppress DeprecatedClass
+ */
+ class TheChildClass extends TheParentClass {}
+ '],
];
}
diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php
index eb28397f3ad..302bdeb773f 100644
--- a/tests/FunctionCallTest.php
+++ b/tests/FunctionCallTest.php
@@ -515,19 +515,41 @@ function foo($s): array {
],
'extractVarCheck' => [
'code' => ' "bar"];
+ $foo = "foo";
+ $a = getUnsealedArray();
extract($a);
takesString($foo);',
'assertions' => [],
'ignored_issues' => [
- 'MixedAssignment',
- 'MixedArrayAccess',
'MixedArgument',
],
],
+ 'extractVarCheckValid' => [
+ 'code' => ' 15];
+ extract($a);
+ takesInt($foo);',
+ ],
+ 'extractSkipExtr' => [
+ 'code' => ' "x", "b" => "y"], EXTR_SKIP);',
+ 'assertions' => [
+ '$a===' => '1',
+ '$b===' => '\'y\'',
+ ],
+ ],
'compact' => [
'code' => ' 'string',
],
],
+ 'filterInput' => [
+ 'code' => ' ["default" => null]]);
+ }
+ function filterIntWithDefault(string $s) : int {
+ return filter_var($s, FILTER_VALIDATE_INT, ["options" => ["default" => 5]]);
+ }
+ function filterBool(string $s) : bool {
+ return filter_var($s, FILTER_VALIDATE_BOOLEAN);
+ }
+ function filterNullableBool(string $s) : ?bool {
+ return filter_var($s, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
+ }
+ function filterNullableBoolWithFlagsArray(string $s) : ?bool {
+ return filter_var($s, FILTER_VALIDATE_BOOLEAN, ["flags" => FILTER_NULL_ON_FAILURE]);
+ }
+ function filterFloat(string $s) : float {
+ $filtered = filter_var($s, FILTER_VALIDATE_FLOAT);
+ if ($filtered === false) {
+ return 0.0;
+ }
+ return $filtered;
+ }
+ function filterFloatWithDefault(string $s) : float {
+ return filter_var($s, FILTER_VALIDATE_FLOAT, ["options" => ["default" => 5.0]]);
+ }',
+ ],
'filterVar' => [
'code' => ' 'InvalidScalarArgument',
],
+ 'invalidArgumentCallableWithoutArgsUnion' => [
+ 'code' => ' 'InvalidArgument',
+ ],
'invalidArgumentWithDeclareStrictTypes' => [
'code' => ' [],
'php_version' => '8.1',
],
+ 'extractVarCheckInvalid' => [
+ 'code' => ' 15];
+ extract($a);
+ takesInt($foo);',
+ 'error_message' => 'InvalidScalarArgument',
+ ],
];
}
diff --git a/tests/ImmutableAnnotationTest.php b/tests/ImmutableAnnotationTest.php
index 11e8313932c..3185b14df01 100644
--- a/tests/ImmutableAnnotationTest.php
+++ b/tests/ImmutableAnnotationTest.php
@@ -766,6 +766,81 @@ public function getShortMutating() : string {
}',
'error_message' => 'ImpurePropertyAssignment',
],
+ 'readonlyByRefInClass' => [
+ 'code' => 'values = $values;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function bar()
+ {
+ return reset($this->values);
+ }
+ }',
+ 'error_message' => 'InaccessibleProperty',
+ ],
+ 'readonlyByRef' => [
+ 'code' => 'values = $values;
+ }
+ }
+
+ $x = new Foo([]);
+ reset($x->values);',
+ 'error_message' => 'InaccessibleProperty',
+ ],
+ 'readonlyByRefCustomFunction' => [
+ 'code' => 'values = $values;
+ }
+ }
+
+ /**
+ * @param string $a
+ * @param array $b
+ * @return void
+ */
+ function bar($a, &$b) {}
+
+ $x = new Foo([]);
+ bar("hello", $x->values);',
+ 'error_message' => 'InaccessibleProperty',
+ ],
'preventUnset' => [
'code' => ' 'UndefinedPropertyAssignment',
],
- 'setPropertiesOfSimpleXMLElement1' => [
- 'code' => '");
- $a->b = "c";
- ',
- 'error_message' => 'UndefinedPropertyAssignment',
- ],
- 'setPropertiesOfSimpleXMLElement2' => [
- 'code' => '");
- if (isset($a->b)) {
- $a->b = "c";
- }
- ',
- 'error_message' => 'UndefinedPropertyAssignment',
- ],
];
}
}
diff --git a/tests/ReturnTypeProvider/BasenameTest.php b/tests/ReturnTypeProvider/BasenameTest.php
index 701255887e0..6e61ffea06a 100644
--- a/tests/ReturnTypeProvider/BasenameTest.php
+++ b/tests/ReturnTypeProvider/BasenameTest.php
@@ -34,5 +34,25 @@ public function providerValidCodeParse(): iterable
'$base===' => 'string',
],
];
+
+ yield 'basenameOfStringPathReturnsNonEmptyString' => [
+ 'code' => ' [
+ '$base===' => 'non-empty-string',
+ ],
+ ];
+
+ yield 'basenameOfStringPathReturnsNonFalsyString' => [
+ 'code' => ' [
+ '$base===' => 'non-falsy-string',
+ ],
+ ];
}
}
diff --git a/tests/ReturnTypeProvider/DirnameTest.php b/tests/ReturnTypeProvider/DirnameTest.php
index 894bd1c1c6d..029f91d0a8c 100644
--- a/tests/ReturnTypeProvider/DirnameTest.php
+++ b/tests/ReturnTypeProvider/DirnameTest.php
@@ -51,7 +51,27 @@ public function providerValidCodeParse(): iterable
$dir = dirname(uniqid() . "abc", 2);
',
'assertions' => [
- '$dir===' => 'non-empty-string',
+ '$dir===' => 'non-falsy-string',
+ ],
+ ];
+
+ yield 'dirnameOfNonEmptyShouldBeNonFalsy' => [
+ 'code' => ' [
+ '$dir===' => 'non-falsy-string',
+ ],
+ ];
+
+ yield 'dirnameOfEmptyShouldBeString' => [
+ 'code' => ' [
+ '$dir===' => 'string',
],
];
}
diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php
index cb03d9f1138..bb1447e26ff 100644
--- a/tests/TypeCombinationTest.php
+++ b/tests/TypeCombinationTest.php
@@ -122,6 +122,13 @@ function takesLiteralString($arg) {}
takesLiteralString($c);
}',
],
+ 'tooLongLiteralShouldBeNonFalsyString' => [
+ 'code' => ' [
+ '$x===' => 'non-falsy-string',
+ ],
+ ],
];
}
diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php
index 460ea919a3c..fe6327a72ed 100644
--- a/tests/TypeReconciliation/ConditionalTest.php
+++ b/tests/TypeReconciliation/ConditionalTest.php
@@ -1420,9 +1420,9 @@ function foo(A $a) : B {
'nullCoalescePossibleMixed' => [
'code' => '|false|string
*/
- function foo() : array {
+ function foo() {
return filter_input(INPUT_POST, "some_var") ?? [];
}',
],