Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions conf/config.level5.neon
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ services:
class: PHPStan\Rules\Functions\ParameterCastableToNumberRule
-
class: PHPStan\Rules\Functions\PrintfParameterTypeRule
arguments:
checkStrictPrintfPlaceholderTypes: %checkStrictPrintfPlaceholderTypes%
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ parameters:
strictRulesInstalled: false
deprecationRulesInstalled: false
inferPrivatePropertyTypeFromConstructor: false
checkStrictPrintfPlaceholderTypes: false
reportMaybes: false
reportMaybesInMethodSignatures: false
reportMaybesInPropertyPhpDocTypes: false
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ parametersSchema:
strictRulesInstalled: bool()
deprecationRulesInstalled: bool()
inferPrivatePropertyTypeFromConstructor: bool()
checkStrictPrintfPlaceholderTypes: bool()

tips: structure([
discoveringSymbols: bool()
Expand Down
31 changes: 20 additions & 11 deletions src/Rules/Functions/PrintfParameterTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public function __construct(
private PrintfHelper $printfHelper,
private ReflectionProvider $reflectionProvider,
private RuleLevelHelper $ruleLevelHelper,
private bool $checkStrictPrintfPlaceholderTypes,
)
{
}
Expand Down Expand Up @@ -100,15 +101,23 @@ public function processNode(Node $node, Scope $scope): array
new NullType(),
);
// Type on the left can go to the type on the right, but not vice versa.
$allowedTypeNameMap = [
'strict-int' => 'int',
'int' => 'castable to int',
'float' => 'castable to float',
// These are here just for completeness. They won't be used because, these types are already enforced by
// CallToFunctionParametersRule.
'string' => 'castable to string',
'mixed' => 'castable to string',
];
$allowedTypeNameMap = $this->checkStrictPrintfPlaceholderTypes
? [
'strict-int' => 'int',
'int' => 'int',
'float' => 'float',
'string' => 'int|float|string|Stringable',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit long and it's not completely accurate either (it accepts classes with __toString() even if they don't implement Stringable). The other alternative I considered was just string, but that might be a bit confusing to users. Is there anything better?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try __stringandstringable. I can see you're already using new StringAlwaysAcceptingObjectWithToStringType in a related place.

'mixed' => 'int|float|string|Stringable',
]
: [
'strict-int' => 'int',
'int' => 'castable to int',
'float' => 'castable to float',
// These are here just for completeness. They won't be used because, these types are already enforced by
// CallToFunctionParametersRule.
'string' => 'castable to string',
'mixed' => 'castable to string',
];

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

