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

Fix !isset() with ArrayDimFetch #2798

Draft
wants to merge 16 commits into
base: 1.10.x
Choose a base branch
from
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ parameters:

-
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#"
count: 3
count: 4
path: src/Analyser/TypeSpecifier.php

-
Expand Down
120 changes: 120 additions & 0 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\ConditionalTypeForParameter;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
Expand Down Expand Up @@ -665,6 +666,125 @@ public function specifyTypesInCondition(
);
}

if (
$issetExpr instanceof ArrayDimFetch
&& $issetExpr->dim !== null
) {
$type = $scope->getType($issetExpr->var);
if ($type instanceof MixedType) {
return new SpecifiedTypes();
}

$dimType = $scope->getType($issetExpr->dim);
if (!($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType)) {
return new SpecifiedTypes();
}

$hasOffsetType = $type->hasOffsetValueType($dimType);
if ($hasOffsetType->no()) {
return new SpecifiedTypes();
}

$hasOffset = $hasOffsetType->yes();
$offsetType = $type->getOffsetValueType($dimType);
$isNullable = !$offsetType->isNull()->no();

$setOffset = static fn (Type $outerType, Type $dimType, bool $optional): Type => TypeTraverser::map(
$outerType,
static function (Type $type, callable $traverse) use ($dimType, $optional): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

if ($type instanceof ConstantArrayType) {
// unset the offset and set a new value, since we don't want to narrow the existing one
$typeWithoutOffset = $type->unsetOffset($dimType);
if (!$typeWithoutOffset instanceof ConstantArrayType) {
throw new ShouldNotHappenException();
}

$builder = ConstantArrayTypeBuilder::createFromConstantArray(
$typeWithoutOffset,
);
$builder->setOffsetValueType(
$dimType,
new NullType(),
$optional,
);
return $builder->getArray();
}

return $type;
},
);

if ($hasOffset === true) {
if ($isNullable) {
$specifiedType = $this->create(
$issetExpr->var,
$setOffset($type, $dimType, false),
$context->negate(),
true,
$scope,
$rootExpr,
);

// keep variable maybe certainty
if ($scope->hasExpressionType($issetExpr->var)->maybe()) {
return $specifiedType->unionWith($this->create(
new IssetExpr($issetExpr->var),
new NullType(),
$context->negate(),
false,
$scope,
$rootExpr,
));
}

return $specifiedType;
}

$typeWithoutOffset = $type->unsetOffset($dimType);
$arraySize = $typeWithoutOffset->getArraySize();
if (
!$arraySize instanceof NeverType
&& (new ConstantIntegerType(0))->isSuperTypeOf($arraySize)->yes()
) {
// variable cannot exist
return $this->create(
new IssetExpr($issetExpr->var),
new NullType(),
$context,
false,
$scope,
$rootExpr,
);
}

return new SpecifiedTypes();
}

if ($isNullable) {
return $this->create(
$issetExpr->var,
$setOffset($type, $dimType, true),
$context->negate(),
false,
$scope,
$rootExpr,
);
}

return $this->create(
$issetExpr->var,
$type->unsetOffset($dimType),
$context->negate(),
false,
$scope,
$rootExpr,
);
}

return new SpecifiedTypes();
}

Expand Down
24 changes: 24 additions & 0 deletions test2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

use PHPStan\TrinaryLogic;
use function PHPStan\Testing\assertType;
use function PHPStan\Testing\assertVariableCertainty;

/**
* @param array{bar?: null}|array{bar?: 'hello'} $a
*/
function optionalOffsetNull($a): void
{
if (isset($a['bar'])) {
assertVariableCertainty(TrinaryLogic::createYes(), $a);
assertType("array{bar: 'hello'}", $a);
$a['bar'] = 1;
assertType("array{bar: 1}", $a);
} else {
assertVariableCertainty(TrinaryLogic::createYes(), $a);
assertType('array{bar?: null}', $a);
}

assertVariableCertainty(TrinaryLogic::createYes(), $a);
assertType("array{bar: 1}|array{bar?: null}", $a);
}
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/falsy-isset.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-coalesce.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/falsey-ternary-certainty.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9908.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7915.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9714.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9105.php');
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/data/bug-4708.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function GetASCConfig()
assertType('array<string>', $result);
if (!isset($result['bsw']))
{
assertType('array<string>', $result);
assertType("array<mixed~'bsw', string>", $result);
$result['bsw'] = 1;
assertType("non-empty-array<1|string>&hasOffsetValue('bsw', 1)", $result);
}
Expand All @@ -66,7 +66,7 @@ function GetASCConfig()

if (!isset($result['bew']))
{
assertType("non-empty-array<int|string>&hasOffsetValue('bsw', int)", $result);
assertType("non-empty-array<mixed~'bew', int|string>&hasOffsetValue('bsw', int)", $result);
$result['bew'] = 5;
assertType("non-empty-array<int|string>&hasOffsetValue('bew', 5)&hasOffsetValue('bsw', int)", $result);
}
Expand Down
27 changes: 27 additions & 0 deletions tests/PHPStan/Analyser/data/bug-9908.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types=1);

namespace Bug9908;

use function PHPStan\Testing\assertType;

class HelloWorld
{
public function test(): void
{
$a = [];
if (rand() % 2) {
$a = ['bar' => 'string'];
}

assertType("array{}|array{bar: 'string'}", $a);
if (isset($a['bar'])) {
assertType("array{bar: 'string'}", $a);
$a['bar'] = 1;
assertType("array{bar: 1}", $a);
} else {
assertType('array{}', $a);
}

assertType('array{}|array{bar: 1}', $a);
}
}
Loading