Skip to content

Array literal with a by-reference item becomes *NEVER* after the referenced variable with a constant type is reassigned #15246

Closed

Description

@ondrejmirtes

Bug report

When an array literal holds a reference to a variable ([1, 2, &$x]) and that variable has a constant type, assigning a different value to the variable makes the whole array *NEVER*:

/** @return array<mixed> */
function constantValue(): array
{
	$x = 0;
	$a = [1, 2, &$x];
	$x = 5;
	\PHPStan\dumpType($a); // *NEVER*, expected array{1, 2, 5}
	return $a;
}

/** @return array<mixed> */
function nonConstantValue(int $x): array
{
	$a = [1, 2, &$x];
	$x = 5;
	\PHPStan\dumpType($a); // array{1, 2, 5} - correct
	return $a;
}

Actual output (level 8):

 9: Dumped type: *NEVER*
18: Dumped type: array{1, 2, 5}

The same happens for any variable whose type is a finite set of constants: with @param 0|1 $x, $a = [&$x]; $x = 5; also gives *NEVER*. Everything read from $a afterwards is *NEVER* too, so rules have nothing to check it against.

Mechanism (line numbers from 2.3.x):

  • AssignHandler::processArrayByRefItems() links $x and $a[2] with two IntertwinedVariableByReferenceWithExpr entries (AssignHandler.php:2566, L2573).
  • On $x = 5, MutatingScope::assignVariable() propagates the new value to the linked slot with $scope->assignExpression($a[2], 5) (MutatingScope.php:3359).
  • For an ArrayDimFetch with a constant dim, assignExpression() ends up in specifyExpressionType(), which also narrows the parent array by intersecting it with HasOffsetValueType(2, 5) (MutatingScope.php:3725). That's the right behaviour for narrowing (if ($a[2] === 5)), but here it is a write. array{1, 2, 0} & hasOffsetValue(2, 5) is never.
  • With int $x the intersection happens to be array{1, 2, 5}, so the bug is invisible. With a constant old value, the stale offset type contradicts the new value.

Related, but a different construct: #14503 and #13967 are about $ref = &$array[...], not about by-reference items in an array literal.

Code snippet that reproduces the problem

https://phpstan.org/r/5ae0b2f9-92ef-4aa4-9210-3efa7f53a4cc

Expected output

 9: Dumped type: array{1, 2, 5}
18: Dumped type: array{1, 2, 5}

Suggested fix: when a by-reference link propagates a new value to an ArrayDimFetch slot, write the offset (setExistingOffsetValueType()/setOffsetValueType() on the parent chain, the way the foreach by-reference link does through SetExistingOffsetValueTypeExpr) instead of going through specifyExpressionType(), which intersects with the old array type.

Did PHPStan help you today? Did it make you happy in any way?

No response

Activity

  1. added this to the Easy fixes milestone on Sep 17, 2026
  2. phpstan-bot commented on Sep 17, 2026

    @phpstan-bot
    Contributor

    Closed via merging phpstan/phpstan-src#6456

  3. github-actions commented on Oct 19, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

  4. locked as resolved and limited conversation to collaborators on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions