From e099481fd71d80d902a1056d69b010ced43b2300 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 30 May 2024 15:59:52 +0200 Subject: [PATCH] Keep numeric-strings in `str_repeat()` --- src/Type/Constant/ConstantStringType.php | 24 ++++++++------ .../StrRepeatFunctionReturnTypeExtension.php | 24 ++++++++++++-- .../PHPStan/Analyser/data/literal-string.php | 31 +++++++++++++++++-- .../Type/Constant/ConstantStringTypeTest.php | 8 ++++- 4 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 010e65777e..78e474380d 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -22,6 +22,7 @@ use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; @@ -405,19 +406,22 @@ public function generalize(GeneralizePrecision $precision): Type } if ($this->getValue() !== '' && $precision->isMoreSpecific()) { + $accessories = [ + new StringType(), + new AccessoryLiteralStringType(), + ]; + + if (is_numeric($this->getValue())) { + $accessories[] = new AccessoryNumericStringType(); + } + if ($this->getValue() !== '0') { - return new IntersectionType([ - new StringType(), - new AccessoryNonFalsyStringType(), - new AccessoryLiteralStringType(), - ]); + $accessories[] = new AccessoryNonFalsyStringType(); + } else { + $accessories[] = new AccessoryNonEmptyStringType(); } - return new IntersectionType([ - new StringType(), - new AccessoryNonEmptyStringType(), - new AccessoryLiteralStringType(), - ]); + return new IntersectionType($accessories); } if ($precision->isMoreSpecific()) { diff --git a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php index 6bf696ac11..3ba27a7f9e 100644 --- a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php @@ -2,12 +2,14 @@ namespace PHPStan\Type\Php; +use Nette\Utils\Strings; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -39,17 +41,17 @@ public function getTypeFromFunctionCall( return new StringType(); } - $inputType = $scope->getType($args[0]->value); $multiplierType = $scope->getType($args[1]->value); if ((new ConstantIntegerType(0))->isSuperTypeOf($multiplierType)->yes()) { return new ConstantStringType(''); } - if ($multiplierType instanceof ConstantIntegerType && $multiplierType->getValue() < 0) { + if (IntegerRangeType::fromInterval(null, 0)->isSuperTypeOf($multiplierType)->yes()) { return new NeverType(); } + $inputType = $scope->getType($args[0]->value); if ( $inputType instanceof ConstantStringType && $multiplierType instanceof ConstantIntegerType @@ -72,13 +74,29 @@ public function getTypeFromFunctionCall( if ($inputType->isLiteralString()->yes()) { $accessoryTypes[] = new AccessoryLiteralStringType(); + + if ( + $inputType->isNumericString()->yes() + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($multiplierType)->yes() + ) { + $onlyNumbers = true; + foreach ($inputType->getConstantStrings() as $constantString) { + if (Strings::match($constantString->getValue(), '#^[0-9]+$#') === null) { + $onlyNumbers = false; + break; + } + } + + if ($onlyNumbers) { + $accessoryTypes[] = new AccessoryNumericStringType(); + } + } } if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); return new IntersectionType($accessoryTypes); } - return new StringType(); } diff --git a/tests/PHPStan/Analyser/data/literal-string.php b/tests/PHPStan/Analyser/data/literal-string.php index c2e3cdfd63..ff63036d9b 100644 --- a/tests/PHPStan/Analyser/data/literal-string.php +++ b/tests/PHPStan/Analyser/data/literal-string.php @@ -7,8 +7,11 @@ class Foo { - /** @param literal-string $literalString */ - public function doFoo($literalString, string $string) + /** + * @param literal-string $literalString + * @param numeric-string $numericString + */ + public function doFoo($literalString, string $string, $numericString) { assertType('literal-string', $literalString); assertType('literal-string', $literalString . ''); @@ -34,6 +37,30 @@ public function doFoo($literalString, string $string) str_repeat('a', 99) ); assertType('literal-string&non-falsy-string', str_repeat('a', 100)); + assertType('literal-string&non-empty-string&numeric-string', str_repeat('0', 100)); // could be non-falsy-string + assertType('literal-string&non-falsy-string&numeric-string', str_repeat('1', 100)); + // Repeating a numeric type multiple times can lead to a non-numeric type: 3v4l.org/aRBdZ + assertType('non-empty-string', str_repeat($numericString, 100)); + + assertType("''", str_repeat('1.23', 0)); + assertType("''", str_repeat($string, 0)); + assertType("''", str_repeat($numericString, 0)); + + // see https://3v4l.org/U4bM2 + assertType("non-empty-string", str_repeat($numericString, 1)); // could be numeric-string + assertType("non-empty-string", str_repeat($numericString, 2)); + assertType("literal-string", str_repeat($literalString, 1)); + $x = rand(1,2); + assertType("literal-string&non-falsy-string", str_repeat(' 1 ', $x)); + assertType("literal-string&non-falsy-string", str_repeat('+1', $x)); + assertType("literal-string&non-falsy-string", str_repeat('1e9', $x)); + assertType("literal-string&non-falsy-string&numeric-string", str_repeat('19', $x)); + + $x = rand(0,2); + assertType("literal-string", str_repeat('19', $x)); + + $x = rand(-10,-1); + assertType("*NEVER*", str_repeat('19', $x)); assertType("'?,?,?,'", str_repeat('?,', 3)); assertType("*NEVER*", str_repeat('?,', -3)); diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index 4acb72d280..976917737b 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -154,7 +154,13 @@ public function testGeneralize(): void $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('NonexistentClass'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string', (new ConstantStringType(''))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); - $this->assertSame('literal-string&non-empty-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-empty-string&numeric-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('+1+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('1e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('1e91e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('string', (new ConstantStringType(''))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise())); $this->assertSame('literal-string&non-falsy-string', (new ConstantStringType(stdClass::class))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));