Skip to content

Commit adedb12

Browse files
committed
fix
1 parent c42265b commit adedb12

File tree

4 files changed

+59
-9
lines changed

4 files changed

+59
-9
lines changed

src/Analyser/LegacyTypeSpecifier.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
3939
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
4040
use PHPStan\Type\Accessory\HasOffsetType;
41+
use PHPStan\Type\Accessory\HasOffsetValueType;
4142
use PHPStan\Type\Accessory\HasPropertyType;
4243
use PHPStan\Type\Accessory\NonEmptyArrayType;
4344
use PHPStan\Type\ArrayType;
@@ -92,6 +93,8 @@
9293
final class LegacyTypeSpecifier implements TypeSpecifier
9394
{
9495

96+
private const MAX_ACCESSORIES_LIMIT = 8;
97+
9598
/** @var MethodTypeSpecifyingExtension[][]|null */
9699
private ?array $methodTypeSpecifyingExtensionsByClass = null;
97100

@@ -1189,7 +1192,27 @@ private function specifyTypesForCountFuncCall(
11891192
$builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes()];
11901193
}
11911194
} else {
1192-
$resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
1195+
$intersection = [];
1196+
$intersection[] = $arrayType;
1197+
$intersection[] = new NonEmptyArrayType();
1198+
1199+
$zero = new ConstantIntegerType(0);
1200+
$i = 0;
1201+
foreach ($builderData as [$offsetType, $valueType]) {
1202+
// non-empty-list already implies the offset 0
1203+
if ($zero->isSuperTypeOf($offsetType)->yes()) {
1204+
continue;
1205+
}
1206+
1207+
if ($i > self::MAX_ACCESSORIES_LIMIT) {
1208+
break;
1209+
}
1210+
1211+
$intersection[] = new HasOffsetValueType($offsetType, $valueType);
1212+
$i++;
1213+
}
1214+
1215+
$resultTypes[] = TypeCombinator::intersect(...$intersection);
11931216
continue;
11941217
}
11951218

src/Type/IntersectionType.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,10 @@ public function getArraySize(): Type
691691
$knownOffsets[$type->getOffsetType()->getValue()] = true;
692692
}
693693

694+
if ($this->isIterableAtLeastOnce()->yes()) {
695+
$knownOffsets[0] = true;
696+
}
697+
694698
if ($knownOffsets !== []) {
695699
return TypeCombinator::intersect($arraySize, IntegerRangeType::fromInterval(count($knownOffsets), null));
696700
}
@@ -830,9 +834,26 @@ public function isOffsetAccessLegal(): TrinaryLogic
830834

831835
public function hasOffsetValueType(Type $offsetType): TrinaryLogic
832836
{
833-
if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) {
837+
if ($this->isList()->yes()) {
834838
$arrayKeyOffsetType = $offsetType->toArrayKey();
835-
if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
839+
840+
$negative = IntegerRangeType::fromInterval(null, -1);
841+
if ($negative->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
842+
return TrinaryLogic::createNo();
843+
}
844+
845+
$size = $this->getArraySize();
846+
if ($size instanceof IntegerRangeType && $size->getMin() !== null) {
847+
$knownOffsets = IntegerRangeType::fromInterval(0, $size->getMin());
848+
} elseif ($size instanceof ConstantIntegerType) {
849+
$knownOffsets = IntegerRangeType::fromInterval(0, $size->getValue());
850+
} elseif ($this->isIterableAtLeastOnce()->yes()) {
851+
$knownOffsets = new ConstantIntegerType(0);
852+
} else {
853+
$knownOffsets = null;
854+
}
855+
856+
if ($knownOffsets !== null && $knownOffsets->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
836857
return TrinaryLogic::createYes();
837858
}
838859

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ public function count($list): void
1414
}
1515

1616
if (count($list) > 2) {
17+
assertType('false', array_key_exists(-1, $list));
18+
assertType('true', array_key_exists(0, $list));
19+
assertType('true', array_key_exists(1, $list));
20+
assertType('true', array_key_exists(2, $list));
21+
assertType('bool', array_key_exists(3, $list));
22+
1723
assertType('non-empty-list<int>&hasOffsetValue(1, int)&hasOffsetValue(2, int)', $list);
1824
assertType('int<3, max>', count($list));
1925
} else {

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,18 @@ public function countList($list): void
8484
{
8585
if (count($list) > 2) {
8686
assertType('int<3, max>', count($list));
87-
assertType('int<1, max>', count($list, COUNT_NORMAL));
88-
assertType('int<1, max>', count($list, COUNT_RECURSIVE));
87+
assertType('int<2, max>', count($list, COUNT_NORMAL));
88+
assertType('int<2, max>', count($list, COUNT_RECURSIVE));
8989
}
9090
}
9191

9292
/** @param list<int> $list */
9393
public function countListNormal($list): void
9494
{
9595
if (count($list, COUNT_NORMAL) > 2) {
96-
assertType('int<1, max>', count($list));
96+
assertType('int<2, max>', count($list));
9797
assertType('int<3, max>', count($list, COUNT_NORMAL));
98-
assertType('int<1, max>', count($list, COUNT_RECURSIVE));
98+
assertType('int<2, max>', count($list, COUNT_RECURSIVE));
9999
}
100100
}
101101

@@ -124,8 +124,8 @@ public function countMixed($arr, $mode): void
124124
public function countListRecursive($list): void
125125
{
126126
if (count($list, COUNT_RECURSIVE) > 2) {
127-
assertType('int<1, max>', count($list));
128-
assertType('int<1, max>', count($list, COUNT_NORMAL));
127+
assertType('int<2, max>', count($list));
128+
assertType('int<2, max>', count($list, COUNT_NORMAL));
129129
assertType('int<3, max>', count($list, COUNT_RECURSIVE));
130130
}
131131
}

0 commit comments

Comments
 (0)