diff --git a/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php index 707910f80..2b2563582 100644 --- a/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php @@ -249,20 +249,30 @@ class FunctionAnalyzer extends FunctionLikeAnalyzer break; case 'explode': - if (count($call_args) === 2) { + if (count($call_args) >= 2) { + $can_return_empty = isset($call_args[2]) + && ( + !$call_args[2]->value instanceof PhpParser\Node\Scalar\LNumber + || $call_args[2]->value->value < 0 + ); + if ($call_args[0]->value instanceof PhpParser\Node\Scalar\String_) { if ($call_args[0]->value->value === '') { return Type::getFalse(); } return new Type\Union([ - new Type\Atomic\TNonEmptyList(Type::getString()) + $can_return_empty + ? new Type\Atomic\TList(Type::getString()) + : new Type\Atomic\TNonEmptyList(Type::getString()) ]); } elseif (isset($call_args[0]->value->inferredType) && $call_args[0]->value->inferredType->hasString() ) { $falsable_array = new Type\Union([ - new Type\Atomic\TNonEmptyList(Type::getString()), + $can_return_empty + ? new Type\Atomic\TList(Type::getString()) + : new Type\Atomic\TNonEmptyList(Type::getString()), new Type\Atomic\TFalse ]); diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 1787ef9ca..bd06e4fb1 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -945,19 +945,93 @@ class FunctionCallTest extends TestCase '$c' => 'int', ], ], + 'explode' => [ + ' [ + '$elements' => 'non-empty-list', + ], + ], + 'explodeWithPositiveLimit' => [ + ' [ + '$elements' => 'non-empty-list', + ], + ], + 'explodeWithNegativeLimit' => [ + ' [ + '$elements' => 'list', + ], + ], + 'explodeWithDynamicLimit' => [ + ' [ + '$elements' => 'list', + ], + ], + 'explodeWithDynamicDelimiter' => [ + ' [ + '$elements' => 'false|non-empty-list', + ], + ], + 'explodeWithDynamicDelimiterAndPositiveLimit' => [ + ' [ + '$elements' => 'false|non-empty-list', + ], + ], + 'explodeWithDynamicDelimiterAndNegativeLimit' => [ + ' [ + '$elements' => 'false|list', + ], + ], + 'explodeWithDynamicDelimiterAndLimit' => [ + ' [ + '$elements' => 'false|list', + ], + ], 'explodeWithPossiblyFalse' => [ ' */ - function exploder(string $s) : array { - return explode(" ", $s); + /** @return non-empty-list */ + function exploder(string $d, string $s) : array { + return explode($d, $s); }', ], - 'explodeWithThirdArg' => [ - ' [ '