Skip to content

Commit

Permalink
TypeSpecifier - handle AlwaysRememberedExpr in handling Identical
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Jul 1, 2023
1 parent 987fb32 commit a769a1c
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 44 deletions.
119 changes: 75 additions & 44 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,10 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
if ($leftExpr instanceof AlwaysRememberedExpr) {
$unwrappedLeftExpr = $leftExpr->getExpr();
}
$unwrappedRightExpr = $rightExpr;
if ($rightExpr instanceof AlwaysRememberedExpr) {
$unwrappedRightExpr = $rightExpr->getExpr();
}
$rightType = $scope->getType($rightExpr);
if (
$context->true()
Expand Down Expand Up @@ -1695,124 +1699,151 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty

$specifiedType = $this->specifyTypesForConstantBinaryExpression($exprNode, $constantType, $context, $scope, $rootExpr);
if ($specifiedType !== null) {
if ($exprNode instanceof AlwaysRememberedExpr) {
$specifiedType->unionWith(
$this->create($exprNode->getExpr(), $constantType, $context, false, $scope, $rootExpr),
);
}
return $specifiedType;
}
}

if ($rightExpr instanceof AlwaysRememberedExpr) {
$rightExpr = $rightExpr->getExpr();
}

if ($leftExpr instanceof AlwaysRememberedExpr) {
$leftExpr = $leftExpr->getExpr();
}

if (
$context->true() &&
$leftExpr instanceof ClassConstFetch &&
$leftExpr->class instanceof Expr &&
$leftExpr->name instanceof Node\Identifier &&
$rightExpr instanceof ClassConstFetch &&
$unwrappedLeftExpr instanceof ClassConstFetch &&
$unwrappedLeftExpr->class instanceof Expr &&
$unwrappedLeftExpr->name instanceof Node\Identifier &&
$unwrappedRightExpr instanceof ClassConstFetch &&
$rightType instanceof ConstantStringType &&
strtolower($leftExpr->name->toString()) === 'class'
strtolower($unwrappedLeftExpr->name->toString()) === 'class'
) {
return $this->specifyTypesInCondition(
$scope,
new Instanceof_(
$leftExpr->class,
$unwrappedLeftExpr->class,
new Name($rightType->getValue()),
),
$context,
$rootExpr,
)->unionWith($this->create($expr->left, $rightType, $context, false, $scope, $rootExpr));
)->unionWith($this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr));
}

$leftType = $scope->getType($leftExpr);
if (
$context->true() &&
$rightExpr instanceof ClassConstFetch &&
$rightExpr->class instanceof Expr &&
$rightExpr->name instanceof Node\Identifier &&
$leftExpr instanceof ClassConstFetch &&
$unwrappedRightExpr instanceof ClassConstFetch &&
$unwrappedRightExpr->class instanceof Expr &&
$unwrappedRightExpr->name instanceof Node\Identifier &&
$unwrappedLeftExpr instanceof ClassConstFetch &&
$leftType instanceof ConstantStringType &&
strtolower($rightExpr->name->toString()) === 'class'
strtolower($unwrappedRightExpr->name->toString()) === 'class'
) {
return $this->specifyTypesInCondition(
$scope,
new Instanceof_(
$rightExpr->class,
$unwrappedRightExpr->class,
new Name($leftType->getValue()),
),
$context,
$rootExpr,
)->unionWith($this->create($expr->right, $leftType, $context, false, $scope, $rootExpr));
)->unionWith($this->create($rightExpr, $leftType, $context, false, $scope, $rootExpr));
}

if ($context->false()) {
$identicalType = $scope->getType($expr);
if ($identicalType instanceof ConstantBooleanType) {
$never = new NeverType();
$contextForTypes = $identicalType->getValue() ? $context->negate() : $context;
$leftTypes = $this->create($expr->left, $never, $contextForTypes, false, $scope, $rootExpr);
$rightTypes = $this->create($expr->right, $never, $contextForTypes, false, $scope, $rootExpr);
$leftTypes = $this->create($leftExpr, $never, $contextForTypes, false, $scope, $rootExpr)
->unionWith($this->create($unwrappedLeftExpr, $never, $contextForTypes, false, $scope, $rootExpr));
$rightTypes = $this->create($rightExpr, $never, $contextForTypes, false, $scope, $rootExpr)
->unionWith($this->create($unwrappedRightExpr, $never, $contextForTypes, false, $scope, $rootExpr));
return $leftTypes->unionWith($rightTypes);
}
}

