Skip to content

Do not parenthesize a benevolent union whose members describe the same - #6961

Merged
ondrejmirtes merged 1 commit into
2.3.xfrom
benevolent-union-single-description
Nov 5, 2026
Merged

ondrejmirtes merged 1 commit into
2.3.xfrom
benevolent-union-single-description

Conversation

@ondrejmirtes

@ondrejmirtes ondrejmirtes commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Since 5a3c562 ("Compute the numeric operators on NumberSet"), arithmetic results stay benevolent when an operand is benevolent. That change is intended. Because of it, (string) of a benevolent (float|int) is now the benevolent union (decimal-int-string|(non-empty-string&uppercase-string)).

At the type-only verbosity level both members describe as string, but BenevolentUnionType::describe() still wrapped the result in parentheses. That produced messages like:

Parameter #1 $num of function ceil expects float|int, (string) given.

This showed up in the Symfony integration baseline in phpstan/phpstan#15668.

The parentheses are now added in UnionType::describe()'s join step, for a benevolent union, only when more than one distinct name is joined. BenevolentUnionType::describe() is removed. Checking the joined count rather than looking for | in the string keeps members that collapse to a description like array<int|string> unwrapped as well. The turbo extension's UnionType/BenevolentUnionType get the same change.

Tests:

  • UnionTypeTest::testDescribe covers a real benevolent union ((int|string) at every level), members that only differ at the precise level (Foo), and members that collapse to Foo<int|string>. The last case fails with a string-based check.
  • CallToFunctionParametersRuleTest::testBenevolentUnionDescription expects string given for the cast and (float|int) given for the uncast value.
  • turbo-ext/tests/type-family.php has benevolentStrings and benevolentSameArrays subjects, so the PHP and native descriptions are compared.

The members of a benevolent union can describe the same at a verbosity
level, like decimal-int-string and non-empty-string&uppercase-string as
string at the type-only level. The union then described as "(string)", for
example in "Parameter #1 $num of function ceil expects float|int, (string)
given." for a string cast of a benevolent (float|int).

UnionType::describe() now parenthesizes a benevolent union only when more
than one distinct name is joined, so a description like array<int|string>
of members that describe the same stays unwrapped too.

Amp-Thread-ID: https://ampcode.com/threads/T-01a19770-5331-74b2-963f-e324ae0f7635
@ondrejmirtes
ondrejmirtes force-pushed the benevolent-union-single-description branch from c8ed529 to 0d22e77 Compare November 5, 2026 22:02
@ondrejmirtes ondrejmirtes changed the title Do not parenthesize a benevolent union that describes as a single type Do not parenthesize a benevolent union whose members describe the same Nov 5, 2026
@ondrejmirtes
ondrejmirtes merged commit 65b0243 into 2.3.x Nov 5, 2026
538 checks passed
@ondrejmirtes
ondrejmirtes deleted the benevolent-union-single-description branch November 5, 2026 22:03
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.

1 participant