diff --git a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php index f12d949d86..1167f5e3fc 100644 --- a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php @@ -2,17 +2,22 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function base64_decode; +use function count; #[AutowiredService] final class Base64DecodeDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -30,10 +35,14 @@ public function getTypeFromFunctionCall( ): Type { $args = $functionCall->getArgs(); - if (!isset($args[1])) { + if (!isset($args[0])) { return new StringType(); } + if (!isset($args[1])) { + return $this->resolveType($scope, $args[0]->value, false) ?? new StringType(); + } + $argType = $scope->getType($args[1]->value); if ($argType instanceof MixedType) { @@ -44,10 +53,11 @@ public function getTypeFromFunctionCall( $isFalseType = $argType->isFalse(); $compareTypes = $isTrueType->compareTo($isFalseType); if ($compareTypes === $isTrueType) { - return new UnionType([new StringType(), new ConstantBooleanType(false)]); + return $this->resolveType($scope, $args[0]->value, true) + ?? new UnionType([new StringType(), new ConstantBooleanType(false)]); } if ($compareTypes === $isFalseType) { - return new StringType(); + return $this->resolveType($scope, $args[0]->value, false) ?? new StringType(); } // second argument could be interpreted as true @@ -55,7 +65,34 @@ public function getTypeFromFunctionCall( return new UnionType([new StringType(), new ConstantBooleanType(false)]); } - return new StringType(); + return $this->resolveType($scope, $args[0]->value, false) ?? new StringType(); + } + + private function resolveType(Scope $scope, Expr $stringArg, bool $strict): ?Type + { + $constantStrings = $scope->getType($stringArg)->getConstantStrings(); + if (count($constantStrings) === 0) { + return null; + } + + $resultTypes = []; + foreach ($constantStrings as $constantString) { + $decoded = base64_decode($constantString->getValue(), true); + if ($decoded === false) { + // In non-strict mode base64_decode is lenient about invalid input, + // so leave the result as a generic string instead of guessing the value. + if (!$strict) { + return null; + } + + $resultTypes[] = new ConstantBooleanType(false); + continue; + } + + $resultTypes[] = new ConstantStringType($decoded); + } + + return TypeCombinator::union(...$resultTypes); } } diff --git a/tests/PHPStan/Analyser/nsrt/base64_decode.php b/tests/PHPStan/Analyser/nsrt/base64_decode.php index 34de145d9a..5d912457b2 100644 --- a/tests/PHPStan/Analyser/nsrt/base64_decode.php +++ b/tests/PHPStan/Analyser/nsrt/base64_decode.php @@ -18,4 +18,13 @@ public function strictMode(string $string): void assertType('string|false', base64_decode($string, true)); } + public function constantInput(): void + { + assertType("'Hello world'", base64_decode('SGVsbG8gd29ybGQ=')); + assertType("'Hello world'", base64_decode('SGVsbG8gd29ybGQ=', false)); + assertType("'Hello world'", base64_decode('SGVsbG8gd29ybGQ=', true)); + assertType('false', base64_decode('not valid base64 @@@', true)); + assertType('string', base64_decode('not valid base64 @@@', false)); + } + } diff --git a/tests/PHPStan/Analyser/nsrt/functions.php b/tests/PHPStan/Analyser/nsrt/functions.php index af90a6a67d..ba6a7cdfb1 100644 --- a/tests/PHPStan/Analyser/nsrt/functions.php +++ b/tests/PHPStan/Analyser/nsrt/functions.php @@ -165,10 +165,10 @@ assertType('array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', $lstat); assertType('array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', $fstat); assertType('array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}', $fileObjectStat); -assertType('string', $base64DecodeWithoutStrict); -assertType('string', $base64DecodeWithStrictDisabled); -assertType('string|false', $base64DecodeWithStrictEnabled); -assertType('string', $base64DecodeDefault); +assertType("''", $base64DecodeWithoutStrict); +assertType("''", $base64DecodeWithStrictDisabled); +assertType("''", $base64DecodeWithStrictEnabled); +assertType("''", $base64DecodeDefault); assertType('(string|false)', $base64DecodeBenevolent); assertType('*ERROR*', $strWordCountWithoutParameters); assertType('*ERROR*', $strWordCountWithTooManyParams);