Skip to content

Commit f6ed272

Browse files
authored
Add optional strict check for printf parameter types
1 parent e4e21c7 commit f6ed272

File tree

7 files changed

+182
-18
lines changed

7 files changed

+182
-18
lines changed

conf/config.level5.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ services:
2020
class: PHPStan\Rules\Functions\ParameterCastableToNumberRule
2121
-
2222
class: PHPStan\Rules\Functions\PrintfParameterTypeRule
23+
arguments:
24+
checkStrictPrintfPlaceholderTypes: %checkStrictPrintfPlaceholderTypes%

conf/config.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ parameters:
6666
strictRulesInstalled: false
6767
deprecationRulesInstalled: false
6868
inferPrivatePropertyTypeFromConstructor: false
69+
checkStrictPrintfPlaceholderTypes: false
6970
reportMaybes: false
7071
reportMaybesInMethodSignatures: false
7172
reportMaybesInPropertyPhpDocTypes: false

conf/parametersSchema.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ parametersSchema:
6969
strictRulesInstalled: bool()
7070
deprecationRulesInstalled: bool()
7171
inferPrivatePropertyTypeFromConstructor: bool()
72+
checkStrictPrintfPlaceholderTypes: bool()
7273

7374
tips: structure([
7475
discoveringSymbols: bool()

src/Rules/Functions/PrintfParameterTypeRule.php

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public function __construct(
4242
private PrintfHelper $printfHelper,
4343
private ReflectionProvider $reflectionProvider,
4444
private RuleLevelHelper $ruleLevelHelper,
45+
private bool $checkStrictPrintfPlaceholderTypes,
4546
)
4647
{
4748
}
@@ -100,15 +101,23 @@ public function processNode(Node $node, Scope $scope): array
100101
new NullType(),
101102
);
102103
// Type on the left can go to the type on the right, but not vice versa.
103-
$allowedTypeNameMap = [
104-
'strict-int' => 'int',
105-
'int' => 'castable to int',
106-
'float' => 'castable to float',
107-
// These are here just for completeness. They won't be used because, these types are already enforced by
108-
// CallToFunctionParametersRule.
109-
'string' => 'castable to string',
110-
'mixed' => 'castable to string',
111-
];
104+
$allowedTypeNameMap = $this->checkStrictPrintfPlaceholderTypes
105+
? [
106+
'strict-int' => 'int',
107+
'int' => 'int',
108+
'float' => 'float',
109+
'string' => '__stringandstringable',
110+
'mixed' => '__stringandstringable',
111+
]
112+
: [
113+
'strict-int' => 'int',
114+
'int' => 'castable to int',
115+
'float' => 'castable to float',
116+
// These are here just for completeness. They won't be used because, these types are already enforced by
117+
// CallToFunctionParametersRule.
118+
'string' => 'castable to string',
119+
'mixed' => 'castable to string',
120+
];
112121

113122
for ($i = $formatArgumentPosition + 1, $j = 0; $i < $argsCount; $i++, $j++) {
114123
// Some arguments may be skipped entirely.
@@ -117,10 +126,10 @@ public function processNode(Node $node, Scope $scope): array
117126
$scope,
118127
$args[$i]->value,
119128
'',
120-
static fn (Type $t) => $placeholder->doesArgumentTypeMatchPlaceholder($t),
129+
fn (Type $t) => $placeholder->doesArgumentTypeMatchPlaceholder($t, $this->checkStrictPrintfPlaceholderTypes),
121130
)->getType();
122131

123-
if ($argType instanceof ErrorType || $placeholder->doesArgumentTypeMatchPlaceholder($argType)) {
132+
if ($argType instanceof ErrorType || $placeholder->doesArgumentTypeMatchPlaceholder($argType, $this->checkStrictPrintfPlaceholderTypes)) {
124133
continue;
125134
}
126135

src/Rules/Functions/PrintfPlaceholder.php

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
use PHPStan\ShouldNotHappenException;
66
use PHPStan\Type\ErrorType;
7+
use PHPStan\Type\FloatType;
78
use PHPStan\Type\IntegerType;
9+
use PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType;
810
use PHPStan\Type\Type;
11+
use PHPStan\Type\TypeCombinator;
912

1013
final class PrintfPlaceholder
1114
{
@@ -20,20 +23,30 @@ public function __construct(
2023
{
2124
}
2225

23-
public function doesArgumentTypeMatchPlaceholder(Type $argumentType): bool
26+
public function doesArgumentTypeMatchPlaceholder(Type $argumentType, bool $strictPlaceholderTypes): bool
2427
{
2528
switch ($this->acceptingType) {
2629
case 'strict-int':
2730
return (new IntegerType())->accepts($argumentType, true)->yes();
2831
case 'int':
29-
return ! $argumentType->toInteger() instanceof ErrorType;
32+
return $strictPlaceholderTypes
33+
? (new IntegerType())->accepts($argumentType, true)->yes()
34+
: ! $argumentType->toInteger() instanceof ErrorType;
3035
case 'float':
31-
return ! $argumentType->toFloat() instanceof ErrorType;
32-
// The function signature already limits the parameters to stringable types, so there's
33-
// no point in checking string again here.
36+
return $strictPlaceholderTypes
37+
? (new FloatType())->accepts($argumentType, true)->yes()
38+
: ! $argumentType->toFloat() instanceof ErrorType;
3439
case 'string':
3540
case 'mixed':
36-
return true;
41+
// The function signature already limits the parameters to stringable types, so there's
42+
// no point in checking string again here.
43+
return !$strictPlaceholderTypes
44+
// Don't accept null or bool. It's likely to be a mistake.
45+
|| TypeCombinator::union(
46+
new StringAlwaysAcceptingObjectWithToStringType(),
47+
// float also accepts int.
48+
new FloatType(),
49+
)->accepts($argumentType, true)->yes();
3750
// Without this PHPStan with PHP 7.4 reports "...should return bool but return statement is missing."
3851
// Presumably, because promoted properties are turned into regular properties and the phpdoc isn't applied to the property.
3952
default:

tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
class PrintfParameterTypeRuleTest extends RuleTestCase
1515
{
1616

17+
private bool $checkStrictPrintfPlaceholderTypes = false;
18+
1719
protected function getRule(): Rule
1820
{
1921
$reflectionProvider = $this->createReflectionProvider();
@@ -30,6 +32,7 @@ protected function getRule(): Rule
3032
true,
3133
false,
3234
),
35+
$this->checkStrictPrintfPlaceholderTypes,
3336
);
3437
}
3538

@@ -111,4 +114,139 @@ public function test(): void
111114
]);
112115
}
113116

117+
public function testStrict(): void
118+
{
119+
$this->checkStrictPrintfPlaceholderTypes = true;
120+
$this->analyse([__DIR__ . '/data/printf-param-types.php'], [
121+
[
122+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), PrintfParamTypes\\FooStringable given.',
123+
15,
124+
],
125+
[
126+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), int|PrintfParamTypes\\FooStringable given.',
127+
16,
128+
],
129+
[
130+
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), PrintfParamTypes\\FooStringable given.',
131+
17,
132+
],
133+
[
134+
'Parameter #2 of function sprintf is expected to be int by placeholder #1 ("%d"), PrintfParamTypes\\FooStringable given.',
135+
18,
136+
],
137+
[
138+
'Parameter #3 of function fprintf is expected to be float by placeholder #1 ("%f"), PrintfParamTypes\\FooStringable given.',
139+
19,
140+
],
141+
[
142+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), string given.',
143+
20,
144+
],
145+
[
146+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), float given.',
147+
21,
148+
],
149+
[
150+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), SimpleXMLElement given.',
151+
22,
152+
],
153+
[
154+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), null given.',
155+
23,
156+
],
157+
[
158+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), true given.',
159+
24,
160+
],
161+
[
162+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%.*s" (precision)), string given.',
163+
25,
164+
],
165+
[
166+
'Parameter #2 of function printf is expected to be int by placeholder #2 ("%3$.*s" (precision)), string given.',
167+
26,
168+
],
169+
[
170+
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%1$-\'X10.2f"), PrintfParamTypes\\FooStringable given.',
171+
27,
172+
],
173+
[
174+
'Parameter #2 of function printf is expected to be float by placeholder #2 ("%1$*.*f" (value)), PrintfParamTypes\\FooStringable given.',
175+
28,
176+
],
177+
[
178+
'Parameter #4 of function printf is expected to be float by placeholder #1 ("%3$f"), PrintfParamTypes\\FooStringable given.',
179+
29,
180+
],
181+
[
182+
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%1$f"), PrintfParamTypes\\FooStringable given.',
183+
30,
184+
],
185+
[
186+
'Parameter #2 of function printf is expected to be int by placeholder #2 ("%1$d"), PrintfParamTypes\\FooStringable given.',
187+
30,
188+
],
189+
[
190+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%1$*d" (width)), float given.',
191+
31,
192+
],
193+
[
194+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%1$*d" (value)), float given.',
195+
31,
196+
],
197+
[
198+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), float given.',
199+
34,
200+
],
201+
[
202+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), float|int given.',
203+
35,
204+
],
205+
[
206+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), string given.',
207+
36,
208+
],
209+
[
210+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), string given.',
211+
37,
212+
],
213+
[
214+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), null given.',
215+
38,
216+
],
217+
[
218+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), true given.',
219+
39,
220+
],
221+
[
222+
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), SimpleXMLElement given.',
223+
40,
224+
],
225+
[
226+
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), string given.',
227+
42,
228+
],
229+
[
230+
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), null given.',
231+
43,
232+
],
233+
[
234+
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), true given.',
235+
44,
236+
],
237+
[
238+
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), SimpleXMLElement given.',
239+
45,
240+
],
241+
[
242+
'Parameter #2 of function printf is expected to be __stringandstringable by placeholder #1 ("%s"), null given.',
243+
47,
244+
],
245+
[
246+
'Parameter #2 of function printf is expected to be __stringandstringable by placeholder #1 ("%s"), true given.',
247+
48,
248+
],
249+
]);
250+
}
251+
114252
}

tests/PHPStan/Rules/Functions/data/printf-param-types.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function __toString(): string
3939
printf('%d', true);
4040
printf('%d', new \SimpleXMLElement('<a>aaa</a>'));
4141

42-
printf('%f', 'a');
42+
printf('%f', '1.2345678901234567890123456789013245678901234567989');
4343
printf('%f', null);
4444
printf('%f', true);
4545
printf('%f', new \SimpleXMLElement('<a>aaa</a>'));

0 commit comments

Comments
 (0)