Skip to content

Commit 133c60e

Browse files
Handle lowercase string in sprintf
1 parent 5ac7a1c commit 133c60e

File tree

5 files changed

+155
-17
lines changed

5 files changed

+155
-17
lines changed

src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
use PHPStan\Internal\CombinationsHelper;
99
use PHPStan\Reflection\FunctionReflection;
1010
use PHPStan\Reflection\InitializerExprTypeResolver;
11+
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
1112
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
1213
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
1314
use PHPStan\Type\Accessory\AccessoryNumericStringType;
15+
use PHPStan\Type\Accessory\AccessoryType;
1416
use PHPStan\Type\Constant\ConstantIntegerType;
1517
use PHPStan\Type\Constant\ConstantStringType;
1618
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
@@ -60,6 +62,13 @@ public function getTypeFromFunctionCall(
6062
$formatType = $scope->getType($args[0]->value);
6163
$formatStrings = $formatType->getConstantStrings();
6264

65+
$isLowercase = $formatType->isLowercaseString()->yes() && $this->allValuesSatisfies(
66+
$functionReflection,
67+
$scope,
68+
$args,
69+
static fn (Type $type): bool => $type->toString()->isLowercaseString()->yes()
70+
);
71+
6372
$singlePlaceholderEarlyReturn = null;
6473
$allPatternsNonEmpty = count($formatStrings) !== 0;
6574
$allPatternsNonFalsy = count($formatStrings) !== 0;
@@ -130,10 +139,10 @@ public function getTypeFromFunctionCall(
130139

131140
$singlePlaceholderEarlyReturn = $checkArgType->toString();
132141
} elseif ($matches['specifier'] !== 's') {
133-
$singlePlaceholderEarlyReturn = new IntersectionType([
134-
new StringType(),
142+
$singlePlaceholderEarlyReturn = $this->getStringReturnType(
135143
new AccessoryNumericStringType(),
136-
]);
144+
$isLowercase,
145+
);
137146
}
138147

139148
continue;
@@ -148,10 +157,7 @@ public function getTypeFromFunctionCall(
148157
}
149158

150159
if ($allPatternsNonFalsy) {
151-
return new IntersectionType([
152-
new StringType(),
153-
new AccessoryNonFalsyStringType(),
154-
]);
160+
return $this->getStringReturnType(new AccessoryNonFalsyStringType(), $isLowercase);
155161
}
156162

157163
$isNonEmpty = $allPatternsNonEmpty;
@@ -165,13 +171,10 @@ public function getTypeFromFunctionCall(
165171
}
166172

167173
if ($isNonEmpty) {
168-
return new IntersectionType([
169-
new StringType(),
170-
new AccessoryNonEmptyStringType(),
171-
]);
174+
return $this->getStringReturnType(new AccessoryNonEmptyStringType(), $isLowercase);
172175
}
173176

174-
return new StringType();
177+
return $this->getStringReturnType(null, $isLowercase);
175178
}
176179

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

