Skip to content

Commit f449d98

Browse files
authored
Improved isset() and ternary operator handling
1 parent aec0406 commit f449d98

32 files changed

+1821
-108
lines changed

src/Analyser/EnsuredNonNullabilityResultExpression.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Analyser;
44

55
use PhpParser\Node\Expr;
6+
use PHPStan\TrinaryLogic;
67
use PHPStan\Type\Type;
78

89
class EnsuredNonNullabilityResultExpression
@@ -12,6 +13,7 @@ public function __construct(
1213
private Expr $expression,
1314
private Type $originalType,
1415
private Type $originalNativeType,
16+
private TrinaryLogic $certainty,
1517
)
1618
{
1719
}
@@ -31,4 +33,9 @@ public function getOriginalNativeType(): Type
3133
return $this->originalNativeType;
3234
}
3335

36+
public function getCertainty(): TrinaryLogic
37+
{
38+
return $this->certainty;
39+
}
40+
3441
}

src/Analyser/MutatingScope.php

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use PHPStan\Node\Expr\PropertyInitializationExpr;
3838
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
3939
use PHPStan\Node\Expr\TypeExpr;
40+
use PHPStan\Node\IssetExpr;
4041
use PHPStan\Node\Printer\ExprPrinter;
4142
use PHPStan\Parser\ArrayMapArgVisitor;
4243
use PHPStan\Parser\NewAssignedToPropertyVisitor;
@@ -2348,6 +2349,10 @@ public function isSpecified(Expr $node): bool
23482349
/** @api */
23492350
public function hasExpressionType(Expr $node): TrinaryLogic
23502351
{
2352+
if ($node instanceof Variable && is_string($node->name)) {
2353+
return $this->hasVariableType($node->name);
2354+
}
2355+
23512356
$exprString = $this->getNodeKey($node);
23522357
if (!isset($this->expressionTypes[$exprString])) {
23532358
return TrinaryLogic::createNo();
@@ -3440,7 +3445,7 @@ public function unsetExpression(Expr $expr): self
34403445
return $scope->invalidateExpression($expr);
34413446
}
34423447

3443-
public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType): self
3448+
public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, ?TrinaryLogic $certainty = null): self
34443449
{
34453450
if ($expr instanceof ConstFetch) {
34463451
$loweredConstName = strtolower($expr->name->toString());
@@ -3474,23 +3479,31 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType):
34743479
if ($dimType instanceof ConstantIntegerType) {
34753480
$types[] = new StringType();
34763481
}
3482+
34773483
$scope = $scope->specifyExpressionType(
34783484
$expr->var,
34793485
TypeCombinator::intersect(
34803486
TypeCombinator::intersect($exprVarType, TypeCombinator::union(...$types)),
34813487
new HasOffsetValueType($dimType, $type),
34823488
),
34833489
$scope->getNativeType($expr->var),
3490+
$certainty,
34843491
);
34853492
}
34863493
}
34873494
}
34883495

3496+
if ($certainty === null) {
3497+
$certainty = TrinaryLogic::createYes();
3498+
} elseif ($certainty->no()) {
3499+
throw new ShouldNotHappenException();
3500+
}
3501+
34893502
$exprString = $this->getNodeKey($expr);
34903503
$expressionTypes = $scope->expressionTypes;
3491-
$expressionTypes[$exprString] = ExpressionTypeHolder::createYes($expr, $type);
3504+
$expressionTypes[$exprString] = new ExpressionTypeHolder($expr, $type, $certainty);
34923505
$nativeTypes = $scope->nativeExpressionTypes;
3493-
$nativeTypes[$exprString] = ExpressionTypeHolder::createYes($expr, $nativeType);
3506+
$nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty);
34943507

34953508
return $this->scopeFactory->create(
34963509
$this->context,
@@ -3707,6 +3720,23 @@ private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): se
37073720
);
37083721
}
37093722

