From 8bcc7e504724d1b9948cf79715e833199b4b7ce4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 15 Jan 2026 17:09:55 +0100 Subject: [PATCH 1/7] Intersection of array&hasOffset is accepted by non-empty-array --- src/Type/Accessory/NonEmptyArrayType.php | 4 ---- .../Functions/ClosureReturnTypeRuleTest.php | 6 ++++++ .../PHPStan/Rules/Functions/data/bug-13964.php | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-13964.php diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 3015e948ce..2718af8a53 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -76,10 +76,6 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { - if ($type instanceof CompoundType) { - return $type->isAcceptedBy($this, $strictTypes); - } - $isArray = $type->isArray(); $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); diff --git a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php index 38d4ec65d8..ffb100f1e9 100644 --- a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php @@ -133,4 +133,10 @@ public function testBugFunctionMethodConstants(): void $this->analyse([__DIR__ . '/data/bug-anonymous-function-method-constant.php'], []); } + public function testBug13964(): void + { + $this->analyse([__DIR__ . '/data/bug-13964.php'], []); + } + + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-13964.php b/tests/PHPStan/Rules/Functions/data/bug-13964.php new file mode 100644 index 0000000000..489a1622f8 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-13964.php @@ -0,0 +1,17 @@ +> $state */ +$state = (fn()=>[])(); + +$state = array_map(function (array $item): array { + if (array_key_exists('type', $item) && array_key_exists('data', $item)) { + return $item; + } + + return [ + 'type' => 'hello', + 'data' => [], + ]; +}, $state); From a4604bf753405227be2ffb698a7da1ecc4f23456 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 15 Jan 2026 17:10:21 +0100 Subject: [PATCH 2/7] Update ClosureReturnTypeRuleTest.php --- tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php index ffb100f1e9..3e02085923 100644 --- a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php @@ -138,5 +138,4 @@ public function testBug13964(): void $this->analyse([__DIR__ . '/data/bug-13964.php'], []); } - } From f3934fb26bdcd3be01f30232c30d70fe84497341 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 16 Jan 2026 07:11:10 +0100 Subject: [PATCH 3/7] Update IntersectionTypeTest.php --- tests/PHPStan/Type/IntersectionTypeTest.php | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index 7318ba9c65..28f6cd4341 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -10,6 +10,8 @@ use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryUppercaseStringType; +use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Accessory\OversizedArrayType; @@ -70,6 +72,30 @@ public static function dataAccepts(): Iterator new CallableType(), TrinaryLogic::createMaybe(), ]; + + yield [ + TypeCombinator::intersect( + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ), + TypeCombinator::intersect( + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType("some-key")), + ), + TrinaryLogic::createYes(), + ]; + + yield [ + TypeCombinator::intersect( + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + ), + TypeCombinator::intersect( + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType(new ConstantStringType("some-key"), new IntegerType()), + ), + TrinaryLogic::createYes(), + ]; } #[DataProvider('dataAccepts')] From 8bdb3c59a2f5911f5d7866878fb10fd588019699 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 16 Jan 2026 07:12:57 +0100 Subject: [PATCH 4/7] Update IntersectionTypeTest.php --- tests/PHPStan/Type/IntersectionTypeTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index 28f6cd4341..e3ed23eb46 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -80,7 +80,7 @@ public static function dataAccepts(): Iterator ), TypeCombinator::intersect( new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new ConstantStringType("some-key")), + new HasOffsetType(new ConstantStringType('some-key')), ), TrinaryLogic::createYes(), ]; @@ -92,7 +92,7 @@ public static function dataAccepts(): Iterator ), TypeCombinator::intersect( new ArrayType(new MixedType(), new MixedType()), - new HasOffsetValueType(new ConstantStringType("some-key"), new IntegerType()), + new HasOffsetValueType(new ConstantStringType('some-key'), new IntegerType()), ), TrinaryLogic::createYes(), ]; From f5d6e26ac9d9ca33a0e8fcedbc32882e9bb6a33f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 18 Jan 2026 19:26:45 +0100 Subject: [PATCH 5/7] resolve feedback --- src/Type/Accessory/NonEmptyArrayType.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 2718af8a53..d06699b90d 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -78,8 +78,17 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult { $isArray = $type->isArray(); $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); + $isNonEmptyArray = $isArray->and($isIterableAtLeastOnce); - return new AcceptsResult($isArray->and($isIterableAtLeastOnce), []); + if ($isNonEmptyArray->yes()) { + return AcceptsResult::createYes(); + } + + if ($type instanceof CompoundType) { + return $type->isAcceptedBy($this, $strictTypes); + } + + return new AcceptsResult($isNonEmptyArray, []); } public function isSuperTypeOf(Type $type): IsSuperTypeOfResult From 7bcab5774a4cd66f326e206074f6db6768e92445 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 19 Jan 2026 07:06:05 +0100 Subject: [PATCH 6/7] prevent duplicate work --- src/Type/Accessory/AccessoryNonEmptyStringType.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index c77cfeaefa..68e6d5b756 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -72,14 +72,16 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { - if ($type->isNonEmptyString()->yes()) { + $isNonEmptyString = $type->isNonEmptyString(); + + if ($isNonEmptyString->yes()) { return AcceptsResult::createYes(); } if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return new AcceptsResult($type->isNonEmptyString(), []); + return new AcceptsResult($isNonEmptyString, []); } public function isSuperTypeOf(Type $type): IsSuperTypeOfResult From 7192a26fb7cdd1393a8348cd6630417f781ffc18 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 19 Jan 2026 07:21:55 +0100 Subject: [PATCH 7/7] harmonize accessory types accepts() --- src/Type/Accessory/AccessoryArrayListType.php | 13 +++++++++---- src/Type/Accessory/AccessoryLiteralStringType.php | 8 +++++++- src/Type/Accessory/AccessoryLowercaseStringType.php | 8 +++++++- src/Type/Accessory/AccessoryNonEmptyStringType.php | 1 + src/Type/Accessory/AccessoryNonFalsyStringType.php | 8 +++++++- src/Type/Accessory/AccessoryNumericStringType.php | 8 +++++++- src/Type/Accessory/AccessoryUppercaseStringType.php | 8 +++++++- 7 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 0be2c190c8..d6624108f2 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -76,14 +76,19 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { + $isArray = $type->isArray(); + $isList = $type->isList(); + $isListArray = $isArray->and($isList); + + if ($isListArray->yes()) { + return AcceptsResult::createYes(); + } + if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - $isArray = $type->isArray(); - $isList = $type->isList(); - - return new AcceptsResult($isArray->and($isList), []); + return new AcceptsResult($isListArray, []); } public function isSuperTypeOf(Type $type): IsSuperTypeOfResult diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 63791b6619..abf0b2cbc4 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -74,11 +74,17 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult if ($type instanceof MixedType) { return AcceptsResult::createNo(); } + + $isLiteralString = $type->isLiteralString(); + if ($isLiteralString->yes()) { + return AcceptsResult::createYes(); + } + if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return new AcceptsResult($type->isLiteralString(), []); + return new AcceptsResult($isLiteralString, []); } public function isSuperTypeOf(Type $type): IsSuperTypeOfResult diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index 4e9a014b32..13ce997f52 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -71,11 +71,17 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { + $isLowercaseString = $type->isLowercaseString(); + + if ($isLowercaseString->yes()) { + return AcceptsResult::createYes(); + } + if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return new AcceptsResult($type->isLowercaseString(), []); + return new AcceptsResult($isLowercaseString, []); } public function isSuperTypeOf(Type $type): IsSuperTypeOfResult diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 68e6d5b756..a635ea8618 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -77,6 +77,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult if ($isNonEmptyString->yes()) { return AcceptsResult::createYes(); } + if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 65f3fe0919..dc5548789a 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -74,11 +74,17 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { + $isNonFalsyString = $type->isNonFalsyString(); + + if ($isNonFalsyString->yes()) { + return AcceptsResult::createYes(); + } + if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return new AcceptsResult($type->isNonFalsyString(), []); + return new AcceptsResult($isNonFalsyString, []); } public function isSuperTypeOf(Type $type): IsSuperTypeOfResult diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 5fe7ae79af..625c82675d 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -71,11 +71,17 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { + $isNumericString = $type->isNumericString(); + + if ($isNumericString->yes()) { + return AcceptsResult::createYes(); + } + if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return new AcceptsResult($type->isNumericString(), []); + return new AcceptsResult($isNumericString, []); } public function isSuperTypeOf(Type $type): IsSuperTypeOfResult diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php index 607aefabdc..f4f63666fe 100644 --- a/src/Type/Accessory/AccessoryUppercaseStringType.php +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -71,11 +71,17 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { + $isUppercaseString = $type->isUppercaseString(); + + if ($isUppercaseString->yes()) { + return AcceptsResult::createYes(); + } + if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } - return new AcceptsResult($type->isUppercaseString(), []); + return new AcceptsResult($isUppercaseString, []); } public function isSuperTypeOf(Type $type): IsSuperTypeOfResult