diff --git a/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php index de22ba0a46..a079f62fe8 100644 --- a/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php @@ -5,7 +5,9 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Rules\Functions\PrintfHelper; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -15,17 +17,27 @@ use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function count; use function in_array; use function preg_match_all; +use function strstr; #[AutowiredService] final class SscanfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct( + private PrintfHelper $printfHelper, + private PhpVersion $phpVersion, + ) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['sscanf', 'fscanf'], true); @@ -48,8 +60,28 @@ public function getTypeFromFunctionCall( return null; } - if (preg_match_all('/%(\d*)(\[[^\]]+\]|[cdeEfosux]{1})/', $formatType->getValue(), $matches) > 0) { - $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $formatValue = $formatType->getValue(); + $placeholderCount = $this->printfHelper->getScanfPlaceholdersCount($formatValue); + if ($placeholderCount === null) { + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new NullType(); + } + + $beforeNul = strstr($formatValue, "\0", true); + if ($beforeNul !== false) { + $formatValue = $beforeNul; + } + + $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + + if ($formatValue === '') { + return $arrayBuilder->getArray(); + } + + if ($placeholderCount === 0) { + return TypeCombinator::addNull($arrayBuilder->getArray()); + } + + if (preg_match_all('/%(\d*)(\[[^\]]+\]|[cdeEfosux]{1})/', $formatValue, $matches) > 0) { for ($i = 0; $i < count($matches[0]); $i++) { $length = $matches[1][$i]; @@ -82,10 +114,9 @@ public function getTypeFromFunctionCall( $arrayBuilder->setOffsetValueType(new ConstantIntegerType($i), $type); } - return TypeCombinator::addNull($arrayBuilder->getArray()); } - return null; + return TypeCombinator::addNull($arrayBuilder->getArray()); } } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index a8903a13df..ced33a0ee5 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -284,6 +284,12 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Variables/data/bug-14124.php'; yield __DIR__ . '/../Rules/Variables/data/bug-14124b.php'; yield __DIR__ . '/../Rules/Arrays/data/bug-14308.php'; + + if (PHP_VERSION_ID < 80000) { + yield __DIR__ . '/data/sscanf-php74.php'; + } else { + yield __DIR__ . '/data/sscanf-php80.php'; + } } /** diff --git a/tests/PHPStan/Analyser/data/sscanf-php74.php b/tests/PHPStan/Analyser/data/sscanf-php74.php new file mode 100644 index 0000000000..19b89bc0bb --- /dev/null +++ b/tests/PHPStan/Analyser/data/sscanf-php74.php @@ -0,0 +1,9 @@ +