Skip to content

Commit

Permalink
Improved sprintf() inference
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm authored Jul 14, 2024
1 parent 09fbc92 commit 8713b14
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 18 deletions.
32 changes: 17 additions & 15 deletions src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,19 @@ public function getTypeFromFunctionCall(
return null;
}

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

$formatType = $scope->getType($args[0]->value);
$formatStrings = $formatType->getConstantStrings();
if (count($formatStrings) === 0) {
return null;
}

$singlePlaceholderEarlyReturn = null;
foreach ($formatType->getConstantStrings() as $constantString) {
foreach ($formatStrings as $constantString) {
// The printf format is %[argnum$][flags][width][.precision]
if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $constantString->getValue(), $matches) === 1) {
if ($matches[1] !== '') {
Expand All @@ -80,9 +81,10 @@ public function getTypeFromFunctionCall(
// if the format string is just a placeholder and specified an argument
// of stringy type, then the return value will be of the same type
$checkArgType = $scope->getType($args[$checkArg]->value);

if ($matches[2] === 's' && $checkArgType->isString()->yes()) {
$singlePlaceholderEarlyReturn = $checkArgType;
if ($matches[2] === 's'
&& ($checkArgType->isString()->yes() || $checkArgType->isInteger()->yes())
) {
$singlePlaceholderEarlyReturn = $checkArgType->toString();
} elseif ($matches[2] !== 's') {
$singlePlaceholderEarlyReturn = new IntersectionType([
new StringType(),
Expand Down Expand Up @@ -115,19 +117,19 @@ public function getTypeFromFunctionCall(
$returnType = new StringType();
}

return $this->getConstantType($args, $returnType, $functionReflection, $scope);
return $returnType;
}

/**
* @param Arg[] $args
*/
private function getConstantType(array $args, ?Type $fallbackReturnType, FunctionReflection $functionReflection, Scope $scope): ?Type
private function getConstantType(array $args, FunctionReflection $functionReflection, Scope $scope): ?Type
{
$values = [];
$combinationsCount = 1;
foreach ($args as $arg) {
if ($arg->unpack) {
return $fallbackReturnType;
return null;
}

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

if (count($constantScalarValues) === 0) {
return $fallbackReturnType;
return null;
}

$values[] = $constantScalarValues;
$combinationsCount *= count($constantScalarValues);
}

if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
return $fallbackReturnType;
return null;
}

$combinations = CombinationsHelper::combinations($values);
$returnTypes = [];
foreach ($combinations as $combination) {
$format = array_shift($combination);
if (!is_string($format)) {
return $fallbackReturnType;
return null;
}

try {
Expand All @@ -168,12 +170,12 @@ private function getConstantType(array $args, ?Type $fallbackReturnType, Functio
$returnTypes[] = $scope->getTypeFromValue(@vsprintf($format, $combination));
}
} catch (Throwable) {
return $fallbackReturnType;
return null;
}
}

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

return TypeCombinator::union(...$returnTypes);
Expand Down
13 changes: 13 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11201.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ function returnsJustString(): string
return rand(0,1) === 1 ? 'foo' : '';
}

function returnsBool(): bool {
return true;
}

$s = sprintf("%s", returnsNonEmptyString());
assertType('non-empty-string', $s);

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

$s = sprintf('%2$s', 1234, returnsNonFalsyString());
assertType('non-falsy-string', $s);

$s = sprintf('%20s', 'abc');
assertType("' abc'", $s);

$s = sprintf('%20s', true);
assertType("' 1'", $s);

$s = sprintf('%20s', returnsBool());
assertType("non-falsy-string", $s);
16 changes: 13 additions & 3 deletions tests/PHPStan/Analyser/nsrt/bug-7387.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function inputTypes(int $i, float $f, string $s) {

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

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

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

}

public function positionalArgs($mixed, int $i, float $f, string $s) {
/**
* @param positive-int $posInt
* @param negative-int $negInt
* @param int<1, 5> $nonZeroIntRange
* @param int<-1, 5> $intRange
*/
public function positionalArgs($mixed, int $i, float $f, string $s, int $posInt, int $negInt, int $nonZeroIntRange, int $intRange) {
// https://3v4l.org/vVL0c
assertType('non-falsy-string', sprintf('%2$14s', $mixed, $i));
assertType('numeric-string', sprintf('%2$14s', $mixed, $i));
assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $posInt));
assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $negInt));
assertType('numeric-string', sprintf('%2$14s', $mixed, $intRange));
assertType('non-falsy-string&numeric-string', sprintf('%2$14s', $mixed, $nonZeroIntRange));

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

0 comments on commit 8713b14

Please sign in to comment.