if ($argType instanceof ErrorType || $placeholder->doesArgumentTypeMatchPlaceholder($argType)) {
if ($argType instanceof ErrorType || $placeholder->doesArgumentTypeMatchPlaceholder($argType, $this->checkStrictPrintfPlaceholderTypes)) {
continue;
}

Expand Down
25 changes: 19 additions & 6 deletions src/Rules/Functions/PrintfPlaceholder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ErrorType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

final class PrintfPlaceholder
{
Expand All @@ -20,20 +23,30 @@ public function __construct(
{
}

public function doesArgumentTypeMatchPlaceholder(Type $argumentType): bool
public function doesArgumentTypeMatchPlaceholder(Type $argumentType, bool $strictPlaceholderTypes): bool
{
switch ($this->acceptingType) {
case 'strict-int':
return (new IntegerType())->accepts($argumentType, true)->yes();
case 'int':
return ! $argumentType->toInteger() instanceof ErrorType;
return $strictPlaceholderTypes
? (new IntegerType())->accepts($argumentType, true)->yes()
: ! $argumentType->toInteger() instanceof ErrorType;
case 'float':
return ! $argumentType->toFloat() instanceof ErrorType;
// The function signature already limits the parameters to stringable types, so there's
// no point in checking string again here.
return $strictPlaceholderTypes
? (new FloatType())->accepts($argumentType, true)->yes()
: ! $argumentType->toFloat() instanceof ErrorType;
case 'string':
case 'mixed':
return true;
// The function signature already limits the parameters to stringable types, so there's
// no point in checking string again here.
return !$strictPlaceholderTypes
// Don't accept null or bool. It's likely to be a mistake.
|| TypeCombinator::union(
new StringAlwaysAcceptingObjectWithToStringType(),
// float also accepts int.
new FloatType(),
)->accepts($argumentType, true)->yes();
// Without this PHPStan with PHP 7.4 reports "...should return bool but return statement is missing."
// Presumably, because promoted properties are turned into regular properties and the phpdoc isn't applied to the property.
default:
Expand Down
138 changes: 138 additions & 0 deletions tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
class PrintfParameterTypeRuleTest extends RuleTestCase
{

private bool $checkStrictPrintfPlaceholderTypes = false;

protected function getRule(): Rule
{
$reflectionProvider = $this->createReflectionProvider();
Expand All @@ -30,6 +32,7 @@ protected function getRule(): Rule
true,
false,
),
$this->checkStrictPrintfPlaceholderTypes,
);
}

Expand Down Expand Up @@ -111,4 +114,139 @@ public function test(): void
]);
}

public function testStrict(): void
{
$this->checkStrictPrintfPlaceholderTypes = true;
$this->analyse([__DIR__ . '/data/printf-param-types.php'], [
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), PrintfParamTypes\\FooStringable given.',
15,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), int|PrintfParamTypes\\FooStringable given.',
16,
],
[
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), PrintfParamTypes\\FooStringable given.',
17,
],
[
'Parameter #2 of function sprintf is expected to be int by placeholder #1 ("%d"), PrintfParamTypes\\FooStringable given.',
18,
],
[
'Parameter #3 of function fprintf is expected to be float by placeholder #1 ("%f"), PrintfParamTypes\\FooStringable given.',
19,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), string given.',
20,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), float given.',
21,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), SimpleXMLElement given.',
22,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), null given.',
23,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%*s" (width)), true given.',
24,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%.*s" (precision)), string given.',
25,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #2 ("%3$.*s" (precision)), string given.',
26,
],
[
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%1$-\'X10.2f"), PrintfParamTypes\\FooStringable given.',
27,
],
[
'Parameter #2 of function printf is expected to be float by placeholder #2 ("%1$*.*f" (value)), PrintfParamTypes\\FooStringable given.',
28,
],
[
'Parameter #4 of function printf is expected to be float by placeholder #1 ("%3$f"), PrintfParamTypes\\FooStringable given.',
29,
],
[
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%1$f"), PrintfParamTypes\\FooStringable given.',
30,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #2 ("%1$d"), PrintfParamTypes\\FooStringable given.',
30,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%1$*d" (width)), float given.',
31,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%1$*d" (value)), float given.',
31,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), float given.',
34,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), float|int given.',
35,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), string given.',
36,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), string given.',
37,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), null given.',
38,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), true given.',
39,
],
[
'Parameter #2 of function printf is expected to be int by placeholder #1 ("%d"), SimpleXMLElement given.',
40,
],
[
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), string given.',
42,
],
[
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), null given.',
43,
],
[
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), true given.',
44,
],
[
'Parameter #2 of function printf is expected to be float by placeholder #1 ("%f"), SimpleXMLElement given.',
45,
],
[
'Parameter #2 of function printf is expected to be int|float|string|Stringable by placeholder #1 ("%s"), null given.',
47,
],
[
'Parameter #2 of function printf is expected to be int|float|string|Stringable by placeholder #1 ("%s"), true given.',
48,
],
]);
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Rules/Functions/data/printf-param-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function __toString(): string
printf('%d', true);
printf('%d', new \SimpleXMLElement('<a>aaa</a>'));

printf('%f', 'a');
printf('%f', '1.2345678901234567890123456789013245678901234567989');
printf('%f', null);
printf('%f', true);
printf('%f', new \SimpleXMLElement('<a>aaa</a>'));
Expand Down
Loading