diff --git a/src/Analyser/ExprHandler/InstanceofHandler.php b/src/Analyser/ExprHandler/InstanceofHandler.php index 59d09b1c9a0..ffc2045c58b 100644 --- a/src/Analyser/ExprHandler/InstanceofHandler.php +++ b/src/Analyser/ExprHandler/InstanceofHandler.php @@ -12,7 +12,6 @@ use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Traverser\InstanceOfClassTypeTraverser; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -21,7 +20,6 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\StaticType; use PHPStan\Type\Type; -use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use function array_merge; use function strtolower; @@ -94,9 +92,9 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type } } else { $classType = $scope->getType($expr->class); - $traverser = new InstanceOfClassTypeTraverser(); - $classType = TypeTraverser::map($classType, $traverser); - $uncertainty = $traverser->getUncertainty(); + $result = $classType->toObjectTypeForInstanceofCheck(); + $classType = $result->type; + $uncertainty = $result->uncertainty; } if ($classType->isSuperTypeOf(new MixedType())->yes()) { diff --git a/src/Analyser/Traverser/InstanceOfClassTypeTraverser.php b/src/Analyser/Traverser/InstanceOfClassTypeTraverser.php deleted file mode 100644 index b5426ff7a08..00000000000 --- a/src/Analyser/Traverser/InstanceOfClassTypeTraverser.php +++ /dev/null @@ -1,47 +0,0 @@ -getObjectClassNames() !== []) { - $this->uncertainty = true; - return $type; - } - if ($type instanceof GenericClassStringType) { - $this->uncertainty = true; - return $type->getGenericType(); - } - if ($type instanceof ConstantStringType) { - return new ObjectType($type->getValue()); - } - return new MixedType(); - } - - public function getUncertainty(): bool - { - return $this->uncertainty; - } - -} diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 972c5cd7d3b..973826839cf 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -165,24 +165,9 @@ public function specifyTypesInCondition( } $classType = $scope->getType($expr->class); - $uncertainty = false; - $type = TypeTraverser::map($classType, static function (Type $type, callable $traverse) use (&$uncertainty): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } - if ($type->getObjectClassNames() !== []) { - $uncertainty = true; - return $type; - } - if ($type instanceof GenericClassStringType) { - $uncertainty = true; - return $type->getGenericType(); - } - if ($type instanceof ConstantStringType) { - return new ObjectType($type->getValue()); - } - return new MixedType(); - }); + $result = $classType->toObjectTypeForInstanceofCheck(); + $type = $result->type; + $uncertainty = $result->uncertainty; if (!$type->isSuperTypeOf(new MixedType())->yes()) { if ($context->true()) { diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 88e19731792..495b8266664 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -93,7 +93,6 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; use PHPStan\Type\TypeResult; -use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; @@ -2451,54 +2450,7 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con } if (strtolower($constantName) === 'class') { - return TypeTraverser::map( - $constantClassType, - function (Type $type, callable $traverse): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } - - if ($type instanceof NullType) { - return $type; - } - - if ($type instanceof EnumCaseObjectType) { - return new IntersectionType([ - new GenericClassStringType(new ObjectType($type->getClassName())), - new AccessoryLiteralStringType(), - ]); - } - - $objectClassNames = $type->getObjectClassNames(); - if (count($objectClassNames) > 1) { - throw new ShouldNotHappenException(); - } - - if ($type instanceof TemplateType && $objectClassNames === []) { - return new IntersectionType([ - new GenericClassStringType($type), - new AccessoryLiteralStringType(), - ]); - } elseif ($objectClassNames !== [] && $this->getReflectionProvider()->hasClass($objectClassNames[0])) { - $reflection = $this->getReflectionProvider()->getClass($objectClassNames[0]); - if ($reflection->isFinalByKeyword()) { - return new ConstantStringType($reflection->getName(), true); - } - - return new IntersectionType([ - new GenericClassStringType($type), - new AccessoryLiteralStringType(), - ]); - } elseif ($type->isObject()->yes()) { - return new IntersectionType([ - new ClassStringType(), - new AccessoryLiteralStringType(), - ]); - } - - return new ErrorType(); - }, - ); + return $constantClassType->toClassConstantType($this->getReflectionProvider()); } if ($constantClassType->isClassString()->yes()) { @@ -2699,33 +2651,7 @@ public function getBitwiseNotType(Expr $expr, callable $getTypeCallback): Type public function getBitwiseNotTypeFromType(Type $exprType): Type { - return TypeTraverser::map($exprType, static function (Type $type, callable $traverse): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } - if ($type instanceof ConstantStringType) { - return new ConstantStringType(~$type->getValue()); - } - if ($type->isString()->yes()) { - $accessories = []; - if (!$type->isNonEmptyString()->yes()) { - return new StringType(); - } - - $accessories[] = new AccessoryNonEmptyStringType(); - // it is not useful to apply numeric and literal strings here. - // numeric string isn't certainly kept numeric: 3v4l.org/JERDB - - return new IntersectionType([new StringType(), ...$accessories]); - } - if ($type instanceof ConstantIntegerType || $type instanceof ConstantFloatType) { - return new ConstantIntegerType(~ (int) $type->getValue()); - } - if ($type->isInteger()->yes() || $type->isFloat()->yes()) { - return new IntegerType(); - } - return new ErrorType(); - }); + return $exprType->toBitwiseNotType(); } private function resolveName(Name $name, ?ClassReflection $classReflection): string diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index e8af2f9a9a0..a5b4a02db2d 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -483,6 +483,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index da1abf5e370..de752651f4b 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -181,6 +181,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new StringType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index 2e5ca831461..cb7983281ba 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -178,6 +178,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new StringType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 2499084ba4c..af8a834852d 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -183,6 +183,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new IntersectionType([new StringType(), new self()]); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 9f2eebdbd2d..43f5aaab9d4 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -185,6 +185,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 88a811bc1be..fc52dfdefa7 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -177,6 +177,11 @@ public function toNumber(): Type ]); } + public function toBitwiseNotType(): Type + { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + public function toAbsoluteNumber(): Type { return $this->toNumber()->toAbsoluteNumber(); diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php index a85c74745be..a67ec87060e 100644 --- a/src/Type/Accessory/AccessoryUppercaseStringType.php +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -178,6 +178,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new StringType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 8146403416d..313de6aeb01 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -427,6 +427,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index f72d0641107..d20ab5c9167 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -506,6 +506,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 6fdeddd96e8..2e8afa85e1d 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -472,6 +472,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index ca6da6e4caa..03c26560b6c 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -439,6 +439,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index 1782dafa777..365c44ac1eb 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -66,6 +66,11 @@ public function toNumber(): Type return $this->toInteger(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return $this->toNumber()->toAbsoluteNumber(); diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index adad99a6331..c2760ef583e 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -409,6 +409,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/ClassNameToObjectTypeResult.php b/src/Type/ClassNameToObjectTypeResult.php new file mode 100644 index 00000000000..2bb0695488c --- /dev/null +++ b/src/Type/ClassNameToObjectTypeResult.php @@ -0,0 +1,24 @@ +getClassStringType(); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + return $this->objectType->toClassConstantType($reflectionProvider); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + return new ClassNameToObjectTypeResult($this, true); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + if ($allowString) { + return new ClassNameToObjectTypeResult( + new UnionType([new ObjectWithoutClassType(), new ClassStringType()]), + false, + ); + } + + return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 282b005c158..6800f7a8105 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -8,6 +8,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\BooleanType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\ErrorType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; @@ -83,6 +84,11 @@ public function toNumber(): Type return new ConstantIntegerType((int) $this->value); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return $this->toNumber()->toAbsoluteNumber(); diff --git a/src/Type/Constant/ConstantFloatType.php b/src/Type/Constant/ConstantFloatType.php index aff555fcfac..27e7d1f6246 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -89,6 +89,11 @@ public function toInteger(): Type return new ConstantIntegerType((int) $this->value); } + public function toBitwiseNotType(): Type + { + return new ConstantIntegerType(~ (int) $this->value); + } + public function toAbsoluteNumber(): Type { return new self(abs($this->value)); diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 6b482c62e6e..3dfb9bff2eb 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -78,6 +78,11 @@ public function toFloat(): Type return new ConstantFloatType($this->value); } + public function toBitwiseNotType(): Type + { + return new self(~$this->value); + } + public function toAbsoluteNumber(): Type { return new self(abs($this->value)); diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 40d0773c48d..23401fac77f 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -25,6 +25,7 @@ use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\AccessoryUppercaseStringType; +use PHPStan\Type\ClassNameToObjectTypeResult; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; @@ -36,6 +37,7 @@ use PHPStan\Type\IntersectionType; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\StaticType; @@ -43,8 +45,11 @@ use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function addcslashes; +use function array_unique; +use function array_values; use function in_array; use function is_float; use function is_int; @@ -288,6 +293,66 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ConstantStringType(~$this->value); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + return new ClassNameToObjectTypeResult(new ObjectType($this->value), false); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + $objectOrClassTypeClassNames = $objectOrClassType->getObjectClassNames(); + if ($allowString) { + foreach ($objectOrClassType->getConstantStrings() as $constantString) { + $objectOrClassTypeClassNames[] = $constantString->getValue(); + } + $objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames)); + } + + $uncertainty = false; + if (!$allowSameClass) { + if ($objectOrClassTypeClassNames === [$this->value]) { + $isSameClass = true; + foreach ($objectOrClassType->getObjectClassReflections() as $classReflection) { + if (!$classReflection->isFinal()) { + $isSameClass = false; + break; + } + } + + if ($isSameClass) { + return new ClassNameToObjectTypeResult(new NeverType(), false); + } + } + + if ( + // For object, as soon as the exact same type is provided + // in the list we cannot be sure of the result + in_array($this->value, $objectOrClassTypeClassNames, true) + // This also occurs for generic class string + || ($allowString && $objectOrClassTypeClassNames === [] && $objectOrClassType->isSuperTypeOf($this)->yes()) + ) { + $uncertainty = true; + } + } + + if ($allowString) { + return new ClassNameToObjectTypeResult( + new UnionType([ + new ObjectType($this->value), + new GenericClassStringType(new ObjectType($this->value)), + ]), + $uncertainty, + ); + } + + return new ClassNameToObjectTypeResult(new ObjectType($this->value), $uncertainty); + } + public function toAbsoluteNumber(): Type { return $this->toNumber()->toAbsoluteNumber(); diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php index b77b957f3b2..5922bfe882b 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -11,14 +11,17 @@ use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Php\EnumPropertyReflection; use PHPStan\Reflection\Php\EnumUnresolvedPropertyPrototypeReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; @@ -228,6 +231,17 @@ public function getClassStringType(): Type return new GenericClassStringType(new ObjectType($this->getClassName())); } + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + // Enum cases always read their `::class` as the bare enum class + // name. Skip the parent's finality collapse: even though enum + // classes are reported as `final` by reflection, `Foo::Bar::class` + // in user code should resolve to `class-string&literal-string`, + // not the literal `'Foo'`, to keep the case-binding visible at + // downstream sites. + return new IntersectionType([$this->getClassStringType(), new AccessoryLiteralStringType()]); + } + public function toPhpDocNode(): TypeNode { return new ConstTypeNode( diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 5df57fe7955..b5439f8048c 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -103,6 +103,11 @@ public function toNumber(): Type return $this; } + public function toBitwiseNotType(): Type + { + return new IntegerType(); + } + public function toAbsoluteNumber(): Type { return $this; diff --git a/src/Type/Generic/GenericClassStringType.php b/src/Type/Generic/GenericClassStringType.php index 3123e533d51..a76e1cb8151 100644 --- a/src/Type/Generic/GenericClassStringType.php +++ b/src/Type/Generic/GenericClassStringType.php @@ -7,6 +7,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Type\AcceptsResult; +use PHPStan\Type\ClassNameToObjectTypeResult; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantStringType; @@ -45,6 +46,27 @@ public function getGenericType(): Type return $this->type; } + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + // `class-string` narrows to `X` for the comparison target, but + // the actual runtime class can be any subclass of `X` — keep + // uncertainty so the caller falls back to `BooleanType` instead + // of a definite yes when `$x instanceof Y` and `Y === X`. + return new ClassNameToObjectTypeResult($this->getGenericType(), true); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + if ($allowString) { + return new ClassNameToObjectTypeResult( + TypeCombinator::union($this->getGenericType(), $this), + false, + ); + } + + return new ClassNameToObjectTypeResult($this->getGenericType(), false); + } + public function getClassStringObjectType(): Type { return $this->getGenericType(); diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index d4ed8baf373..694dd1d61af 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -4,13 +4,17 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\RecursionGuard; use PHPStan\Type\SubtractableType; use PHPStan\Type\Type; @@ -264,6 +268,32 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + // `T::class` keeps the template variable visible — express the + // result as `class-string&literal-string` rather than the bound + // class, regardless of whether `T` has an object bound or not. + // Only when the bound is a known final class does the result + // collapse to the literal class name (the bound is the only + // possible substitution in that case). + if ($this->isNull()->yes()) { + return new NullType(); + } + + $classNames = $this->getObjectClassNames(); + if (count($classNames) === 1 && $reflectionProvider->hasClass($classNames[0])) { + $reflection = $reflectionProvider->getClass($classNames[0]); + if ($reflection->isFinalByKeyword()) { + return new ConstantStringType($reflection->getName(), true); + } + } + + return new IntersectionType([ + new GenericClassStringType($this), + new AccessoryLiteralStringType(), + ]); + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ( diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 62dafd4ade1..a378a7e7bcd 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -57,6 +57,11 @@ public function toNumber(): Type return $this; } + public function toBitwiseNotType(): Type + { + return new IntegerType(); + } + public function toAbsoluteNumber(): Type { return IntegerRangeType::createAllGreaterThanOrEqualTo(0); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 714b8360284..4b45e2453a4 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -19,6 +19,7 @@ use PHPStan\Reflection\MissingMethodFromReflectionException; use PHPStan\Reflection\MissingPropertyFromReflectionException; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection; @@ -1411,6 +1412,55 @@ public function toNumber(): Type return $type; } + public function toBitwiseNotType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->toBitwiseNotType()); + } + + public function toGetClassResultType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->toGetClassResultType()); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->toClassConstantType($reflectionProvider)); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + $types = []; + $uncertainty = false; + foreach ($this->getTypes() as $innerType) { + $result = $innerType->toObjectTypeForInstanceofCheck(); + $types[] = $result->type; + if (!$result->uncertainty) { + continue; + } + + $uncertainty = true; + } + + return new ClassNameToObjectTypeResult(TypeCombinator::intersect(...$types), $uncertainty); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + $types = []; + $uncertainty = false; + foreach ($this->getTypes() as $innerType) { + $result = $innerType->toObjectTypeForIsACheck($objectOrClassType, $allowString, $allowSameClass); + $types[] = $result->type; + if (!$result->uncertainty) { + continue; + } + + $uncertainty = true; + } + + return new ClassNameToObjectTypeResult(TypeCombinator::intersect(...$types), $uncertainty); + } + public function toAbsoluteNumber(): Type { $type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber()); diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 2cf46b754e9..80846cf0983 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -200,6 +200,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 807a5ab259b..8fee88984df 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -13,6 +13,7 @@ use PHPStan\Reflection\Dummy\DummyPropertyReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; @@ -614,6 +615,50 @@ public function toNumber(): Type ); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + + public function toGetClassResultType(): Type + { + $isObject = $this->isObject(); + if ($isObject->no()) { + return new ConstantBooleanType(false); + } + + $classString = $this->getClassStringType(); + if ($isObject->yes()) { + return $classString; + } + + return new UnionType([$classString, new ConstantBooleanType(false)]); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + // `mixed::class` is undefined — the original `TypeTraverser` cb fell + // through to `ErrorType` for any leaf that wasn't a definite object. + return new ErrorType(); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + return new ClassNameToObjectTypeResult(new MixedType(), false); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + if ($allowString) { + return new ClassNameToObjectTypeResult( + new UnionType([new ObjectWithoutClassType(), new ClassStringType()]), + false, + ); + } + + return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false); + } + public function toAbsoluteNumber(): Type { return $this->toNumber()->toAbsoluteNumber(); diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 9b1bcd9e363..c73d5813a62 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -9,6 +9,7 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; @@ -434,6 +435,31 @@ public function toNumber(): Type return $this; } + public function toBitwiseNotType(): Type + { + return $this; + } + + public function toGetClassResultType(): Type + { + return $this; + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + return $this; + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + return new ClassNameToObjectTypeResult($this, false); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + return new ClassNameToObjectTypeResult($this, false); + } + public function toAbsoluteNumber(): Type { return $this; diff --git a/src/Type/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index 5c217604f60..23eae76a12a 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -9,10 +9,12 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; @@ -163,6 +165,38 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + + public function toGetClassResultType(): Type + { + return $this->getClassStringType(); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + return new IntersectionType([$this->getClassStringType(), new AccessoryLiteralStringType()]); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + return new ClassNameToObjectTypeResult($this, true); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + if ($allowString) { + return new ClassNameToObjectTypeResult( + new UnionType([new ObjectWithoutClassType(), new ClassStringType()]), + false, + ); + } + + return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 5c7730ee9f7..db804ebba79 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -144,6 +145,18 @@ public function toNumber(): Type return new ConstantIntegerType(0); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + // Null `::class` reads as `null` (mirrors how `null::class` flows + // through the `::class` resolution pipeline alongside object types). + return $this; + } + public function toAbsoluteNumber(): Type { return $this->toNumber()->toAbsoluteNumber(); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index ddd98803841..7fc355d3ed0 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -23,6 +23,7 @@ use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; @@ -33,6 +34,7 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\HasOffsetValueType; @@ -763,6 +765,45 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + + public function toGetClassResultType(): Type + { + return $this->getClassStringType(); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + if ($reflectionProvider->hasClass($this->className)) { + $reflection = $reflectionProvider->getClass($this->className); + if ($reflection->isFinalByKeyword()) { + return new ConstantStringType($reflection->getName(), true); + } + } + + return new IntersectionType([$this->getClassStringType(), new AccessoryLiteralStringType()]); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + return new ClassNameToObjectTypeResult($this, true); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + if ($allowString) { + return new ClassNameToObjectTypeResult( + new UnionType([new ObjectWithoutClassType(), new ClassStringType()]), + false, + ); + } + + return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false); + } + public function toAbsoluteNumber(): Type { return $this->toNumber()->toAbsoluteNumber(); diff --git a/src/Type/Php/GetClassDynamicReturnTypeExtension.php b/src/Type/Php/GetClassDynamicReturnTypeExtension.php index c79c2126f71..649cda40956 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -10,11 +10,8 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntersectionType; use PHPStan\Type\Type; -use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; use function count; #[AutowiredService] @@ -48,29 +45,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new ClassStringType(); } - return TypeTraverser::map( - $argType, - static function (Type $type, callable $traverse): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } - - $isObject = $type->isObject(); - if ($isObject->no()) { - return new ConstantBooleanType(false); - } - - $classStringType = $type->getClassStringType(); - if ($isObject->yes()) { - return $classStringType; - } - - return new UnionType([ - $classStringType, - new ConstantBooleanType(false), - ]); - }, - ); + return $argType->toGetClassResultType(); } } diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php index 603d9a15cc2..3af3ef9ba28 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -3,20 +3,7 @@ namespace PHPStan\Type\Php; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Type\ClassStringType; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\NeverType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeTraverser; -use PHPStan\Type\UnionType; -use function array_unique; -use function array_values; -use function in_array; #[AutowiredService] final class IsAFunctionTypeSpecifyingHelper @@ -29,84 +16,20 @@ public function determineType( bool $allowSameClass, ): ?Type { - $objectOrClassTypeClassNames = $objectOrClassType->getObjectClassNames(); - if ($allowString) { - foreach ($objectOrClassType->getConstantStrings() as $constantString) { - $objectOrClassTypeClassNames[] = $constantString->getValue(); - } - $objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames)); - } - - $isUncertain = $classType->getConstantStrings() === []; - - $resultType = TypeTraverser::map( - $classType, - static function (Type $type, callable $traverse) use ($objectOrClassType, $objectOrClassTypeClassNames, $allowString, $allowSameClass, &$isUncertain): Type { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); - } - if ($type instanceof ConstantStringType) { - if (!$allowSameClass) { - if ($objectOrClassTypeClassNames === [$type->getValue()]) { - $isSameClass = true; - foreach ($objectOrClassType->getObjectClassReflections() as $classReflection) { - if (!$classReflection->isFinal()) { - $isSameClass = false; - break; - } - } - - if ($isSameClass) { - return new NeverType(); - } - } - - if ( - // For object, as soon as the exact same type is provided - // in the list we cannot be sure of the result - in_array($type->getValue(), $objectOrClassTypeClassNames, true) - // This also occurs for generic class string - || ($allowString && $objectOrClassTypeClassNames === [] && $objectOrClassType->isSuperTypeOf($type)->yes()) - ) { - $isUncertain = true; - } - } - if ($allowString) { - return new UnionType([ - new ObjectType($type->getValue()), - new GenericClassStringType(new ObjectType($type->getValue())), - ]); - } - - return new ObjectType($type->getValue()); - } - if ($type instanceof GenericClassStringType) { - if ($allowString) { - return TypeCombinator::union( - $type->getGenericType(), - $type, - ); - } - - return $type->getGenericType(); - } - if ($allowString) { - return new UnionType([ - new ObjectWithoutClassType(), - new ClassStringType(), - ]); - } + $result = $classType->toObjectTypeForIsACheck($objectOrClassType, $allowString, $allowSameClass); - return new ObjectWithoutClassType(); - }, - ); + // `getConstantStrings() === []` propagates uncertainty from + // the input as a whole — preserved from the original + // `$isUncertain` initial state to keep the false-positive + // suppression below identical. + $isUncertain = $result->uncertainty || $classType->getConstantStrings() === []; // prevent false-positives - if ($isUncertain && $resultType->isSuperTypeOf($objectOrClassType)->yes()) { + if ($isUncertain && $result->type->isSuperTypeOf($objectOrClassType)->yes()) { return null; } - return $resultType; + return $result->type; } } diff --git a/src/Type/ResourceType.php b/src/Type/ResourceType.php index 9f28095dd80..e284f16d1fc 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -55,6 +55,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 6e9cfc841bb..ff646bb5332 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -10,11 +10,13 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; @@ -753,6 +755,45 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + + public function toGetClassResultType(): Type + { + // Preserve static binding (`class-string` / + // `class-string<$this>`) by going through `getClassStringType()` + // directly instead of delegating to the underlying object type, + // which would resolve `static` away. + return $this->getClassStringType(); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + // Like `toGetClassResultType()`, project through this `StaticType`'s + // own `getClassStringType()` so that `static::class` reads as + // `class-string` rather than the underlying class. + return new IntersectionType([$this->getClassStringType(), new AccessoryLiteralStringType()]); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + return new ClassNameToObjectTypeResult($this, true); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + if ($allowString) { + return new ClassNameToObjectTypeResult( + new UnionType([new ObjectWithoutClassType(), new ClassStringType()]), + false, + ); + } + + return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index af20367941f..42ef8e71cc0 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -9,10 +9,12 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateTypeMap; @@ -401,6 +403,38 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + + public function toGetClassResultType(): Type + { + return new ConstantBooleanType(false); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + return new ErrorType(); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + return new ClassNameToObjectTypeResult(new MixedType(), false); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + if ($allowString) { + return new ClassNameToObjectTypeResult( + new UnionType([new ObjectWithoutClassType(), new ClassStringType()]), + false, + ); + } + + return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 730869022fc..8c5fc17f4a8 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -145,6 +145,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new StringType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/ThisType.php b/src/Type/ThisType.php index 39d4949ca8f..19a9fadda11 100644 --- a/src/Type/ThisType.php +++ b/src/Type/ThisType.php @@ -5,6 +5,8 @@ use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Type\Constant\ConstantStringType; use function sprintf; /** @api */ @@ -85,4 +87,18 @@ public function toPhpDocNode(): TypeNode return new ThisTypeNode(); } + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + // `$this` in a `final` class is pinned to that one class, so + // `$this::class` collapses to its literal name. For non-final + // classes `$this` could still be a subclass, so fall back to the + // `class-string<$this>` projection from the parent. + $reflection = $this->getClassReflection(); + if ($reflection->isFinalByKeyword()) { + return new ConstantStringType($reflection->getName(), true); + } + + return parent::toClassConstantType($reflectionProvider); + } + } diff --git a/src/Type/Traits/ArrayTypeTrait.php b/src/Type/Traits/ArrayTypeTrait.php index a019125c3f0..9ac5e1d97db 100644 --- a/src/Type/Traits/ArrayTypeTrait.php +++ b/src/Type/Traits/ArrayTypeTrait.php @@ -80,6 +80,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index ff9e91a6f61..e5d2b583737 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -7,11 +7,13 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; use PHPStan\Type\BooleanType; +use PHPStan\Type\ClassNameToObjectTypeResult; use PHPStan\Type\CompoundType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\TemplateTypeMap; @@ -419,6 +421,31 @@ public function toNumber(): Type return $this->resolve()->toNumber(); } + public function toBitwiseNotType(): Type + { + return $this->resolve()->toBitwiseNotType(); + } + + public function toGetClassResultType(): Type + { + return $this->resolve()->toGetClassResultType(); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + return $this->resolve()->toClassConstantType($reflectionProvider); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + return $this->resolve()->toObjectTypeForInstanceofCheck(); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + return $this->resolve()->toObjectTypeForIsACheck($objectOrClassType, $allowString, $allowSameClass); + } + public function toAbsoluteNumber(): Type { return $this->resolve()->toAbsoluteNumber(); diff --git a/src/Type/Traits/MaybeObjectTypeTrait.php b/src/Type/Traits/MaybeObjectTypeTrait.php index d6ff3913746..365af72a83e 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -9,14 +9,20 @@ use PHPStan\Reflection\Dummy\DummyPropertyReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\ClassNameToObjectTypeResult; use PHPStan\Type\ClassStringType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use PHPStan\Type\UnionType; trait MaybeObjectTypeTrait { @@ -36,6 +42,33 @@ public function getClassStringType(): Type return new ClassStringType(); } + public function toGetClassResultType(): Type + { + return new UnionType([$this->getClassStringType(), new ConstantBooleanType(false)]); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + return new ErrorType(); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + return new ClassNameToObjectTypeResult(new MixedType(), false); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + if ($allowString) { + return new ClassNameToObjectTypeResult( + new UnionType([new ObjectWithoutClassType(), new ClassStringType()]), + false, + ); + } + + return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false); + } + public function isEnum(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Traits/NonObjectTypeTrait.php b/src/Type/Traits/NonObjectTypeTrait.php index 21eb0e8aed3..a5aad74a182 100644 --- a/src/Type/Traits/NonObjectTypeTrait.php +++ b/src/Type/Traits/NonObjectTypeTrait.php @@ -6,13 +6,20 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\ClassNameToObjectTypeResult; +use PHPStan\Type\ClassStringType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; +use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use PHPStan\Type\UnionType; trait NonObjectTypeTrait { @@ -27,6 +34,33 @@ public function getClassStringType(): Type return new ErrorType(); } + public function toGetClassResultType(): Type + { + return new ConstantBooleanType(false); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + return new ErrorType(); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + return new ClassNameToObjectTypeResult(new MixedType(), false); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + if ($allowString) { + return new ClassNameToObjectTypeResult( + new UnionType([new ObjectWithoutClassType(), new ClassStringType()]), + false, + ); + } + + return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false); + } + public function isEnum(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 51a4922f43f..af087676454 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -10,17 +10,26 @@ use PHPStan\Reflection\Dummy\DummyPropertyReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; +use PHPStan\Type\ClassNameToObjectTypeResult; +use PHPStan\Type\ClassStringType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; +use function count; trait ObjectTypeTrait { @@ -41,6 +50,46 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createYes(); } + public function toGetClassResultType(): Type + { + return $this->getClassStringType(); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + $classNames = $this->getObjectClassNames(); + if (count($classNames) === 1 && $reflectionProvider->hasClass($classNames[0])) { + $reflection = $reflectionProvider->getClass($classNames[0]); + if ($reflection->isFinalByKeyword()) { + return new ConstantStringType($reflection->getName(), true); + } + } + + return new IntersectionType([$this->getClassStringType(), new AccessoryLiteralStringType()]); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + // Definite-object types keep themselves as the comparison target — + // they're already an `ObjectType` (or its accessory). The + // uncertainty flag is set because the `instanceof` decision can + // only be made at runtime when comparing against a class name + // computed from the value. + return new ClassNameToObjectTypeResult($this, true); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + if ($allowString) { + return new ClassNameToObjectTypeResult( + new UnionType([new ObjectWithoutClassType(), new ClassStringType()]), + false, + ); + } + + return new ClassNameToObjectTypeResult(new ObjectWithoutClassType(), false); + } + public function isEnum(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -285,6 +334,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 73c46a213ad..9eed2d71408 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -10,6 +10,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; @@ -373,6 +374,46 @@ public function toBoolean(): BooleanType; /** Models numeric coercion for arithmetic operators. */ public function toNumber(): Type; + /** Models the bitwise-not (`~$x`) operator. Returns `ErrorType` for types where `~` is undefined. */ + public function toBitwiseNotType(): Type; + + /** + * Models `get_class($x)`'s return type per leaf: definite objects yield + * their `class-string` projection, definite non-objects yield `false`, + * and possibly-objects yield the union of both. + */ + public function toGetClassResultType(): Type; + + /** + * Models the type of `$x::class`. For known final classes the literal + * class name is returned; for everything else an + * `IntersectionType[ClassString, AccessoryLiteralStringType]`. + * `NullType` passes through (mirrors PHP's nullsafe `::class` semantics). + * `ReflectionProvider` is needed for the final-class lookup. + */ + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type; + + /** + * Projects a class-name-or-object `Type` (the right-hand side of + * `$x instanceof `) to the `ObjectType` it should be compared + * against. Constant class strings collapse to their `ObjectType` + * exactly; everything kept symbolically (object class names, + * `class-string`) carries an uncertainty flag so the caller can + * fall back to `BooleanType` instead of a definite yes/no. + */ + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult; + + /** + * Projects a class-name-or-object `Type` (the second argument of + * `is_a($x, $class, $allow_string)`) to the `ObjectType` to narrow + * `$x` against. When `$allowString` is true, the `is_a()` result also + * keeps the original class-string accepted alongside the object. + * `$allowSameClass` controls whether matching the input's own class + * collapses to `NeverType` for final classes (the call site's + * "always-true" suppression). + */ + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult; + /** Models the (int) cast. */ public function toInteger(): Type; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 6a9c3c14657..f69dff57684 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -17,6 +17,7 @@ use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MissingMethodFromReflectionException; use PHPStan\Reflection\MissingPropertyFromReflectionException; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\Type\UnionTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnionTypeUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; @@ -1087,6 +1088,55 @@ public function toNumber(): Type return $type; } + public function toBitwiseNotType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->toBitwiseNotType()); + } + + public function toGetClassResultType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->toGetClassResultType()); + } + + public function toClassConstantType(ReflectionProvider $reflectionProvider): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->toClassConstantType($reflectionProvider)); + } + + public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult + { + $types = []; + $uncertainty = false; + foreach ($this->getTypes() as $innerType) { + $result = $innerType->toObjectTypeForInstanceofCheck(); + $types[] = $result->type; + if (!$result->uncertainty) { + continue; + } + + $uncertainty = true; + } + + return new ClassNameToObjectTypeResult(TypeCombinator::union(...$types), $uncertainty); + } + + public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult + { + $types = []; + $uncertainty = false; + foreach ($this->getTypes() as $innerType) { + $result = $innerType->toObjectTypeForIsACheck($objectOrClassType, $allowString, $allowSameClass); + $types[] = $result->type; + if (!$result->uncertainty) { + continue; + } + + $uncertainty = true; + } + + return new ClassNameToObjectTypeResult(TypeCombinator::union(...$types), $uncertainty); + } + public function toAbsoluteNumber(): Type { $type = $this->unionTypes(static fn (Type $type): Type => $type->toAbsoluteNumber()); diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index ca864245e67..e8532c299cc 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -89,6 +89,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toBitwiseNotType(): Type + { + return new ErrorType(); + } + public function toAbsoluteNumber(): Type { return new ErrorType(); diff --git a/tests/PHPStan/Analyser/nsrt/class-constant-narrowing.php b/tests/PHPStan/Analyser/nsrt/class-constant-narrowing.php new file mode 100644 index 00000000000..69ae05995d3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/class-constant-narrowing.php @@ -0,0 +1,52 @@ +&literal-string', $other::class); + + if ($this::class !== $other::class) { + return false; + } + + assertType("'ClassConstantNarrowing\\\\SingleType'", $this::class); + assertType("'ClassConstantNarrowing\\\\SingleType'", static::class); + assertType("'ClassConstantNarrowing\\\\SingleType'", $other::class); + + return $this->name === $other->name; + } + +} + +class NonFinal +{ + + public function compare(self $other): bool + { + assertType('class-string<$this(ClassConstantNarrowing\NonFinal)>&literal-string', $this::class); + assertType('class-string', static::class); + assertType('class-string&literal-string', $other::class); + + return $this::class === $other::class; + } + +}