Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
use PHPStan\Internal\CombinationsHelper;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\Accessory\AccessoryType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
Expand Down Expand Up @@ -60,6 +62,13 @@ public function getTypeFromFunctionCall(
$formatType = $scope->getType($args[0]->value);
$formatStrings = $formatType->getConstantStrings();

$isLowercase = $formatType->isLowercaseString()->yes() && $this->allValuesSatisfies(
$functionReflection,
$scope,
$args,
static fn (Type $type): bool => $type->toString()->isLowercaseString()->yes()
);

$singlePlaceholderEarlyReturn = null;
$allPatternsNonEmpty = count($formatStrings) !== 0;
$allPatternsNonFalsy = count($formatStrings) !== 0;
Expand Down Expand Up @@ -130,10 +139,10 @@ public function getTypeFromFunctionCall(

$singlePlaceholderEarlyReturn = $checkArgType->toString();
Copy link
Contributor

@staabm staabm Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder this line is also missing lowercase handling

maybe instead of fiddling with this complex loop and all its paths, just add it once here:

		if ($singlePlaceholderEarlyReturn !== null) {
			return $singlePlaceholderEarlyReturn;
		}

Copy link
Contributor Author

@VincentLanglet VincentLanglet Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I shouldn't add lowercase handling here and just rely on toString.

I'll open a PR to update IntegerType::toString to fix https://phpstan.org/r/79f89e3e-f19f-4a44-bd58-63f3c77db487

} elseif ($matches['specifier'] !== 's') {
$singlePlaceholderEarlyReturn = new IntersectionType([
new StringType(),
$singlePlaceholderEarlyReturn = $this->getStringReturnType(
new AccessoryNumericStringType(),
]);
$isLowercase,
);
}

continue;
Expand All @@ -148,10 +157,7 @@ public function getTypeFromFunctionCall(
}

if ($allPatternsNonFalsy) {
return new IntersectionType([
new StringType(),
new AccessoryNonFalsyStringType(),
]);
return $this->getStringReturnType(new AccessoryNonFalsyStringType(), $isLowercase);
}

$isNonEmpty = $allPatternsNonEmpty;
Expand All @@ -165,13 +171,10 @@ public function getTypeFromFunctionCall(
}

if ($isNonEmpty) {
return new IntersectionType([
new StringType(),
new AccessoryNonEmptyStringType(),
]);
return $this->getStringReturnType(new AccessoryNonEmptyStringType(), $isLowercase);
}

return new StringType();
return $this->getStringReturnType(null, $isLowercase);
}

/**
Expand Down Expand Up @@ -347,4 +350,23 @@ private function getConstantType(array $args, FunctionReflection $functionReflec
return TypeCombinator::union(...$returnTypes);
}

private function getStringReturnType(?AccessoryType $accessoryType, bool $isLowercase): Type
{
$accessoryTypes = [];
if ($accessoryType !== null) {
$accessoryTypes[] = $accessoryType;
}
if ($isLowercase) {
$accessoryTypes[] = new AccessoryLowercaseStringType();
}

if (count($accessoryTypes) === 0) {
return new StringType();
}

$accessoryTypes[] = new StringType();

return new IntersectionType($accessoryTypes);
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-11201.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ function returnsBool(): bool {
assertType("' 1'", $s);

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

public function vsprintf(array $array)
{
assertType('numeric-string', vsprintf("%4d", explode('-', '1988-8-1')));
assertType('lowercase-string&numeric-string', vsprintf("%4d", explode('-', '1988-8-1')));
assertType('numeric-string', vsprintf("%4d", $array));
assertType('numeric-string', vsprintf("%4d", ['123']));
assertType('lowercase-string&numeric-string', vsprintf("%4d", ['123']));
assertType('\'123\'', vsprintf("%s", ['123']));
// too many arguments.. php silently allows it
assertType('numeric-string', vsprintf("%4d", ['123', '456']));
assertType('lowercase-string&numeric-string', vsprintf("%4d", ['123', '456']));
}
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function integerRange(int $a, string $b): void
*/
public function tooBigRange(int $a, string $b): void
{
assertType("non-falsy-string", sprintf('%d %s', $a, $b));
assertType("lowercase-string&non-falsy-string", sprintf('%d %s', $a, $b));
}

}
116 changes: 116 additions & 0 deletions tests/PHPStan/Analyser/nsrt/lowercase-string-sprintf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace LowercaseStringSprintf;

use function PHPStan\Testing\assertType;

