Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Rules/Comparison/ImpossibleCheckTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
use function implode;
use function in_array;
use function is_string;
use function ltrim;
use function sprintf;
use function strtolower;

Expand Down Expand Up @@ -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() !== []) {
Expand Down
30 changes: 21 additions & 9 deletions src/Type/Constant/ConstantStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -76,6 +77,8 @@ class ConstantStringType extends StringType implements ConstantScalarType

private ?Type $arrayKeyType = null;

private ?string $valueAsClassName = null;

/** @var array<int, string> */
private array $cachedDescriptions = [];

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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()) {
Expand All @@ -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())
) {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions src/Type/Generic/GenericClassStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function count;
use function ltrim;
use function sprintf;

/** @api */
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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();
Expand All @@ -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()) {
Expand Down
72 changes: 72 additions & 0 deletions tests/PHPStan/Analyser/nsrt/class-string-leading-backslash.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php declare(strict_types = 1);

namespace ClassStringLeadingBackslash;

use function PHPStan\Testing\assertType;

class Foo
{

}

final class FinalFoo
{

}

/**
* @template T of object
* @param class-string<T> $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<ClassStringLeadingBackslash\Foo>', $className);
}
};

/**
* @param class-string<FinalFoo> $className
*/
function removeWithoutLeadingBackslash(string $className): void
{
if ($className !== 'ClassStringLeadingBackslash\FinalFoo') {
assertType('*NEVER*', $className);
}
}

/**
* @param class-string<FinalFoo> $className
*/
function removeWithLeadingBackslash(string $className): void
{
if ($className !== '\ClassStringLeadingBackslash\FinalFoo') {
assertType('*NEVER*', $className);
}
}
Loading