Skip to content

Commit aa40406

Browse files
authored
Merge branch refs/heads/1.12.x into 2.0.x
2 parents 922d978 + 08dc679 commit aa40406

File tree

10 files changed

+188
-94
lines changed

10 files changed

+188
-94
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 106 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
use PHPStan\Type\ArrayType;
4040
use PHPStan\Type\BooleanType;
4141
use PHPStan\Type\ConditionalTypeForParameter;
42+
use PHPStan\Type\Constant\ConstantArrayType;
4243
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
4344
use PHPStan\Type\Constant\ConstantBooleanType;
4445
use PHPStan\Type\Constant\ConstantIntegerType;
@@ -1049,7 +1050,7 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
10491050
$offsetType = new ConstantIntegerType($i);
10501051
$valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), true);
10511052
}
1052-
} else {
1053+
} elseif ($type->isConstantArray()->yes()) {
10531054
for ($i = $sizeType->getMin();; $i++) {
10541055
$offsetType = new ConstantIntegerType($i);
10551056
$hasOffset = $type->hasOffsetValueType($offsetType);
@@ -1060,7 +1061,11 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
10601061
}
10611062

10621063
}
1063-
return $valueTypesBuilder->getArray();
1064+
1065+
$arrayType = $valueTypesBuilder->getArray();
1066+
if ($arrayType->isIterableAtLeastOnce()->yes()) {
1067+
return $arrayType;
1068+
}
10641069
}
10651070

10661071
return null;
@@ -1102,54 +1107,6 @@ private function specifyTypesForConstantBinaryExpression(
11021107
));
11031108
}
11041109

1105-
if (
1106-
!$context->null()
1107-
&& $exprNode instanceof FuncCall
1108-
&& count($exprNode->getArgs()) >= 1
1109-
&& $exprNode->name instanceof Name
1110-
&& in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true)
1111-
&& $constantType instanceof ConstantIntegerType
1112-
) {
1113-
if ($constantType->getValue() < 0) {
1114-
return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr);
1115-
}
1116-
1117-
$argType = $scope->getType($exprNode->getArgs()[0]->value);
1118-
1119-
if ($argType instanceof UnionType) {
1120-
$narrowed = $this->narrowUnionByArraySize($exprNode, $argType, $constantType, $context, $scope, $rootExpr);
1121-
if ($narrowed !== null) {
1122-
return $narrowed;
1123-
}
1124-
}
1125-
1126-
if ($context->truthy() || $constantType->getValue() === 0) {
1127-
$newContext = $context;
1128-
if ($constantType->getValue() === 0) {
1129-
$newContext = $newContext->negate();
1130-
}
1131-
1132-
if ($argType->isArray()->yes()) {
1133-
if (
1134-
$context->truthy()
1135-
&& $argType->isConstantArray()->yes()
1136-
&& $constantType->isSuperTypeOf($argType->getArraySize())->no()
1137-
) {
1138-
return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr);
1139-
}
1140-
1141-
$funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
1142-
$constArray = $this->turnListIntoConstantArray($exprNode, $argType, $constantType, $scope);
1143-
if ($context->truthy() && $constArray !== null) {
1144-
$valueTypes = $this->create($exprNode->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr);
1145-
} else {
1146-
$valueTypes = $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope, $rootExpr);
1147-
}
1148-
return $funcTypes->unionWith($valueTypes);
1149-
}
1150-
}
1151-
}
1152-
11531110
if (
11541111
!$context->null()
11551112
&& $exprNode instanceof FuncCall
@@ -1200,41 +1157,6 @@ private function specifyTypesForConstantStringBinaryExpression(
12001157
}
12011158
$constantStringValue = $scalarValues[0];
12021159

1203-
if (
1204-
$context->truthy()
1205-
&& $exprNode instanceof FuncCall
1206-
&& $exprNode->name instanceof Name
1207-
&& in_array(strtolower($exprNode->name->toString()), [
1208-
'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'ucfirst', 'lcfirst',
1209-
'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper', 'mb_ucfirst', 'mb_lcfirst',
1210-
'ucwords', 'mb_convert_case', 'mb_convert_kana',
1211-
], true)
1212-
&& isset($exprNode->getArgs()[0])
1213-
&& $constantStringValue !== ''
1214-
) {
1215-
$argType = $scope->getType($exprNode->getArgs()[0]->value);
1216-
1217-
if ($argType->isString()->yes()) {
1218-
if ($constantStringValue !== '0') {
1219-
return $this->create(
1220-
$exprNode->getArgs()[0]->value,
1221-
TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()),
1222-
$context,
1223-
false,
1224-
$scope,
1225-
);
1226-
}
1227-
1228-
return $this->create(
1229-
$exprNode->getArgs()[0]->value,
1230-
TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()),
1231-
$context,
1232-
false,
1233-
$scope,
1234-
);
1235-
}
1236-
}
1237-
12381160
if (
12391161
$exprNode instanceof FuncCall
12401162
&& $exprNode->name instanceof Name
@@ -2137,6 +2059,70 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21372059
}
21382060
$rightType = $scope->getType($rightExpr);
21392061

