Skip to content

Commit e099481

Browse files
authored
Keep numeric-strings in str_repeat()
1 parent 366982a commit e099481

File tree

4 files changed

+71
-16
lines changed

4 files changed

+71
-16
lines changed

src/Type/Constant/ConstantStringType.php

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
2323
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
2424
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
25+
use PHPStan\Type\Accessory\AccessoryNumericStringType;
2526
use PHPStan\Type\ClassStringType;
2627
use PHPStan\Type\CompoundType;
2728
use PHPStan\Type\ConstantScalarType;
@@ -405,19 +406,22 @@ public function generalize(GeneralizePrecision $precision): Type
405406
}
406407

407408
if ($this->getValue() !== '' && $precision->isMoreSpecific()) {
409+
$accessories = [
410+
new StringType(),
411+
new AccessoryLiteralStringType(),
412+
];
413+
414+
if (is_numeric($this->getValue())) {
415+
$accessories[] = new AccessoryNumericStringType();
416+
}
417+
408418
if ($this->getValue() !== '0') {
409-
return new IntersectionType([
410-
new StringType(),
411-
new AccessoryNonFalsyStringType(),
412-
new AccessoryLiteralStringType(),
413-
]);
419+
$accessories[] = new AccessoryNonFalsyStringType();
420+
} else {
421+
$accessories[] = new AccessoryNonEmptyStringType();
414422
}
415423

416-
return new IntersectionType([
417-
new StringType(),
418-
new AccessoryNonEmptyStringType(),
419-
new AccessoryLiteralStringType(),
420-
]);
424+
return new IntersectionType($accessories);
421425
}
422426

423427
if ($precision->isMoreSpecific()) {

src/Type/Php/StrRepeatFunctionReturnTypeExtension.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace PHPStan\Type\Php;
44

5+
use Nette\Utils\Strings;
56
use PhpParser\Node\Expr\FuncCall;
67
use PHPStan\Analyser\Scope;
78
use PHPStan\Reflection\FunctionReflection;
89
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
910
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
1011
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
12+
use PHPStan\Type\Accessory\AccessoryNumericStringType;
1113
use PHPStan\Type\Constant\ConstantIntegerType;
1214
use PHPStan\Type\Constant\ConstantStringType;
1315
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
@@ -39,17 +41,17 @@ public function getTypeFromFunctionCall(
3941
return new StringType();
4042
}
4143

42-
$inputType = $scope->getType($args[0]->value);
4344
$multiplierType = $scope->getType($args[1]->value);
4445

4546
if ((new ConstantIntegerType(0))->isSuperTypeOf($multiplierType)->yes()) {
4647
return new ConstantStringType('');
4748
}
4849

49-
if ($multiplierType instanceof ConstantIntegerType && $multiplierType->getValue() < 0) {
50+
if (IntegerRangeType::fromInterval(null, 0)->isSuperTypeOf($multiplierType)->yes()) {
5051
return new NeverType();
5152
}
5253

54+
$inputType = $scope->getType($args[0]->value);
5355
if (
5456
$inputType instanceof ConstantStringType
5557
&& $multiplierType instanceof ConstantIntegerType
@@ -72,13 +74,29 @@ public function getTypeFromFunctionCall(
7274

7375
if ($inputType->isLiteralString()->yes()) {
7476
$accessoryTypes[] = new AccessoryLiteralStringType();
77+
78+
if (
79+
$inputType->isNumericString()->yes()
80+
&& IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($multiplierType)->yes()
81+
) {
82+
$onlyNumbers = true;
83+
foreach ($inputType->getConstantStrings() as $constantString) {
84+
if (Strings::match($constantString->getValue(), '#^[0-9]+$#') === null) {
85+
$onlyNumbers = false;
86+
break;
87+
}
88+
}
89+
90+
if ($onlyNumbers) {
91+
$accessoryTypes[] = new AccessoryNumericStringType();
92+
}
93+
}
7594
}
7695

7796
if (count($accessoryTypes) > 0) {
7897
$accessoryTypes[] = new StringType();
7998
return new IntersectionType($accessoryTypes);
8099
}
81-
82100
return new StringType();
83101
}
84102

tests/PHPStan/Analyser/data/literal-string.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
class Foo
88
{
99

10-
/** @param literal-string $literalString */
11-
public function doFoo($literalString, string $string)
10+
/**
11+
* @param literal-string $literalString
12+
* @param numeric-string $numericString
13+
*/
14+
public function doFoo($literalString, string $string, $numericString)
1215
{
1316
assertType('literal-string', $literalString);
1417
assertType('literal-string', $literalString . '');
@@ -34,6 +37,30 @@ public function doFoo($literalString, string $string)
3437
str_repeat('a', 99)
3538
);
3639
assertType('literal-string&non-falsy-string', str_repeat('a', 100));
40+
assertType('literal-string&non-empty-string&numeric-string', str_repeat('0', 100)); // could be non-falsy-string
41+
assertType('literal-string&non-falsy-string&numeric-string', str_repeat('1', 100));
42+
// Repeating a numeric type multiple times can lead to a non-numeric type: 3v4l.org/aRBdZ
43+
assertType('non-empty-string', str_repeat($numericString, 100));
44+
45+
assertType("''", str_repeat('1.23', 0));
46+
assertType("''", str_repeat($string, 0));
47+
assertType("''", str_repeat($numericString, 0));
48+
49+
// see https://3v4l.org/U4bM2
50+
assertType("non-empty-string", str_repeat($numericString, 1)); // could be numeric-string
51+
assertType("non-empty-string", str_repeat($numericString, 2));
52+
assertType("literal-string", str_repeat($literalString, 1));
53+
$x = rand(1,2);
54+
assertType("literal-string&non-falsy-string", str_repeat(' 1 ', $x));
55+
assertType("literal-string&non-falsy-string", str_repeat('+1', $x));
56+
assertType("literal-string&non-falsy-string", str_repeat('1e9', $x));
57+
assertType("literal-string&non-falsy-string&numeric-string", str_repeat('19', $x));
58+
59+
$x = rand(0,2);
60+
assertType("literal-string", str_repeat('19', $x));
61+
62+
$x = rand(-10,-1);
63+
assertType("*NEVER*", str_repeat('19', $x));
3764
assertType("'?,?,?,'", str_repeat('?,', 3));
3865
assertType("*NEVER*", str_repeat('?,', -3));
3966

tests/PHPStan/Type/Constant/ConstantStringTypeTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,13 @@ public function testGeneralize(): void
154154
$this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('NonexistentClass'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));
155155
$this->assertSame('literal-string', (new ConstantStringType(''))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));
156156
$this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));
157-
$this->assertSame('literal-string&non-empty-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));
157+
$this->assertSame('literal-string&non-empty-string&numeric-string', (new ConstantStringType('0'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));
158+
$this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('1.123'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));
159+
$this->assertSame('literal-string&non-falsy-string', (new ConstantStringType(' 1 1 '))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));
160+
$this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));
161+
$this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('+1+1'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));
162+
$this->assertSame('literal-string&non-falsy-string&numeric-string', (new ConstantStringType('1e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));
163+
$this->assertSame('literal-string&non-falsy-string', (new ConstantStringType('1e91e9'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));
158164
$this->assertSame('string', (new ConstantStringType(''))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise()));
159165
$this->assertSame('string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise()));
160166
$this->assertSame('literal-string&non-falsy-string', (new ConstantStringType(stdClass::class))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise()));

0 commit comments

Comments
 (0)