3723+
private function setExpressionCertainty(Expr $expr, TrinaryLogic $certainty): self
3724+
{
3725+
if ($this->hasExpressionType($expr)->no()) {
3726+
throw new ShouldNotHappenException();
3727+
}
3728+
3729+
$originalExprType = $this->getType($expr);
3730+
$nativeType = $this->getNativeType($expr);
3731+
3732+
return $this->specifyExpressionType(
3733+
$expr,
3734+
$originalExprType,
3735+
$nativeType,
3736+
$certainty,
3737+
);
3738+
}
3739+
37103740
private function addTypeToExpression(Expr $expr, Type $type): self
37113741
{
37123742
$originalExprType = $this->getType($expr);
@@ -3816,6 +3846,23 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
38163846
foreach ($typeSpecifications as $typeSpecification) {
38173847
$expr = $typeSpecification['expr'];
38183848
$type = $typeSpecification['type'];
3849+
3850+
if ($expr instanceof IssetExpr) {
3851+
$issetExpr = $expr;
3852+
$expr = $issetExpr->getExpr();
3853+
3854+
if ($typeSpecification['sure']) {
3855+
$scope = $scope->setExpressionCertainty(
3856+
$expr,
3857+
TrinaryLogic::createMaybe(),
3858+
);
3859+
} else {
3860+
$scope = $scope->unsetExpression($expr);
3861+
}
3862+
3863+
continue;
3864+
}
3865+
38193866
if ($typeSpecification['sure']) {
38203867
if ($specifiedTypes->shouldOverwrite()) {
38213868
$scope = $scope->assignExpression($expr, $type, $type);
@@ -3828,6 +3875,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
38283875
$specifiedExpressions[$this->getNodeKey($expr)] = ExpressionTypeHolder::createYes($expr, $scope->getType($expr));
38293876
}
38303877

3878+
$conditions = [];
38313879
foreach ($scope->conditionalExpressions as $conditionalExprString => $conditionalExpressions) {
38323880
foreach ($conditionalExpressions as $conditionalExpression) {
38333881
foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {
@@ -3836,18 +3884,25 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
38363884
}
38373885
}
38383886

3839-
if ($conditionalExpression->getTypeHolder()->getCertainty()->no()) {
3840-
unset($scope->expressionTypes[$conditionalExprString]);
3841-
} else {
3842-
$scope->expressionTypes[$conditionalExprString] = array_key_exists($conditionalExprString, $scope->expressionTypes)
3843-
? new ExpressionTypeHolder(
3844-
$scope->expressionTypes[$conditionalExprString]->getExpr(),
3845-
TypeCombinator::intersect($scope->expressionTypes[$conditionalExprString]->getType(), $conditionalExpression->getTypeHolder()->getType()),
3846-
TrinaryLogic::maxMin($scope->expressionTypes[$conditionalExprString]->getCertainty(), $conditionalExpression->getTypeHolder()->getCertainty()),
3847-
)
3848-
: $conditionalExpression->getTypeHolder();
3849-
$specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder();
3850-
}
3887+
$conditions[$conditionalExprString][] = $conditionalExpression;
3888+
$specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder();
3889+
}
3890+
}
3891+
3892+
foreach ($conditions as $conditionalExprString => $expressions) {
3893+
$certainty = TrinaryLogic::lazyExtremeIdentity($expressions, static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getCertainty());
3894+
if ($certainty->no()) {
3895+
unset($scope->expressionTypes[$conditionalExprString]);
3896+
} else {
3897+
$type = TypeCombinator::intersect(...array_map(static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getType(), $expressions));
3898+
3899+
$scope->expressionTypes[$conditionalExprString] = array_key_exists($conditionalExprString, $scope->expressionTypes)
3900+
? new ExpressionTypeHolder(
3901+
$scope->expressionTypes[$conditionalExprString]->getExpr(),
3902+
TypeCombinator::intersect($scope->expressionTypes[$conditionalExprString]->getType(), $type),
3903+
TrinaryLogic::maxMin($scope->expressionTypes[$conditionalExprString]->getCertainty(), $certainty),
3904+
)
3905+
: $expressions[0]->getTypeHolder();
38513906
}
38523907
}
38533908

src/Analyser/NodeScopeResolver.php

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,14 +1759,22 @@ private function ensureShallowNonNullability(MutatingScope $scope, Scope $origin
17591759
if ($isNull->yes()) {
17601760
return new EnsuredNonNullabilityResult($scope, []);
17611761
}
1762+
1763+
// keep certainty
1764+
$certainty = TrinaryLogic::createYes();
1765+
$hasExpressionType = $originalScope->hasExpressionType($exprToSpecify);
1766+
if (!$hasExpressionType->no()) {
1767+
$certainty = $hasExpressionType;
1768+
}
1769+
17621770
$exprTypeWithoutNull = TypeCombinator::removeNull($exprType);
17631771
if ($exprType->equals($exprTypeWithoutNull)) {
17641772
$originalExprType = $originalScope->getType($exprToSpecify);
17651773
if (!$originalExprType->equals($exprTypeWithoutNull)) {
17661774
$originalNativeType = $originalScope->getNativeType($exprToSpecify);
17671775

17681776
return new EnsuredNonNullabilityResult($scope, [
1769-
new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType),
1777+
new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $certainty),
17701778
]);
17711779
}
17721780
return new EnsuredNonNullabilityResult($scope, []);
@@ -1782,7 +1790,7 @@ private function ensureShallowNonNullability(MutatingScope $scope, Scope $origin
17821790
return new EnsuredNonNullabilityResult(
17831791
$scope,
17841792
[
1785-
new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType),
1793+
new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType, $certainty),
17861794
],
17871795
);
17881796
}
@@ -1812,6 +1820,7 @@ private function revertNonNullability(MutatingScope $scope, array $specifiedExpr
18121820
$specifiedExpressionResult->getExpression(),
18131821
$specifiedExpressionResult->getOriginalType(),
18141822
$specifiedExpressionResult->getOriginalNativeType(),
1823+
$specifiedExpressionResult->getCertainty(),
18151824
);
18161825
}
18171826

