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

Copy link
Contributor

Choose a reason for hiding this comment

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

do you think it would make sense to adjust array_merge() and array_replace() with this PR, so we have it in sync over all implementations?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Honestly I'm not sure about it.

Those implementations seems more complicated, especially because there is an iteration

foreach ($argTypes as $argType) {

Also the logic seems to be different since there is some looking at code like

TrinaryLogic::createFromBoolean($argType->hasOffsetValueType($keyType)->yes());

or

if ($hasOffsetValue->yes()) {
					// the last string-keyed offset will overwrite previous values
					$hasOffsetType = new HasOffsetValueType(
						$keyType,
						$offsetType,
					);
				} elseif ($hasOffsetValue->maybe()) {
					$hasOffsetType = new HasOffsetType(
						$keyType,
					);
				} else {
					continue;
				}

which I don't have here.

So I would prefer avoid to delay with PR (with the risk of introducing a regression) and fixing the existing bug, and we could try a dedicated PR with the refacto for array_merge/replace ?

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