Do not flatten template benevolent unions in TypeCombinator::union()#5975
Do not flatten template benevolent unions in TypeCombinator::union()#5975gnutix wants to merge 1 commit into
Conversation
9854e0f to
4b4d4d5
Compare
|
Hi @gnutix please target 2.2.x for all your PR |
4b4d4d5 to
6ccde47
Compare
|
@VincentLanglet done ! |
6ccde47 to
6c1cfce
Compare
6c1cfce to
a5bd8b4
Compare
|
please rebase and have a look at the integration tests. old PHPUnit test is sometimes flaky and false positives. issue-bot also need to be double checked, how this PR affects other reported phpstan issues |
a5bd8b4 to
2974f04
Compare
The "transform A | (B | C) to A | B | C" loop spliced the members of any BenevolentUnionType into the outer union, including TemplateBenevolentUnionType — discarding the template before the TemplateType guard (which already protects TemplateUnionType) could apply. Resolving a PHPDoc union like `K|null` where `K of array-key` therefore degraded the template to its bound at parse time. The reconstruction path only restored the template when all resulting union members came from the benevolent union, which fails as soon as another member (e.g. `null`) is present. Exclude TemplateType from the benevolent flattening branch so TemplateBenevolentUnionType falls through to the same guard as TemplateUnionType, mirroring how intersect() already handles templates before benevolent flattening (57e3cbf). The template-rewrapping machinery in the reconstruction path becomes unreachable and is removed; plain benevolent unions behave exactly as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2974f04 to
4a860bb
Compare
Rebased all six onto latest 2.2.x; the old-PHPUnit flakiness and the e2e failure are both accounted for (agreed they're unrelated to these changes), and the mutation/line-shift issues that were real have been fixed. |
|
What about the 2 integration tests? |
|
The two integration tests are caused by a widening of the array-key type now being int|string where it used to collapse to string/TKey. I must admit I don't know if it's the right thing here, or if this PR's changes are too "aggressive". We could refine TypeCombinator::union() so it stops flattening but still collapses int|TKey correctly, or we could adapt Larastan and doctrine/collections repo to this new change. Your call, I can't tell. (I don't understand most of these PR, I'm just guiding Claude) |
I'd like to flag a concern with the approach here A PR carries an implicit contract: the submitter has understood the change and is asking maintainers to verify it. When the submitter can't vouch for the code, that contract breaks down in a few ways:
To be clear: using Claude or any AI tool is completely fine — many great contributors do. The line is being able to say "I understand and stand behind every line of this." If you can get to that point with this PR, we would genuinely welcome the contribution. But if the purpose is just to generate PR with Claude, we already have a bot for this. |
|
I understand your stance, and it's fine if you decide to save your time and close this PR. On my end, I simply cannot allocate the time that would be required for me to "level up" my PHPStan knowledge enough to be able to be accountable for the changes in this PR. I'm relying on the quality and width of the project's test suite (which is quite impressive at this point). I've opened these as issues months/years ago, and no one have had the opportunity to get onto them (and that's fine and expected). Today with AI, especially powerful models like Fable (which is available "at a reasonable price" only for a few days still), I thought I'd get 90% of the job done while still having a good enough level of confidence that it's not "utter shit". But that doesn't mean I can get it past the finish line alone, so I'm requesting your help for the missing 10%. And maybe you'll have time for it, and maybe not, and that's fine and expected too. I'm much aware that if any of you guys had the time to work on that issue, you'd have done a better job (AI assisted or not) than me. But tokens cost (especially Fable's), your time cost, so I don't think it's necessarily a bad thing that I took care of some of it for you. PS: "It takes minutes to generate a PR like this" : it took me a good 4-6 hours and ~2.7M tokens (≈$232) to do those 6 PRs, make sure I drilled the AI enough so it doesn't take shortcuts, make all the CI pass, and testing the fixes against projects I have access to. |
Closes phpstan/phpstan#7279
Root cause
@template K of array-keyproduces aTemplateBenevolentUnionType(array-key= benevolentint|string). When resolving a PHPDoc union such asK|null(e.g. in a conditional return type($array is non-empty-array ? K|null : null)),TypeCombinator::union()'s "transform A | (B | C) to A | B | C" loop hit theinstanceof BenevolentUnionTypeflattening branch before theinstanceof TemplateType → continueguard that already protectsTemplateUnionType(which is why@template K of int|stringworked fine). The template's members were spliced into the outer union, so the template was destroyed at PHPDoc-resolution time — this was never an inference failure; by the time inference ran,Kno longer existed and only its boundint|stringremained.The reconstruction path at the end of
union()only re-wrapped the template when all resulting union members came from the benevolent union, which fails as soon as any other member (herenull) is present.Fix
Exclude
TemplateTypefrom the benevolent flattening branch, lettingTemplateBenevolentUnionTypefall through to the sameTemplateTypeguard asTemplateUnionType. This mirrors 57e3cbf, which fixed the identical problem on theintersect()side (templates handled before benevolent flattening and re-wrapped viaTemplateTypeFactory);union()never got the same treatment.With the guard in place, the
$benevolentUnionObjectre-wrapping machinery in the reconstruction path became unreachable (confirmed by PHPStan self-analysis flagginginstanceof between null and TemplateBenevolentUnionType will always evaluate to false) and is removed. Plain (non-template) benevolent unions still enter the flattening branch and behave exactly as before.Semantic notes
union(K of array-key, int)now yieldsint|Kinstead of the widenedK— consistent with how non-benevolent template unions (K of int|string) already behave.stubs/ArrayObject.stubline 23 (@param TKey|null $offset) now resolves to a real template union instead of a flattened bound, which can makeArrayObject/ArrayIteratoroffset parameter checks slightly stricter.No existing test expectations needed updating; full
NodeScopeResolverTest,AnalyserIntegrationTest,TypeCombinatorTest,UnionTypeTest,BenevolentUnionTypeTest, generics tests, and PHPStan self-analysis all pass.🤖 Generated with Claude Code, model Fable 5.