Skip to content

Commit 54284c8

Browse files
committed
Basic dependent variables
1 parent c0c6565 commit 54284c8

File tree

4 files changed

+438
-8
lines changed

4 files changed

+438
-8
lines changed

src/Analyser/MutatingScope.php

Lines changed: 174 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3182,7 +3182,8 @@ public function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType
31823182
}
31833183

31843184
$nativeTypes = $this->nativeExpressionTypes;
3185-
$nativeTypes[sprintf('$%s', $variableName)] = $nativeType;
3185+
$exprString = sprintf('$%s', $variableName);
3186+
$nativeTypes[$exprString] = $nativeType;
31863187

31873188
return $this->scopeFactory->create(
31883189
$this->context,
@@ -3362,20 +3363,90 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
33623363
});
33633364

33643365
$scope = $this;
3366+
$typeGuards = [];
33653367
foreach ($typeSpecifications as $typeSpecification) {
33663368
$expr = $typeSpecification['expr'];
33673369
$type = $typeSpecification['type'];
33683370
if ($typeSpecification['sure']) {
3369-
$scope = $scope->specifyExpressionType($expr, $specifiedTypes->shouldOverwrite() ? $type : TypeCombinator::intersect($type, $this->getType($expr)));
3371+
$typeGuard = TypeCombinator::intersect($type, $this->getType($expr));
3372+
$scope = $scope->specifyExpressionType($expr, $specifiedTypes->shouldOverwrite() ? $type : $typeGuard);
33703373

33713374
if ($expr instanceof Variable && is_string($expr->name)) {
33723375
$scope->nativeExpressionTypes[sprintf('$%s', $expr->name)] = $specifiedTypes->shouldOverwrite() ? $type : TypeCombinator::intersect($type, $this->getNativeType($expr));
33733376
}
33743377
} else {
33753378
$scope = $scope->removeTypeFromExpression($expr, $type);
3379+
$typeGuard = TypeCombinator::remove($scope->getType($expr), $type);
33763380
}
3381+
3382+
if (
3383+
!$expr instanceof Variable
3384+
|| !is_string($expr->name)
3385+
|| $specifiedTypes->shouldOverwrite()
3386+
) {
3387+
continue;
3388+
}
3389+
3390+
$scope->typeGuards['$' . $expr->name] = $typeGuard;
3391+
$typeGuards['$' . $expr->name] = $typeGuard;
33773392
}
33783393

3394+
$newConditionalExpressions = [];
3395+
foreach ($this->conditionalExpressions as $variableExprString => $conditionalExpressions) {
3396+
if (array_key_exists($variableExprString, $typeGuards)) {
3397+
continue;
3398+
}
3399+
3400+
$typeHolder = null;
3401+
3402+
$variableName = substr($variableExprString, 1);
3403+
foreach ($conditionalExpressions as $conditionalExpression) {
3404+
$matchingConditions = [];
3405+
foreach ($conditionalExpression->getConditionExpressionTypes() as $conditionExprString => $conditionalType) {
3406+
if (!array_key_exists($conditionExprString, $typeGuards)) {
3407+
continue;
3408+
}
3409+
3410+
if (!$typeGuards[$conditionExprString]->equals($conditionalType)) {
3411+
continue 2;
3412+
}
3413+
3414+
$matchingConditions[$conditionExprString] = $conditionalType;
3415+
}
3416+
3417+
if (count($matchingConditions) === 0) {
3418+
$newConditionalExpressions[$variableExprString][$conditionalExpression->getKey()] = $conditionalExpression;
3419+
continue;
3420+
}
3421+
3422+
if (count($matchingConditions) < count($conditionalExpression->getConditionExpressionTypes())) {
3423+
$filteredConditions = $conditionalExpression->getConditionExpressionTypes();
3424+
foreach (array_keys($matchingConditions) as $conditionExprString) {
3425+
unset($filteredConditions[$conditionExprString]);
3426+
}
3427+
3428+
$holder = new ConditionalExpressionHolder($filteredConditions, $conditionalExpression->getTypeHolder());
3429+
$newConditionalExpressions[$variableExprString][$holder->getKey()] = $holder;
3430+
continue;
3431+
}
3432+
3433+
$typeHolder = $conditionalExpression->getTypeHolder();
3434+
break;
3435+
}
3436+
3437+
if ($typeHolder === null) {
3438+
continue;
3439+
}
3440+
3441+
if ($typeHolder->getCertainty()->no()) {
3442+
unset($scope->variableTypes[$variableName]);
3443+
} else {
3444+
$scope->variableTypes[$variableName] = $typeHolder;
3445+
}
3446+
}
3447+
3448+
$scope->conditionalExpressions = $newConditionalExpressions;
3449+
33793450
return $scope;
33803451
}
33813452

