Skip to content

in_array() narrowing drops the list flag from a list shape with optional keys (regression in 2.1.41) #15391

Closed
phpstan/phpstan-src
#6717

Description

@janedbal

Bug report

When in_array() narrows a list{...} shape that has two or more optional keys, the narrowed type loses the list flag and becomes array{...}. A narrowed list is still a list, and the result should be a subtype of the original list{...} type. In practice this gives false return.type errors for list<T> return types.

Versions

version result
2.0.0, 2.1.0, 2.1.20, 2.1.38, 2.1.40 list{0: 'x', 1?: 'a', 2?: 'a'} (correct)
2.1.41, 2.1.42, 2.1.47, 2.1.56, 2.2.0, 2.2.14 – 2.2.17, 2.3.0, 2.3.x-dev@b830e66 array{0: 'x', 1?: 'a', 2?: 'a'}

Broken since 2.1.41; 2.1.40 is fine. Bleeding edge is not needed.

Bisect

First bad commit: phpstan/phpstan-src@951d9e4 "Improve intersection of ConstantArray and AccessoryIsList" (phpstan/phpstan-src#5067).

Since that commit, a list{...} shape is a ConstantArrayType with the list flag set (makeList()), and no longer an intersection with AccessoryArrayListType. TypeCombinator::intersect() handles a constant array and an ArrayType by rebuilding the shape with ConstantArrayTypeBuilder (TypeCombinator.php#L2263-L2312). With optional keys, the builder can only mark the result as "maybe a list" (ConstantArrayTypeBuilder.php#L246-L254), so the flag of the original list is lost.

With bleeding edge in 2.3.0 the same loss also shows when a closure appends to a use (&$list) array, because phpstan/phpstan-src#6610 now applies the closure body where the closure is called. That is how we found it.

Code snippet that reproduces the problem

Playground: https://phpstan.org/r/7bdb1bbc-8905-4127-9d32-0a3ee90e4548

<?php declare(strict_types = 1);

/**
 * @param list{0: 'x', 1?: 'a'|'b', 2?: 'a'|'b'} $l
 * @return list<string>
 */
function f(array $l): array
{
	if (!in_array('b', $l, true)) {
		\PHPStan\dumpType($l);
		return $l;
	}

	return [];
}

Expected output

Actual (2.1.41 and later):

10: Dumped type: array{0: 'x', 1?: 'a', 2?: 'a'}
11: Function f() should return list<string> but returns array{0: 'x', 1?: 'a', 2?: 'a'}.

Expected (2.1.40):

10: Dumped type: list{0: 'x', 1?: 'a', 2?: 'a'}

and no return.type error.

Possibly related: #12725.

A fix with a regression test is coming in a PR.

Claude

Activity

  1. added a commit that references this issue on Oct 14, 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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions