Skip to content

Commit 14d0951

Browse files
staabmondrejmirtes
authored andcommitted
Refactor PrintfParametersRule
1 parent c5f4a47 commit 14d0951

File tree

4 files changed

+125
-81
lines changed

4 files changed

+125
-81
lines changed

conf/config.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,6 +1886,9 @@ services:
18861886
-
18871887
class: PHPStan\Type\Constant\OversizedArrayBuilder
18881888

1889+
-
1890+
class: PHPStan\Rules\Functions\PrintfHelper
1891+
18891892
exceptionTypeResolver:
18901893
class: PHPStan\Rules\Exceptions\ExceptionTypeResolver
18911894
factory: @PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver

src/Rules/Functions/PrintfHelper.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use Nette\Utils\Strings;
6+
use PHPStan\Php\PhpVersion;
7+
use function array_filter;
8+
use function count;
9+
use function max;
10+
use function sprintf;
11+
use function strlen;
12+
use const PREG_SET_ORDER;
13+
14+
final class PrintfHelper
15+
{
16+
17+
public function __construct(private PhpVersion $phpVersion)
18+
{
19+
}
20+
21+
public function getPrintfPlaceholdersCount(string $format): int
22+
{
23+
return $this->getPlaceholdersCount('(?:[bs%s]|l?[cdeEgfFGouxX])', $format);
24+
}
25+
26+
public function getScanfPlaceholdersCount(string $format): int
27+
{
28+
return $this->getPlaceholdersCount('(?:[cdDeEfinosuxX%s]|\[[^\]]+\])', $format);
29+
}
30+
31+
private function getPlaceholdersCount(string $specifiersPattern, string $format): int
32+
{
33+
$addSpecifier = '';
34+
if ($this->phpVersion->supportsHhPrintfSpecifier()) {
35+
$addSpecifier .= 'hH';
36+
}
37+
38+
$specifiers = sprintf($specifiersPattern, $addSpecifier);
39+
40+
$pattern = '~(?<before>%*)%(?:(?<position>\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?(?<width>\*)?-?\d*(?:\.(?:\d+|(?<precision>\*))?)?' . $specifiers . '~';
41+
42+
$matches = Strings::matchAll($format, $pattern, PREG_SET_ORDER);
43+
44+
if (count($matches) === 0) {
45+
return 0;
46+
}
47+
48+
$placeholders = array_filter($matches, static fn (array $match): bool => strlen($match['before']) % 2 === 0);
49+
50+
if (count($placeholders) === 0) {
51+
return 0;
52+
}
53+
54+
$maxPositionedNumber = 0;
55+
$maxOrdinaryNumber = 0;
56+
foreach ($placeholders as $placeholder) {
57+
if (isset($placeholder['width']) && $placeholder['width'] !== '') {
58+
$maxOrdinaryNumber++;
59+
}
60+
61+
if (isset($placeholder['precision']) && $placeholder['precision'] !== '') {
62+
$maxOrdinaryNumber++;
63+
}
64+
65+
if (isset($placeholder['position']) && $placeholder['position'] !== '') {
66+
$maxPositionedNumber = max((int) $placeholder['position'], $maxPositionedNumber);
67+
} else {
68+
$maxOrdinaryNumber++;
69+
}
70+
}
71+
72+
return max($maxPositionedNumber, $maxOrdinaryNumber);
73+
}
74+
75+
}

src/Rules/Functions/PrintfParametersRule.php

Lines changed: 43 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,40 @@
22

33
namespace PHPStan\Rules\Functions;
44

5-
use Nette\Utils\Strings;
65
use PhpParser\Node;
76
use PhpParser\Node\Expr\FuncCall;
87
use PHPStan\Analyser\Scope;
9-
use PHPStan\Php\PhpVersion;
8+
use PHPStan\Reflection\ReflectionProvider;
109
use PHPStan\Rules\Rule;
1110
use PHPStan\Rules\RuleErrorBuilder;
12-
use function array_filter;
1311
use function array_key_exists;
1412
use function count;
1513
use function in_array;
16-
use function max;
1714
use function sprintf;
18-
use function strlen;
19-
use function strtolower;
20-
use const PREG_SET_ORDER;
2115

2216
/**
2317
* @implements Rule<Node\Expr\FuncCall>
2418
*/
2519
class PrintfParametersRule implements Rule
2620
{
2721

28-
public function __construct(private PhpVersion $phpVersion)
22+
private const FORMAT_ARGUMENT_POSITIONS = [
23+
'printf' => 0,
24+
'sprintf' => 0,
25+
'sscanf' => 1,
26+
'fscanf' => 1,
27+
];
28+
private const MINIMUM_NUMBER_OF_ARGUMENTS = [
29+
'printf' => 1,
30+
'sprintf' => 1,
31+
'sscanf' => 3,
32+
'fscanf' => 3,
33+
];
34+
35+
public function __construct(
36+
private PrintfHelper $printfHelper,
37+
private ReflectionProvider $reflectionProvider,
38+
)
2939
{
3040
}
3141

@@ -40,25 +50,17 @@ public function processNode(Node $node, Scope $scope): array
4050
return [];
4151
}
4252

43-
$functionsArgumentPositions = [
44-
'printf' => 0,
45-
'sprintf' => 0,
46-
'sscanf' => 1,
47-
'fscanf' => 1,
48-
];
49-
$minimumNumberOfArguments = [
50-
'printf' => 1,
51-
'sprintf' => 1,
52-
'sscanf' => 3,
53-
'fscanf' => 3,
54-
];
55-
56-
$name = strtolower((string) $node->name);
57-
if (!array_key_exists($name, $functionsArgumentPositions)) {
53+
if (!$this->reflectionProvider->hasFunction($node->name, $scope)) {
5854
return [];
5955
}
6056

61-
$formatArgumentPosition = $functionsArgumentPositions[$name];
57+
$functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);
58+
$name = $functionReflection->getName();
59+
if (!array_key_exists($name, self::FORMAT_ARGUMENT_POSITIONS)) {
60+
return [];
61+
}
62+
63+
$formatArgumentPosition = self::FORMAT_ARGUMENT_POSITIONS[$name];
6264

6365
$args = $node->getArgs();
6466
foreach ($args as $arg) {
@@ -67,38 +69,44 @@ public function processNode(Node $node, Scope $scope): array
6769
}
6870
}
6971
$argsCount = count($args);
70-
if ($argsCount < $minimumNumberOfArguments[$name]) {
72+
if ($argsCount < self::MINIMUM_NUMBER_OF_ARGUMENTS[$name]) {
7173
return []; // caught by CallToFunctionParametersRule
7274
}
7375

7476
$formatArgType = $scope->getType($args[$formatArgumentPosition]->value);
75-
$placeHoldersCount = null;
77+
$maxPlaceHoldersCount = null;
7678
foreach ($formatArgType->getConstantStrings() as $formatString) {
7779
$format = $formatString->getValue();
78-
$tempPlaceHoldersCount = $this->getPlaceholdersCount($name, $format);
79-
if ($placeHoldersCount === null) {
80-
$placeHoldersCount = $tempPlaceHoldersCount;
81-
} elseif ($tempPlaceHoldersCount > $placeHoldersCount) {
82-
$placeHoldersCount = $tempPlaceHoldersCount;
80+
81+
if (in_array($name, ['sprintf', 'printf'], true)) {
82+
$tempPlaceHoldersCount = $this->printfHelper->getPrintfPlaceholdersCount($format);
83+
} else {
84+
$tempPlaceHoldersCount = $this->printfHelper->getScanfPlaceholdersCount($format);
85+
}
86+
87+
if ($maxPlaceHoldersCount === null) {
88+
$maxPlaceHoldersCount = $tempPlaceHoldersCount;
89+
} elseif ($tempPlaceHoldersCount > $maxPlaceHoldersCount) {
90+
$maxPlaceHoldersCount = $tempPlaceHoldersCount;
8391
}
8492
}
8593

86-
if ($placeHoldersCount === null) {
94+
if ($maxPlaceHoldersCount === null) {
8795
return [];
8896
}
8997

9098
$argsCount -= $formatArgumentPosition;
9199

92-
if ($argsCount !== $placeHoldersCount + 1) {
100+
if ($argsCount !== $maxPlaceHoldersCount + 1) {
93101
return [
94102
RuleErrorBuilder::message(sprintf(
95103
sprintf(
96104
'%s, %s.',
97-
$placeHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders',
105+
$maxPlaceHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders',
98106
$argsCount - 1 === 1 ? '%d value given' : '%d values given',
99107
),
100108
$name,
101-
$placeHoldersCount,
109+
$maxPlaceHoldersCount,
102110
$argsCount - 1,
103111
))->identifier(sprintf('argument.%s', $name))->build(),
104112
];
@@ -107,49 +115,4 @@ public function processNode(Node $node, Scope $scope): array
107115
return [];
108116
}
109117

110-
private function getPlaceholdersCount(string $functionName, string $format): int
111-
{
112-
$specifiers = in_array($functionName, ['sprintf', 'printf'], true) ? '(?:[bs%s]|l?[cdeEgfFGouxX])' : '(?:[cdDeEfinosuxX%s]|\[[^\]]+\])';
113-
$addSpecifier = '';
114-
if ($this->phpVersion->supportsHhPrintfSpecifier()) {
115-
$addSpecifier .= 'hH';
116-
}
117-
118-
$specifiers = sprintf($specifiers, $addSpecifier);
119-
120-
$pattern = '~(?<before>%*)%(?:(?<position>\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?(?<width>\*)?-?\d*(?:\.(?:\d+|(?<precision>\*))?)?' . $specifiers . '~';
121-
122-
$matches = Strings::matchAll($format, $pattern, PREG_SET_ORDER);
123-
124-
if (count($matches) === 0) {
125-
return 0;
126-
}
127-
128-
$placeholders = array_filter($matches, static fn (array $match): bool => strlen($match['before']) % 2 === 0);
129-
130-
if (count($placeholders) === 0) {
131-
return 0;
132-
}
133-
134-
$maxPositionedNumber = 0;
135-
$maxOrdinaryNumber = 0;
136-
foreach ($placeholders as $placeholder) {
137-
if (isset($placeholder['width']) && $placeholder['width'] !== '') {
138-
$maxOrdinaryNumber++;
139-
}
140-
141-
if (isset($placeholder['precision']) && $placeholder['precision'] !== '') {
142-
$maxOrdinaryNumber++;
143-
}
144-
145-
if (isset($placeholder['position']) && $placeholder['position'] !== '') {
146-
$maxPositionedNumber = max((int) $placeholder['position'], $maxPositionedNumber);
147-
} else {
148-
$maxOrdinaryNumber++;
149-
}
150-
}
151-
152-
return max($maxPositionedNumber, $maxOrdinaryNumber);
153-
}
154-
155118
}

tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ class PrintfParametersRuleTest extends RuleTestCase
1515

1616
protected function getRule(): Rule
1717
{
18-
return new PrintfParametersRule(new PhpVersion(PHP_VERSION_ID));
18+
return new PrintfParametersRule(
19+
new PrintfHelper(new PhpVersion(PHP_VERSION_ID)),
20+
$this->createReflectionProvider(),
21+
);
1922
}
2023

2124
public function testFile(): void

0 commit comments

Comments
 (0)