diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 13f62f78cb6..ea940bc1bcf 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -39,6 +39,7 @@ use function implode; use function in_array; use function is_string; +use function ltrim; use function sprintf; use function strtolower; @@ -256,7 +257,7 @@ private function getSpecifiedType( if ($methodType instanceof ConstantStringType) { if ($objectType instanceof ConstantStringType) { - $objectType = new ObjectType($objectType->getValue()); + $objectType = new ObjectType(ltrim($objectType->getValue(), '\\')); } if ($objectType->getObjectClassNames() !== []) { diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 5994a945569..06971617f27 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -56,6 +56,7 @@ use function is_int; use function is_numeric; use function key; +use function ltrim; use function strlen; use function strtolower; use function strtoupper; @@ -76,6 +77,8 @@ class ConstantStringType extends StringType implements ConstantScalarType private ?Type $arrayKeyType = null; + private ?string $valueAsClassName = null; + /** @var array */ private array $cachedDescriptions = []; @@ -109,7 +112,7 @@ public function isClassString(): TrinaryLogic public function getClassStringObjectType(): Type { if ($this->isClassString()->yes()) { - return new ObjectType($this->value); + return new ObjectType($this->getValueAsClassName()); } return new ErrorType(); @@ -311,22 +314,23 @@ public function toBitwiseNotType(): Type public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult { - return new ClassNameToObjectTypeResult(new ObjectType($this->value), false); + return new ClassNameToObjectTypeResult(new ObjectType($this->getValueAsClassName()), false); } public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult { + $className = $this->getValueAsClassName(); $objectOrClassTypeClassNames = $objectOrClassType->getObjectClassNames(); if ($allowString) { foreach ($objectOrClassType->getConstantStrings() as $constantString) { - $objectOrClassTypeClassNames[] = $constantString->getValue(); + $objectOrClassTypeClassNames[] = ltrim($constantString->getValue(), '\\'); } $objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames)); } $uncertainty = false; if (!$allowSameClass) { - if ($objectOrClassTypeClassNames === [$this->value]) { + if ($objectOrClassTypeClassNames === [$className]) { $isSameClass = true; foreach ($objectOrClassType->getObjectClassReflections() as $classReflection) { if (!$classReflection->isFinal()) { @@ -343,7 +347,7 @@ public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowStri 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) + in_array($className, $objectOrClassTypeClassNames, true) // This also occurs for generic class string || ($allowString && $objectOrClassTypeClassNames === [] && $objectOrClassType->isSuperTypeOf($this)->yes()) ) { @@ -354,14 +358,14 @@ public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowStri if ($allowString) { return new ClassNameToObjectTypeResult( new UnionType([ - new ObjectType($this->value), - new GenericClassStringType(new ObjectType($this->value)), + new ObjectType($className), + new GenericClassStringType(new ObjectType($className)), ]), $uncertainty, ); } - return new ClassNameToObjectTypeResult(new ObjectType($this->value), $uncertainty); + return new ClassNameToObjectTypeResult(new ObjectType($className), $uncertainty); } public function toAbsoluteNumber(): Type @@ -642,7 +646,15 @@ public function getConstant(string $constantName): ClassConstantReflection private function getObjectType(): ObjectType { - return $this->objectType ??= new ObjectType($this->value); + return $this->objectType ??= new ObjectType($this->getValueAsClassName()); + } + + /** + * The value used as a class name: PHP accepts class names with a leading backslash, but the object types don't have it. + */ + private function getValueAsClassName(): string + { + return $this->valueAsClassName ??= ltrim($this->value, '\\'); } public function toPhpDocNode(): TypeNode diff --git a/src/Type/Generic/GenericClassStringType.php b/src/Type/Generic/GenericClassStringType.php index 7fd22cc8ecb..76f43f09021 100644 --- a/src/Type/Generic/GenericClassStringType.php +++ b/src/Type/Generic/GenericClassStringType.php @@ -25,6 +25,7 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function count; +use function ltrim; use function sprintf; /** @api */ @@ -95,7 +96,7 @@ public function accepts(Type $type, bool $strictTypes): AcceptsResult return AcceptsResult::createNo(); } - $objectType = new ObjectType($type->getValue()); + $objectType = new ObjectType(ltrim($type->getValue(), '\\')); } elseif ($type instanceof self) { $objectType = $type->type; } elseif ($type instanceof ClassStringType) { @@ -127,7 +128,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult // We are transforming constant class-string to ObjectType. But we need to filter out // an uncertainty originating in possible ObjectType's class subtypes. - $objectType = new ObjectType($type->getValue()); + $objectType = new ObjectType(ltrim($type->getValue(), '\\')); // Do not use TemplateType's isSuperTypeOf handling directly because it takes ObjectType // uncertainty into account. @@ -180,7 +181,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap } if ($receivedType instanceof ConstantStringType) { - $typeToInfer = new ObjectType($receivedType->getValue()); + $typeToInfer = new ObjectType(ltrim($receivedType->getValue(), '\\')); } elseif ($receivedType instanceof self) { $typeToInfer = $receivedType->type; } elseif ($receivedType->isClassString()->yes()) { @@ -242,12 +243,12 @@ public function tryRemove(Type $typeToRemove): ?Type if (count($genericObjectClassNames) === 1) { if ($reflectionProvider->hasClass($genericObjectClassNames[0])) { $classReflection = $reflectionProvider->getClass($genericObjectClassNames[0]); - if ($classReflection->isFinal() && $genericObjectClassNames[0] === $typeToRemove->getValue()) { + if ($classReflection->isFinal() && $genericObjectClassNames[0] === ltrim($typeToRemove->getValue(), '\\')) { return new NeverType(); } if ($classReflection->getAllowedSubTypes() !== null) { - $objectTypeToRemove = new ObjectType($typeToRemove->getValue()); + $objectTypeToRemove = new ObjectType(ltrim($typeToRemove->getValue(), '\\')); $remainingType = TypeCombinator::remove($generic, $objectTypeToRemove); if ($remainingType instanceof NeverType) { return new NeverType(); @@ -259,7 +260,7 @@ public function tryRemove(Type $typeToRemove): ?Type } } } elseif (count($genericObjectClassNames) > 1) { - $objectTypeToRemove = new ObjectType($typeToRemove->getValue()); + $objectTypeToRemove = new ObjectType(ltrim($typeToRemove->getValue(), '\\')); if ($reflectionProvider->hasClass($typeToRemove->getValue())) { $classReflection = $reflectionProvider->getClass($typeToRemove->getValue()); if ($classReflection->isFinal()) { diff --git a/tests/PHPStan/Analyser/nsrt/class-string-leading-backslash.php b/tests/PHPStan/Analyser/nsrt/class-string-leading-backslash.php new file mode 100644 index 00000000000..204d916d79e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/class-string-leading-backslash.php @@ -0,0 +1,72 @@ + $className + * @return T + */ +function create(string $className): object +{ + return new $className(); +} + +function (): void { + assertType('ClassStringLeadingBackslash\Foo', create(Foo::class)); + assertType('ClassStringLeadingBackslash\Foo', create('ClassStringLeadingBackslash\Foo')); + assertType('ClassStringLeadingBackslash\Foo', create('\ClassStringLeadingBackslash\Foo')); +}; + +function (object $object): void { + $className = '\ClassStringLeadingBackslash\Foo'; + assertType('ClassStringLeadingBackslash\Foo', new $className()); + + if ($object instanceof $className) { + assertType('ClassStringLeadingBackslash\Foo', $object); + } +}; + +function (object $object): void { + if (is_a($object, '\ClassStringLeadingBackslash\Foo')) { + assertType('ClassStringLeadingBackslash\Foo', $object); + } +}; + +function (string $className): void { + if (is_a($className, '\ClassStringLeadingBackslash\Foo', true)) { + assertType('class-string', $className); + } +}; + +/** + * @param class-string $className + */ +function removeWithoutLeadingBackslash(string $className): void +{ + if ($className !== 'ClassStringLeadingBackslash\FinalFoo') { + assertType('*NEVER*', $className); + } +} + +/** + * @param class-string $className + */ +function removeWithLeadingBackslash(string $className): void +{ + if ($className !== '\ClassStringLeadingBackslash\FinalFoo') { + assertType('*NEVER*', $className); + } +}