Skip to content

Commit 4a6e0e9

Browse files
committed
Type a float cast to string as a numeric string or 'INF', '-INF', 'NAN'
(string) $float was non-empty-string&uppercase-string since 4a36d38, because INF, -INF and NAN are not numeric. Every other float does stringify as a numeric string, though, and losing that dropped int from array keys built from it: (string) 10.0 is '10', which PHP turns into an int key, but $map[(string) $float] became array<non-empty-string, ...>, so passing it where array<string, ...> is expected was no longer reported. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b1c2-4005-7120-a2fa-17285a96ea5b
1 parent 07b3911 commit 4a6e0e9

9 files changed

Lines changed: 70 additions & 36 deletions

File tree

‎src/Type/FloatType.php‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
99
use PHPStan\TrinaryLogic;
1010
use PHPStan\Turbo\ShadowedByTurboExtension;
11-
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
11+
use PHPStan\Type\Accessory\AccessoryNumericStringType;
1212
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
1313
use PHPStan\Type\Constant\ConstantArrayType;
1414
use PHPStan\Type\Constant\ConstantFloatType;
1515
use PHPStan\Type\Constant\ConstantIntegerType;
16+
use PHPStan\Type\Constant\ConstantStringType;
1617
use PHPStan\Type\Traits\NonArrayTypeTrait;
1718
use PHPStan\Type\Traits\NonCallableTypeTrait;
1819
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
@@ -169,12 +170,17 @@ public function toInteger(): Type
169170

170171
public function toString(): Type
171172
{
172-
// INF, -INF and NAN are not numeric
173-
return new IntersectionType([
174-
new StringType(),
175-
new AccessoryNonEmptyStringType(),
176-
new AccessoryUppercaseStringType(),
177-
]);
173+
// a finite float is numeric, INF, -INF and NAN are not
174+
return TypeCombinator::union(
175+
new IntersectionType([
176+
new StringType(),
177+
new AccessoryNumericStringType(),
178+
new AccessoryUppercaseStringType(),
179+
]),
180+
new ConstantStringType('INF'),
181+
new ConstantStringType('-INF'),
182+
new ConstantStringType('NAN'),
183+
);
178184
}
179185

180186
public function toArray(): Type

‎tests/PHPStan/Analyser/nsrt/array-fill-keys.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function withNotConstantArray(array $foo, array $bar, array $baz, array $floats,
9191
assertType("array<string, null>", array_fill_keys($foo, null));
9292
assertType("array<int, null>", array_fill_keys($bar, null));
9393
assertType("array<'foo', null>", array_fill_keys($baz, null));
94-
assertType("array<non-empty-string&uppercase-string, null>", array_fill_keys($floats, null));
94+
assertType("array<'-INF'|'INF'|'NAN'|int|(numeric-string&uppercase-string), null>", array_fill_keys($floats, null));
9595
assertType("array<int|string, null>", array_fill_keys($mixed, null));
9696
assertType('array<string, null>', array_fill_keys($list, null));
9797
assertType('*ERROR*', array_fill_keys($objectsWithoutToString, null));

‎tests/PHPStan/Analyser/nsrt/bug-12393b.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ class FooFloatString
436436
public function doFoo(float $b): void
437437
{
438438
$this->foo = $b;
439-
assertType('non-empty-string&uppercase-string', $this->foo);
439+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", $this->foo);
440440
}
441441

442442
public function doBar(): void
@@ -472,7 +472,7 @@ class FooNumericToString
472472
public function doFoo(float|int $b): void
473473
{
474474
$this->foo = $b;
475-
assertType('non-empty-string&uppercase-string', $this->foo);
475+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", $this->foo);
476476
}
477477

478478
}