$types = null;
$exprLeftType = $scope->getType($expr->left);
$exprRightType = $scope->getType($expr->right);
if (
count($exprLeftType->getFiniteTypes()) === 1
|| ($exprLeftType->isConstantValue()->yes() && !$exprRightType->equals($exprLeftType) && $exprRightType->isSuperTypeOf($exprLeftType)->yes())
count($leftType->getFiniteTypes()) === 1
|| ($leftType->isConstantValue()->yes() && !$rightType->equals($leftType) && $rightType->isSuperTypeOf($leftType)->yes())
) {
$types = $this->create(
$expr->right,
$exprLeftType,
$rightExpr,
$leftType,
$context,
false,
$scope,
$rootExpr,
);
if ($rightExpr instanceof AlwaysRememberedExpr) {
$types = $types->unionWith($this->create(
$unwrappedRightExpr,
$leftType,
$context,
false,
$scope,
$rootExpr,
));
}
}
if (
count($exprRightType->getFiniteTypes()) === 1
|| ($exprRightType->isConstantValue()->yes() && !$exprLeftType->equals($exprRightType) && $exprLeftType->isSuperTypeOf($exprRightType)->yes())
count($rightType->getFiniteTypes()) === 1
|| ($rightType->isConstantValue()->yes() && !$leftType->equals($rightType) && $leftType->isSuperTypeOf($rightType)->yes())
) {
$leftType = $this->create(
$expr->left,
$exprRightType,
$leftTypes = $this->create(
$leftExpr,
$rightType,
$context,
false,
$scope,
$rootExpr,
);
if ($leftExpr instanceof AlwaysRememberedExpr) {
$leftTypes = $leftTypes->unionWith($this->create(
$unwrappedLeftExpr,
$rightType,
$context,
false,
$scope,
$rootExpr,
));
}
if ($types !== null) {
$types = $types->unionWith($leftType);
$types = $types->unionWith($leftTypes);
} else {
$types = $leftType;
$types = $leftTypes;
}
}

if ($types !== null) {
return $types;
}

$leftExprString = $this->exprPrinter->printExpr($expr->left);
$rightExprString = $this->exprPrinter->printExpr($expr->right);
$leftExprString = $this->exprPrinter->printExpr($unwrappedLeftExpr);
$rightExprString = $this->exprPrinter->printExpr($unwrappedRightExpr);
if ($leftExprString === $rightExprString) {
if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) {
if (!$unwrappedLeftExpr instanceof Expr\Variable || !$unwrappedRightExpr instanceof Expr\Variable) {
return new SpecifiedTypes([], [], false, [], $rootExpr);
}
}

if ($context->true()) {
$leftTypes = $this->create($expr->left, $exprRightType, $context, false, $scope, $rootExpr);
$rightTypes = $this->create($expr->right, $exprLeftType, $context, false, $scope, $rootExpr);
$leftTypes = $this->create($leftExpr, $rightType, $context, false, $scope, $rootExpr);
$rightTypes = $this->create($rightExpr, $leftType, $context, false, $scope, $rootExpr);
if ($leftExpr instanceof AlwaysRememberedExpr) {
$leftTypes = $leftTypes->unionWith(
$this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr),
);
}
if ($rightExpr instanceof AlwaysRememberedExpr) {
$rightTypes = $rightTypes->unionWith(
$this->create($unwrappedRightExpr, $leftType, $context, false, $scope, $rootExpr),
);
}
return $leftTypes->unionWith($rightTypes);
} elseif ($context->false()) {
return $this->create($expr->left, $exprLeftType, $context, false, $scope, $rootExpr)->normalize($scope)
->intersectWith($this->create($expr->right, $exprRightType, $context, false, $scope, $rootExpr)->normalize($scope));
return $this->create($leftExpr, $leftType, $context, false, $scope, $rootExpr)->normalize($scope)
->intersectWith($this->create($rightExpr, $rightType, $context, false, $scope, $rootExpr)->normalize($scope));
}

return new SpecifiedTypes([], [], false, [], $rootExpr);
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8827.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4907.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8924.php');
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-9499.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5998.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/trait-type-alias.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8609.php');
Expand Down
32 changes: 32 additions & 0 deletions tests/PHPStan/Analyser/TypeSpecifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Analyser;

