Skip to content

Commit a799ce4

Browse files
committed
fixup! Guard *scanf() return type extension by counter
Return `NeverType` on PHP 8+ (where an invalid format throws `ValueError`) and `NullType` on PHP < 8.0. This makes the gatekeeper precise about the call never returning on modern PHP. The test strategy for the version‑dependent types will be settled in review – the CI now whispers `*NEVER*` where `null` stood before.
1 parent 6d1d527 commit a799ce4

5 files changed

Lines changed: 31 additions & 6 deletions

File tree

src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpParser\Node\Expr\FuncCall;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\DependencyInjection\AutowiredService;
8+
use PHPStan\Php\PhpVersion;
89
use PHPStan\Reflection\FunctionReflection;
910
use PHPStan\Rules\Functions\PrintfHelper;
1011
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
@@ -16,6 +17,7 @@
1617
use PHPStan\Type\FloatType;
1718
use PHPStan\Type\IntegerType;
1819
use PHPStan\Type\IntersectionType;
20+
use PHPStan\Type\NeverType;
1921
use PHPStan\Type\NullType;
2022
use PHPStan\Type\StringType;
2123
use PHPStan\Type\Type;
@@ -28,7 +30,10 @@
2830
final class SscanfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
2931
{
3032

31-
public function __construct(private PrintfHelper $printfHelper)
33+
public function __construct(
34+
private PrintfHelper $printfHelper,
35+
private PhpVersion $phpVersion,
36+
)
3237
{
3338
}
3439

@@ -56,7 +61,7 @@ public function getTypeFromFunctionCall(
5661

5762
$placeholderCount = $this->printfHelper->getScanfPlaceholdersCount($formatType->getValue());
5863
if ($placeholderCount === null) {
59-
return new NullType();
64+
return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new NullType();
6065
}
6166

6267
if (preg_match_all('/%(\d*)(\[[^\]]+\]|[cdeEfosux]{1})/', $formatType->getValue(), $matches) > 0) {

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,12 @@ private static function findTestFiles(): iterable
284284
yield __DIR__ . '/../Rules/Variables/data/bug-14124.php';
285285
yield __DIR__ . '/../Rules/Variables/data/bug-14124b.php';
286286
yield __DIR__ . '/../Rules/Arrays/data/bug-14308.php';
287+
288+
if (PHP_VERSION_ID < 80000) {
289+
yield __DIR__ . '/data/sscanf-php74.php';
290+
} else {
291+
yield __DIR__ . '/data/sscanf-php80.php';
292+
}
287293
}
288294

289295
/**
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace SscanfPHP74;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function sscanfInvalidFormatMixingPositionalWithSequential(string $s) {
8+
assertType('null', sscanf($s, '%1$s %s'));
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace SscanfPHP80;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function sscanfInvalidFormatMixingPositionalWithSequential(string $s) {
8+
assertType('*NEVER*', sscanf($s, '%1$s %s'));
9+
}

tests/PHPStan/Analyser/nsrt/sscanf.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,3 @@ function sscanfSuppression(string $s) {
5555
assertType('array{string|null}|null', sscanf($s, '%*d %s'));
5656
assertType('array{int|null}|null', sscanf($s, '%*[a-z]%d'));
5757
}
58-
59-
function sscanfInvalidFormatMixingPositionalWithSequential(string $s) {
60-
assertType('null', sscanf($s, '%1$s %s'));
61-
}

0 commit comments

Comments
 (0)