diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 158b918330..5218a2367f 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -304,6 +304,18 @@ public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsRes static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes), ); + if ($result->yes() && $acceptingType->isArray()->yes()) { + foreach ($this->types as $innerType) { + if ($innerType->isArray()->yes()) { + continue; + } + $innerResult = $acceptingType->accepts($innerType, $strictTypes); + if ($innerResult->no()) { + return $innerResult; + } + } + } + if ($this->isOversizedArray()->yes()) { if (!$result->no()) { return AcceptsResult::createYes(); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 8b4e9fd5bc..135113d0a0 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -4025,4 +4025,41 @@ public function testBug13272(): void $this->analyse([__DIR__ . '/data/bug-13272.php'], []); } + public function testBug14549(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-14549-bis.php'], [ + [ + 'Parameter #1 $param of method Bug14549Bis\Foo::callArrayInt() expects array, array&callable given.', + 33, + ], + [ + 'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayStringString() expects array{string, string}, array&callable(): mixed given.', + 34, + ], + [ + 'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayObjectOrStringStringString() expects array{object|string, string, string}, array&callable(): mixed given.', + 36, + ], + [ + 'Parameter #1 $param of method Bug14549Bis\Foo::callArrayInt() expects array, array&callable given.', + 44, + ], + [ + 'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayStringString() expects array{string, string}, array&callable(): mixed given.', + 45, + ], + [ + 'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayObjectOrStringStringString() expects array{object|string, string, string}, array&callable(): mixed given.', + 47, + ], + [ + 'Parameter #1 $param of method Bug14549Bis\Foo::callArrayString() expects array, array given.', + 58, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14549-bis.php b/tests/PHPStan/Rules/Methods/data/bug-14549-bis.php new file mode 100644 index 0000000000..ea38ed6806 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-14549-bis.php @@ -0,0 +1,63 @@ + $param */ + public function callArrayInt(array $param): void + { + } + + /** @param array{string, string} $param */ + public function callConstantArrayStringString(array $param): void + { + } + + /** @param array{object|string, string} $param */ + public function callConstantArrayObjectOrStringString(array $param): void + { + } + + /** @param array{object|string, string, string} $param */ + public function callConstantArrayObjectOrStringStringString(array $param): void + { + } + + /** + * @param callable-array $task + */ + public function doCallWithCallableArray(array $task): void + { + $this->callArrayInt($task); + $this->callConstantArrayStringString($task); + $this->callConstantArrayObjectOrStringString($task); + $this->callConstantArrayObjectOrStringStringString($task); + } + + /** + * @param callable&array $task + */ + public function doCallWithCallableAndArray(array $task): void + { + $this->callArrayInt($task); + $this->callConstantArrayStringString($task); + $this->callConstantArrayObjectOrStringString($task); + $this->callConstantArrayObjectOrStringStringString($task); + } + + /** @param array $param */ + public function callArrayString(array $param): void + { + } + + public function doCallWithHasOffsetValue(array $arr): void + { + if (isset($arr[1]) && $arr[1] === 1) { + $this->callArrayString($arr); + $this->callArrayInt($arr); + } + } + +} diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index e3ed23eb46..05d07a9f7a 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -109,6 +109,118 @@ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedR ); } + /** + * @return Iterator + */ + public static function dataIsAcceptedBy(): Iterator + { + // array&callable isAcceptedBy array - success + yield [ + new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createYes(), + ]; + + // array&callable isAcceptedBy array - failure + yield [ + new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]), + new ArrayType(new MixedType(), new IntegerType()), + TrinaryLogic::createNo(), + ]; + + // array&callable isAcceptedBy constantArray{stdClass, string} - maybe + yield [ + new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new UnionType([new ObjectType('stdClass'), new StringType()]), new StringType()], + ), + TrinaryLogic::createMaybe(), + ]; + + // array&callable isAcceptedBy constantArray{string, string} - maybe + yield [ + new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new StringType(), new StringType()], + ), + TrinaryLogic::createMaybe(), + ]; + + // array&hasOffsetValue isAcceptedBy array - success + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()), + ]), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createYes(), + ]; + + // array&hasOffsetValue isAcceptedBy array - failure + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()), + ]), + new ArrayType(new MixedType(), new StringType()), + TrinaryLogic::createNo(), + ]; + + // array&hasOffsetValue isAcceptedBy array - success (matching value type) + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()), + ]), + new ArrayType(new MixedType(), new IntegerType()), + TrinaryLogic::createYes(), + ]; + + // array&hasOffsetValue isAcceptedBy constantArray{int, int} - success + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + new HasOffsetValueType(new ConstantIntegerType(0), new IntegerType()), + ]), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new IntegerType(), new IntegerType()], + ), + TrinaryLogic::createMaybe(), + ]; + + // array&hasOffsetValue isAcceptedBy constantArray{string, string} - failure + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + new HasOffsetValueType(new ConstantIntegerType(0), new IntegerType()), + ]), + new ConstantArrayType( + [new ConstantIntegerType(0), new ConstantIntegerType(1)], + [new StringType(), new StringType()], + ), + TrinaryLogic::createNo(), + ]; + } + + #[DataProvider('dataIsAcceptedBy')] + public function testIsAcceptedBy(Type $type, Type $acceptingType, TrinaryLogic $expectedResult): void + { + $actualResult = $acceptingType->accepts($type, true)->result; + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isAcceptedBy(%s)', $type->describe(VerbosityLevel::precise()), $acceptingType->describe(VerbosityLevel::precise())), + ); + } + public static function dataIsCallable(): array { return [ @@ -362,6 +474,53 @@ public static function dataIsSubTypeOf(): Iterator ]), TrinaryLogic::createYes(), ]; + + // array&callable isSubTypeOf array - success + yield [ + new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createYes(), + ]; + + // array&callable isSubTypeOf array - failure + yield [ + new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]), + new ArrayType(new MixedType(), new IntegerType()), + TrinaryLogic::createNo(), + ]; + + // array&hasOffsetValue isSubTypeOf array - success + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()), + ]), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createYes(), + ]; + + // array&hasOffsetValue isSubTypeOf array - maybe + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()), + ]), + new ArrayType(new MixedType(), new IntegerType()), + TrinaryLogic::createMaybe(), + ]; + + // array&hasOffsetValue isSubTypeOf array - failure + yield [ + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new NonEmptyArrayType(), + new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()), + ]), + new ArrayType(new MixedType(), new StringType()), + TrinaryLogic::createNo(), + ]; } #[DataProvider('dataIsSubTypeOf')]