Skip to content

Commit

Permalink
Consider numeric-string types after string concat
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm authored Jun 3, 2024
1 parent c80f0df commit 193756d
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 1 deletion.
29 changes: 29 additions & 0 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Reflection;

use Nette\Utils\Strings;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp;
Expand All @@ -28,6 +29,7 @@
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BenevolentUnionType;
Expand Down Expand Up @@ -78,6 +80,7 @@
use function is_finite;
use function is_float;
use function is_int;
use function is_numeric;
use function max;
use function min;
use function sprintf;
Expand Down Expand Up @@ -475,6 +478,32 @@ public function resolveConcatType(Type $left, Type $right): Type
$accessoryTypes[] = new AccessoryLiteralStringType();
}

$leftNumericStringNonEmpty = TypeCombinator::remove($leftStringType, new ConstantStringType(''));
if ($leftNumericStringNonEmpty->isNumericString()->yes()) {
$allRightConstantsZeroOrMore = false;
foreach ($rightConstantStrings as $rightConstantString) {
if ($rightConstantString->getValue() === '') {
continue;
}

if (
!is_numeric($rightConstantString->getValue())
|| Strings::match($rightConstantString->getValue(), '#^[0-9]+$#') === null
) {
$allRightConstantsZeroOrMore = false;
break;
}

$allRightConstantsZeroOrMore = true;
}

$zeroOrMoreInteger = IntegerRangeType::fromInterval(0, null);
$nonNegativeRight = $allRightConstantsZeroOrMore || $zeroOrMoreInteger->isSuperTypeOf($right)->yes();
if ($nonNegativeRight) {
$accessoryTypes[] = new AccessoryNumericStringType();
}
}

if (count($accessoryTypes) > 0) {
$accessoryTypes[] = new StringType();
return new IntersectionType($accessoryTypes);
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10122.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10189.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10317.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-11129.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-interface-extends.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-extends.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-implements.php');
Expand Down
83 changes: 83 additions & 0 deletions tests/PHPStan/Analyser/data/bug-11129.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace Bug11129;

use function PHPStan\Testing\assertType;

class HelloWorld
{
/**
* @param positive-int $positiveInt
* @param negative-int $negativeInt
* @param numeric-string $numericString
* @param 0|'0'|'1'|'2' $positiveConstStrings
* @param 0|-1|'2' $maybeNegativeConstStrings
* @param 0|1|'a' $maybeNonNumericConstStrings
* @param 0|1|0.2 $maybeFloatConstStrings
*/
public function foo(
int $i, $positiveInt, $negativeInt, $positiveConstStrings,
$numericString,
$maybeNegativeConstStrings, $maybeNonNumericConstStrings, $maybeFloatConstStrings,
bool $bool, float $float
): void {
assertType('non-falsy-string', '0'.$i);
assertType('non-falsy-string&numeric-string', $i.'0');

assertType('non-falsy-string&numeric-string', '0'.$positiveInt);
assertType('non-falsy-string&numeric-string', $positiveInt.'0');

assertType('non-falsy-string', '0'.$negativeInt);
assertType('non-falsy-string&numeric-string', $negativeInt.'0');

assertType("'00'|'01'|'02'", '0'.$positiveConstStrings);
assertType( "'00'|'10'|'20'", $positiveConstStrings.'0');

assertType("'0-1'|'00'|'02'", '0'.$maybeNegativeConstStrings);
assertType("'-10'|'00'|'20'", $maybeNegativeConstStrings.'0');

assertType("'00'|'01'|'0a'", '0'.$maybeNonNumericConstStrings);
assertType("'00'|'10'|'a0'", $maybeNonNumericConstStrings.'0');

assertType('non-falsy-string&numeric-string', $i.$positiveConstStrings);
assertType( 'non-falsy-string', $positiveConstStrings.$i);

assertType('non-falsy-string', $i.$maybeNegativeConstStrings);
assertType('non-falsy-string', $maybeNegativeConstStrings.$i);

assertType('non-falsy-string', $i.$maybeNonNumericConstStrings);
assertType('non-falsy-string', $maybeNonNumericConstStrings.$i);

assertType('non-falsy-string', $i.$maybeFloatConstStrings); // could be 'non-falsy-string&numeric-string'
assertType('non-falsy-string', $maybeFloatConstStrings.$i);

assertType('non-empty-string&numeric-string', $i.$bool);
assertType('non-empty-string', $bool.$i);
assertType('non-empty-string&numeric-string', $positiveInt.$bool); // could be 'non-falsy-string&numeric-string'
assertType('non-empty-string&numeric-string', $bool.$positiveInt); // could be 'non-falsy-string&numeric-string'
assertType('non-empty-string&numeric-string', $negativeInt.$bool); // could be 'non-falsy-string&numeric-string'
assertType('non-empty-string', $bool.$negativeInt);

assertType('non-falsy-string', $i.$i);
assertType('non-falsy-string', $negativeInt.$negativeInt);
assertType('non-falsy-string', $maybeNegativeConstStrings.$negativeInt);
assertType('non-falsy-string', $negativeInt.$maybeNegativeConstStrings);

// https://3v4l.org/BCS2K
assertType('non-falsy-string', $float.$float);
assertType('non-falsy-string&numeric-string', $float.$positiveInt);
assertType('non-falsy-string', $float.$negativeInt);
assertType('non-falsy-string', $float.$i);
assertType('non-falsy-string', $i.$float); // could be 'non-falsy-string&numeric-string'
assertType('non-falsy-string', $numericString.$float);
assertType('non-falsy-string', $numericString.$maybeFloatConstStrings);

// https://3v4l.org/Ia4r0
$scientificFloatAsString = '3e4';
assertType('non-falsy-string', $numericString.$scientificFloatAsString);
assertType('non-falsy-string', $i.$scientificFloatAsString);
assertType('non-falsy-string', $scientificFloatAsString.$numericString);
assertType('non-falsy-string', $scientificFloatAsString.$i);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ public function testArrayUdiffCallback(): void
6,
],
[
'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.',
'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.',
14,
],
[
Expand Down

0 comments on commit 193756d

Please sign in to comment.