Skip to content
Open
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ lint:
--exclude tests/PHPStan/Rules/Arrays/data/offset-access-without-dim-for-reading.php \
--exclude tests/PHPStan/Rules/Classes/data/bug-11891.php \
--exclude tests/PHPStan/Rules/Classes/data/bug-13768.php \
--exclude tests/PHPStan/Rules/Classes/data/closure-bind-scope-nested-class.php \
--exclude tests/PHPStan/Rules/Classes/data/duplicate-declarations.php \
--exclude tests/PHPStan/Rules/Classes/data/duplicate-enum-cases.php \
--exclude tests/PHPStan/Rules/Classes/data/enum-sanity.php \
Expand Down
32 changes: 30 additions & 2 deletions src/Analyser/ExprHandler/ClassConstFetchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Type;
use function array_merge;

Expand Down Expand Up @@ -84,7 +88,28 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
// the enclosing class is lexical - fixed at this node, identical on every
// (possibly narrowed) scope the callback may later be invoked with - so
// resolve it once here instead of reading it off the callback's scope.
$classReflection = $beforeScope->isInClass() ? $beforeScope->getClassReflection() : null;
// Inside a closure scoped by Closure::bind(), self/parent/static name the bound class;
// bound to a class that is not exactly one known class, they name none.
$classReflection = $beforeScope->getClosureBindScopeClassReflection();
if ($classReflection === null && $beforeScope->isInClass() && !$beforeScope->isClosureBindScopeClassAmbiguous()) {
$classReflection = $beforeScope->getClassReflection();
}
// ...and their ::class is a class-string of the closest class all candidates extend
$ambiguousClassStringType = null;
if (
$expr->class instanceof Name
&& $expr->class->isSpecialClassName()
&& $expr->name instanceof Identifier
&& $expr->name->toLowerString() === 'class'
&& $beforeScope->isClosureBindScopeClassAmbiguous()
) {
$commonAncestor = $beforeScope->getClosureBindScopeCommonAncestor($expr->class);
if ($commonAncestor === null) {
$ambiguousClassStringType = new ClassStringType();
} else {
$ambiguousClassStringType = new GenericClassStringType($expr->class->toLowerString() === 'static' ? new StaticType($commonAncestor) : new ObjectType($commonAncestor->getName()));
}
}

$result = $this->expressionResultFactory->create(
$scope,
Expand All @@ -95,10 +120,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
isAlwaysTerminating: $isAlwaysTerminating,
throwPoints: $throwPoints,
impurePoints: $impurePoints,
typeCallback: function (bool $nativeTypesPromoted) use ($expr, $classResult, $classReflection): Type {
typeCallback: function (bool $nativeTypesPromoted) use ($expr, $classResult, $classReflection, $ambiguousClassStringType): Type {
if (!$expr->name instanceof Identifier) {
return new MixedType();
}
if ($ambiguousClassStringType !== null) {
return $ambiguousClassStringType;
}

return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection(
$expr->class,
Expand Down
12 changes: 12 additions & 0 deletions src/Analyser/ExprHandler/NewHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
use PHPStan\Type\NeverType;
use PHPStan\Type\NonexistentParentClassType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
Expand Down Expand Up @@ -504,6 +505,17 @@ private function resolveReturnType(MutatingScope $scope, Expr $expr, ?Parameters
*/
private function exactInstantiation(MutatingScope $scope, New_ $node, Name $className, ?ParametersAcceptor $preResolvedAcceptor, ?ArgsResult $argsResult = null, bool $allowUnresolved = true): Type
{
if ($className->isSpecialClassName() && $scope->isClosureBindScopeClassAmbiguous()) {
// bound to a class that is not exactly one known class: an instance of the
// closest class all candidates extend, or of some class
$commonAncestor = $scope->getClosureBindScopeCommonAncestor($className);
if ($commonAncestor === null) {
return new ObjectWithoutClassType();
}

return $className->toLowerString() === 'static' ? new StaticType($commonAncestor) : new ObjectType($commonAncestor->getName());
}

$resolvedClassName = $scope->resolveName($className);
$isStatic = false;
$lowercasedClassName = $className->toLowerString();
Expand Down
14 changes: 13 additions & 1 deletion src/Analyser/ExprHandler/StaticCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Type;
Expand Down Expand Up @@ -156,7 +158,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$declaringClass->getName() === 'Closure'
&& strtolower($methodName) === 'bind'
) {
$closureBindScopeFactory = static function (MutatingScope $boundScope) use ($expr, $storage): MutatingScope {
$closureBindScopeFactory = static function (MutatingScope $boundScope) use ($expr, $storage, $parametersAcceptor): MutatingScope {
// normalized so that $newThis and $newScope are found at their
// parameter positions even when the call names its arguments
$expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr;
// invoked while the closure argument is walked; the other
// arguments were processed before it (processArgs orders
// closures last), so their results are already stored. A
Expand All @@ -183,9 +188,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
}
}
$scopeClasses = ['static'];
$bindsUnknownScopeClass = false;
if (isset($expr->getArgs()[2])) {
$argValue = $expr->getArgs()[2]->value;
$argValueType = $readArgType($argValue, false);
// a newScope that may name a class binds to one even when it is unknown;
// 'static' and null name none
$bindsUnknownScopeClass = !TypeCombinator::union(new ConstantStringType('static'), new NullType())->isSuperTypeOf($argValueType)->yes();

$directClassNames = $argValueType->getObjectClassNames();
if (count($directClassNames) > 0) {
Expand All @@ -200,6 +209,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$scopeClasses = $thisType->getObjectClassNames();
}
}
if ($scopeClasses === [] && $bindsUnknownScopeClass) {
$scopeClasses = [MutatingScope::UNKNOWN_CLOSURE_BIND_SCOPE_CLASS];
}
return $boundScope->enterClosureBind($thisType, $nativeThisType, $scopeClasses);
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/InternalScopeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface InternalScopeFactory
* @param array<string, ExpressionTypeHolder> $expressionTypes
* @param array<string, ExpressionTypeHolder> $nativeExpressionTypes
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
* @param list<non-empty-string> $inClosureBindScopeClasses
* @param list<non-empty-string> $inClosureBindScopeClasses 'static' and MutatingScope::UNKNOWN_CLOSURE_BIND_SCOPE_CLASS included
* @param array<string, bool> $currentlyAssignedExpressions
* @param array<string, true> $currentlyAllowedUndefinedExpressions
* @param list<array{FunctionReflection|MethodReflection|null, ParameterReflection|null}> $inFunctionCallsStack
Expand Down
Loading
Loading