@@ -2568,7 +2577,7 @@ static function (): void {
25682577
$scope = $this->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
25692578
$scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left);
25702579

2571-
$rightScope = $scope->filterByFalseyValue(new Expr\Isset_([$expr->left]));
2580+
$rightScope = $scope->filterByFalseyValue($expr);
25722581
$rightResult = $this->processExprNode($expr->right, $rightScope, $nodeCallback, $context->enterDeep());
25732582
$rightExprType = $scope->getType($expr->right);
25742583
if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
@@ -2787,15 +2796,22 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void {
27872796
$throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints());
27882797
$ifFalseScope = $elseResult->getScope();
27892798

2790-
if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) {
2799+
$condType = $scope->getType($expr->cond);
2800+
if ($condType->isTrue()->yes()) {
2801+
$finalScope = $ifTrueScope;
2802+
} elseif ($condType->isFalse()->yes()) {
27912803
$finalScope = $ifFalseScope;
27922804
} else {
2793-
$ifFalseType = $ifFalseScope->getType($expr->else);
2794-
2795-
if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) {
2796-
$finalScope = $ifTrueScope;
2805+
if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) {
2806+
$finalScope = $ifFalseScope;
27972807
} else {
2798-
$finalScope = $ifTrueScope->mergeWith($ifFalseScope);
2808+
$ifFalseType = $ifFalseScope->getType($expr->else);
2809+
2810+
if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) {
2811+
$finalScope = $ifTrueScope;
2812+
} else {
2813+
$finalScope = $ifTrueScope->mergeWith($ifFalseScope);
2814+
}
27992815
}
28002816
}
28012817

@@ -3751,10 +3767,35 @@ private function processAssignVar(
37513767
$throwPoints = $result->getThrowPoints();
37523768
$assignedExpr = $this->unwrapAssign($assignedExpr);
37533769
$type = $scope->getType($assignedExpr);
3754-
$truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy());
3755-
$falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey());
37563770

37573771
$conditionalExpressions = [];
3772+
if ($assignedExpr instanceof Ternary) {
3773+
$if = $assignedExpr->if;
3774+
if ($if === null) {
3775+
$if = $assignedExpr->cond;
3776+
}
3777+
$condScope = $this->processExprNode($assignedExpr->cond, $scope, static function (): void {
3778+
}, ExpressionContext::createDeep())->getScope();
3779+
$truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createTruthy());
3780+
$falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createFalsey());
3781+
$truthyScope = $condScope->filterBySpecifiedTypes($truthySpecifiedTypes);
3782+
$falsyScope = $condScope->filterBySpecifiedTypes($falseySpecifiedTypes);
3783+
$truthyType = $truthyScope->getType($if);
3784+
$falseyType = $falsyScope->getType($assignedExpr->else);
3785+
3786+
if (
3787+
$truthyType->isSuperTypeOf($falseyType)->no()
3788+
&& $falseyType->isSuperTypeOf($truthyType)->no()
3789+
) {
3790+
$conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
3791+
$conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
3792+
$conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
3793+
$conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
3794+
}
3795+
}
3796+
3797+
$truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy());
3798+
$falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey());
37583799

37593800
$truthyType = TypeCombinator::removeFalsey($type);
37603801
$falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey());
@@ -3764,7 +3805,6 @@ private function processAssignVar(
37643805
$conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
37653806
$conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
37663807

3767-
// TODO conditional expressions for native type should be handled too
37683808
$scope = $result->getScope()->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr));
37693809
foreach ($conditionalExpressions as $exprString => $holders) {
37703810
$scope = $scope->addConditionalExpressions($exprString, $holders);

0 commit comments

Comments
 (0)