‎tests/PHPStan/Analyser/nsrt/cast-to-numeric-string.php‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
*/
1515
function foo(int $a, float $b, $numeric, $numeric2, $number, $positive, $negative, $constantInt): void {
1616
assertType('decimal-int-string', (string)$a);
17-
assertType('non-empty-string&uppercase-string', (string)$b);
18-
assertType('(non-empty-string&uppercase-string)|numeric-string', (string)$numeric);
19-
assertType('(non-empty-string&uppercase-string)|numeric-string', (string)$numeric2);
20-
assertType('non-empty-string&uppercase-string', (string)$number);
17+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", (string)$b);
18+
assertType("'-INF'|'INF'|'NAN'|numeric-string", (string)$numeric);
19+
assertType("'-INF'|'INF'|'NAN'|numeric-string", (string)$numeric2);
20+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", (string)$number);
2121
assertType('decimal-int-string&non-falsy-string', (string)$positive);
2222
assertType('decimal-int-string&non-falsy-string', (string)$negative);
2323
assertType("'1'", (string)$constantInt);
@@ -33,19 +33,19 @@ function foo(int $a, float $b, $numeric, $numeric2, $number, $positive, $negativ
3333
*/
3434
function concatEmptyString(int $a, float $b, $numeric, $numeric2, $number, $positive, $negative, $constantInt): void {
3535
assertType('decimal-int-string', '' . $a);
36-
assertType('non-empty-string&uppercase-string', '' . $b);
37-
assertType('(non-empty-string&uppercase-string)|numeric-string', '' . $numeric);
38-
assertType('(non-empty-string&uppercase-string)|numeric-string', '' . $numeric2);
39-
assertType('non-empty-string&uppercase-string', '' . $number);
36+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", '' . $b);
37+
assertType("'-INF'|'INF'|'NAN'|numeric-string", '' . $numeric);
38+
assertType("'-INF'|'INF'|'NAN'|numeric-string", '' . $numeric2);
39+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", '' . $number);
4040
assertType('decimal-int-string&non-falsy-string', '' . $positive);
4141
assertType('decimal-int-string&non-falsy-string', '' . $negative);
4242
assertType("'1'", '' . $constantInt);
4343

4444
assertType('decimal-int-string', $a . '');
45-
assertType('non-empty-string&uppercase-string', $b . '');
46-
assertType('(non-empty-string&uppercase-string)|numeric-string', $numeric . '');
47-
assertType('(non-empty-string&uppercase-string)|numeric-string', $numeric2 . '');
48-
assertType('non-empty-string&uppercase-string', $number . '');
45+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", $b . '');
46+
assertType("'-INF'|'INF'|'NAN'|numeric-string", $numeric . '');
47+
assertType("'-INF'|'INF'|'NAN'|numeric-string", $numeric2 . '');
48+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", $number . '');
4949
assertType('decimal-int-string&non-falsy-string', $positive . '');
5050
assertType('decimal-int-string&non-falsy-string', $negative . '');
5151
assertType("'1'", $constantInt . '');
@@ -57,7 +57,7 @@ function concatAssignEmptyString(int $i, float $f) {
5757

5858
$s = '';
5959
$s .= $f;
60-
assertType('non-empty-string&uppercase-string', $s);
60+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", $s);
6161
}
6262

6363
/**

‎tests/PHPStan/Analyser/nsrt/filter-var.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public function scalars(bool $bool, float $float, int $int, string $string, int
154154
assertType("''|'1'", filter_var($bool));
155155
assertType("'1'", filter_var(true));
156156
assertType("''", filter_var(false));
157-
assertType('non-empty-string&uppercase-string', filter_var($float));
157+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", filter_var($float));
158158
assertType("'17'", filter_var(17.0));
159159
assertType("'17.1'", filter_var(17.1));
160160
assertType("'1.0E-50'", filter_var(1e-50));

‎tests/PHPStan/Analyser/nsrt/float-to-string.php‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,26 @@
99
*/
1010
function doFoo(float $float, $number): void
1111
{
12-
// INF, -INF and NAN stringify as 'INF', '-INF' and 'NAN', which are not numeric
13-
assertType('non-empty-string&uppercase-string', (string) $float);
14-
assertType('non-empty-string&uppercase-string', '' . $float);
12+
// a finite float stringifies as a numeric string, INF, -INF and NAN as 'INF', '-INF' and 'NAN'
13+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", (string) $float);
14+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", '' . $float);
1515
assertType('non-falsy-string&uppercase-string', $float . '1');
16-
assertType('non-empty-string&uppercase-string', (string) $number);
16+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", (string) $number);
1717

1818
// constant floats stay exact
1919
assertType("'1.5'", (string) 1.5);
2020
assertType("'INF'", (string) INF);
2121
}
22+
23+
/**
24+
* @param list<float|null> $prices
25+
*/
26+
function arrayKey(array $prices): void
27+
{
28+
// (string) 10.0 is '10', which PHP turns into an int key
29+
$map = [];
30+
foreach ($prices as $i => $price) {
31+
$map[(string) ($price ?? 0.0)][] = $i;
32+
}
33+
assertType("array<'-INF'|'INF'|'NAN'|int|(numeric-string&uppercase-string), non-empty-list<int<0, max>>>", $map);
34+
}

‎tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function replace(string $search, string $replacement, string $subject){
2626

2727
function foo(float $f) {
2828
$s = (string) $f;
29-
assertType('non-empty-string&uppercase-string', $s);
29+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", $s);
3030

3131
$price = str_replace(',', '.', $s);
3232
assertType('non-empty-string&uppercase-string', $price);

‎tests/PHPStan/Analyser/nsrt/set-type-type-specifying.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function doString(string $s, int $i, float $f, array $a, object $o)
1414
assertType('decimal-int-string', $i);
1515

1616
settype($f, 'string');
17-
assertType('non-empty-string&uppercase-string', $f);
17+
assertType("'-INF'|'INF'|'NAN'|(numeric-string&uppercase-string)", $f);
1818

1919
settype($a, 'string');
2020
assertType('*ERROR*', $a);

‎turbo-ext/src/FloatType.cpp‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,22 +129,37 @@ class FloatType
129129

130130
static zv::Val toInteger() { return integerType(); }
131131

132-
/* new IntersectionType([new StringType(), new AccessoryNonEmptyStringType(),
133-
* new AccessoryUppercaseStringType()]): INF, -INF and NAN are not numeric */
132+
/* TypeCombinator::union(new IntersectionType([new StringType(),
133+
* new AccessoryNumericStringType(), new AccessoryUppercaseStringType()]),
134+
* 'INF', '-INF', 'NAN'): a finite float is numeric, INF, -INF and NAN
135+
* are not */
134136
static zv::Val toString()
135137
{
136138
zval stringZv;
137139
if (UNEXPECTED(!pt_string_type_new(&stringZv))) return zv::Val();
138140
zv::Val string = zv::Val::adopt(stringZv);
139-
zv::Val nonEmpty = pt_type_new_shadowed(pt_accessory_non_empty_string_type_new);
140-
if (UNEXPECTED(nonEmpty.isUndef())) return zv::Val();
141+
zv::Val numeric = pt_type_new_shadowed(pt_accessory_numeric_string_type_new);
142+
if (UNEXPECTED(numeric.isUndef())) return zv::Val();
141143
zv::Val uppercase = pt_type_new_shadowed(pt_accessory_uppercase_string_type_new);
142144
if (UNEXPECTED(uppercase.isUndef())) return zv::Val();
143145
zv::Arr types = zv::Arr::create(3);
144146
types.push(std::move(string));
145-
types.push(std::move(nonEmpty));
147+
types.push(std::move(numeric));
146148
types.push(std::move(uppercase));
147-
return pt_intersection_of(std::move(types));
149+
zv::Val finite = pt_intersection_of(std::move(types));
150+
if (UNEXPECTED(finite.isUndef())) return zv::Val();
151+
zv::Val inf = pt_type_new_constant_string(PT_LC("INF"));
152+
if (UNEXPECTED(inf.isUndef())) return zv::Val();
153+
zv::Val negInf = pt_type_new_constant_string(PT_LC("-INF"));
154+
if (UNEXPECTED(negInf.isUndef())) return zv::Val();
155+
zv::Val nan = pt_type_new_constant_string(PT_LC("NAN"));
156+
if (UNEXPECTED(nan.isUndef())) return zv::Val();
157+
zval args[4];
158+
ZVAL_COPY_VALUE(&args[0], finite.raw());
159+
ZVAL_COPY_VALUE(&args[1], inf.raw());
160+
ZVAL_COPY_VALUE(&args[2], negInf.raw());
161+
ZVAL_COPY_VALUE(&args[3], nan.raw());
162+
return pt_type_combinator_call(PT_LC("union"), 4, args);
148163
}
149164

150165
/* new ConstantArrayType([new ConstantIntegerType(0)], [$this], [1],

0 commit comments

Comments
 (0)