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
29 changes: 24 additions & 5 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1499,18 +1499,37 @@
$keyType = TypeCombinator::union(...$keyTypes);
}

$leftIterableValueType = $leftType->getIterableValueType();
$arrayType = new ArrayType(
$keyType,
TypeCombinator::union($leftType->getIterableValueType(), $rightType->getIterableValueType()),
TypeCombinator::union($leftIterableValueType, $rightType->getIterableValueType()),
);

$accessories = [];
foreach ($leftType->getConstantArrays() as $type) {
foreach ($type->getKeyTypes() as $i => $offsetType) {
if ($type->isOptionalKey($i)) {
if ($leftCount > 0) {
// Use the first constant array as a reference to list potential offsets.
// We only need to check the first array because we're looking for offsets that exist in ALL arrays.
$constantArray = $leftConstantArrays[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why this only works on index 0.

in array_merge we iterate over all left arrays

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something you could try for array_merge too I think.

Since we're looking only for certainty of hasOffsetValueType YES the key has to be in all the constant arrays, so checking only one constant array is enough.

  • only keys in the first constant array can have YES certainty
  • key which are not in the first contant array will at most have the MAYBE certainty

foreach ($constantArray->getKeyTypes() as $offsetType) {
if (!$leftType->hasOffsetValueType($offsetType)->yes()) {
continue;
}
$valueType = $type->getValueTypes()[$i];

$valueType = $leftType->getOffsetValueType($offsetType);
$accessories[] = new HasOffsetValueType($offsetType, $valueType);
}
}

if ($rightCount > 0) {
// Use the first constant array as a reference to list potential offsets.
// We only need to check the first array because we're looking for offsets that exist in ALL arrays.
$constantArray = $rightConstantArrays[0];
foreach ($constantArray->getKeyTypes() as $offsetType) {
if (!$rightType->hasOffsetValueType($offsetType)->yes()) {

Check warning on line 1528 in src/Reflection/InitializerExprTypeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // We only need to check the first array because we're looking for offsets that exist in ALL arrays. $constantArray = $rightConstantArrays[0]; foreach ($constantArray->getKeyTypes() as $offsetType) { - if (!$rightType->hasOffsetValueType($offsetType)->yes()) { + if ($rightType->hasOffsetValueType($offsetType)->no()) { continue; }

Check warning on line 1528 in src/Reflection/InitializerExprTypeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // We only need to check the first array because we're looking for offsets that exist in ALL arrays. $constantArray = $rightConstantArrays[0]; foreach ($constantArray->getKeyTypes() as $offsetType) { - if (!$rightType->hasOffsetValueType($offsetType)->yes()) { + if ($rightType->hasOffsetValueType($offsetType)->no()) { continue; }
continue;
}

$valueType = TypeCombinator::union($leftIterableValueType, $rightType->getOffsetValueType($offsetType));
$accessories[] = new HasOffsetValueType($offsetType, $valueType);
}
}
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2768,7 +2768,7 @@ public static function dataBinaryOperations(): array
'[1, 2, 3] + [4, 5, 6]',
],
[
'non-empty-array<int>',
'non-empty-array<int>&hasOffsetValue(0, int)&hasOffsetValue(1, int)&hasOffsetValue(2, int)',
'$arrayOfUnknownIntegers + [1, 2, 3]',
],
[
Expand Down
50 changes: 50 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13561.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php // lint >= 8.0

namespace Bug13552;

use function PHPStan\Testing\assertType;

interface MyInterface {
public function doThing(): bool;

/**
* @return array<string, string>
*/
public function getArray(): array;
}

function test_addition(MyInterface $i): void {
$x = $i->doThing() ? ['thing' => 'do'] : [];
assertType("array{}|array{thing: 'do'}", $x);

$x += $i->getArray();
assertType('array<string, string>', $x);

$x = $x ?: ['test' => 'string'];
}

function more_test(MyInterface $i): void {
$x = $i->doThing() ? ['thing' => 'do', 'always_here' => true] : ['always_here' => 42];
assertType("array{always_here: 42}|array{thing: 'do', always_here: true}", $x);

$a = $i->getArray() + $x;
assertType("non-empty-array<string, 42|string|true>&hasOffsetValue('always_here', 42|string|true)", $a);
assertType('true', isset($a['always_here']));

$b = $x + $i->getArray();
assertType("non-empty-array<string, 42|string|true>&hasOffsetValue('always_here', 42|true)", $b);
assertType('true', isset($b['always_here']));
}

/**
* @param array{thing?: 'do', always_here: 42|true} $x
*/
function more_test_2(MyInterface $i, array $x): void {
$a = $i->getArray() + $x;
assertType("non-empty-array<string, 42|string|true>&hasOffsetValue('always_here', 42|string|true)", $a);
assertType('true', isset($a['always_here']));

$b = $x + $i->getArray();
assertType("non-empty-array<string, 42|string|true>&hasOffsetValue('always_here', 42|true)", $b);
assertType('true', isset($b['always_here']));
}
Loading