Skip to content
Draft
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