Skip to content

Commit

Permalink
Normalize specified types before intersection
Browse files Browse the repository at this point in the history
  • Loading branch information
herndlm authored and ondrejmirtes committed Feb 15, 2022
1 parent 9a57d0c commit 7abb7c9
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 6 deletions.
32 changes: 26 additions & 6 deletions src/Analyser/SpecifiedTypes.php
Expand Up @@ -58,28 +58,31 @@ public function getNewConditionalExpressionHolders(): array
/** @api */
public function intersectWith(SpecifiedTypes $other): self
{
$normalized = $this->normalize();
$otherNormalized = $other->normalize();

$sureTypeUnion = [];
$sureNotTypeUnion = [];

foreach ($this->sureTypes as $exprString => [$exprNode, $type]) {
if (!isset($other->sureTypes[$exprString])) {
foreach ($normalized->sureTypes as $exprString => [$exprNode, $type]) {
if (!isset($otherNormalized->sureTypes[$exprString])) {
continue;
}

$sureTypeUnion[$exprString] = [
$exprNode,
TypeCombinator::union($type, $other->sureTypes[$exprString][1]),
TypeCombinator::union($type, $otherNormalized->sureTypes[$exprString][1]),
];
}

foreach ($this->sureNotTypes as $exprString => [$exprNode, $type]) {
if (!isset($other->sureNotTypes[$exprString])) {
foreach ($normalized->sureNotTypes as $exprString => [$exprNode, $type]) {
if (!isset($otherNormalized->sureNotTypes[$exprString])) {
continue;
}

$sureNotTypeUnion[$exprString] = [
$exprNode,
TypeCombinator::intersect($type, $other->sureNotTypes[$exprString][1]),
TypeCombinator::intersect($type, $otherNormalized->sureNotTypes[$exprString][1]),
];
}

Expand Down Expand Up @@ -117,4 +120,21 @@ public function unionWith(SpecifiedTypes $other): self
return new self($sureTypeUnion, $sureNotTypeUnion);
}

private function normalize(): self
{
$sureTypes = $this->sureTypes;
$sureNotTypes = [];

foreach ($this->sureNotTypes as $exprString => [$exprNode, $sureNotType]) {
if (!isset($sureTypes[$exprString])) {
$sureNotTypes[$exprString] = [$exprNode, $sureNotType];
continue;
}

$sureTypes[$exprString][1] = TypeCombinator::remove($sureTypes[$exprString][1], $sureNotType);
}

return new self($sureTypes, $sureNotTypes, $this->overwrite, $this->newConditionalExpressionHolders);
}

}
4 changes: 4 additions & 0 deletions src/Analyser/TypeSpecifier.php
Expand Up @@ -482,6 +482,8 @@ public function specifyTypesInCondition(
$argType = $scope->getType($expr->right->getArgs()[0]->value);
if ($argType->isArray()->yes()) {
$result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope));
} else {
$result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new ConstantArrayType([], []), $context->negate(), false, $scope));
}
}
}
Expand All @@ -501,6 +503,8 @@ public function specifyTypesInCondition(
$argType = $scope->getType($expr->right->getArgs()[0]->value);
if ($argType instanceof StringType) {
$result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new AccessoryNonEmptyStringType(), $context, false, $scope));
} else {
$result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new ConstantStringType(''), $context->negate(), false, $scope));
}
}
}
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -692,6 +692,7 @@ public function dataFileAsserts(): iterable
if (PHP_VERSION_ID >= 80000) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6308.php');
}
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6329.php');

if (PHP_VERSION_ID >= 70400) {
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-6473.php');
Expand Down
39 changes: 39 additions & 0 deletions tests/PHPStan/Analyser/TypeSpecifierTest.php
Expand Up @@ -965,6 +965,45 @@ public function dataCondition(): array
],
[],
],
[
new Expr\BinaryOp\BooleanOr(
new Expr\BinaryOp\BooleanAnd(
$this->createFunctionCall('is_string', 'a'),
new NotIdentical(new String_(''), new Variable('a')),
),
new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')),
),
['$a' => 'non-empty-string|null'],
['$a' => '~null'],
],
[
new Expr\BinaryOp\BooleanOr(
new Expr\BinaryOp\BooleanAnd(
$this->createFunctionCall('is_string', 'a'),
new Expr\BinaryOp\Greater(
$this->createFunctionCall('strlen', 'a'),
new LNumber(0),
),
),
new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')),
),
['$a' => 'non-empty-string|null'],
['$a' => '~null'],
],
[
new Expr\BinaryOp\BooleanOr(
new Expr\BinaryOp\BooleanAnd(
$this->createFunctionCall('is_array', 'a'),
new Expr\BinaryOp\Greater(
$this->createFunctionCall('count', 'a'),
new LNumber(0),
),
),
new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')),
),
['$a' => 'non-empty-array|null'],
['$a' => '~null'],
],
];
}

