Skip to content

Keep list on unset() with nested dim-fetch #3964

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

Open
wants to merge 17 commits into
base: 2.1.x
Choose a base branch
from

Conversation

staabm
Copy link
Contributor

@staabm staabm commented Apr 25, 2025

@staabm staabm marked this pull request as ready for review April 25, 2025 15:35
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

@ondrejmirtes
Copy link
Member

Looks like this fixes a few more issues :) https://github.com/phpstan/phpstan-src/actions/runs/14669281251 Please add regression tests.

@herndlm
Copy link
Contributor

herndlm commented Apr 26, 2025

Nice 👍

@@ -799,6 +799,9 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
}

$result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
if ($this->isList()->yes() && $valueType->isArray()->yes()) {
$result = TypeCombinator::intersect($result, new AccessoryArrayListType());
Copy link
Member

Choose a reason for hiding this comment

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

Again, I don't get why does it matter what $valueType is.

Copy link
Contributor Author

@staabm staabm Apr 26, 2025

Choose a reason for hiding this comment

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

the idea was:

/** @param list<array<int>> $list */
function doFoo($list, int $i) {
  $list[$i][3] = 123;
}

when setting offset $i on $list to array(3 => 123), it does not turn $list into array.
(which is only true when $i is a existing offset)

Copy link
Member

Choose a reason for hiding this comment

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

But that's true for anything, not just arrays. I feel like we should call setExistingOffsetValueType more if we're sure the offset exists.

Copy link
Member

Choose a reason for hiding this comment

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

This just doesn't make sense. A simple code:

/** @param list<array<int>> $list */
function doFoo($list, int $i) {
	$list[$i][3] = 123;
	\PHPStan\dumpType($list);
}

We don't know whether $list[$i] exists. 2.1.x is correct to dump this as non-empty-array<int, non-empty-array<int>&hasOffsetValue(3, 123)>.

Your branch dumps this as non-empty-list<non-empty-array<int>&hasOffsetValue(3, 123)> which is wrong.

Copy link
Member

Choose a reason for hiding this comment

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

For example, with code like this:

foreach ($list as $i => $value) {
    $list[$i][3] = 123;
}

We should preserve $list being a list because $list[$i] exists and this information is already carried in Scope (hasExpressionType).

Right now setExistingOffsetValueType is only called for unset() but we could call it for when we're sure the offset exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am now using setExistingOffsetValueType more, but in the process somehow broke some tests.
need to investigate further, but as I kind of working thru the fog was thinking it would be worth sharing the progress to verify I am working in the right direction

assertType("non-empty-array<array<mixed>>&hasOffsetValue('Review', array<mixed~'review', mixed>)&hasOffsetValue('SurveyInvitation', array<mixed~'review', mixed>)", $review);
assertType("non-empty-array<array<mixed>>&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', array<mixed~'review', mixed>)", $review);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this expectation change is correct.. previously unset($review['SurveyInvitation']['review']); affected the offset Review (uppercase R) which was a bug IMO

return new self(
$this->keyType,
TypeCombinator::union($this->itemType, $valueType),
if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {
Copy link
Member

Choose a reason for hiding this comment

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

The idea is that we're overwriting an existing offset so $this->keyType should stay intact as is in the original version of this method.

Of course setExistingOffsetValueType was used when unsetting an offset so some details somewhere might be wrong, but what you're doing here now is basically the same thing setOffsetValueType is doing so it seems wrong.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants