Skip to content

Commit

Permalink
Dependent types - understand truthy BooleanOr and falsey BooleanAnd s…
Browse files Browse the repository at this point in the history
…cope
  • Loading branch information
ondrejmirtes committed Mar 22, 2021
1 parent 5d37113 commit 2c42ef1
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 6 deletions.
30 changes: 27 additions & 3 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3560,7 +3560,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
$typeGuards['$' . $variableName] = $typeGuard;
}

$newConditionalExpressions = [];
$newConditionalExpressions = $specifiedTypes->getNewConditionalExpressionHolders();
foreach ($this->conditionalExpressions as $variableExprString => $conditionalExpressions) {
if (array_key_exists($variableExprString, $typeGuards)) {
continue;
Expand Down Expand Up @@ -3614,9 +3614,33 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
}
}

$scope->conditionalExpressions = $newConditionalExpressions;
return $scope->changeConditionalExpressions($newConditionalExpressions);
}

return $scope;
/**
* @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressionHolders
* @return self
*/
public function changeConditionalExpressions(array $newConditionalExpressionHolders): self
{
return $this->scopeFactory->create(
$this->context,
$this->isDeclareStrictTypes(),
$this->constantTypes,
$this->getFunction(),
$this->getNamespace(),
$this->variableTypes,
$this->moreSpecificTypes,
$newConditionalExpressionHolders,
$this->inClosureBindScopeClass,
$this->anonymousFunctionReflection,
$this->inFirstLevelStatement,
$this->currentlyAssignedExpressions,
$this->nativeExpressionTypes,
$this->inFunctionCallsStack,
$this->afterExtractCall,
$this->parentScope
);
}

/**
Expand Down
16 changes: 15 additions & 1 deletion src/Analyser/SpecifiedTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,26 @@ class SpecifiedTypes

private bool $overwrite;

/** @var array<string, ConditionalExpressionHolder[]> */
private array $newConditionalExpressionHolders;

/**
* @param mixed[] $sureTypes
* @param mixed[] $sureNotTypes
* @param bool $overwrite
* @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressionHolders
*/
public function __construct(
array $sureTypes = [],
array $sureNotTypes = [],
bool $overwrite = false
bool $overwrite = false,
array $newConditionalExpressionHolders = []
)
{
$this->sureTypes = $sureTypes;
$this->sureNotTypes = $sureNotTypes;
$this->overwrite = $overwrite;
$this->newConditionalExpressionHolders = $newConditionalExpressionHolders;
}

/**
Expand All @@ -52,6 +58,14 @@ public function shouldOverwrite(): bool
return $this->overwrite;
}

/**
* @return array<string, ConditionalExpressionHolder[]>
*/
public function getNewConditionalExpressionHolders(): array
{
return $this->newConditionalExpressionHolders;
}

public function intersectWith(SpecifiedTypes $other): self
{
$sureTypeUnion = [];
Expand Down
55 changes: 53 additions & 2 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Name;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\HasOffsetType;
use PHPStan\Type\Accessory\HasPropertyType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
Expand Down Expand Up @@ -578,11 +579,21 @@ public function specifyTypesInCondition(
} elseif ($expr instanceof BooleanAnd || $expr instanceof LogicalAnd) {
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context);
$rightTypes = $this->specifyTypesInCondition($scope, $expr->right, $context);
return $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->intersectWith($rightTypes);
$types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->intersectWith($rightTypes);
if ($context->false()) {
return $this->processBooleanConditionalTypes($scope, $types, $leftTypes, $rightTypes);
}

return $types;
} elseif ($expr instanceof BooleanOr || $expr instanceof LogicalOr) {
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context);
$rightTypes = $this->specifyTypesInCondition($scope, $expr->right, $context);
return $context->true() ? $leftTypes->intersectWith($rightTypes) : $leftTypes->unionWith($rightTypes);
$types = $context->true() ? $leftTypes->intersectWith($rightTypes) : $leftTypes->unionWith($rightTypes);
if ($context->true()) {
return $this->processBooleanConditionalTypes($scope, $types, $leftTypes, $rightTypes);
}

return $types;
} elseif ($expr instanceof Node\Expr\BooleanNot && !$context->null()) {
return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate());
} elseif ($expr instanceof Node\Expr\Assign) {
Expand Down Expand Up @@ -744,6 +755,46 @@ private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $contex
return new SpecifiedTypes();
}

private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $types, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): SpecifiedTypes
{
$conditionExpressionTypes = [];
foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
if (!$expr instanceof Expr\Variable) {
continue;
}
if (!is_string($expr->name)) {
continue;
}

$conditionExpressionTypes[$exprString] = TypeCombinator::intersect($scope->getType($expr), $type);
}

if (count($conditionExpressionTypes) > 0) {
$holders = [];
foreach ($rightTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
if (!$expr instanceof Expr\Variable) {
continue;
}
if (!is_string($expr->name)) {
continue;
}

if (!isset($holders[$exprString])) {
$holders[$exprString] = [];
}

$holders[$exprString][] = new ConditionalExpressionHolder(
$conditionExpressionTypes,
new VariableTypeHolder(TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()) // todo yes is wrong
);
}

return new SpecifiedTypes($types->getSureTypes(), $types->getSureNotTypes(), false, $holders);
}

return $types;
}

/**
* @param \PHPStan\Analyser\Scope $scope
* @param \PhpParser\Node\Expr\BinaryOp $binaryOperation
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5710,6 +5710,11 @@ public function dataBug4725(): array
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4725.php');
}

public function dataBug4733(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4733.php');
}

/**
* @dataProvider dataArrayFunctions
* @param string $description
Expand Down Expand Up @@ -11335,6 +11340,7 @@ private function gatherAssertTypes(string $file): array
* @dataProvider dataBug4545
* @dataProvider dataBug4714
* @dataProvider dataBug4725
* @dataProvider dataBug4733
* @param string $assertType
* @param string $file
* @param mixed ...$args
Expand Down
38 changes: 38 additions & 0 deletions tests/PHPStan/Analyser/data/bug-4733.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Bug4733;

use function PHPStan\Analyser\assertType;

class HelloWorld
{
public function getDescription(?\DateTimeImmutable $start, ?string $someObject): void
{
if ($start === null && $someObject === null) {
return;
}

// $start !== null || $someObject !== null

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

// $start === null therefore $someObject !== null

assertType('string', $someObject);
}

public function getDescription2(?\DateTimeImmutable $start, ?string $someObject): void
{
if ($start !== null || $someObject !== null) {
if ($start !== null) {
return;
}

// $start === null therefore $someObject !== null

assertType('string', $someObject);
}
}
}

0 comments on commit 2c42ef1

Please sign in to comment.