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'));