Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/Analyser/ExprHandler/CoalesceHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,34 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$scope = $this->nonNullabilityHelper->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
$scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left);

$chainResults = [];
$this->defaultNarrowingHelper->captureChainResults($expr->left, $storage, $chainResults);

// the falsey narrowing of this very node - asking the scope about it
// mid-processing would take the on-demand path and recurse
$rightScope = $scope->applySpecifiedTypes($this->coalesceCompositionHelper->getFalseySpecifiedTypes($scope, $scope, $expr->left, $condResult, $expr, TypeSpecifierContext::createFalsey()));
$rightSideSpecifiedTypes = $this->coalesceCompositionHelper->getFalseySpecifiedTypes($scope, $scope, $expr->left, $condResult, $expr, TypeSpecifierContext::createFalsey());
$leftSurelySetNonNull = $condResult->getIssetabilityResolution($scope, false)->isSet(static function (Type $type): ?bool {
$isNull = $type->isNull();
if ($isNull->maybe()) {
return null;
}

return !$isNull->yes();
}) === true;
if (!$leftSurelySetNonNull) {
// the right side only evaluates when the left side is null or unset -
// the falsey isset() narrowing of the left side, like `??=`; skipped
// when the right side cannot evaluate at all, so its counterfactual
// certainty reductions do not survive the merge below
$rightSideSpecifiedTypes = $rightSideSpecifiedTypes->unionWith(
$this->coalesceCompositionHelper->getRightSideScopeSpecifiedTypes($scope, $expr->left, $condResult, $chainResults, $expr),
);
}
$rightScope = $scope->applySpecifiedTypes($rightSideSpecifiedTypes);
$rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $rightScope, $storage, $nodeCallback, $context->enterDeep());
// the left-is-set narrowing, composed from the already-processed chain
// results - the inside-out equivalent of narrowing by isset($expr->left)
// without synthesizing an Isset_ node and re-walking the chain on demand
$chainResults = [];
$this->defaultNarrowingHelper->captureChainResults($expr->left, $storage, $chainResults);
$leftIssetTypes = $this->defaultNarrowingHelper->createIssetTruthyChainTypes(
$scope,
$expr->left,
Expand Down
22 changes: 20 additions & 2 deletions src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -687,13 +687,31 @@ public function createIssetSingleSubjectNonTrueTypes(
if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) {
$constantArrays = $varType->getConstantArrays();
$typesToRemove = [];
$hasOptionalNonNullableOffset = false;
$hasPossiblyNullOffsetValue = false;
foreach ($constantArrays as $constantArray) {
$hasOffset = $constantArray->hasOffsetValueType($dimType);
if (!$hasOffset->yes() || !$constantArray->getOffsetValueType($dimType)->isNull()->no()) {
if ($hasOffset->no()) {
continue;
}
if (!$constantArray->getOffsetValueType($dimType)->isNull()->no()) {
$hasPossiblyNullOffsetValue = true;
continue;
}

if ($hasOffset->yes()) {
$typesToRemove[] = $constantArray;
continue;
}

$hasOptionalNonNullableOffset = true;
}

$typesToRemove[] = $constantArray;
// !isset() on an optional key with a non-nullable value means the
// key is absent - but only when no member can hold null at that
// offset (the removal distributes over every union member).
if ($hasOptionalNonNullableOffset && !$hasPossiblyNullOffsetValue) {
$typesToRemove[] = new HasOffsetType($dimType);
}

if ($typesToRemove !== []) {
Expand Down
24 changes: 4 additions & 20 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
use function get_class;
use function implode;
use function in_array;
use function is_int;
use function sprintf;
use function usort;
use const PHP_INT_MAX;
Expand Down Expand Up @@ -995,19 +994,11 @@ private static function processArrayTypes(array $arrayTypes): array
$keyTypesForGeneralArray = [];
$valueTypesForGeneralArray = [];
$generalArrayOccurred = false;
$constantKeyTypesNumbered = [];
$filledArrays = 0;
$overflowed = false;

/** @var int|float $nextConstantKeyTypeIndex */
$nextConstantKeyTypeIndex = 1;
$seenConstantKeyTypes = [];

foreach ($arrayTypes as $arrayType) {
$constantArrays = $arrayType->getConstantArrays();
$isConstantArray = $constantArrays !== [];
if (!$isConstantArray || !$arrayType->isIterableAtLeastOnce()->no()) {
$filledArrays++;
}

if (!$isConstantArray) {
foreach ($arrayType->getArrays() as $type) {
Expand All @@ -1024,23 +1015,16 @@ private static function processArrayTypes(array $arrayTypes): array
$valueTypesForGeneralArray[] = $valueTypes[$i];

$keyTypeValue = $keyType->getValue();
if (array_key_exists($keyTypeValue, $constantKeyTypesNumbered)) {
if (array_key_exists($keyTypeValue, $seenConstantKeyTypes)) {
continue;
}
$seenConstantKeyTypes[$keyTypeValue] = true;
$keyTypesForGeneralArray[] = $keyType;

$constantKeyTypesNumbered[$keyTypeValue] = $nextConstantKeyTypeIndex;
$nextConstantKeyTypeIndex *= 2;
if (!is_int($nextConstantKeyTypeIndex)) {
$generalArrayOccurred = true;
$overflowed = true;
continue 2;
}
}
}
}

if ($generalArrayOccurred && (!$overflowed || $filledArrays > 1)) {
if ($generalArrayOccurred) {
$reducedArrayTypes = self::reduceArrays($arrayTypes, false);
if (count($reducedArrayTypes) === 1) {
return [self::intersect($reducedArrayTypes[0], ...$accessoryTypes)];
Expand Down
31 changes: 31 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-12786.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug12786;

use function PHPStan\Testing\assertType;

class A
{
/** @param ?string[] $should_be_array */
public function __construct(
public ?array $should_be_array,
)
{
}

/** @param array{y?: string|string[]} $x */
public static function fromRequest(array $x): self
{
if (isset($x['y']) && is_string($x['y'])) {
$x['y'] = explode(',', $x['y']);
} // $x['y'] can't be string anymore

assertType('array<string>|null', $x['y'] ?? null);

return new self(
should_be_array: $x['y'] ?? null,
);
}
}
12 changes: 6 additions & 6 deletions tests/PHPStan/Analyser/nsrt/bug-15021.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
/** @param array{foo?: string, bar?: string} $data */
function optionalOffset(array $data): void
{
$data['foo'] ??= assertType('array{foo?: string, bar?: string}', $data);
$data['foo'] ??= assertType('array{bar?: string}', $data);
}

/** @param array{foo?: string, bar?: string} $data */
Expand Down Expand Up @@ -65,7 +65,7 @@ function nonNullableStaticProperty(): void

function propertyOffset(Foo $foo): void
{
$foo->data['foo'] ??= assertType('array{foo?: string, bar?: string}', $foo->data);
$foo->data['foo'] ??= assertType('array{bar?: string}', $foo->data);
}

/** @param \ArrayAccess<string, string> $a */
Expand Down Expand Up @@ -133,22 +133,22 @@ function unsetTargetBeforeAssignOp(array $data): void
function emptyTargetBeforeAssignOp(array $data): void
{
if (empty($data['foo'])) {
$data['foo'] ??= assertType('array{foo?: string, bar?: string}', $data);
$data['foo'] ??= assertType('array{bar?: string}', $data);
}
}

/** @param array{foo?: string, bar?: string} $data */
function assignOpInsideEmpty(array $data): void
{
if (empty($data['foo'] ??= assertType('array{foo?: string, bar?: string}', $data))) {
if (empty($data['foo'] ??= assertType('array{bar?: string}', $data))) {
echo 'empty';
}
}

/** @param array{foo?: string, bar?: string} $data */
function assignOpInsideIsset(array $data): void
{
if (isset($data['foo'] ??= assertType('array{foo?: string, bar?: string}', $data))) {
if (isset($data['foo'] ??= assertType('array{bar?: string}', $data))) {
echo 'isset';
}
}
Expand All @@ -157,7 +157,7 @@ function assignOpInsideIsset(array $data): void
function assignOpInsideUnsetOffset(array $data): void
{
$other = ['x' => 1, 'fallback' => 2];
unset($other[$data['foo'] ??= assertType('array{foo?: string, bar?: string}', $data)]);
unset($other[$data['foo'] ??= assertType('array{bar?: string}', $data)]);
}

/** @param array{foo?: string, bar?: string} $data */
Expand Down
30 changes: 30 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-6379.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php declare(strict_types = 1);

namespace Bug6379Types;

use function PHPStan\Testing\assertType;

class HelloWorld
{

/**
* @param array{
* cr?: string,
* c?: string
* } $params
*/
public static function paramsToString(array $params): string
{
if (isset($params['cr']) === true || isset($params['c']) === true) {
assertType('non-empty-array{cr?: string, c?: string}', $params);
if (!isset($params['cr'])) {
assertType('array{c: string}', $params);
}

return sprintf('-c%s', $params['cr'] ?? $params['c']);
}

return '';
}

}
42 changes: 42 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-9426.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Bug9426;

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

final class A
{

/**
* @param array{something?: string} $a
*/
public function something(array $a): void
{
if (isset($a['something'])) {
$b = new \DateTimeImmutable();
}

if (!isset($a['something'])) {
assertType('array{}', $a);
assertVariableCertainty(TrinaryLogic::createNo(), $b);
} else {
assertType('array{something: string}', $a);
assertVariableCertainty(TrinaryLogic::createYes(), $b);
}
}

/**
* @param array{something?: string|null} $a
*/
public function nullableValueStaysUntouched(array $a): void
{
if (!isset($a['something'])) {
assertType('array{something?: string|null}', $a);
} else {
assertType('array{something: string}', $a);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ function coalesceWithoutDefault(Holder $a, Holder $b): void
throw new \LogicException();
}

assertType('int|null', $a->noDefault ?? $b->noDefault);
assertType('int', $a->noDefault ?? $b->noDefault);
}
13 changes: 12 additions & 1 deletion tests/PHPStan/Analyser/nsrt/preserve-large-constant-array.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function multiKeys(array $arr): void
return;
}

assertType('array{1: string|null, 2: int|null, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float}', $arr);
assertType('array{1: null, 2: null, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float}|array{1: string, 2: int, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float}', $arr);
echo 1;
}

Expand Down Expand Up @@ -65,3 +65,14 @@ function multipleOptions(array $arr): void
assertType('array{1: string, 2: int, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float}', $brr);
echo 1;
}

/**
* @param array{1: string, 2: int, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float} $full
* @param array{2: int, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float} $withoutFirst
*/
function unionOfManyKeys(array $full, array $withoutFirst): void
{
$union = rand(0, 1) === 0 ? $full : $withoutFirst;
assertType('array{1?: string, 2: int, 3: bool, 4: string, 5: int, 6: bool, 7: string, 8: int, 9: bool, 10: string, 11: int, 12: bool, 13: string, 14: int, 15: bool, 16: string, 17: int, 18: bool, 19: string, 20: int, 21: bool, 22: string, 23: int, 24: bool, 25: string, 26: int, 27: bool, 28: string, 29: int, 30: bool, 31: string, 32: int, 33: bool, 34: string, 35: int, 36: bool, 37: string, 38: int, 39: bool, 40: string, 41: int, 42: bool, 43: string, 44: int, 45: bool, 46: string, 47: int, 48: bool, 49: string, 50: int, 51: bool, 52: string, 53: int, 54: bool, 55: string, 56: int, 57: bool, 58: string, 59: int, 60: bool, 61: string, 62: int, 63: bool, 64: float}', $union);
echo 1;
}
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,22 @@ public function testBug13688(): void
$this->analyse([__DIR__ . '/data/bug-13688.php'], []);
}

public function testBug6379(): void
{
$this->analyse([__DIR__ . '/data/bug-6379.php'], []);
}

public function testBug13075(): void
{
$this->reportPossiblyNonexistentConstantArrayOffset = true;
$this->analyse([__DIR__ . '/data/bug-13075.php'], [
[
'Offset \'c\' might not exist on array{c?: string}.',
40,
],
]);
}

public static function dataUnsealedArrayShapes(): iterable
{
foreach ([false, true] as $reportPossiblyNonexistentGeneralArrayOffset) {
Expand Down
41 changes: 41 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-13075.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types = 1);

namespace Bug13075;

// In all the examples the caller ensures there's at least one key present in
// the $data parameter and we want to give that information to phpstan.

/**
* @param array{a?: string, b?: string} $data
*/
function works(array $data): string {
if (isset($data['a'])) {
return $data['a'];
}
assert(isset($data['b']));
return $data['b'];
}

/**
* @param array{a?: string, b?: string} $data
*/
function fail1(array $data): string {
assert(isset($data['a']) || isset($data['b']));
return $data['a'] ?? $data['b'];
}

/**
* @param array{a?: string, b?: string} $data
*/
function fail2(array $data): string {
assert(array_key_exists('a', $data) || array_key_exists('b', $data));
return $data['a'] ?? $data['b'];
}

/**
* @param array{a?: string, b?: string, c?: string} $data
*/
function fail3(array $data): string {
assert((bool) array_intersect_key($data, array_flip(['a', 'b', 'c'])));
return $data['a'] ?? $data['b'] ?? $data['c'];
}
Loading
Loading