Skip to content

Commit 26e0a93

Browse files
ondrejmirtesphpstan-bot
authored andcommitted
Improve type inference for coalesce with ErrorType, strpos === int narrowing, and str_repeat return types
- Handle ErrorType in InitializerExprTypeResolver coalesce: `[][0] ?? 42` now resolves to `42` instead of `*ERROR*` - Apply truthy narrowing when function/method calls are compared to truthy constants via ===, enabling `strpos($s, ':') === 5` to narrow `$s` to non-falsy-string - Return non-falsy-string from str_repeat when input is non-empty and multiplier >= 2 (result length >= 2 means it can't be '0' or '') - Preserve numeric-string in str_repeat when multiplier is exactly 1 - Update test assertions for all improved type inference cases
1 parent d2d6328 commit 26e0a93

6 files changed

Lines changed: 39 additions & 8 deletions

File tree

src/Analyser/TypeSpecifier.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,6 +1548,23 @@ private function specifyTypesForConstantBinaryExpression(
15481548
)->setRootExpr($rootExpr));
15491549
}
15501550

1551+
if (
1552+
$context->true()
1553+
&& $constantType->toBoolean()->isTrue()->yes()
1554+
&& ($exprNode instanceof FuncCall || $exprNode instanceof Expr\MethodCall || $exprNode instanceof Expr\StaticCall)
1555+
) {
1556+
$additionalTypes = $this->specifyTypesInCondition(
1557+
$scope,
1558+
$exprNode,
1559+
TypeSpecifierContext::createTrue(),
1560+
)->setRootExpr($rootExpr);
1561+
1562+
if ($additionalTypes->getSureTypes() !== [] || $additionalTypes->getSureNotTypes() !== []) {
1563+
$types = $this->create($exprNode, $constantType, $context, $scope)->setRootExpr($rootExpr);
1564+
return $types->unionWith($additionalTypes);
1565+
}
1566+
}
1567+
15511568
return null;
15521569
}
15531570

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ public function getType(Expr $expr, InitializerExprContext $context): Type
281281
$leftType = $this->getType($expr->left, $context);
282282
$rightType = $this->getType($expr->right, $context);
283283

284+
if ($leftType instanceof ErrorType) {
285+
return $rightType;
286+
}
287+
284288
return TypeCombinator::union(TypeCombinator::removeNull($leftType), $rightType);
285289
}
286290

src/Type/Php/StrRepeatFunctionReturnTypeExtension.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,15 @@ public function getTypeFromFunctionCall(
6868
$accessoryTypes = [];
6969
if ($inputType->isNonEmptyString()->yes()) {
7070
if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($multiplierType)->yes()) {
71-
if ($inputType->isNonFalsyString()->yes()) {
71+
if ($inputType->isNonFalsyString()->yes() || IntegerRangeType::fromInterval(2, null)->isSuperTypeOf($multiplierType)->yes()) {
7272
$accessoryTypes[] = new AccessoryNonFalsyStringType();
7373
} else {
7474
$accessoryTypes[] = new AccessoryNonEmptyStringType();
7575
}
7676
}
7777
}
7878

79+
$addedNumericString = false;
7980
if ($inputType->isLiteralString()->yes()) {
8081
$accessoryTypes[] = new AccessoryLiteralStringType();
8182

@@ -93,10 +94,19 @@ public function getTypeFromFunctionCall(
9394

9495
if ($onlyNumbers) {
9596
$accessoryTypes[] = new AccessoryNumericStringType();
97+
$addedNumericString = true;
9698
}
9799
}
98100
}
99101

102+
if (
103+
!$addedNumericString
104+
&& $inputType->isNumericString()->yes()
105+
&& (new ConstantIntegerType(1))->isSuperTypeOf($multiplierType)->yes()
106+
) {
107+
$accessoryTypes[] = new AccessoryNumericStringType();
108+
}
109+
100110
if ($inputType->isLowercaseString()->yes()) {
101111
$accessoryTypes[] = new AccessoryLowercaseStringType();
102112
}

tests/PHPStan/Analyser/nsrt/initializer-expr-type-resolver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Foo
1414

1515
public function doFoo(): void
1616
{
17-
assertType('*ERROR*', self::COALESCE_SPECIAL); // could be 42
17+
assertType('42', self::COALESCE_SPECIAL);
1818
assertType("0|1|2|'foo'", self::COALESCE);
1919
assertType("'bar'|'foo'|true", self::TERNARY_SHORT);
2020
assertType("'bar'|'foo'", self::TERNARY_FULL);

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,18 @@ public function doFoo($literalString, string $string, $numericString)
3838
);
3939
assertType('literal-string&lowercase-string&non-falsy-string', str_repeat('a', 100));
4040
assertType('literal-string&non-falsy-string&uppercase-string', str_repeat('A', 100));
41-
assertType('literal-string&lowercase-string&non-empty-string&numeric-string&uppercase-string', str_repeat('0', 100)); // could be non-falsy-string
41+
assertType('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', str_repeat('0', 100));
4242
assertType('literal-string&lowercase-string&non-falsy-string&numeric-string&uppercase-string', str_repeat('1', 100));
4343
// Repeating a numeric type multiple times can lead to a non-numeric type: 3v4l.org/aRBdZ
44-
assertType('non-empty-string', str_repeat($numericString, 100));
44+
assertType('non-falsy-string', str_repeat($numericString, 100));
4545

4646
assertType("''", str_repeat('1.23', 0));
4747
assertType("''", str_repeat($string, 0));
4848
assertType("''", str_repeat($numericString, 0));
4949

5050
// see https://3v4l.org/U4bM2
51-
assertType("non-empty-string", str_repeat($numericString, 1)); // could be numeric-string
52-
assertType("non-empty-string", str_repeat($numericString, 2));
51+
assertType("non-empty-string&numeric-string", str_repeat($numericString, 1));
52+
assertType("non-falsy-string", str_repeat($numericString, 2));
5353
assertType("literal-string", str_repeat($literalString, 1));
5454
$x = rand(1,2);
5555
assertType("literal-string&lowercase-string&non-falsy-string&uppercase-string", str_repeat(' 1 ', $x));

tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public function variants(string $s) {
100100
assertType('string', $s);
101101

102102
if (strpos($s, ':') === 5) {
103-
assertType('string', $s); // could be non-empty-string
103+
assertType('non-falsy-string', $s);
104104
}
105105
assertType('string', $s);
106106
if (strpos($s, ':') !== 5) {
@@ -152,7 +152,7 @@ public function variants(string $s) {
152152
assertType('string', $s);
153153

154154
if (mb_strpos($s, ':') === 5) {
155-
assertType('string', $s); // could be non-empty-string
155+
assertType('non-falsy-string', $s);
156156
}
157157
assertType('string', $s);
158158
if (mb_strpos($s, ':') !== 5) {

0 commit comments

Comments
 (0)