Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Normalize specified types before intersection #1016

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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));
herndlm marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
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));
herndlm marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
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'),
herndlm marked this conversation as resolved.
Show resolved Hide resolved
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) {
herndlm marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}