Skip to content

Commit ac55b36

Browse files
committed
Fix counting *scanf() format string placeholders
Update PrintfHelper.php to make public function getScanfPlaceholdersCount(string $format): ?int` return the sscanf() vetted number of placeholders that return/assign conversions. Addresses long-standing regressions reported originally in [phpstan-10260]. References: - [phpstan-10260] - phpstan/phpstan#10260 (comment) - [phpstan-src-5591] - [phpstan-14567] - https://3v4l.org/WR85Q [phpstan-10260]: phpstan/phpstan#10260 [phpstan-src-5591]: #5591 [phpstan-14567]: phpstan/phpstan#14567
1 parent ee9819a commit ac55b36

2 files changed

Lines changed: 92 additions & 10 deletions

File tree

src/Rules/Functions/PrintfHelper.php

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
use Nette\Utils\Strings;
66
use PHPStan\DependencyInjection\AutowiredService;
77
use PHPStan\Php\PhpVersion;
8+
use ValueError;
89
use function array_filter;
910
use function array_keys;
1011
use function count;
1112
use function in_array;
1213
use function max;
1314
use function sprintf;
15+
use function sscanf;
1416
use function strlen;
1517
use const PREG_SET_ORDER;
1618

@@ -26,24 +28,36 @@ public function __construct(private PhpVersion $phpVersion)
2628

2729
public function getPrintfPlaceholdersCount(string $format): ?int
2830
{
29-
return $this->getPlaceholdersCount(self::PRINTF_SPECIFIER_PATTERN, $format, false);
31+
return $this->getPlaceholdersCount(self::PRINTF_SPECIFIER_PATTERN, $format);
3032
}
3133

3234
/** @phpstan-return array<int, non-empty-list<PrintfPlaceholder>> parameter index => placeholders */
3335
public function getPrintfPlaceholders(string $format): ?array
3436
{
35-
return $this->parsePlaceholders(self::PRINTF_SPECIFIER_PATTERN, $format, false);
37+
return $this->parsePlaceholders(self::PRINTF_SPECIFIER_PATTERN, $format);
3638
}
3739

3840
public function getScanfPlaceholdersCount(string $format): ?int
3941
{
40-
return $this->getPlaceholdersCount('(?<specifier>[cdDeEfinosuxX%s]|\[[^\]]+\])', $format, true);
42+
if ($this->phpVersion->throwsValueErrorForInternalFunctions()) {
43+
try {
44+
$result = sscanf('', '%*n' . $format);
45+
} catch (ValueError) {
46+
return null;
47+
}
48+
} else {
49+
$result = @sscanf('', '%*n' . $format);
50+
}
51+
if ($result === null) {
52+
return null;
53+
}
54+
return count($result);
4155
}
4256

4357
/**
4458
* @phpstan-return array<int, non-empty-list<PrintfPlaceholder>>|null parameter index => placeholders
4559
*/
46-
private function parsePlaceholders(string $specifiersPattern, string $format, bool $isScanf): ?array
60+
private function parsePlaceholders(string $specifiersPattern, string $format): ?array
4761
{
4862
$addSpecifier = '';
4963
if ($this->phpVersion->supportsHhPrintfSpecifier()) {
@@ -72,10 +86,6 @@ private function parsePlaceholders(string $specifiersPattern, string $format, bo
7286
$showValueSuffix = false;
7387

7488
if (isset($placeholder['width']) && $placeholder['width'] !== '') {
75-
if ($isScanf) {
76-
// In scanf, * means assignment suppression - skip this placeholder entirely
77-
continue;
78-
}
7989
$parsedPlaceholders[] = new PrintfPlaceholder(
8090
sprintf('"%s" (width)', $placeholder[0]),
8191
$parameterIdx++,
@@ -136,9 +146,9 @@ private function getAcceptingTypeBySpecifier(string $specifier): string
136146
return 'mixed';
137147
}
138148

139-
private function getPlaceholdersCount(string $specifiersPattern, string $format, bool $isScanf): ?int
149+
private function getPlaceholdersCount(string $specifiersPattern, string $format): ?int
140150
{
141-
$placeholdersMap = $this->parsePlaceholders($specifiersPattern, $format, $isScanf);
151+
$placeholdersMap = $this->parsePlaceholders($specifiersPattern, $format);
142152
if ($placeholdersMap === null) {
143153
return null;
144154
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use Override;
6+
use PHPStan\Php\PhpVersion;
7+
use PHPStan\Testing\PHPStanTestCase;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use PHPUnit\Framework\Attributes\RequiresPhp;
10+
use ValueError;
11+
use const PHP_VERSION_ID;
12+
13+
class PrintfHelperTest extends PHPStanTestCase
14+
{
15+
16+
private PrintfHelper $printf;
17+
18+
#[Override]
19+
protected function setUp(): void
20+
{
21+
$this->printf = $this->getPhpVersionIdAwareHelper(PHP_VERSION_ID);
22+
}
23+
24+
#[RequiresPhp('< 8.0.0')]
25+
public function testReturnsNullForInvalidPatternOnLegacyPhpVersion(): void
26+
{
27+
$this->assertNull($this->printf->getScanfPlaceholdersCount('%a'));
28+
}
29+
30+
#[RequiresPhp('>= 8.0.0')]
31+
public function testReturnsNullForInvalidPatternOnPhp8(): void
32+
{
33+
$this->assertNull($this->printf->getScanfPlaceholdersCount('%a'));
34+
}
35+
36+
#[RequiresPhp('>= 8.0.0')]
37+
#[DataProvider('dataLegacyVersionIds')]
38+
public function testLegacyVersionStillThrowsValueErrorOnPhp8(int $versionId): void
39+
{
40+
$helper = $this->getPhpVersionIdAwareHelper($versionId);
41+
$this->expectException(ValueError::class);
42+
$this->expectExceptionMessage('Bad scan conversion character "a"');
43+
$helper->getScanfPlaceholdersCount('%a');
44+
$this->fail('check your phpunit');
45+
}
46+
47+
private function getPhpVersionIdAwareHelper(int $versionId): PrintfHelper
48+
{
49+
return new PrintfHelper($this->getPhpVersion($versionId));
50+
}
51+
52+
private function getPhpVersion(int $versionId): PhpVersion
53+
{
54+
return new PhpVersion($versionId);
55+
}
56+
57+
public static function dataLegacyVersionIds(): array
58+
{
59+
return [
60+
'PHP 4.3.14' => [40314],
61+
'PHP 5.0.20' => [50020],
62+
'PHP 5.3.23' => [50323],
63+
'PHP 5.5.36' => [50536],
64+
'PHP 7.0.16' => [70016],
65+
'PHP 7.1.0' => [70100],
66+
'PHP 7.2.22' => [70222],
67+
'PHP 7.3.0' => [70300],
68+
'PHP 7.4.33' => [70433],
69+
];
70+
}
71+
72+
}

0 commit comments

Comments
 (0)