Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Narrow down static and $this together #2972

Open
wants to merge 14 commits into
base: 1.11.x
Choose a base branch
from
Open
79 changes: 68 additions & 11 deletions src/Analyser/MutatingScope.php
Expand Up @@ -547,7 +547,21 @@ public function getVariableType(string $variableName): Type
return new MixedType();
}

return TypeUtils::resolveLateResolvableTypes($this->expressionTypes[$varExprString]->getType());
$result = TypeUtils::resolveLateResolvableTypes($this->expressionTypes[$varExprString]->getType());

if ($variableName !== 'this') {
return $result;
}

$staticClassNode = new Expr\ClassConstFetch(new Name('static'), 'class');

if ($this->hasExpressionType($staticClassNode)->yes()) {
$staticType = $this->expressionTypes[$this->getNodeKey($staticClassNode)]->getType();

return TypeCombinator::intersect($result, $staticType->getClassStringObjectType());
}

return $result;
}

/**
Expand Down Expand Up @@ -1166,15 +1180,11 @@ private function resolveType(string $exprString, Expr $node): Type
}

if ($node instanceof Expr\StaticCall) {
if (!$node->class instanceof Name) {
return new ObjectType(Closure::class);
}

if (!$node->name instanceof Node\Identifier) {
return new ObjectType(Closure::class);
}

$classType = $this->resolveTypeByName($node->class);
$classType = $this->getType(new Expr\ClassConstFetch($node->class, 'class'))->getClassStringObjectType();
$methodName = $node->name->toString();
if (!$classType->hasMethod($methodName)->yes()) {
return new ObjectType(Closure::class);
Expand Down Expand Up @@ -1782,6 +1792,9 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
if ($this->hasExpressionType($node)->yes()) {
return $this->expressionTypes[$exprString]->getType();
}
if ($node->name->toLowerString() === 'class' && $node->class instanceof Name && $node->class->toLowerString() === 'static') {
return new GenericClassStringType($this->resolveTypeByNameWithoutClassName($node->class));
}
return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection(
$node->class,
$node->name->name,
Expand Down Expand Up @@ -1897,7 +1910,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
if ($this->nativeTypesPromoted) {
$typeCallback = function () use ($node): Type {
if ($node->class instanceof Name) {
$staticMethodCalledOnType = $this->resolveTypeByName($node->class);
$staticMethodCalledOnType = $this->resolveTypeByNameWithoutClassName($node->class);
} else {
$staticMethodCalledOnType = $this->getNativeType($node->class);
}
Expand All @@ -1922,7 +1935,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu

$typeCallback = function () use ($node): Type {
if ($node->class instanceof Name) {
$staticMethodCalledOnType = $this->resolveTypeByName($node->class);
$staticMethodCalledOnType = $this->resolveTypeByNameWithoutClassName($node->class);
} else {
$staticMethodCalledOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType();
}
Expand Down Expand Up @@ -2014,7 +2027,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu

$typeCallback = function () use ($node): Type {
if ($node->class instanceof Name) {
$staticPropertyFetchedOnType = $this->resolveTypeByName($node->class);
$staticPropertyFetchedOnType = $this->resolveTypeByNameWithoutClassName($node->class);
} else {
$staticPropertyFetchedOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType();
}
Expand Down Expand Up @@ -2566,6 +2579,45 @@ public function resolveTypeByName(Name $name): TypeWithClassName
return new ObjectType($originalClass);
}

private function resolveTypeByNameWithoutClassName(Name $name): Type
{
$typeWithClassName = $this->resolveTypeByName($name);

if ($name->toLowerString() !== 'static' || ! $this->isInClass()) {
return $typeWithClassName;
}

$typesToIntersect = [$typeWithClassName];
$thisNode = new Variable('this');

if ($this->hasExpressionType($thisNode)->yes()) {
$thisType = $this->expressionTypes[$this->getNodeKey($thisNode)]->getType();
$typesToIntersect[] = TypeTraverser::map(
$thisType,
static function (Type $type, callable $traverse): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

if ($type instanceof ThisType) {
return new StaticType($type->getClassReflection());
}

return $type;
},
);
}

$staticClassNode = new Expr\ClassConstFetch(new Name('static'), 'class');

if ($this->hasExpressionType($staticClassNode)->yes()) {
$typesToIntersect[] = $this->expressionTypes[$this->getNodeKey($staticClassNode)]->getType()
->getClassStringObjectType();
}

return TypeCombinator::intersect(...$typesToIntersect);
}

/**
* @api
* @param mixed $value
Expand Down Expand Up @@ -3033,12 +3085,17 @@ public function enterClosureBind(?Type $thisType, ?Type $nativeThisType, array $
unset($nativeExpressionTypes['$this']);
}

$context = $this->context;

if ($scopeClasses === ['static'] && $this->isInClass()) {
$scopeClasses = [$this->getClassReflection()->getName()];
} elseif (count($scopeClasses) === 1 && $this->reflectionProvider->hasClass($scopeClasses[0])) {
$context = ScopeContext::create($this->context->getFile());
$context = $context->enterClass($this->reflectionProvider->getClass($scopeClasses[0]));
}

return $this->scopeFactory->create(
$this->context,
$context,
$this->isDeclareStrictTypes(),
$this->getFunction(),
$this->getNamespace(),
Expand Down Expand Up @@ -3067,7 +3124,7 @@ public function restoreOriginalScopeAfterClosureBind(self $originalScope): self
}

return $this->scopeFactory->create(
$this->context,
$originalScope->context,
$this->isDeclareStrictTypes(),
$this->getFunction(),
$this->getNamespace(),
Expand Down
32 changes: 13 additions & 19 deletions src/Analyser/NodeScopeResolver.php
Expand Up @@ -2051,7 +2051,7 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr
$methodCalledOnType = $scope->getType($expr->var);
} else {
if ($expr->class instanceof Name) {
$methodCalledOnType = $scope->resolveTypeByName($expr->class);
$methodCalledOnType = $scope->getType(new Expr\ClassConstFetch($expr->class, 'class'))->getClassStringObjectType();
} else {
$methodCalledOnType = $scope->getType($expr->class);
}
Expand Down Expand Up @@ -2678,19 +2678,12 @@ static function (): void {
if (isset($expr->getArgs()[2])) {
$argValue = $expr->getArgs()[2]->value;
$argValueType = $scope->getType($argValue);

$directClassNames = $argValueType->getObjectClassNames();
if (count($directClassNames) > 0) {
$scopeClasses = $directClassNames;
$thisTypes = [];
foreach ($directClassNames as $directClassName) {
$thisTypes[] = new ObjectType($directClassName);
}
$thisType = TypeCombinator::union(...$thisTypes);
} else {
$thisType = $argValueType->getClassStringObjectType();
$scopeClasses = $thisType->getObjectClassNames();
}
$scopeObjectType = $argValueType->getObjectTypeOrClassStringObjectType();
$thisType = $thisType !== null
// $thisType could be mixed, error, ...
? TypeCombinator::intersect($thisType, $scopeObjectType)
: $scopeObjectType;
$scopeClasses = $scopeObjectType->getObjectClassNames();
}
$closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses);
}
Expand Down Expand Up @@ -4878,7 +4871,7 @@ static function (): void {

} elseif ($var instanceof Expr\StaticPropertyFetch) {
if ($var->class instanceof Node\Name) {
$propertyHolderType = $scope->resolveTypeByName($var->class);
$propertyHolderType = $scope->getType(new Expr\ClassConstFetch($var->class, 'class'))->getClassStringObjectType();
} else {
$this->processExprNode($stmt, $var->class, $scope, $nodeCallback, $context);
$propertyHolderType = $scope->getType($var->class);
Expand Down Expand Up @@ -5128,9 +5121,10 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt,

$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
$function !== null ? $function->getName() : null,
// Closure bind can be in different class which can prevent phpdoc resolving.
$scope->isInClass() && ! $scope->isInClosureBind() ? $scope->getClassReflection()->getName() : null,
$scope->isInTrait() && ! $scope->isInClosureBind() ? $scope->getTraitReflection()->getName() : null,
$function !== null && ! $scope->isInClosureBind() ? $function->getName() : null,
$comment->getText(),
);

Expand Down Expand Up @@ -5159,7 +5153,7 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt,
continue;
}

if ($scope->isInClass() && $scope->getFunction() === null) {
if ($scope->isInClass() && $scope->getFunction() === null && ! $scope->isInClosureBind()) {
continue;
}

Expand Down
4 changes: 2 additions & 2 deletions src/Analyser/TypeSpecifier.php
Expand Up @@ -485,7 +485,7 @@ public function specifyTypesInCondition(
return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope);
} elseif ($expr instanceof StaticCall && $expr->name instanceof Node\Identifier) {
if ($expr->class instanceof Name) {
$calleeType = $scope->resolveTypeByName($expr->class);
$calleeType = $scope->getType(new ClassConstFetch($expr->class, 'class'))->getClassStringObjectType();
} else {
$calleeType = $scope->getType($expr->class);
}
Expand Down Expand Up @@ -1649,7 +1649,7 @@ private function createForExpr(
) {
$methodName = $expr->name->toString();
if ($expr->class instanceof Name) {
$calledOnType = $scope->resolveTypeByName($expr->class);
$calledOnType = $scope->getType(new ClassConstFetch($expr->class, 'class'))->getClassStringObjectType();
} else {
$calledOnType = $scope->getType($expr->class);
}
Expand Down
32 changes: 20 additions & 12 deletions src/Reflection/InitializerExprTypeResolver.php
Expand Up @@ -1770,8 +1770,10 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T
public function getClassConstFetchTypeByReflection(Name|Expr $class, string $constantName, ?ClassReflection $classReflection, callable $getTypeCallback): Type
{
$isObject = false;
$loweredConstantName = strtolower($constantName);
if ($class instanceof Name) {
$constantClass = (string) $class;
$loweredConstantClass = strtolower($constantClass);
$constantClassType = new ObjectType($constantClass);
$namesToResolve = [
'self',
Expand All @@ -1780,34 +1782,40 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
if ($classReflection !== null) {
if ($classReflection->isFinal()) {
$namesToResolve[] = 'static';
} elseif (strtolower($constantClass) === 'static') {
if (strtolower($constantName) === 'class') {
} elseif ($loweredConstantClass === 'static') {
if ($loweredConstantName === 'class') {
return new GenericClassStringType(new StaticType($classReflection));
}

$namesToResolve[] = 'static';
$isObject = true;
}
}
if (in_array(strtolower($constantClass), $namesToResolve, true)) {
$resolvedName = $this->resolveName($class, $classReflection);
if (strtolower($resolvedName) === 'parent' && strtolower($constantName) === 'class') {
return new ClassStringType();

// Exclude ::class to prevent infinite cycle.
if ($loweredConstantClass === 'static' && $loweredConstantName !== 'class') {
$constantClassType = $getTypeCallback(new ClassConstFetch(new Name('static'), 'class'))->getClassStringObjectType();
} else {
if (in_array($loweredConstantClass, $namesToResolve, true)) {
$resolvedName = $this->resolveName($class, $classReflection);
if (strtolower($resolvedName) === 'parent' && $loweredConstantName === 'class') {
return new ClassStringType();
}
$constantClassType = $this->resolveTypeByName($class, $classReflection);
}
$constantClassType = $this->resolveTypeByName($class, $classReflection);
}

if (strtolower($constantName) === 'class') {
return new ConstantStringType($constantClassType->getClassName(), true);
if ($loweredConstantName === 'class') {
return new ConstantStringType($constantClassType->getClassName(), true);
}
}
} elseif ($class instanceof String_ && strtolower($constantName) === 'class') {
} elseif ($class instanceof String_ && $loweredConstantName === 'class') {
return new ConstantStringType($class->value, true);
} else {
$constantClassType = $getTypeCallback($class);
$isObject = true;
}

if (strtolower($constantName) === 'class') {
if ($loweredConstantName === 'class') {
return TypeTraverser::map(
$constantClassType,
function (Type $type, callable $traverse): Type {
Expand Down
4 changes: 3 additions & 1 deletion src/Rules/Classes/ClassConstantRule.php
Expand Up @@ -66,7 +66,9 @@ public function processNode(Node $node, Scope $scope): array
];
}

$classType = $scope->resolveTypeByName($class);
$classType = $lowercasedClassName === 'static'
? $scope->getType(new ClassConstFetch(new Node\Name('static'), 'class'))->getClassStringObjectType()
: $scope->resolveTypeByName($class);
} elseif ($lowercasedClassName === 'parent') {
if (!$scope->isInClass()) {
return [
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Comparison/ImpossibleCheckTypeHelper.php
Expand Up @@ -387,7 +387,7 @@ private function determineContext(Scope $scope, Expr $node): TypeSpecifierContex
}
} elseif ($node instanceof StaticCall && $node->name instanceof Node\Identifier) {
if ($node->class instanceof Node\Name) {
$calleeType = $scope->resolveTypeByName($node->class);
$calleeType = $scope->getType(new Expr\ClassConstFetch($node->class, 'class'))->getClassStringObjectType();
} else {
$calleeType = $scope->getType($node->class);
}
Expand Down
Expand Up @@ -103,7 +103,7 @@ private function getMethod(
): MethodReflection
{
if ($class instanceof Node\Name) {
$calledOnType = $scope->resolveTypeByName($class);
$calledOnType = $scope->getType(new Expr\ClassConstFetch($class, 'class'))->getClassStringObjectType();
} else {
$calledOnType = $scope->getType($class);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Methods/CallPrivateMethodThroughStaticRule.php
Expand Up @@ -36,7 +36,7 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$classType = $scope->resolveTypeByName($className);
$classType = $scope->getType(new Node\Expr\ClassConstFetch(new Name('static'), 'class'))->getClassStringObjectType();
if (!$classType->hasMethod($methodName)->yes()) {
return [];
}
Expand Down
4 changes: 4 additions & 0 deletions src/Rules/Methods/StaticMethodCallCheck.php
Expand Up @@ -146,6 +146,10 @@ public function check(
}
}
}

if ($lowercasedClassName === 'static') {
$classType = $scope->getType(new Expr\ClassConstFetch(new Name('static'), 'class'))->getClassStringObjectType();
}
} else {
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
Expand Down
4 changes: 3 additions & 1 deletion src/Rules/Properties/AccessStaticPropertiesRule.php
Expand Up @@ -85,7 +85,9 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node,
))->identifier(sprintf('outOfClass.%s', $lowercasedClass))->build(),
];
}
$classType = $scope->resolveTypeByName($node->class);
$classType = $lowercasedClass === 'static'
? $scope->getType(new Node\Expr\ClassConstFetch(new Name('static'), 'class'))->getClassStringObjectType()
: $scope->resolveTypeByName($node->class);
} elseif ($lowercasedClass === 'parent') {
if (!$scope->isInClass()) {
return [
Expand Down
6 changes: 4 additions & 2 deletions src/Rules/Properties/PropertyReflectionFinder.php
Expand Up @@ -49,7 +49,7 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a
}

if ($propertyFetch->class instanceof Node\Name) {
$propertyHolderType = $scope->resolveTypeByName($propertyFetch->class);
$propertyHolderType = $scope->getType(new Expr\ClassConstFetch($propertyFetch->class, 'class'))->getClassStringObjectType();
} else {
$propertyHolderType = $scope->getType($propertyFetch->class);
}
Expand Down Expand Up @@ -98,7 +98,9 @@ public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?F
}

if ($propertyFetch->class instanceof Node\Name) {
$propertyHolderType = $scope->resolveTypeByName($propertyFetch->class);
$propertyHolderType = $propertyFetch->class->toLowerString() === 'static'
? $scope->getType(new Expr\ClassConstFetch($propertyFetch->class, 'class'))->getClassStringObjectType()
: $scope->resolveTypeByName($propertyFetch->class);
} else {
$propertyHolderType = $scope->getType($propertyFetch->class);
}
Expand Down