Skip to content

Commit

Permalink
Support unions of scalars in sprintf() arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Jun 12, 2023
1 parent 2a8d675 commit 1f95570
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 19 deletions.
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1487,11 +1487,6 @@ parameters:
count: 1
path: src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php

-
message: "#^Doing instanceof PHPStan\\\\Type\\\\ConstantScalarType is error\\-prone and deprecated\\. Use Type\\:\\:isConstantScalarValue\\(\\) or Type\\:\\:getConstantScalarTypes\\(\\) or Type\\:\\:getConstantScalarValues\\(\\) instead\\.$#"
count: 1
path: src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php

-
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#"
count: 1
Expand Down
58 changes: 44 additions & 14 deletions src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use Throwable;
use function array_key_exists;
use function array_shift;
Expand Down Expand Up @@ -86,29 +87,58 @@ public function getTypeFromFunctionCall(
$values = [];
foreach ($args as $arg) {
$argType = $scope->getType($arg->value);
if (!$argType instanceof ConstantScalarType) {
if (count($argType->getConstantScalarValues()) === 0) {
return $returnType;
}

$values[] = $argType->getValue();
$values[] = $argType->getConstantScalarValues();
}

$format = array_shift($values);
if (!is_string($format)) {
return $returnType;
}
$combinations = $this->combinations($values);
$returnTypes = [];
foreach ($combinations as $combination) {
$format = array_shift($combination);
if (!is_string($format)) {
return $returnType;
}

try {
if ($functionReflection->getName() === 'sprintf') {
$value = @sprintf($format, ...$values);
} else {
$value = @vsprintf($format, $values);
try {
if ($functionReflection->getName() === 'sprintf') {
$returnTypes[] = $scope->getTypeFromValue(@sprintf($format, ...$combination));
} else {
$returnTypes[] = $scope->getTypeFromValue(@vsprintf($format, $combination));
}
} catch (Throwable) {
return $returnType;
}
} catch (Throwable) {
}

if (count($returnTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
return $returnType;
}

return $scope->getTypeFromValue($value);
return TypeCombinator::union(...$returnTypes);
}

/**
* @param array<mixed> $arrays
* @return iterable<mixed>
*/
private function combinations(array $arrays): iterable
{
// from https://stackoverflow.com/a/70800936/565782 by Arnaud Le Blanc
if ($arrays === []) {
yield [];
return;
}

$head = array_shift($arrays);

foreach ($head as $elem) {
foreach ($this->combinations($arrays) as $combination) {
yield [$elem, ...$combination];
}
}
}

}
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ public function dataFileAsserts(): iterable

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

yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-sprintf.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2816-2.php');
Expand Down
21 changes: 21 additions & 0 deletions tests/PHPStan/Analyser/data/dynamic-sprintf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace DynamicSprintf;

use function PHPStan\Testing\assertType;
use function sprintf;

class Foo
{

/**
* @param 'a'|'aa' $a
* @param 'b'|'bb' $b
* @param 'c'|'cc' $c
*/
public function doFoo(string $a, string $b, string $c): void
{
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));
}

}

0 comments on commit 1f95570

Please sign in to comment.