@@ -3473,6 +3544,33 @@ public function mergeWith(?self $otherScope): self
34733544
}
34743545
}
34753546

3547+
$mergedVariableHolders = $this->mergeVariableHolders($ourVariableTypes, $theirVariableTypes);
3548+
$conditionalExpressions = $this->intersectConditionalExpressions($otherScope->conditionalExpressions);
3549+
$conditionalExpressions = $this->createConditionalExpressions(
3550+
$conditionalExpressions,
3551+
$this->typeGuards,
3552+
$ourVariableTypes,
3553+
$mergedVariableHolders
3554+
);
3555+
$conditionalExpressions = $this->createConditionalExpressions(
3556+
$conditionalExpressions,
3557+
$otherScope->typeGuards,
3558+
$theirVariableTypes,
3559+
$mergedVariableHolders
3560+
);
3561+
$typeGuards = [];
3562+
foreach ($this->typeGuards as $guardExprString => $typeGuard) {
3563+
if (!array_key_exists($guardExprString, $otherScope->typeGuards)) {
3564+
continue;
3565+
}
3566+
3567+
if (!$typeGuard->equals($otherScope->typeGuards[$guardExprString])) {
3568+
continue;
3569+
}
3570+
3571+
$typeGuards[$guardExprString] = $typeGuard;
3572+
}
3573+
34763574
return $this->scopeFactory->create(
34773575
$this->context,
34783576
$this->isDeclareStrictTypes(),
@@ -3482,10 +3580,10 @@ public function mergeWith(?self $otherScope): self
34823580
)),
34833581
$this->getFunction(),
34843582
$this->getNamespace(),
3485-
$this->mergeVariableHolders($ourVariableTypes, $theirVariableTypes),
3583+
$mergedVariableHolders,
34863584
$this->mergeVariableHolders($this->moreSpecificTypes, $otherScope->moreSpecificTypes),
3487-
[], // todo
3488-
[], // todo
3585+
$typeGuards,
3586+
$conditionalExpressions,
34893587
$this->inClosureBindScopeClass,
34903588
$this->anonymousFunctionReflection,
34913589
$this->inFirstLevelStatement,
@@ -3502,6 +3600,77 @@ public function mergeWith(?self $otherScope): self
35023600
);
35033601
}
35043602

