Skip to content

Commit 1f95570

Browse files
committed
Support unions of scalars in sprintf() arguments
1 parent 2a8d675 commit 1f95570

File tree

4 files changed

+67
-19
lines changed

4 files changed

+67
-19
lines changed

phpstan-baseline.neon

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,11 +1487,6 @@ parameters:
14871487
count: 1
14881488
path: src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php
14891489

1490-
-
1491-
message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#"
1492-
count: 1
1493-
path: src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php
1494-
14951490
-
14961491
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#"
14971492
count: 1

src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55
use PhpParser\Node\Expr\FuncCall;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Reflection\InitializerExprTypeResolver;
89
use PHPStan\Reflection\ParametersAcceptorSelector;
910
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
1011
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
1112
use PHPStan\Type\Accessory\AccessoryNumericStringType;
12-
use PHPStan\Type\ConstantScalarType;
1313
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1414
use PHPStan\Type\IntersectionType;
1515
use PHPStan\Type\StringType;
1616
use PHPStan\Type\Type;
17+
use PHPStan\Type\TypeCombinator;
1718
use Throwable;
1819
use function array_key_exists;
1920
use function array_shift;
@@ -86,29 +87,58 @@ public function getTypeFromFunctionCall(
8687
$values = [];
8788
foreach ($args as $arg) {
8889
$argType = $scope->getType($arg->value);
89-
if (!$argType instanceof ConstantScalarType) {
90+
if (count($argType->getConstantScalarValues()) === 0) {
9091
return $returnType;
9192
}
9293

93-
$values[] = $argType->getValue();
94+
$values[] = $argType->getConstantScalarValues();
9495
}
9596

96-
$format = array_shift($values);
97-
if (!is_string($format)) {
98-
return $returnType;
99-
}
97+
$combinations = $this->combinations($values);
98+
$returnTypes = [];
99+
foreach ($combinations as $combination) {
100+
$format = array_shift($combination);
101+
if (!is_string($format)) {
102+
return $returnType;
103+
}
100104

101-
try {
102-
if ($functionReflection->getName() === 'sprintf') {
103-
$value = @sprintf($format, ...$values);
104-
} else {
105-
$value = @vsprintf($format, $values);
105+
try {
106+
if ($functionReflection->getName() === 'sprintf') {
107+
$returnTypes[] = $scope->getTypeFromValue(@sprintf($format, ...$combination));
108+
} else {
109+
$returnTypes[] = $scope->getTypeFromValue(@vsprintf($format, $combination));
110+
}
111+
} catch (Throwable) {
112+
return $returnType;
106113
}
107-
} catch (Throwable) {
114+
}
115+
116+
if (count($returnTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
108117
return $returnType;
109118
}
110119

111-
return $scope->getTypeFromValue($value);
120+
return TypeCombinator::union(...$returnTypes);
121+
}
122+
123+
/**
124+
* @param array<mixed> $arrays
125+
* @return iterable<mixed>
126+
*/
127+
private function combinations(array $arrays): iterable
128+
{
129+
// from https://stackoverflow.com/a/70800936/565782 by Arnaud Le Blanc
130+
if ($arrays === []) {
131+
yield [];
132+
return;
133+
}
134+
135+
$head = array_shift($arrays);
136+
137+
foreach ($head as $elem) {
138+
foreach ($this->combinations($arrays) as $combination) {
139+
yield [$elem, ...$combination];
140+
}
141+
}
112142
}
113143

114144
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ public function dataFileAsserts(): iterable
193193

194194
yield from $this->gatherAssertTypes(__DIR__ . '/data/count-type.php');
195195

196+
yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-sprintf.php');
197+
196198
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816.php');
197199

198200
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816-2.php');
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace DynamicSprintf;
4+
5+
use function PHPStan\Testing\assertType;
6+
use function sprintf;
7+
8+
class Foo
9+
{
10+
11+
/**
12+
* @param 'a'|'aa' $a
13+
* @param 'b'|'bb' $b
14+
* @param 'c'|'cc' $c
15+
*/
16+
public function doFoo(string $a, string $b, string $c): void
17+
{
18+
assertType("'a b c'|'a b cc'|'a bb c'|'a bb cc'|'aa b c'|'aa b cc'|'aa bb c'|'aa bb cc'", sprintf('%s %s %s', $a, $b, $c));
19+
}
20+
21+
}

0 commit comments

Comments
 (0)