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
21 changes: 21 additions & 0 deletions src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,27 @@ public function revertNonNullability(MutatingScope $scope, array $specifiedExpre
return $scope;
}

/**
* Walk a nullsafe chain (NullsafePropertyFetch/NullsafeMethodCall) and narrow
* each intermediate var to non-null. Used so that method call arguments can see
* the narrowed types without affecting the scope during var processing.
*/
public function narrowNullsafeVarChain(MutatingScope $scope, Expr $expr): EnsuredNonNullabilityResult
{
$specifiedExpressions = [];
$currentExpr = $expr;
while ($currentExpr instanceof Expr\NullsafePropertyFetch || $currentExpr instanceof Expr\NullsafeMethodCall) {
$result = $this->ensureShallowNonNullability($scope, $scope, $currentExpr->var);
$scope = $result->getScope();
foreach ($result->getSpecifiedExpressions() as $specifiedExpression) {
$specifiedExpressions[] = $specifiedExpression;
}
$currentExpr = $currentExpr->var;
}

return new EnsuredNonNullabilityResult($scope, $specifiedExpressions);
}

/**
* @param Closure(MutatingScope, Expr): MutatingScope $callback
*/
Expand Down
15 changes: 15 additions & 0 deletions src/Analyser/ExprHandler/MethodCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper;
use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper;
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
use PHPStan\Analyser\ImpurePoint;
use PHPStan\Analyser\InternalThrowPoint;
Expand Down Expand Up @@ -59,6 +60,7 @@ final class MethodCallHandler implements ExprHandler
public function __construct(
private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
private MethodCallReturnTypeHelper $methodCallReturnTypeHelper,
private NonNullabilityHelper $nonNullabilityHelper,
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
private bool $implicitThrows,
#[AutowiredParameter]
Expand Down Expand Up @@ -139,6 +141,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit());
}

// For virtual nullsafe method calls, narrow the vars in the nullsafe
// chain so arguments see non-null types. E.g. in $a?->b?->method($a),
// $a must be non-null when method() is reached.
$nullsafeNarrowingResult = null;
if ($expr->getAttribute('virtualNullsafeMethodCall') === true) {
$nullsafeNarrowingResult = $this->nonNullabilityHelper->narrowNullsafeVarChain($scope, $expr->var);
$scope = $nullsafeNarrowingResult->getScope();
}

$argsResult = $nodeScopeResolver->processArgs(
$stmt,
$methodReflection,
Expand All @@ -152,6 +163,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
);
$scope = $argsResult->getScope();

if ($nullsafeNarrowingResult !== null) {
$scope = $this->nonNullabilityHelper->revertNonNullability($scope, $nullsafeNarrowingResult->getSpecifiedExpressions());
}

if ($methodReflection !== null) {
$methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
if ($methodThrowPoint !== null) {
Expand Down
32 changes: 32 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-6934.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug6934;

use DOMNode;
use function PHPStan\Testing\assertType;

function removeFromParent(?DOMNode $node): void {
$node?->parentNode?->removeChild($node);
$node?->removeChild($node);

assertType('DOMNode|null', $node);
assertType('DOMNode|null', $node?->parentNode);
}

function testNarrowing(?DOMNode $node): void {
$node?->parentNode?->removeChild(assertType('DOMNode', $node));
$node?->removeChild(assertType('DOMNode', $node));
}

class Foo {
public function doSomething($mixed): string {
return 'hello';
}
}

function testNullsafeChainArgs(?Foo $foo): void {
$foo?->doSomething(assertType('Bug6934\Foo', $foo));
assertType('Bug6934\Foo|null', $foo);
}
9 changes: 9 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3945,6 +3945,15 @@ public function testBug7369(): void
]);
}

#[RequiresPhp('>= 8.0')]
public function testBug6934(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6934.php'], []);
}

public function testBug11463(): void
{
$this->checkThisOnly = false;
Expand Down
Loading