Skip to content

Commit

Permalink
Fix chaining nullsafe operator
Browse files Browse the repository at this point in the history
  • Loading branch information
olsavmic committed Nov 6, 2021
1 parent e3a0e0d commit 6bb0fd6
Show file tree
Hide file tree
Showing 16 changed files with 147 additions and 11 deletions.
15 changes: 15 additions & 0 deletions src/Analyser/NullsafeOperatorHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,25 @@
namespace PHPStan\Analyser;

use PhpParser\Node\Expr;
use PHPStan\Type\TypeCombinator;

class NullsafeOperatorHelper
{

public static function getNullsafeShortcircuitedExprRespectingScope(Scope $scope, Expr $expr): Expr
{
if (!TypeCombinator::containsNull($scope->getType($expr))) {
// We're in most likely in context of a null-safe operator ($scope->moreSpecificType is defined for $expr)
// Modifying the expression would not bring any value or worse ruin the context information
return $expr;
}

return self::getNullsafeShortcircuitedExpr($expr);
}

/**
* @internal Use NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope
*/
public static function getNullsafeShortcircuitedExpr(Expr $expr): Expr
{
if ($expr instanceof Expr\NullsafeMethodCall) {
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function check(
{
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($var),
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $var),
$unknownClassPattern,
static function (Type $type) use ($dimType): bool {
return $type->hasOffsetValueType($dimType)->yes();
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array

$isOffsetAccessibleTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($node->var),
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var),
$unknownClassPattern,
static function (Type $type): bool {
return $type->isOffsetAccessible()->yes();
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Arrays/OffsetAccessAssignmentRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array

$varTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($node->var),
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var),
'',
static function (Type $varType) use ($potentialDimType): bool {
$arrayDimType = $varType->setOffsetValueType($potentialDimType, new MixedType());
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Classes/ClassConstantRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public function processNode(Node $node, Scope $scope): array
} else {
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($class),
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $class),
sprintf('Access to constant %s on an unknown class %%s.', SprintfHelper::escapeFormatString($constantName)),
static function (Type $type) use ($constantName): bool {
return $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes();
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Functions/CallCallablesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function processNode(

$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($node->name),
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->name),
'Invoking callable on an unknown class %s.',
static function (Type $type): bool {
return $type->isCallable()->yes();
Expand Down
3 changes: 2 additions & 1 deletion src/Rules/Methods/CallMethodsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ public function processNode(Node $node, Scope $scope): array

$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($node->var),
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var),
sprintf('Call to method %s() on an unknown class %%s.', SprintfHelper::escapeFormatString($name)),
static function (Type $type) use ($name): bool {
return $type->canCallMethods()->yes() && $type->hasMethod($name)->yes();
}
);

$type = $typeResult->getType();
if ($type instanceof ErrorType) {
return $typeResult->getUnknownClassErrors();
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Methods/CallStaticMethodsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public function processNode(Node $node, Scope $scope): array
} else {
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($class),
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $class),
sprintf('Call to static method %s() on an unknown class %%s.', SprintfHelper::escapeFormatString($methodName)),
static function (Type $type) use ($methodName): bool {
return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function processNode(Node $node, Scope $scope): array

$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($methodCall->var),
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $methodCall->var),
'',
static function (Type $type) use ($methodName): bool {
return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array
} else {
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($staticCall->class),
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $staticCall->class),
'',
static function (Type $type) use ($methodName): bool {
return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes();
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Properties/AccessPropertiesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string
{
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($node->var),
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var),
sprintf('Access to property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)),
static function (Type $type) use ($name): bool {
return $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes();
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Properties/AccessStaticPropertiesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node,
} else {
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($node->class),
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class),
sprintf('Access to static property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)),
static function (Type $type) use ($name): bool {
return $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes();
Expand Down
25 changes: 25 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2193,4 +2193,29 @@ public function testBug3465(): void
$this->analyse([__DIR__ . '/data/bug-3465.php'], []);
}

public function testBug5868(): void
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped('Test requires PHP 8.0');
}

$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-5868.php'], [
[
'Cannot call method nullable1() on Bug5868\HelloWorld|null.',
14,
],
[
'Cannot call method nullable2() on Bug5868\HelloWorld|null.',
15,
],
[
'Cannot call method nullable3() on Bug5868\HelloWorld|null.',
16,
],
]);
}

}
30 changes: 30 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-5868.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php // lint >= 8.0

namespace Bug5868;

class HelloWorld
{
public function nullable1(): ?self
{
// OK
$tmp = $this->nullable1()?->nullable1()?->nullable2();
$tmp = $this->nullable1()?->nullable3()->nullable2()?->nullable3()->nullable1();

// Error
$tmp = $this->nullable1()->nullable1()?->nullable2();
$tmp = $this->nullable1()?->nullable1()->nullable2();
$tmp = $this->nullable1()?->nullable3()->nullable2()->nullable3()->nullable1();

return $this->nullable1()?->nullable3();
}

public function nullable2(): ?self
{
return $this;
}

public function nullable3(): self
{
return $this;
}
}
28 changes: 28 additions & 0 deletions tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,32 @@ public function testBug4808(): void
$this->analyse([__DIR__ . '/data/bug-4808.php'], []);
}


public function testBug5868(): void
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped('Test requires PHP 8.0');
}
$this->checkThisOnly = false;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-5868.php'], [
[
'Cannot access property $child on Bug5868PropertyFetch\Foo|null.',
31,
],
[
'Cannot access property $child on Bug5868PropertyFetch\Child|null.',
32,
],
[
'Cannot access property $existingChild on Bug5868PropertyFetch\Child|null.',
33,
],
[
'Cannot access property $existingChild on Bug5868PropertyFetch\Child|null.',
34,
],
]);
}

}
37 changes: 37 additions & 0 deletions tests/PHPStan/Rules/Properties/data/bug-5868.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php // lint >= 8.0

namespace Bug5868PropertyFetch;

class Child
{

public ?self $child;

public self $existingChild;

}

class Foo
{
public ?Child $child;
}

class HelloWorld
{

function getAttributeInNode(?Foo $node): ?Child
{
// Ok
$tmp = $node?->child;
$tmp = $node?->child?->child?->child;
$tmp = $node?->child?->existingChild->child;
$tmp = $node?->child?->existingChild->child?->existingChild;

// Errors
$tmp = $node->child;
$tmp = $node?->child->child;
$tmp = $node?->child->existingChild->child;
$tmp = $node?->child?->existingChild->child->existingChild;
}

}

0 comments on commit 6bb0fd6

Please sign in to comment.