Skip to content

Commit f513931

Browse files
authored
fix return type of sprintf with single %s format
1 parent 1bce0b4 commit f513931

File tree

3 files changed

+77
-11
lines changed

3 files changed

+77
-11
lines changed

src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
use function array_shift;
2222
use function count;
2323
use function in_array;
24+
use function intval;
2425
use function is_string;
2526
use function preg_match;
2627
use function sprintf;
28+
use function substr;
2729
use function vsprintf;
2830

2931
class SprintfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
@@ -46,28 +48,49 @@ public function getTypeFromFunctionCall(
4648
}
4749

4850
$formatType = $scope->getType($args[0]->value);
51+
4952
if (count($formatType->getConstantStrings()) > 0) {
50-
$skip = false;
53+
$singlePlaceholderEarlyReturn = null;
5154
foreach ($formatType->getConstantStrings() as $constantString) {
5255
// The printf format is %[argnum$][flags][width][.precision]
53-
if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*[bdeEfFgGhHouxX]$/', $constantString->getValue(), $matches) === 1) {
54-
// invalid positional argument
55-
if (array_key_exists(1, $matches) && $matches[1] === '0$') {
56+
if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1) {
57+
if (array_key_exists(1, $matches) && ($matches[1] !== '')) {
58+
// invalid positional argument
59+
if ($matches[1] === '0$') {
60+
return null;
61+
}
62+
$checkArg = intval(substr($matches[1], 0, -1));
63+
} else {
64+
$checkArg = 1;
65+
}
66+
67+
// constant string specifies a numbered argument that does not exist
68+
if (!array_key_exists($checkArg, $args)) {
5669
return null;
5770
}
5871

72+
// if the format string is just a placeholder and specified an argument
73+
// of stringy type, then the return value will be of the same type
74+
$checkArgType = $scope->getType($args[$checkArg]->value);
75+
76+
if ($matches[2] === 's' && $checkArgType->isString()->yes()) {
77+
$singlePlaceholderEarlyReturn = $checkArgType;
78+
} elseif ($matches[2] !== 's') {
79+
$singlePlaceholderEarlyReturn = new IntersectionType([
80+
new StringType(),
81+
new AccessoryNumericStringType(),
82+
]);
83+
}
84+
5985
continue;
6086
}
6187

62-
$skip = true;
88+
$singlePlaceholderEarlyReturn = null;
6389
break;
6490
}
6591

66-
if (!$skip) {
67-
return new IntersectionType([
68-
new StringType(),
69-
new AccessoryNumericStringType(),
70-
]);
92+
if ($singlePlaceholderEarlyReturn !== null) {
93+
return $singlePlaceholderEarlyReturn;
7194
}
7295
}
7396

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug11201;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/** @return array<string> */
8+
function returnsArray(){
9+
return [];
10+
}
11+
12+
/** @return non-empty-string */
13+
function returnsNonEmptyString(): string
14+
{
15+
return 'a';
16+
}
17+
18+
/** @return non-falsy-string */
19+
function returnsNonFalsyString(): string
20+
{
21+
return '1';
22+
}
23+
24+
/** @return string */
25+
function returnsJustString(): string
26+
{
27+
return rand(0,1) === 1 ? 'foo' : '';
28+
}
29+
30+
$s = sprintf("%s", returnsNonEmptyString());
31+
assertType('non-empty-string', $s);
32+
33+
$s = sprintf("%s", returnsNonFalsyString());
34+
assertType('non-falsy-string', $s);
35+
36+
$s = sprintf("%s", returnsJustString());
37+
assertType('string', $s);
38+
39+
$s = sprintf("%s", implode(', ', array_map('intval', returnsArray())));
40+
assertType('string', $s);
41+
42+
$s = sprintf('%2$s', 1234, returnsNonFalsyString());
43+
assertType('non-falsy-string', $s);

tests/PHPStan/Analyser/nsrt/bug-7387.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function positionalArgs($mixed, int $i, float $f, string $s) {
6161
assertType('numeric-string', sprintf('%2$14F', $mixed, $f));
6262
assertType('numeric-string', sprintf('%2$14F', $mixed, $s));
6363

64-
assertType('numeric-string', sprintf('%10$14F', $mixed, $s));
64+
assertType('string', sprintf('%10$14F', $mixed, $s));
6565
}
6666

6767
public function invalidPositionalArgFormat($mixed, string $s) {

0 commit comments

Comments
 (0)