353+
private function getStringReturnType(?AccessoryType $accessoryType, bool $isLowercase): Type
354+
{
355+
$accessoryTypes = [];
356+
if ($accessoryType !== null) {
357+
$accessoryTypes[] = $accessoryType;
358+
}
359+
if ($isLowercase) {
360+
$accessoryTypes[] = new AccessoryLowercaseStringType();
361+
}
362+
363+
if (count($accessoryTypes) === 0) {
364+
return new StringType();
365+
}
366+
367+
$accessoryTypes[] = new StringType();
368+
369+
return new IntersectionType($accessoryTypes);
370+
}
371+
350372
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ function returnsBool(): bool {
5353
assertType("' 1'", $s);
5454

5555
$s = sprintf('%20s', returnsBool());
56-
assertType("non-falsy-string", $s);
56+
assertType("lowercase-string&non-falsy-string", $s);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,11 @@ public function escapedPercent(int $i) {
107107

108108
public function vsprintf(array $array)
109109
{
110-
assertType('numeric-string', vsprintf("%4d", explode('-', '1988-8-1')));
110+
assertType('lowercase-string&numeric-string', vsprintf("%4d", explode('-', '1988-8-1')));
111111
assertType('numeric-string', vsprintf("%4d", $array));
112-
assertType('numeric-string', vsprintf("%4d", ['123']));
112+
assertType('lowercase-string&numeric-string', vsprintf("%4d", ['123']));
113113
assertType('\'123\'', vsprintf("%s", ['123']));
114114
// too many arguments.. php silently allows it
115-
assertType('numeric-string', vsprintf("%4d", ['123', '456']));
115+
assertType('lowercase-string&numeric-string', vsprintf("%4d", ['123', '456']));
116116
}
117117
}

tests/PHPStan/Analyser/nsrt/dynamic-sprintf.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public function integerRange(int $a, string $b): void
3333
*/
3434
public function tooBigRange(int $a, string $b): void
3535
{
36-
assertType("non-falsy-string", sprintf('%d %s', $a, $b));
36+
assertType("lowercase-string&non-falsy-string", sprintf('%d %s', $a, $b));
3737
}
3838

3939
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
namespace LowercaseStringSprintf;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param lowercase-string $lowercase
12+
* @param lowercase-string&non-empty-string $nonEmptyLowercase
13+
* @param lowercase-string&non-falsy-string $nonFalsyLowercase
14+
*/
15+
public function doSprintf(
16+
string $string,
17+
string $lowercase,
18+
string $nonEmptyLowercase,
19+
string $nonFalsyLowercase,
20+
bool $bool
21+
): void {
22+
$format = $bool ? 'Foo 1 %s' : 'Foo 2 %s';
23+
$formatLower = $bool ? 'foo 1 %s' : 'foo 2 %s';
24+
$constant = $bool ? 'A' : 'B';
25+
$constantLower = $bool ? 'a' : 'b';
26+
27+
assertType("'A'|'B'", sprintf('%s', $constant));
28+
assertType("'0'", sprintf('%d', $constant));
29+
assertType("'Foo 1 A'|'Foo 1 B'|'Foo 2 A'|'Foo 2 B'", sprintf($format, $constant));
30+
assertType("'foo 1 A'|'foo 1 B'|'foo 2 A'|'foo 2 B'", sprintf($formatLower, $constant));
31+
assertType('string', sprintf($lowercase, $constant));
32+
assertType('string', sprintf($string, $constant));
33+
34+
assertType("'a'|'b'", sprintf('%s', $constantLower));
35+
assertType("'0'", sprintf('%d', $constantLower));
36+
assertType("'Foo 1 a'|'Foo 1 b'|'Foo 2 a'|'Foo 2 b'", sprintf($format, $constantLower));
37+
assertType("'foo 1 a'|'foo 1 b'|'foo 2 a'|'foo 2 b'", sprintf($formatLower, $constantLower));
38+
assertType('lowercase-string', sprintf($lowercase, $constantLower));
39+
assertType('string', sprintf($string, $constantLower));
40+
41+
assertType('lowercase-string', sprintf('%s', $lowercase));
42+
assertType('lowercase-string&numeric-string', sprintf('%d', $lowercase));
43+
assertType('non-falsy-string', sprintf($format, $lowercase));
44+
assertType('lowercase-string&non-falsy-string', sprintf($formatLower, $lowercase));
45+
assertType('lowercase-string', sprintf($lowercase, $lowercase));
46+
assertType('string', sprintf($string, $lowercase));
47+
48+
assertType('lowercase-string&non-empty-string', sprintf('%s', $nonEmptyLowercase));
49+
assertType('lowercase-string&numeric-string', sprintf('%d', $nonEmptyLowercase));
50+
assertType('non-falsy-string', sprintf($format, $nonEmptyLowercase));
51+
assertType('lowercase-string&non-falsy-string', sprintf($formatLower, $nonEmptyLowercase));
52+
assertType('lowercase-string&non-empty-string', sprintf($nonEmptyLowercase, $nonEmptyLowercase));
53+
assertType('string', sprintf($string, $nonEmptyLowercase));
54+
55+
assertType('lowercase-string&non-falsy-string', sprintf('%s', $nonFalsyLowercase));
56+
assertType('lowercase-string&numeric-string', sprintf('%d', $nonFalsyLowercase));
57+
assertType('non-falsy-string', sprintf($format, $nonFalsyLowercase));
58+
assertType('lowercase-string&non-falsy-string', sprintf($formatLower, $nonFalsyLowercase));
59+
assertType('lowercase-string&non-empty-string', sprintf($nonFalsyLowercase, $nonFalsyLowercase));
60+
assertType('string', sprintf($string, $nonFalsyLowercase));
61+
}
62+
63+
/**
64+
* @param lowercase-string $lowercase
65+
* @param lowercase-string&non-empty-string $nonEmptyLowercase
66+
* @param lowercase-string&non-falsy-string $nonFalsyLowercase
67+
*/
68+
public function doVSprintf(
69+
string $string,
70+
string $lowercase,
71+
string $nonEmptyLowercase,
72+
string $nonFalsyLowercase,
73+
bool $bool
74+
): void {
75+
$format = $bool ? 'Foo 1 %s' : 'Foo 2 %s';
76+
$formatLower = $bool ? 'foo 1 %s' : 'foo 2 %s';
77+
$constant = $bool ? 'A' : 'B';
78+
$constantLower = $bool ? 'a' : 'b';
79+
80+
assertType("'A'|'B'", vsprintf('%s', [$constant]));
81+
assertType('numeric-string', vsprintf('%d', [$constant]));
82+
assertType('non-falsy-string', vsprintf($format, [$constant]));
83+
assertType('non-falsy-string', vsprintf($formatLower, [$constant]));
84+
assertType('string', vsprintf($lowercase, [$constant]));
85+
assertType('string', vsprintf($string, [$constant]));
86+
87+
assertType("'a'|'b'", vsprintf('%s', [$constantLower]));
88+
assertType('lowercase-string&numeric-string', vsprintf('%d', [$constantLower]));
89+
assertType('non-falsy-string', vsprintf($format, [$constantLower]));
90+
assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$constantLower]));
91+
assertType('lowercase-string', vsprintf($lowercase, [$constantLower]));
92+
assertType('string', vsprintf($string, [$constantLower]));
93+
94+
assertType('lowercase-string', vsprintf('%s', [$lowercase]));
95+
assertType('lowercase-string&numeric-string', vsprintf('%d', [$lowercase]));
96+
assertType('non-falsy-string', vsprintf($format, [$lowercase]));
97+
assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$lowercase]));
98+
assertType('lowercase-string', vsprintf($lowercase, [$lowercase]));
99+
assertType('string', vsprintf($string, [$lowercase]));
100+
101+
assertType('lowercase-string&non-empty-string', vsprintf('%s', [$nonEmptyLowercase]));
102+
assertType('lowercase-string&numeric-string', vsprintf('%d', [$nonEmptyLowercase]));
103+
assertType('non-falsy-string', vsprintf($format, [$nonEmptyLowercase]));
104+
assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$nonEmptyLowercase]));
105+
assertType('lowercase-string&non-empty-string', vsprintf($nonEmptyLowercase, [$nonEmptyLowercase]));
106+
assertType('string', vsprintf($string, [$nonEmptyLowercase]));
107+
108+
assertType('lowercase-string&non-falsy-string', vsprintf('%s', [$nonFalsyLowercase]));
109+
assertType('lowercase-string&numeric-string', vsprintf('%d', [$nonFalsyLowercase]));
110+
assertType('non-falsy-string', vsprintf($format, [$nonFalsyLowercase]));
111+
assertType('lowercase-string&non-falsy-string', vsprintf($formatLower, [$nonFalsyLowercase]));
112+
assertType('lowercase-string&non-empty-string', vsprintf($nonFalsyLowercase, [$nonFalsyLowercase]));
113+
assertType('string', vsprintf($string, [$nonFalsyLowercase]));
114+
}
115+
116+
}

0 commit comments

Comments
 (0)