diff --git a/conf/config.level5.neon b/conf/config.level5.neon
index 4dfbc72598..b4518ba7e2 100644
--- a/conf/config.level5.neon
+++ b/conf/config.level5.neon
@@ -20,3 +20,5 @@ services:
class: PHPStan\Rules\Functions\ParameterCastableToNumberRule
-
class: PHPStan\Rules\Functions\PrintfParameterTypeRule
+ arguments:
+ checkStrictPrintfPlaceholderTypes: %checkStrictPrintfPlaceholderTypes%
diff --git a/conf/config.neon b/conf/config.neon
index 6056ba948c..a5c6368e1c 100644
--- a/conf/config.neon
+++ b/conf/config.neon
@@ -66,6 +66,7 @@ parameters:
strictRulesInstalled: false
deprecationRulesInstalled: false
inferPrivatePropertyTypeFromConstructor: false
+ checkStrictPrintfPlaceholderTypes: false
reportMaybes: false
reportMaybesInMethodSignatures: false
reportMaybesInPropertyPhpDocTypes: false
diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon
index 1781ffad33..d1d8617871 100644
--- a/conf/parametersSchema.neon
+++ b/conf/parametersSchema.neon
@@ -69,6 +69,7 @@ parametersSchema:
strictRulesInstalled: bool()
deprecationRulesInstalled: bool()
inferPrivatePropertyTypeFromConstructor: bool()
+ checkStrictPrintfPlaceholderTypes: bool()
tips: structure([
discoveringSymbols: bool()
diff --git a/src/Rules/Functions/PrintfParameterTypeRule.php b/src/Rules/Functions/PrintfParameterTypeRule.php
index 8e2997c67f..3c83e765b9 100644
--- a/src/Rules/Functions/PrintfParameterTypeRule.php
+++ b/src/Rules/Functions/PrintfParameterTypeRule.php
@@ -42,6 +42,7 @@ public function __construct(
private PrintfHelper $printfHelper,
private ReflectionProvider $reflectionProvider,
private RuleLevelHelper $ruleLevelHelper,
+ private bool $checkStrictPrintfPlaceholderTypes,
)
{
}
@@ -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' => '__stringandstringable',
+ 'mixed' => '__stringandstringable',
+ ]
+ : [
+ '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.
@@ -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;
}
diff --git a/src/Rules/Functions/PrintfPlaceholder.php b/src/Rules/Functions/PrintfPlaceholder.php
index 8e2c1336fa..4bdeb156d6 100644
--- a/src/Rules/Functions/PrintfPlaceholder.php
+++ b/src/Rules/Functions/PrintfPlaceholder.php
@@ -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
{
@@ -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:
diff --git a/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php
index 1259a4cd53..c945ff723f 100644
--- a/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php
+++ b/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php
@@ -14,6 +14,8 @@
class PrintfParameterTypeRuleTest extends RuleTestCase
{
+ private bool $checkStrictPrintfPlaceholderTypes = false;
+
protected function getRule(): Rule
{
$reflectionProvider = $this->createReflectionProvider();
@@ -30,6 +32,7 @@ protected function getRule(): Rule
true,
false,
),
+ $this->checkStrictPrintfPlaceholderTypes,
);
}
@@ -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 __stringandstringable by placeholder #1 ("%s"), null given.',
+ 47,
+ ],
+ [
+ 'Parameter #2 of function printf is expected to be __stringandstringable by placeholder #1 ("%s"), true given.',
+ 48,
+ ],
+ ]);
+ }
+
}
diff --git a/tests/PHPStan/Rules/Functions/data/printf-param-types.php b/tests/PHPStan/Rules/Functions/data/printf-param-types.php
index b2c5748627..32b8f7b366 100644
--- a/tests/PHPStan/Rules/Functions/data/printf-param-types.php
+++ b/tests/PHPStan/Rules/Functions/data/printf-param-types.php
@@ -39,7 +39,7 @@ public function __toString(): string
printf('%d', true);
printf('%d', new \SimpleXMLElement('aaa'));
-printf('%f', 'a');
+printf('%f', '1.2345678901234567890123456789013245678901234567989');
printf('%f', null);
printf('%f', true);
printf('%f', new \SimpleXMLElement('aaa'));