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: 3 additions & 0 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,9 @@ public function applyWrite(
$scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString());
}
}

// offsetSet() filled the ArrayAccess object, so its `never` generics no longer hold
$scope = $nodeScopeResolver->widenNeverTypeArguments($scope, $var);
}

foreach ($additionalExpressions as $k => $additionalExpression) {
Expand Down
2 changes: 2 additions & 0 deletions src/Analyser/ExprHandler/MethodCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
if ($methodReflection->getName() === '__construct' || $methodReflection->hasSideEffects()->yes()) {
$nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage);
$scope = $scope->invalidateExpression($normalizedExpr->var, true, $methodReflection->getDeclaringClass());
$scope = $nodeScopeResolver->widenNeverTypeArguments($scope, $normalizedExpr->var);
} elseif ($this->rememberPossiblyImpureFunctionValues && $methodReflection->hasSideEffects()->maybe() && !$methodReflection->getDeclaringClass()->isBuiltin()) {
// the remembered call value and the @phpstan-self-out type are
// generic-sensitive: resolve them from the type-driven acceptor
Expand Down Expand Up @@ -208,6 +209,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
} else {
$nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage);
$scope = $scope->invalidateExpression($normalizedExpr->var, true);
$scope = $nodeScopeResolver->widenNeverTypeArguments($scope, $normalizedExpr->var);
$throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
}
if (
Expand Down
32 changes: 28 additions & 4 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
use PHPStan\Type\FunctionParameterClosureThisExtension;
use PHPStan\Type\FunctionParameterClosureTypeExtension;
use PHPStan\Type\FunctionParameterOutTypeExtension;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\MethodParameterClosureThisExtension;
use PHPStan\Type\MethodParameterClosureTypeExtension;
use PHPStan\Type\MethodParameterOutTypeExtension;
Expand Down Expand Up @@ -1443,6 +1444,24 @@
return $closureScope->withAnonymousFunctionReflection($refinedClosureType);
}

/**
* An object created empty - like `new ArrayObject()` - has `never` generic type
* arguments. After it might have been mutated they no longer describe its contents,
* so they're widened to the bounds of the class' template types.
*/
public function widenNeverTypeArguments(MutatingScope $scope, Expr $expr): MutatingScope
{
$type = $scope->getType($expr);
$widenedType = TemplateTypeHelper::widenNeverTypeArguments($type);
if ($widenedType === $type) {
return $scope;
}

$nativeType = $scope->getNativeType($expr);

return $scope->assignExpression($expr, $widenedType, TemplateTypeHelper::widenNeverTypeArguments($nativeType));
}

/**
* @param InvalidateExprNode[] $invalidatedExpressions
* @param string[] $uses
Expand Down Expand Up @@ -2307,6 +2326,7 @@
}
} elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) {
$argType = $scope->getType($arg->value);
$mayMutate = true;
if (!$argType->isObject()->no()) {
$nakedReturnType = null;
if ($nakedMethodReflection !== null) {
Expand All @@ -2319,18 +2339,22 @@
);
$nakedReturnType = $nakedParametersAcceptor->getReturnType();
}
if (
$nakedReturnType === null
$mayMutate = $nakedReturnType === null
|| !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes()
|| $nakedMethodReflection->isPure()->no()
) {
|| $nakedMethodReflection->isPure()->no();

Check warning on line 2344 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $mayMutate = $nakedReturnType === null || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes() - || $nakedMethodReflection->isPure()->no(); + || !$nakedMethodReflection->isPure()->yes(); if ($mayMutate) { $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); $scope = $scope->invalidateExpression($arg->value, true);

Check warning on line 2344 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $mayMutate = $nakedReturnType === null || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes() - || $nakedMethodReflection->isPure()->no(); + || !$nakedMethodReflection->isPure()->yes(); if ($mayMutate) { $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); $scope = $scope->invalidateExpression($arg->value, true);
if ($mayMutate) {
$this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage);
$scope = $scope->invalidateExpression($arg->value, true);
}
} elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) {
$this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage);
$scope = $scope->invalidateExpression($arg->value, true);
}

if ($mayMutate) {
// objects reachable from the argument might have been filled in by the callee
$scope = $this->widenNeverTypeArguments($scope, $arg->value);
}
}
}
}
Expand Down
71 changes: 71 additions & 0 deletions src/Type/Generic/TemplateTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

namespace PHPStan\Type\Generic;

use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Type\ErrorType;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\NeverType;
use PHPStan\Type\NonAcceptingNeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\VerbosityLevel;
use function array_values;

final class TemplateTypeHelper
{
Expand Down Expand Up @@ -94,6 +97,74 @@ public static function resolveToBounds(Type $type): Type
});
}

/**
* Widens `never` type arguments of generic objects to their template bounds.
*
* An object constructed empty - like `new ArrayObject()` - gets `never` type
* arguments inferred. Once the object might have been mutated, `never` no longer
* describes what it can contain, but a wider type inferred from the call site
* would be unsound, so the bounds are the safest thing to fall back to.
*/
public static function widenNeverTypeArguments(Type $type): Type
{
return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {
if ($type instanceof GenericObjectType) {
$widenedTypes = self::widenNeverTypesToBounds($type->getTypes(), $type->getClassReflection());
if ($widenedTypes !== null) {
return $traverse(new GenericObjectType(
$type->getClassName(),
$widenedTypes,
$type->getSubtractedType(),
variances: $type->getVariances(),
));
}
} elseif ($type instanceof GenericStaticType) {
$widenedTypes = self::widenNeverTypesToBounds($type->getTypes(), $type->getClassReflection());
if ($widenedTypes !== null) {
return $traverse(new GenericStaticType(
$type->getClassReflection(),
$widenedTypes,
$type->getSubtractedType(),
$type->getVariances(),
));
}
}

return $traverse($type);
});
}

/**
* @param array<int, Type> $typeArguments
* @return array<int, Type>|null null when nothing was widened
*/
private static function widenNeverTypesToBounds(array $typeArguments, ?ClassReflection $classReflection): ?array
{
if ($classReflection === null) {
return null;
}

$templateTypes = array_values($classReflection->getTemplateTypeMap()->getTypes());
$widened = false;
foreach ($typeArguments as $i => $typeArgument) {
if (!$typeArgument instanceof NeverType) {
continue;
}
if (!isset($templateTypes[$i])) {
continue;
}
$templateType = $templateTypes[$i];
if (!$templateType instanceof TemplateType) {
continue;
}

$typeArguments[$i] = $templateType->getBound();
$widened = true;
}

return $widened ? $typeArguments : null;
}

/**
* @template T of Type
* @param T $type
Expand Down
Loading
Loading