Skip to content

Keep the list flag when a list shape is intersected with an array type - #6717

Merged
staabm merged 4 commits into
phpstan:2.3.xfrom
janedbal:fix-byref-closure-list
Oct 14, 2026
Merged

staabm merged 4 commits into
phpstan:2.3.xfrom
janedbal:fix-byref-closure-list

Conversation

@janedbal

@janedbal janedbal commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Closes phpstan/phpstan#15391

in_array() narrowing drops the list flag from a list{...} shape that has two or more optional keys. This is a regression in 2.1.41 from 951d9e4 (#5067).

/**
 * @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)) {
		return $l; // should return list<string> but returns array{0: 'x', 1?: 'a', 2?: 'a'}
	}

	return [];
}

TypeCombinator::intersect() rebuilds a ConstantArrayType through ConstantArrayTypeBuilder when the other side is an ArrayType. The builder marks each optional key as a possible non-list key, so the rebuilt shape is only "maybe a list".

  • A subset of a list is a list. TypeCombinator::intersect() now intersects the rebuilt shape with AccessoryArrayListType when the source shape is a list.
  • turbo-ext/src/TypeCombinator.cpp gets the same change.
  • tests/PHPStan/Analyser/nsrt/bug-15391.php is the regression test. It fails without the change in TypeCombinator.php and passes with it.

— Claude

@janedbal
janedbal marked this pull request as ready for review October 9, 2026 11:10
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

@staabm
staabm requested a balanced review from Copilot October 14, 2026 05:36
@staabm

staabm commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

//cc @SanderMuller

@staabm staabm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

needs rebase. otherwise LGTM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The fix is focused, mirrored consistently, and backed by suitable regression coverage.

0 open findings

What changed in this PR

Fixes #15391 by preserving list semantics when intersecting list-shaped constant arrays with array types.

Changes:

  • Retains the list flag during intersection rebuilding.
  • Mirrors behavior in the Turbo extension.
  • Adds regression coverage and updates Turbo compatibility.
File Description
src/​Type/​TypeCombinator.php Preserves list semantics after rebuilding shapes.
turbo-ext/​src/​TypeCombinator.cpp Mirrors the PHP implementation.
tests/​PHPStan/​Analyser/​nsrt/​bug-15391.php Covers narrowing, direct intersection, and loops.
src/​Turbo/​TurboExtensionEnabler.php Updates the expected Turbo version.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

TypeCombinator::intersect() rebuilds a ConstantArrayType through the
builder when the other side is an ArrayType. The builder marks every
optional key as a possible non-list key, so a list shape with two or more
optional keys became a plain array shape. This happens for example when
in_array() narrows the haystack, and it made a returned list{...} fail
a list<T> return type.

A subset of a list is a list, so the rebuilt shape now keeps the list flag
when the source shape is a list.
…e in turbo-ext

Port b3d727229 to the native TypeCombinator. When intersectConstantArrayWith()
rebuilds a list shape through the builder, it now intersects the result with
AccessoryArrayListType, the same as the PHP twin.
@janedbal
janedbal force-pushed the fix-byref-closure-list branch from ea802c0 to 0c9e5be Compare October 14, 2026 09:52
@janedbal
janedbal requested a review from staabm October 14, 2026 11:26
@staabm

staabm commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

thank you!

@staabm
staabm merged commit 579a7f1 into phpstan:2.3.x Oct 14, 2026
973 of 975 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

5 participants