diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index fa06a05bdf..8b950f5d32 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use ArrayAccess; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\Dummy\DummyConstantReflection; @@ -16,6 +17,10 @@ use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Generic\TemplateMixedType; @@ -129,11 +134,10 @@ public function unsetOffset(Type $offsetType): Type public function isCallable(): TrinaryLogic { - if ( - $this->subtractedType !== null - && $this->subtractedType->isCallable()->yes() - ) { - return TrinaryLogic::createNo(); + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new CallableType())->yes()) { + return TrinaryLogic::createNo(); + } } return TrinaryLogic::createMaybe(); @@ -336,12 +340,18 @@ public function toArray(): Type public function isIterable(): TrinaryLogic { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new IterableType(new MixedType(), new MixedType()))->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function isIterableAtLeastOnce(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return $this->isIterable(); } public function getIterableKeyType(): Type @@ -356,6 +366,17 @@ public function getIterableValueType(): Type public function isOffsetAccessible(): TrinaryLogic { + if ($this->subtractedType !== null) { + $offsetAccessibles = new UnionType([ + new StringType(), + new ArrayType(new MixedType(), new MixedType()), + new ObjectType(ArrayAccess::class), + ]); + + if ($this->subtractedType->isSuperTypeOf($offsetAccessibles)->yes()) { + return TrinaryLogic::createNo(); + } + } return TrinaryLogic::createMaybe(); } @@ -408,31 +429,86 @@ public function traverse(callable $cb): Type public function isArray(): TrinaryLogic { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new ArrayType(new MixedType(), new MixedType()))->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function isString(): TrinaryLogic { + if ($this->subtractedType !== null) { + if ($this->subtractedType->isSuperTypeOf(new StringType())->yes()) { + return TrinaryLogic::createNo(); + } + } return TrinaryLogic::createMaybe(); } public function isNumericString(): TrinaryLogic { + if ($this->subtractedType !== null) { + $numericString = TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($numericString)->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function isNonEmptyString(): TrinaryLogic { + if ($this->subtractedType !== null) { + $nonEmptyString = TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($nonEmptyString)->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function isNonFalsyString(): TrinaryLogic { + if ($this->subtractedType !== null) { + $nonFalsyString = TypeCombinator::intersect( + new StringType(), + new AccessoryNonFalsyStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($nonFalsyString)->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } public function isLiteralString(): TrinaryLogic { + if ($this->subtractedType !== null) { + $literalString = TypeCombinator::intersect( + new StringType(), + new AccessoryLiteralStringType(), + ); + + if ($this->subtractedType->isSuperTypeOf($literalString)->yes()) { + return TrinaryLogic::createNo(); + } + } + return TrinaryLogic::createMaybe(); } diff --git a/tests/PHPStan/Type/MixedTypeTest.php b/tests/PHPStan/Type/MixedTypeTest.php index e639b15708..32af9c48e0 100644 --- a/tests/PHPStan/Type/MixedTypeTest.php +++ b/tests/PHPStan/Type/MixedTypeTest.php @@ -2,9 +2,16 @@ namespace PHPStan\Type; +use ArrayAccess; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use function sprintf; class MixedTypeTest extends PHPStanTestCase @@ -164,4 +171,474 @@ public function testIsSuperTypeOf(MixedType $type, Type $otherType, TrinaryLogic ); } + public function dataSubstractedIsArray(): array + { + return [ + [ + new MixedType(), + new ArrayType(new IntegerType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new ConstantArrayType( + [new ConstantIntegerType(1)], + [new ConstantStringType('hello')], + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new ArrayType(new MixedType(), new MixedType())]), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new ArrayType(new StringType(), new MixedType())]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([new FloatType(), new IntegerType()]), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new FloatType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(true), + new FloatType(), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsArray + */ + public function testSubstractedIsArray(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isArray(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isArray()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsString + */ + public function testSubstractedIsString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isString(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isString()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsNumericString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsNumericString + */ + public function testSubstractedIsNumericString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isNumericString(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isNumericString()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsNonEmptyString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonFalsyStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsNonEmptyString + */ + public function testSubstractedIsNonEmptyString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isNonEmptyString(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isNonEmptyString()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsNonFalsyString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonFalsyStringType(), + ), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsNonFalsyString + */ + public function testSubstractedIsNonFalsyString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isNonFalsyString(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isNonFalsyString()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsLiteralString(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryLiteralStringType(), + ), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonEmptyStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNonFalsyStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryNumericStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsLiteralString + */ + public function testSubstractedIsLiteralString(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isLiteralString(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isLiteralString()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsIterable(): array + { + return [ + [ + new MixedType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + TypeCombinator::intersect( + new StringType(), + new AccessoryLiteralStringType(), + ), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IntegerType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new IterableType(new MixedType(), new MixedType()), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new IterableType(new StringType(), new StringType()), + TrinaryLogic::createMaybe(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsIterable + */ + public function testSubstractedIsIterable(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isIterable(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isIterable()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + + public function dataSubstractedIsOffsetAccessible(): array + { + return [ + [ + new MixedType(), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new ObjectType(ArrayAccess::class), + TrinaryLogic::createMaybe(), + ], + [ + new MixedType(), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new StringType(), + new ObjectType(ArrayAccess::class), + ]), + TrinaryLogic::createNo(), + ], + [ + new MixedType(), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new StringType(), + new ObjectType(ArrayAccess::class), + new FloatType(), + ]), + TrinaryLogic::createNo(), + ], + ]; + } + + /** + * @dataProvider dataSubstractedIsOffsetAccessible + */ + public function testSubstractedIsOffsetAccessible(MixedType $mixedType, Type $typeToSubtract, TrinaryLogic $expectedResult): void + { + $subtracted = $mixedType->subtract($typeToSubtract); + $actualResult = $subtracted->isOffsetAccessible(); + + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isOffsetAccessible()', $subtracted->describe(VerbosityLevel::precise())), + ); + } + }