2062+
if (
2063+
!$context->null()
2064+
&& $unwrappedLeftExpr instanceof FuncCall
2065+
&& count($unwrappedLeftExpr->getArgs()) >= 1
2066+
&& $unwrappedLeftExpr->name instanceof Name
2067+
&& in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true)
2068+
&& $rightType->isInteger()->yes()
2069+
) {
2070+
if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
2071+
return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr);
2072+
}
2073+
2074+
$argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value);
2075+
$isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType);
2076+
if ($isZero->yes()) {
2077+
$funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr);
2078+
2079+
if ($context->truthy() && !$argType->isArray()->yes()) {
2080+
$newArgType = new UnionType([
2081+
new ObjectType(Countable::class),
2082+
new ConstantArrayType([], []),
2083+
]);
2084+
} else {
2085+
$newArgType = new ConstantArrayType([], []);
2086+
}
2087+
2088+
return $funcTypes->unionWith(
2089+
$this->create($unwrappedLeftExpr->getArgs()[0]->value, $newArgType, $context, false, $scope, $rootExpr),
2090+
);
2091+
}
2092+
2093+
if ($argType instanceof UnionType) {
2094+
$narrowed = $this->narrowUnionByArraySize($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $rootExpr);
2095+
if ($narrowed !== null) {
2096+
return $narrowed;
2097+
}
2098+
}
2099+
2100+
if ($context->truthy()) {
2101+
if ($argType->isArray()->yes()) {
2102+
if (
2103+
$argType->isConstantArray()->yes()
2104+
&& $rightType->isSuperTypeOf($argType->getArraySize())->no()
2105+
) {
2106+
return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr);
2107+
}
2108+
2109+
$funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr);
2110+
$constArray = $this->turnListIntoConstantArray($unwrappedLeftExpr, $argType, $rightType, $scope);
2111+
if ($constArray !== null) {
2112+
return $funcTypes->unionWith(
2113+
$this->create($unwrappedLeftExpr->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr),
2114+
);
2115+
} elseif (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) {
2116+
return $funcTypes->unionWith(
2117+
$this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope, $rootExpr),
2118+
);
2119+
}
2120+
2121+
return $funcTypes;
2122+
}
2123+
}
2124+
}
2125+
21402126
if (
21412127
$context->true()
21422128
&& $unwrappedLeftExpr instanceof FuncCall
@@ -2171,6 +2157,41 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21712157
}
21722158
}
21732159