Expand Down
160 changes: 160 additions & 0 deletions tests/PHPStan/Analyser/data/bug-6329.php
@@ -0,0 +1,160 @@
<?php

namespace Bug6329;

use function PHPStan\Testing\assertType;

/**
* @param mixed $a
*/
function nonEmptyString1($a): void
{
if (is_string($a) && '' !== $a || null === $a) {
assertType('non-empty-string|null', $a);
}

if ('' !== $a && is_string($a) || null === $a) {
assertType('non-empty-string|null', $a);
}

if (null === $a || is_string($a) && '' !== $a) {
assertType('non-empty-string|null', $a);
}

if (null === $a || '' !== $a && is_string($a)) {
assertType('non-empty-string|null', $a);
}
}

/**
* @param mixed $a
*/
function nonEmptyString2($a): void
{
if (is_string($a) && strlen($a) > 0 || null === $a) {
assertType('non-empty-string|null', $a);
}

if (null === $a || is_string($a) && strlen($a) > 0) {
assertType('non-empty-string|null', $a);
}
}


/**
* @param mixed $a
*/
function int1($a): void
{
if (is_int($a) && 0 !== $a || null === $a) {
assertType('int<min, -1>|int<1, max>|null', $a);
}

if (0 !== $a && is_int($a) || null === $a) {
assertType('int<min, -1>|int<1, max>|null', $a);
}

if (null === $a || is_int($a) && 0 !== $a) {
assertType('int<min, -1>|int<1, max>|null', $a);
}

if (null === $a || 0 !== $a && is_int($a)) {
assertType('int<min, -1>|int<1, max>|null', $a);
}
}

/**
* @param mixed $a
*/
function int2($a): void
{
if (is_int($a) && $a > 0 || null === $a) {
assertType('int<1, max>|null', $a);
}

if (null === $a || is_int($a) && $a > 0) {
assertType('int<1, max>|null', $a);
}
}


/**
* @param mixed $a
*/
function true($a): void
{
if (is_bool($a) && false !== $a || null === $a) {
assertType('true|null', $a);
}

if (false !== $a && is_bool($a) || null === $a) {
assertType('true|null', $a);
}

if (null === $a || is_bool($a) && false !== $a) {
assertType('true|null', $a);
}

if (null === $a || false !== $a && is_bool($a)) {
assertType('true|null', $a);
}
}

/**
* @param mixed $a
*/
function nonEmptyArray1($a): void
{
if (is_array($a) && [] !== $a || null === $a) {
assertType('non-empty-array|null', $a);
}

if ([] !== $a && is_array($a) || null === $a) {
assertType('non-empty-array|null', $a);
}

if (null === $a || is_array($a) && [] !== $a) {
assertType('non-empty-array|null', $a);
}

if (null === $a || [] !== $a && is_array($a)) {
assertType('non-empty-array|null', $a);
}
}

/**
* @param mixed $a
*/
function nonEmptyArray2($a): void
{
if (is_array($a) && count($a) > 0 || null === $a) {
assertType('non-empty-array|null', $a);
}

if (null === $a || is_array($a) && count($a) > 0) {
assertType('non-empty-array|null', $a);
}
}

/**
* @param mixed $a
* @param mixed $b
* @param mixed $c
*/
function inverse($a, $b, $c): void
{
if ((!is_string($a) || '' === $a) && null !== $a) {
} else {
assertType('non-empty-string|null', $a);
}

if ((!is_int($b) || $b <= 0) && null !== $b) {
} else {
assertType('int<1, max>|null', $b);
}

if (null !== $c && (!is_array($c) || count($c) <= 0)) {
} else {
assertType('non-empty-array|null', $c);
}
}

0 comments on commit 7abb7c9

Please sign in to comment.