use Bug9499\FooEnum;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Equal;
Expand All @@ -18,6 +19,7 @@
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\VarLikeIdentifier;
use PhpParser\PrettyPrinter\Standard;
use PHPStan\Node\Expr\AlwaysRememberedExpr;
use PHPStan\Node\Printer\Printer;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\Type\ArrayType;
Expand Down Expand Up @@ -1211,6 +1213,36 @@ public function dataCondition(): array
],
[],
],
[
new Identical(
new PropertyFetch(new Variable('foo'), 'bar'),
new Expr\ClassConstFetch(new Name(FooEnum::class), 'A'),

Check failure on line 1219 in tests/PHPStan/Analyser/TypeSpecifierTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Class Bug9499\FooEnum not found.

Check failure on line 1219 in tests/PHPStan/Analyser/TypeSpecifierTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, windows-latest)

Class Bug9499\FooEnum not found.
),
[
'$foo->bar' => 'Bug9499\FooEnum::A',
],
[
'$foo->bar' => '~Bug9499\FooEnum::A',
],
],
[
new Identical(
new AlwaysRememberedExpr(
new PropertyFetch(new Variable('foo'), 'bar'),
new ObjectType(FooEnum::class),

Check failure on line 1232 in tests/PHPStan/Analyser/TypeSpecifierTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Class Bug9499\FooEnum not found.

Check failure on line 1232 in tests/PHPStan/Analyser/TypeSpecifierTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, windows-latest)

Class Bug9499\FooEnum not found.
new ObjectType(FooEnum::class),

Check failure on line 1233 in tests/PHPStan/Analyser/TypeSpecifierTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Class Bug9499\FooEnum not found.

Check failure on line 1233 in tests/PHPStan/Analyser/TypeSpecifierTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, windows-latest)

Class Bug9499\FooEnum not found.
),
new Expr\ClassConstFetch(new Name(FooEnum::class), 'A'),

Check failure on line 1235 in tests/PHPStan/Analyser/TypeSpecifierTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Class Bug9499\FooEnum not found.

Check failure on line 1235 in tests/PHPStan/Analyser/TypeSpecifierTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, windows-latest)

Class Bug9499\FooEnum not found.
),
[
'__phpstanRembered($foo->bar)' => 'Bug9499\FooEnum::A',
'$foo->bar' => 'Bug9499\FooEnum::A',
],
[
'__phpstanRembered($foo->bar)' => '~Bug9499\FooEnum::A',
'$foo->bar' => '~Bug9499\FooEnum::A',
],
],
];
}

Expand Down
9 changes: 9 additions & 0 deletions tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -516,4 +516,13 @@ public function testBug8536(): void
$this->analyse([__DIR__ . '/data/bug-8536.php'], []);
}

public function testBug9499(): void
{
if (PHP_VERSION_ID < 80100) {
$this->markTestSkipped('Test requires PHP 8.1.');
}

$this->analyse([__DIR__ . '/data/bug-9499.php'], []);
}

}
52 changes: 52 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-9499.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php // lint >= 8.1

namespace Bug9499;

use function PHPStan\Testing\assertType;

enum FooEnum
{
case A;
case B;
case C;
case D;
}

class Foo
{
public function __construct(public readonly FooEnum $f)
{
}
}

function test(FooEnum $f, Foo $foo): void
{
$arr = ['f' => $f];
match ($arr['f']) {
FooEnum::A, FooEnum::B => match ($arr['f']) {
FooEnum::A => 'a',
FooEnum::B => 'b',
},
default => '',
};
match ($foo->f) {
FooEnum::A, FooEnum::B => match ($foo->f) {
FooEnum::A => 'a',
FooEnum::B => 'b',
},
default => '',
};
}

function test2(FooEnum $f, Foo $foo): void
{
$arr = ['f' => $f];
match ($arr['f']) {
FooEnum::A, FooEnum::B => assertType(FooEnum::class . '::A|' . FooEnum::class . '::B', $arr['f']),
default => '',
};
match ($foo->f) {
FooEnum::A, FooEnum::B => assertType(FooEnum::class . '::A|' . FooEnum::class . '::B', $foo->f),
default => '',
};
}

0 comments on commit a769a1c

Please sign in to comment.