3603+
/**
3604+
* @param array<string, ConditionalExpressionHolder[]> $otherConditionalExpressions
3605+
* @return array<string, ConditionalExpressionHolder[]>
3606+
*/
3607+
private function intersectConditionalExpressions(array $otherConditionalExpressions): array
3608+
{
3609+
$newConditionalExpressions = [];
3610+
foreach ($this->conditionalExpressions as $exprString => $holders) {
3611+
if (!array_key_exists($exprString, $otherConditionalExpressions)) {
3612+
continue;
3613+
}
3614+
3615+
$otherHolders = $otherConditionalExpressions[$exprString];
3616+
foreach (array_keys($holders) as $key) {
3617+
if (!array_key_exists($key, $otherHolders)) {
3618+
continue 2;
3619+
}
3620+
}
3621+
3622+
$newConditionalExpressions[$exprString] = $holders;
3623+
}
3624+
3625+
return $newConditionalExpressions;
3626+
}
3627+
3628+
/**
3629+
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
3630+
* @param array<string, Type> $typeGuards
3631+
* @param array<string, VariableTypeHolder> $variableTypes
3632+
* @param array<string, VariableTypeHolder> $mergedVariableHolders
3633+
* @return array<string, ConditionalExpressionHolder[]>
3634+
*/
3635+
private function createConditionalExpressions(
3636+
array $conditionalExpressions,
3637+
array $typeGuards,
3638+
array $variableTypes,
3639+
array $mergedVariableHolders
3640+
): array
3641+
{
3642+
if (count($typeGuards) === 0) {
3643+
return $conditionalExpressions;
3644+
}
3645+
3646+
foreach ($variableTypes as $name => $holder) {
3647+
if (array_key_exists('$' . $name, $typeGuards)) {
3648+
continue;
3649+
}
3650+
3651+
if (
3652+
array_key_exists($name, $mergedVariableHolders)
3653+
&& $mergedVariableHolders[$name]->equals($holder)
3654+
) {
3655+
continue;
3656+
}
3657+
3658+
$conditionalExpression = new ConditionalExpressionHolder($typeGuards, $holder);
3659+
$conditionalExpressions['$' . $name][$conditionalExpression->getKey()] = $conditionalExpression;
3660+
}
3661+
3662+
foreach (array_keys($mergedVariableHolders) as $name) {
3663+
if (array_key_exists($name, $variableTypes)) {
3664+
continue;
3665+
}
3666+
3667+
$conditionalExpression = new ConditionalExpressionHolder($typeGuards, new VariableTypeHolder(new ErrorType(), TrinaryLogic::createNo()));
3668+
$conditionalExpressions['$' . $name][$conditionalExpression->getKey()] = $conditionalExpression;
3669+
}
3670+
3671+
return $conditionalExpressions;
3672+
}
3673+
35053674
/**
35063675
* @param VariableTypeHolder[] $ourVariableTypeHolders
35073676
* @param VariableTypeHolder[] $theirVariableTypeHolders

src/Analyser/VariableTypeHolder.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ class VariableTypeHolder
1515

1616
public function __construct(Type $type, TrinaryLogic $certainty)
1717
{
18-
if ($certainty->no()) {
19-
throw new \PHPStan\ShouldNotHappenException();
20-
}
2118
$this->type = $type;
2219
$this->certainty = $certainty;
2320
}
@@ -32,6 +29,15 @@ public static function createMaybe(Type $type): self
3229
return new self($type, TrinaryLogic::createMaybe());
3330
}
3431

32+
public function equals(self $other): bool
33+
{
34+
if (!$this->certainty->equals($other->certainty)) {
35+
return false;
36+
}
37+
38+
return $this->type->equals($other->type);
39+
}
40+
3541
public function and(self $other): self
3642
{
3743
if ($this->getType()->equals($other->getType())) {

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10531,6 +10531,11 @@ public function dataBug4205(): array
1053110531
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4205.php');
1053210532
}
1053310533

10534+
public function dataDependentVariableCertainty(): array
10535+
{
10536+
return $this->gatherAssertTypes(__DIR__ . '/data/dependent-variable-certainty.php');
10537+
}
10538+
1053410539
/**
1053510540
* @param string $file
1053610541
* @return array<string, mixed[]>
@@ -10723,6 +10728,7 @@ private function gatherAssertTypes(string $file): array
1072310728
* @dataProvider dataBug4206
1072410729
* @dataProvider dataBugEmptyArray
1072510730
* @dataProvider dataBug4205
10731+
* @dataProvider dataDependentVariableCertainty
1072610732
* @param string $assertType
1072710733
* @param string $file
1072810734
* @param mixed ...$args

0 commit comments

Comments
 (0)