2160+
if (
2161+
$context->truthy()
2162+
&& $unwrappedLeftExpr instanceof FuncCall
2163+
&& $unwrappedLeftExpr->name instanceof Name
2164+
&& in_array(strtolower($unwrappedLeftExpr->name->toString()), [
2165+
'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'ucfirst', 'lcfirst',
2166+
'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper', 'mb_ucfirst', 'mb_lcfirst',
2167+
'ucwords', 'mb_convert_case', 'mb_convert_kana',
2168+
], true)
2169+
&& isset($unwrappedLeftExpr->getArgs()[0])
2170+
&& $rightType->isNonEmptyString()->yes()
2171+
) {
2172+
$argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value);
2173+
2174+
if ($argType->isString()->yes()) {
2175+
if ($rightType->isNonFalsyString()->yes()) {
2176+
return $this->create(
2177+
$unwrappedLeftExpr->getArgs()[0]->value,
2178+
TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()),
2179+
$context,
2180+
false,
2181+
$scope,
2182+
);
2183+
}
2184+
2185+
return $this->create(
2186+
$unwrappedLeftExpr->getArgs()[0]->value,
2187+
TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()),
2188+
$context,
2189+
false,
2190+
$scope,
2191+
);
2192+
}
2193+
}
2194+
21742195
if ($rightType->isInteger()->yes() || $rightType->isString()->yes()) {
21752196
$types = null;
21762197
foreach ($rightType->getFiniteTypes() as $finiteType) {

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,17 +1188,16 @@ public function getMulType(Expr $left, Expr $right, callable $getTypeCallback):
11881188
return TypeCombinator::union(...$resultTypes);
11891189
}
11901190

1191-
$floatType = new FloatType();
11921191
$leftNumberType = $leftType->toNumber();
11931192
if ($leftNumberType instanceof ConstantIntegerType && $leftNumberType->getValue() === 0) {
1194-
if ($floatType->isSuperTypeOf($rightType)->yes()) {
1193+
if ($rightType->isFloat()->yes()) {
11951194
return new ConstantFloatType(0.0);
11961195
}
11971196
return new ConstantIntegerType(0);
11981197
}
11991198
$rightNumberType = $rightType->toNumber();
12001199
if ($rightNumberType instanceof ConstantIntegerType && $rightNumberType->getValue() === 0) {
1201-
if ($floatType->isSuperTypeOf($leftType)->yes()) {
1200+
if ($leftType->isFloat()->yes()) {
12021201
return new ConstantFloatType(0.0);
12031202
}
12041203
return new ConstantIntegerType(0);

src/Type/ExponentiateHelper.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ public static function exponentiate(Type $base, Type $exponent): Type
4646
}
4747

4848
// exponentiation of a float, stays a float
49-
$float = new FloatType();
50-
$isFloatBase = $float->isSuperTypeOf($base)->yes();
49+
$isFloatBase = $base->isFloat()->yes();
5150

5251
$isLooseZero = (new ConstantIntegerType(0))->isSuperTypeOf($exponent->toNumber());
5352
if ($isLooseZero->yes()) {

src/Type/Php/RegexArrayShapeMatcher.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ private function getOnlyTopLevelAlternation(array $captureGroups): ?RegexAlterna
280280
return null;
281281
}
282282

283+
if ($captureGroup->inOptionalQuantification()) {
284+
return null;
285+
}
286+
283287
if ($alternation === null) {
284288
$alternation = $captureGroup->getAlternation();
285289
} elseif ($alternation->getId() !== $captureGroup->getAlternation()->getId()) {

src/Type/Regex/RegexCapturingGroup.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ public function isOptional(): bool
8282
|| $this->parent !== null && $this->parent->isOptional();
8383
}
8484

85+
public function inOptionalQuantification(): bool
86+
{
87+
return $this->inOptionalQuantification;
88+
}
89+
8590
public function inOptionalAlternation(): bool
8691
{
8792
if (!$this->inAlternation()) {

tests/PHPStan/Analyser/nsrt/bug-3993.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public function doFoo($arguments)
1313
return;
1414
}
1515

16-
assertType('mixed~null', $arguments);
16+
assertType('mixed~array{}|null', $arguments);
1717

1818
array_shift($arguments);
1919

tests/PHPStan/Analyser/nsrt/count-type.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ public function doFooBar(
4444
if (count($arr) == $maybeZero) {
4545
assertType('array', $arr);
4646
} else {
47-
assertType('non-empty-array', $arr);
47+
assertType('array', $arr);
4848
}
4949
if (count($arr) === $maybeZero) {
5050
assertType('array', $arr);
5151
} else {
52-
assertType('non-empty-array', $arr);
52+
assertType('array', $arr);
5353
}
5454

5555
if (count($arr) == $negative) {
@@ -65,3 +65,24 @@ public function doFooBar(
6565
}
6666

6767
}
68+
69+
/**
70+
* @param \ArrayObject<int, mixed> $obj
71+
*/
72+
function(\ArrayObject $obj): void {
73+
if (count($obj) === 0) {
74+
assertType('ArrayObject', $obj);
75+
return;
76+
}
77+
78+
assertType('ArrayObject', $obj);
79+
};
80+
81+
function($mixed): void {
82+
if (count($mixed) === 0) {
83+
assertType('array{}|Countable', $mixed);
84+
return;
85+
}
86+
87+
assertType('mixed~array{}', $mixed);
88+
};

tests/PHPStan/Analyser/nsrt/non-empty-string-substr-specifying.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
use function PHPStan\Testing\assertType;
66

7-
class Foo {
7+
class Foo
8+
{
89
public function nonEmptySubstr(string $s, int $offset, int $length): void
910
{
1011
if (substr($s, 10) === 'hallo') {
@@ -81,4 +82,19 @@ public function nonEmptySubstr(string $s, int $offset, int $length): void
8182
assertType('\'hallo\'', $x);
8283
}
8384
}
85+
86+
/**
87+
* @param non-empty-string $nonES
88+
* @param non-falsy-string $falsyString
89+
*/
90+
public function stringTypes(string $s, $nonES, $falsyString): void
91+
{
92+
if (substr($s, 10) === $nonES) {
93+
assertType('non-empty-string', $s);
94+
}
95+
96+
if (substr($s, 10) === $falsyString) {
97+
assertType('non-falsy-string', $s);
98+
}
99+
}
84100
}

tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,3 +753,18 @@ function bug11622 (string $expression): void {
753753
assertType("array{string, string}", $matches);
754754
}
755755
}
756+
757+
function bug11604 (string $string): void {
758+
if (! preg_match('/(XX)|(YY)?ZZ/', $string, $matches)) {
759+
return;
760+
}
761+
762+
assertType("array{0: string, 1?: ''|'XX', 2?: 'YY'}", $matches);
763+
// could be array{string, '', 'YY'}|array{string, 'XX'}|array{string}
764+
}
765+
766+
function bug11604b (string $string): void {
767+
if (preg_match('/(XX)|(YY)?(ZZ)/', $string, $matches)) {
768+
assertType("array{0: string, 1?: ''|'XX', 2?: ''|'YY', 3?: 'ZZ'}", $matches);
769+
}
770+
}

0 commit comments

Comments
 (0)