Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

add optional strict check for printf parameter types #4349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
View file Open in desktop
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
View file Open in desktop
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
View file Open in desktop
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
View file Open in desktop
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' => '__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.
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
View file Open in desktop
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
View file Open in desktop
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 __stringandstringable by placeholder #1 ("%s"), null given.',
Copy link
Contributor

Choose a reason for hiding this comment

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

@schlndh @ondrejmirtes After enabling this rule I noticed a lot of errors related to string|null types.

Personally, I think replacing those sprintf call arguments with $value ?? '' feels like making a computer happy. As it does exactly the same as what sprintf does for %s.

See https://3v4l.org/TlHU9#v8.4.13

I know this is a strict rule, but wonder if this was really intentional or something that should be reconsidered?

Copy link
Member

@ondrejmirtes ondrejmirtes Sep 25, 2025

Choose a reason for hiding this comment

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

I feel like it's useful that PHPStan lets you know you're potentially passing null to sprintf. You might want to deal with null differently than what sprintf does.

derrabus and VincentLanglet reacted with thumbs up emoji
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I intentionally implemented it like that to match my understanding of "strict". However, I encountered the same thing: hundreds of issues like this, most of which I don't care about (though I'm fine with letting them all fall into the baseline). So I would be open to changing it. I'm just not sure what's the best way to go about it.

Strict-rules do allow $null . $string and "{$null}", so I guess this wouldn't have to be as strict either (unless it's allowed by omission rather than intentionally). At the very least it's a bit inconsistent.

On the other hand, there are third-party rules (e.g. shipmonk rules) which do prohibit it, so clearly there is also interest in the fully strict version.

Another thing to consider is whether it should be allowed only in %s or also other placehoders?

Copy link
Contributor

Choose a reason for hiding this comment

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

Strict-rules do allow $null . $string and "{$null}", so I guess this wouldn't have to be as strict either (unless it's allowed by omission rather than intentionally). At the very least it's a bit inconsistent.

This is an excellent point.

Another thing to consider is whether it should be allowed only in %s or also other placehoders?

I would say only for %s.

Copy link
Member

@ondrejmirtes ondrejmirtes Sep 25, 2025

Choose a reason for hiding this comment

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

An idea: string|null should be reported only on level 8+, basicallh RuleLevelHelper should be utilized.

derrabus reacted with thumbs up emoji
Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunately, that won't help me (level 9 🙈)

Copy link

Choose a reason for hiding this comment

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

I would say only for %s.

To expand on that point: %d + null make 0, which is confusable with, well, passing an actual 0 integer. 😄 So for that reason I wouldn't allow it either.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ondrejmirtes Re: level 8

  1. report calling methods and accessing properties on nullable types

IMO that's stretching it a little bit. And it wouldn't be consistent with how string + null is handled in other cases (concatenation, interpolation, ...).

Let's consider consistency with phpstan-strict-rules (playground):

  • Strict-rules prohibit bool and null in numerical operations (consistent with %d and %f).
  • Strict-rules allow numeric-string in numerical operations (inconsistent with %f, %d is probably OK since float is excluded as well).
  • Strict-rules prohibit general string in numerical operations (consistent with %d and %f).
  • Strict-rules allow bool and null in string operations (inconsistent with %s).

Idea:

  • Let's enable passing null to %s. If PHPStan later adds a config option to limit type coercions then it could be used here (as well as $string . $null, "{$null}", ...).
  • Let's enable passing numeric-string to %f. I excluded it, because numeric-string could lose precision by doing so. But it's probably too strict.
  • Let's keep passing bool to %s prohibited. I could later try to prohibit bool in $string . $bool and "{$bool}" in strict-rules (no promises).

ruudk, angeleg, and derrabus reacted with thumbs up emoji
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a small side note, PHPStorm will tell me to remove (string) $null for %s in a sprintf. It's only fine with $null ?? ''.

Screenshot 2025年09月26日 at 12 42 39@2x

schlndh reacted with thumbs up emoji
Copy link

Choose a reason for hiding this comment

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

Idea:

  • Let's enable passing null to %s. If PHPStan later adds a config option to limit type coercions then it could be used here (as well as $string . $null, "{$null}", ...).

I don't know if that helps, but I've reviewed the new errors that popped up in my current project. The dominant error reported was that we pass something that could be null. And in most of the cases, it would be a problem is the value was actually null. It was also old news because we already knew that the code in question is a bit too lax when it comes to handling null. So all errors that the new rule added to our baseline were actually just "more of the same".

  • Let's enable passing numeric-string to %f. I excluded it, because numeric-string could lose precision by doing so. But it's probably too strict.

Yes, it is. In fact, you actually might lose precision when casing a numeric string to a float. And besides it doesn't make much sense to cast a string to a float just to format it as a string again.

47,
],
[
'Parameter #2 of function printf is expected to be __stringandstringable by placeholder #1 ("%s"), true given.',
48,
],
]);
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Rules/Functions/data/printf-param-types.php
View file Open in desktop
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

AltStyle によって変換されたページ (->オリジナル) /