diff --git a/src/Rules/Functions/PrintfHelper.php b/src/Rules/Functions/PrintfHelper.php index 411972885d..19d76accf4 100644 --- a/src/Rules/Functions/PrintfHelper.php +++ b/src/Rules/Functions/PrintfHelper.php @@ -12,6 +12,7 @@ use function max; use function sprintf; use function strlen; +use function strstr; use const PREG_SET_ORDER; #[AutowiredService] @@ -37,7 +38,7 @@ public function getPrintfPlaceholders(string $format): ?array public function getScanfPlaceholdersCount(string $format): ?int { - return $this->getPlaceholdersCount('(?[cdDeEfinosuxX%s]|\[[^\]]+\])', $format, true); + return $this->getPlaceholdersCount('(?:[lLh]?(?[cdDeEfginosuxX%s]|\[[^\]]+\]))', $format, true); } /** @@ -45,6 +46,13 @@ public function getScanfPlaceholdersCount(string $format): ?int */ private function parsePlaceholders(string $specifiersPattern, string $format, bool $isScanf): ?array { + if ($isScanf) { + $beforeNul = strstr($format, "\0", true); + if ($beforeNul !== false) { + $format = $beforeNul; + } + } + $addSpecifier = ''; if ($this->phpVersion->supportsHhPrintfSpecifier()) { $addSpecifier .= 'hH'; diff --git a/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php index de22ba0a46..efaec2f755 100644 --- a/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php @@ -21,6 +21,7 @@ use function count; use function in_array; use function preg_match_all; +use function strstr; #[AutowiredService] final class SscanfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -48,9 +49,15 @@ public function getTypeFromFunctionCall( return null; } - if (preg_match_all('/%(\d*)(\[[^\]]+\]|[cdeEfosux]{1})/', $formatType->getValue(), $matches) > 0) { - $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + $formatValue = $formatType->getValue(); + $beforeNul = strstr($formatValue, "\0", true); + if ($beforeNul !== false) { + $formatValue = $beforeNul; + } + + $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + if (preg_match_all('/%(\d*)[lLh]?(\[[^\]]+\]|[cDdeEfginosuxX])/', $formatValue, $matches) > 0) { for ($i = 0; $i < count($matches[0]); $i++) { $length = $matches[1][$i]; $specifier = $matches[2][$i]; @@ -70,22 +77,24 @@ public function getTypeFromFunctionCall( } } - if (in_array($specifier, ['d', 'o', 'u', 'x'], true)) { + if (in_array($specifier, ['d', 'D', 'i', 'n', 'o', 'x', 'X'], true)) { $type = new IntegerType(); } - if (in_array($specifier, ['e', 'E', 'f'], true)) { + if ($specifier === 'u') { + $type = TypeCombinator::union(new IntegerType(), new StringType()); + } + + if (in_array($specifier, ['e', 'E', 'f', 'g'], true)) { $type = new FloatType(); } $type = TypeCombinator::addNull($type); $arrayBuilder->setOffsetValueType(new ConstantIntegerType($i), $type); } - - return TypeCombinator::addNull($arrayBuilder->getArray()); } - return null; + return TypeCombinator::addNull($arrayBuilder->getArray()); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14567.php b/tests/PHPStan/Analyser/nsrt/bug-14567.php new file mode 100644 index 0000000000..51ce2b4222 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14567.php @@ -0,0 +1,56 @@ + PHP_INT_MAX + assertType('array{int|string|null}|null', sscanf($s, "%u")); + + // mixed specifiers with %n + assertType('array{int|null, int|null}|null', sscanf($s, "%d%n")); + + // Size modifiers (l, L, h) — consumed by ValidateFormat, no effect on PHP type + assertType('array{int|null}|null', sscanf($s, "%ld")); + assertType('array{float|null}|null', sscanf($s, "%lf")); + assertType('array{float|null}|null', sscanf($s, "%Lf")); + assertType('array{int|null}|null', sscanf($s, "%hd")); + assertType('array{int|string|null}|null', sscanf($s, "%lu")); + assertType('array{int|null, float|null, string|null}|null', sscanf($s, "%ld %lf %s")); +} diff --git a/tests/PHPStan/Analyser/nsrt/sscanf.php b/tests/PHPStan/Analyser/nsrt/sscanf.php index 484febdf9b..49bc59c785 100644 --- a/tests/PHPStan/Analyser/nsrt/sscanf.php +++ b/tests/PHPStan/Analyser/nsrt/sscanf.php @@ -20,7 +20,7 @@ function sscanfFormatInference(string $s) { assertType('array{float|null}|null', sscanf($s, '%f')); assertType('array{int|null}|null', sscanf($s, '%o')); assertType('array{string|null}|null', sscanf($s, '%s')); - assertType('array{int|null}|null', sscanf($s, '%u')); + assertType('array{int|string|null}|null', sscanf($s, '%u')); assertType('array{int|null}|null', sscanf($s, '%x')); $mandate = "January 01 2000"; diff --git a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php index 1e3ab9ddd7..2ce8eb01a8 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -147,4 +147,9 @@ public function testBug10260(): void $this->analyse([__DIR__ . '/data/bug-10260.php'], []); } + public function testBug14567(): void + { + $this->analyse([__DIR__ . '/data/bug-14567.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-14567.php b/tests/PHPStan/Rules/Functions/data/bug-14567.php new file mode 100644 index 0000000000..8b2f01218b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-14567.php @@ -0,0 +1,51 @@ +