Skip to content

Commit 8713b14

Browse files
authored
Improved sprintf() inference
1 parent 09fbc92 commit 8713b14

File tree

3 files changed

+43
-18
lines changed

3 files changed

+43
-18
lines changed

src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,19 @@ public function getTypeFromFunctionCall(
4848
return null;
4949
}
5050

51-
$formatType = $scope->getType($args[0]->value);
52-
if (count($args) === 1) {
53-
return $this->getConstantType($args, null, $functionReflection, $scope);
51+
$constantType = $this->getConstantType($args, $functionReflection, $scope);
52+
if ($constantType !== null) {
53+
return $constantType;
5454
}
5555

56+
$formatType = $scope->getType($args[0]->value);
5657
$formatStrings = $formatType->getConstantStrings();
5758
if (count($formatStrings) === 0) {
5859
return null;
5960
}
6061

6162
$singlePlaceholderEarlyReturn = null;
62-
foreach ($formatType->getConstantStrings() as $constantString) {
63+
foreach ($formatStrings as $constantString) {
6364
// The printf format is %[argnum$][flags][width][.precision]
6465
if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1) {
6566
if ($matches[1] !== '') {
@@ -80,9 +81,10 @@ public function getTypeFromFunctionCall(
8081
// if the format string is just a placeholder and specified an argument
8182
// of stringy type, then the return value will be of the same type
8283
$checkArgType = $scope->getType($args[$checkArg]->value);
83-
84-
if ($matches[2] === 's' && $checkArgType->isString()->yes()) {
85-
$singlePlaceholderEarlyReturn = $checkArgType;
84+
if ($matches[2] === 's'
85+
&& ($checkArgType->isString()->yes() || $checkArgType->isInteger()->yes())
86+
) {
87+
$singlePlaceholderEarlyReturn = $checkArgType->toString();
8688
} elseif ($matches[2] !== 's') {
8789
$singlePlaceholderEarlyReturn = new IntersectionType([
8890
new StringType(),
@@ -115,19 +117,19 @@ public function getTypeFromFunctionCall(
115117
$returnType = new StringType();
116118
}
117119

118-
return $this->getConstantType($args, $returnType, $functionReflection, $scope);
120+
return $returnType;
119121
}
120122

121123
/**
122124
* @param Arg[] $args
123125
*/
124-
private function getConstantType(array $args, ?Type $fallbackReturnType, FunctionReflection $functionReflection, Scope $scope): ?Type
126+
private function getConstantType(array $args, FunctionReflection $functionReflection, Scope $scope): ?Type
125127
{
126128
$values = [];
127129
$combinationsCount = 1;
128130
foreach ($args as $arg) {
129131
if ($arg->unpack) {
130-
return $fallbackReturnType;
132+
return null;
131133
}
132134

133135
$argType = $scope->getType($arg->value);
@@ -142,23 +144,23 @@ private function getConstantType(array $args, ?Type $fallbackReturnType, Functio
142144
}
143145

144146
if (count($constantScalarValues) === 0) {
145-
return $fallbackReturnType;
147+
return null;
146148
}
147149

148150
$values[] = $constantScalarValues;
149151
$combinationsCount *= count($constantScalarValues);
150152
}
151153

152154
if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
153-
return $fallbackReturnType;
155+
return null;
154156
}
155157

156158
$combinations = CombinationsHelper::combinations($values);
157159
$returnTypes = [];
158160
foreach ($combinations as $combination) {
159161
$format = array_shift($combination);
160162
if (!is_string($format)) {
161-
return $fallbackReturnType;
163+
return null;
162164
}
163165

164166
try {
@@ -168,12 +170,12 @@ private function getConstantType(array $args, ?Type $fallbackReturnType, Functio
168170
$returnTypes[] = $scope->getTypeFromValue(@vsprintf($format, $combination));
169171
}
170172
} catch (Throwable) {
171-
return $fallbackReturnType;
173+
return null;
172174
}
173175
}
174176

175177
if (count($returnTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
176-
return $fallbackReturnType;
178+
return null;
177179
}
178180

179181
return TypeCombinator::union(...$returnTypes);

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ function returnsJustString(): string
2727
return rand(0,1) === 1 ? 'foo' : '';
2828
}
2929

30+
function returnsBool(): bool {
31+
return true;
32+
}
33+
3034
$s = sprintf("%s", returnsNonEmptyString());
3135
assertType('non-empty-string', $s);
3236

@@ -41,3 +45,12 @@ function returnsJustString(): string
4145

4246
$s = sprintf('%2$s', 1234, returnsNonFalsyString());
4347
assertType('non-falsy-string', $s);
48+
49+
$s = sprintf('%20s', 'abc');
50+
assertType("' abc'", $s);
51+
52+
$s = sprintf('%20s', true);
53+
assertType("' 1'", $s);
54+
55+
$s = sprintf('%20s', returnsBool());
56+
assertType("non-falsy-string", $s);

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function inputTypes(int $i, float $f, string $s) {
2323

2424
public function specifiers(int $i) {
2525
// https://3v4l.org/fmVIg
26-
assertType('non-falsy-string', sprintf('%14s', $i));
26+
assertType('numeric-string', sprintf('%14s', $i));
2727

2828
assertType('numeric-string', sprintf('%d', $i));
2929

@@ -45,9 +45,19 @@ public function specifiers(int $i) {
4545

4646
}
4747

48-
public function positionalArgs($mixed, int $i, float $f, string $s) {
48+
/**
49+
* @param positive-int $posInt
50+
* @param negative-int $negInt
51+
* @param int<1, 5> $nonZeroIntRange
52+
* @param int<-1, 5> $intRange
53+
*/
54+
public function positionalArgs($mixed, int $i, float $f, string $s, int $posInt, int $negInt, int $nonZeroIntRange, int $intRange) {
4955
// https://3v4l.org/vVL0c
50-
assertType('non-falsy-string', sprintf('%2$14s', $mixed, $i));
56+
assertType('numeric-string', sprintf('%2$14s', $mixed, $i));
57+
assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $posInt));
58+
assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $negInt));
59+
assertType('numeric-string', sprintf('%2$14s', $mixed, $intRange));
60+
assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $nonZeroIntRange));
5161

5262
assertType('numeric-string', sprintf('%2$.14F', $mixed, $i));
5363
assertType('numeric-string', sprintf('%2$.14F', $mixed, $f));

0 commit comments

Comments
 (0)