Skip to content

Commit

Permalink
Keep numeric-strings in str_repeat()
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed May 30, 2024
1 parent 366982a commit e099481
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 16 deletions.
24 changes: 14 additions & 10 deletions src/Type/Constant/ConstantStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down
24 changes: 21 additions & 3 deletions src/Type/Php/StrRepeatFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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();
}

Expand Down
31 changes: 29 additions & 2 deletions tests/PHPStan/Analyser/data/literal-string.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 . '');
Expand All @@ -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));

Expand Down
8 changes: 7 additions & 1 deletion tests/PHPStan/Type/Constant/ConstantStringTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down

0 comments on commit e099481

Please sign in to comment.