Skip to content

Commit 193756d

Browse files
authored
Consider numeric-string types after string concat
1 parent c80f0df commit 193756d

File tree

4 files changed

+114
-1
lines changed

4 files changed

+114
-1
lines changed

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Reflection;
44

5+
use Nette\Utils\Strings;
56
use PhpParser\Node\Arg;
67
use PhpParser\Node\Expr;
78
use PhpParser\Node\Expr\BinaryOp;
@@ -28,6 +29,7 @@
2829
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
2930
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
3031
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
32+
use PHPStan\Type\Accessory\AccessoryNumericStringType;
3133
use PHPStan\Type\Accessory\NonEmptyArrayType;
3234
use PHPStan\Type\ArrayType;
3335
use PHPStan\Type\BenevolentUnionType;
@@ -78,6 +80,7 @@
7880
use function is_finite;
7981
use function is_float;
8082
use function is_int;
83+
use function is_numeric;
8184
use function max;
8285
use function min;
8386
use function sprintf;
@@ -475,6 +478,32 @@ public function resolveConcatType(Type $left, Type $right): Type
475478
$accessoryTypes[] = new AccessoryLiteralStringType();
476479
}
477480

481+
$leftNumericStringNonEmpty = TypeCombinator::remove($leftStringType, new ConstantStringType(''));
482+
if ($leftNumericStringNonEmpty->isNumericString()->yes()) {
483+
$allRightConstantsZeroOrMore = false;
484+
foreach ($rightConstantStrings as $rightConstantString) {
485+
if ($rightConstantString->getValue() === '') {
486+
continue;
487+
}
488+
489+
if (
490+
!is_numeric($rightConstantString->getValue())
491+
|| Strings::match($rightConstantString->getValue(), '#^[0-9]+$#') === null
492+
) {
493+
$allRightConstantsZeroOrMore = false;
494+
break;
495+
}
496+
497+
$allRightConstantsZeroOrMore = true;
498+
}
499+
500+
$zeroOrMoreInteger = IntegerRangeType::fromInterval(0, null);
501+
$nonNegativeRight = $allRightConstantsZeroOrMore || $zeroOrMoreInteger->isSuperTypeOf($right)->yes();
502+
if ($nonNegativeRight) {
503+
$accessoryTypes[] = new AccessoryNumericStringType();
504+
}
505+
}
506+
478507
if (count($accessoryTypes) > 0) {
479508
$accessoryTypes[] = new StringType();
480509
return new IntersectionType($accessoryTypes);

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,7 @@ public function dataFileAsserts(): iterable
14821482
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10122.php');
14831483
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10189.php');
14841484
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10317.php');
1485+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-11129.php');
14851486
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-interface-extends.php');
14861487
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-extends.php');
14871488
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-implements.php');
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace Bug11129;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/**
10+
* @param positive-int $positiveInt
11+
* @param negative-int $negativeInt
12+
* @param numeric-string $numericString
13+
* @param 0|'0'|'1'|'2' $positiveConstStrings
14+
* @param 0|-1|'2' $maybeNegativeConstStrings
15+
* @param 0|1|'a' $maybeNonNumericConstStrings
16+
* @param 0|1|0.2 $maybeFloatConstStrings
17+
*/
18+
public function foo(
19+
int $i, $positiveInt, $negativeInt, $positiveConstStrings,
20+
$numericString,
21+
$maybeNegativeConstStrings, $maybeNonNumericConstStrings, $maybeFloatConstStrings,
22+
bool $bool, float $float
23+
): void {
24+
assertType('non-falsy-string', '0'.$i);
25+
assertType('non-falsy-string&numeric-string', $i.'0');
26+
27+
assertType('non-falsy-string&numeric-string', '0'.$positiveInt);
28+
assertType('non-falsy-string&numeric-string', $positiveInt.'0');
29+
30+
assertType('non-falsy-string', '0'.$negativeInt);
31+
assertType('non-falsy-string&numeric-string', $negativeInt.'0');
32+
33+
assertType("'00'|'01'|'02'", '0'.$positiveConstStrings);
34+
assertType( "'00'|'10'|'20'", $positiveConstStrings.'0');
35+
36+
assertType("'0-1'|'00'|'02'", '0'.$maybeNegativeConstStrings);
37+
assertType("'-10'|'00'|'20'", $maybeNegativeConstStrings.'0');
38+
39+
assertType("'00'|'01'|'0a'", '0'.$maybeNonNumericConstStrings);
40+
assertType("'00'|'10'|'a0'", $maybeNonNumericConstStrings.'0');
41+
42+
assertType('non-falsy-string&numeric-string', $i.$positiveConstStrings);
43+
assertType( 'non-falsy-string', $positiveConstStrings.$i);
44+
45+
assertType('non-falsy-string', $i.$maybeNegativeConstStrings);
46+
assertType('non-falsy-string', $maybeNegativeConstStrings.$i);
47+
48+
assertType('non-falsy-string', $i.$maybeNonNumericConstStrings);
49+
assertType('non-falsy-string', $maybeNonNumericConstStrings.$i);
50+
51+
assertType('non-falsy-string', $i.$maybeFloatConstStrings); // could be 'non-falsy-string&numeric-string'
52+
assertType('non-falsy-string', $maybeFloatConstStrings.$i);
53+
54+
assertType('non-empty-string&numeric-string', $i.$bool);
55+
assertType('non-empty-string', $bool.$i);
56+
assertType('non-empty-string&numeric-string', $positiveInt.$bool); // could be 'non-falsy-string&numeric-string'
57+
assertType('non-empty-string&numeric-string', $bool.$positiveInt); // could be 'non-falsy-string&numeric-string'
58+
assertType('non-empty-string&numeric-string', $negativeInt.$bool); // could be 'non-falsy-string&numeric-string'
59+
assertType('non-empty-string', $bool.$negativeInt);
60+
61+
assertType('non-falsy-string', $i.$i);
62+
assertType('non-falsy-string', $negativeInt.$negativeInt);
63+
assertType('non-falsy-string', $maybeNegativeConstStrings.$negativeInt);
64+
assertType('non-falsy-string', $negativeInt.$maybeNegativeConstStrings);
65+
66+
// https://3v4l.org/BCS2K
67+
assertType('non-falsy-string', $float.$float);
68+
assertType('non-falsy-string&numeric-string', $float.$positiveInt);
69+
assertType('non-falsy-string', $float.$negativeInt);
70+
assertType('non-falsy-string', $float.$i);
71+
assertType('non-falsy-string', $i.$float); // could be 'non-falsy-string&numeric-string'
72+
assertType('non-falsy-string', $numericString.$float);
73+
assertType('non-falsy-string', $numericString.$maybeFloatConstStrings);
74+
75+
// https://3v4l.org/Ia4r0
76+
$scientificFloatAsString = '3e4';
77+
assertType('non-falsy-string', $numericString.$scientificFloatAsString);
78+
assertType('non-falsy-string', $i.$scientificFloatAsString);
79+
assertType('non-falsy-string', $scientificFloatAsString.$numericString);
80+
assertType('non-falsy-string', $scientificFloatAsString.$i);
81+
}
82+
83+
}

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ public function testArrayUdiffCallback(): void
636636
6,
637637
],
638638
[
639-
'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(int, int): (literal-string&non-falsy-string) given.',
639+
'Parameter #3 $data_comp_func of function array_udiff expects callable(1|2|3|4|5|6, 1|2|3|4|5|6): int, Closure(int, int): (literal-string&non-falsy-string&numeric-string) given.',
640640
14,
641641
],
642642
[

0 commit comments

Comments
 (0)