class Foo
{

/**
* @param lowercase-string $lowercase
* @param lowercase-string&non-empty-string $nonEmptyLowercase
* @param lowercase-string&non-falsy-string $nonFalsyLowercase
*/
public function doSprintf(
string $string,
string $lowercase,
string $nonEmptyLowercase,
string $nonFalsyLowercase,
bool $bool
): void {
$format = $bool ? 'Foo 1 %s' : 'Foo 2 %s';
$formatLower = $bool ? 'foo 1 %s' : 'foo 2 %s';
$constant = $bool ? 'A' : 'B';
$constantLower = $bool ? 'a' : 'b';

assertType("'A'|'B'", sprintf('%s', $constant));
assertType("'0'", sprintf('%d', $constant));
assertType("'Foo 1 A'|'Foo 1 B'|'Foo 2 A'|'Foo 2 B'", sprintf($format, $constant));
assertType("'foo 1 A'|'foo 1 B'|'foo 2 A'|'foo 2 B'", sprintf($formatLower, $constant));
assertType('string', sprintf($lowercase, $constant));
assertType('string', sprintf($string, $constant));

assertType("'a'|'b'", sprintf('%s', $constantLower));
assertType("'0'", sprintf('%d', $constantLower));
assertType("'Foo 1 a'|'Foo 1 b'|'Foo 2 a'|'Foo 2 b'", sprintf($format, $constantLower));
assertType("'foo 1 a'|'foo 1 b'|'foo 2 a'|'foo 2 b'", sprintf($formatLower, $constantLower));
assertType('lowercase-string', sprintf($lowercase, $constantLower));
assertType('string', sprintf($string, $constantLower));

assertType('lowercase-string', sprintf('%s', $lowercase));
assertType('lowercase-string&numeric-string', sprintf('%d', $lowercase));
assertType('non-falsy-string', sprintf($format, $lowercase));
assertType('lowercase-string&non-falsy-string', sprintf($formatLower, $lowercase));
assertType('lowercase-string', sprintf($lowercase, $lowercase));
assertType('string', sprintf($string, $lowercase));

assertType('lowercase-string&non-empty-string', sprintf('%s', $nonEmptyLowercase));
assertType('lowercase-string&numeric-string', sprintf('%d', $nonEmptyLowercase));
assertType('non-falsy-string', sprintf($format, $nonEmptyLowercase));
assertType('lowercase-string&non-falsy-string', sprintf($formatLower, $nonEmptyLowercase));
assertType('lowercase-string&non-empty-string', sprintf($nonEmptyLowercase, $nonEmptyLowercase));
assertType('string', sprintf($string, $nonEmptyLowercase));

assertType('lowercase-string&non-falsy-string', sprintf('%s', $nonFalsyLowercase));
assertType('lowercase-string&numeric-string', sprintf('%d', $nonFalsyLowercase));
assertType('non-falsy-string', sprintf($format, $nonFalsyLowercase));
assertType('lowercase-string&non-falsy-string', sprintf($formatLower, $nonFalsyLowercase));
assertType('lowercase-string&non-empty-string', sprintf($nonFalsyLowercase, $nonFalsyLowercase));
assertType('string', sprintf($string, $nonFalsyLowercase));
}

/**
* @param lowercase-string $lowercase
* @param lowercase-string&non-empty-string $nonEmptyLowercase
* @param lowercase-string&non-falsy-string $nonFalsyLowercase
*/
public function doVSprintf(
string $string,
string $lowercase,
string $nonEmptyLowercase,
string $nonFalsyLowercase,
bool $bool
): void {
$format = $bool ? 'Foo 1 %s' : 'Foo 2 %s';
$formatLower = $bool ? 'foo 1 %s' : 'foo 2 %s';
$constant = $bool ? 'A' : 'B';
$constantLower = $bool ? 'a' : 'b';

assertType("'A'|'B'", vsprintf('%s', [$constant]));
assertType('numeric-string', vsprintf('%d', [$constant]));
assertType('non-falsy-string', vsprintf($format, [$constant]));
assertType('non-falsy-string', vsprintf($formatLower, [$constant]));
assertType('string', vsprintf($lowercase, [$constant]));
assertType('string', vsprintf($string, [$constant]));

assertType("'a'|'b'", vsprintf('%s', [$constantLower]));
assertType('lowercase-string&numeric-string', vsprintf('%d', [$constantLower]));
assertType('non-falsy-string', vsprintf($format, [$constantLower]));
assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$constantLower]));
assertType('lowercase-string', vsprintf($lowercase, [$constantLower]));
assertType('string', vsprintf($string, [$constantLower]));

assertType('lowercase-string', vsprintf('%s', [$lowercase]));
assertType('lowercase-string&numeric-string', vsprintf('%d', [$lowercase]));
assertType('non-falsy-string', vsprintf($format, [$lowercase]));
assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$lowercase]));
assertType('lowercase-string', vsprintf($lowercase, [$lowercase]));
assertType('string', vsprintf($string, [$lowercase]));

assertType('lowercase-string&non-empty-string', vsprintf('%s', [$nonEmptyLowercase]));
assertType('lowercase-string&numeric-string', vsprintf('%d', [$nonEmptyLowercase]));
assertType('non-falsy-string', vsprintf($format, [$nonEmptyLowercase]));
assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$nonEmptyLowercase]));
assertType('lowercase-string&non-empty-string', vsprintf($nonEmptyLowercase, [$nonEmptyLowercase]));
assertType('string', vsprintf($string, [$nonEmptyLowercase]));

assertType('lowercase-string&non-falsy-string', vsprintf('%s', [$nonFalsyLowercase]));
assertType('lowercase-string&numeric-string', vsprintf('%d', [$nonFalsyLowercase]));
assertType('non-falsy-string', vsprintf($format, [$nonFalsyLowercase]));
assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$nonFalsyLowercase]));
assertType('lowercase-string&non-empty-string', vsprintf($nonFalsyLowercase, [$nonFalsyLowercase]));
assertType('string', vsprintf($string, [$nonFalsyLowercase]));
}

}
Loading