Skip to content

Commit a118f45

Browse files
committed
Consolidate operand union traversal
Share template-aware traversal and test setup between the ordinary and benevolent operand wrappers so their behavior cannot drift. Add focused coverage for identity, candidate grouping, and union flattening across supported PHPStan versions.
1 parent d3f1638 commit a118f45

8 files changed

Lines changed: 390 additions & 303 deletions

src/ArrayMergeTypeOperandBenevolentUnionType.php

Lines changed: 6 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
2525
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
2626
use PHPStan\Type\BenevolentUnionType;
27-
use PHPStan\Type\Generic\TemplateType;
2827
use PHPStan\Type\Type;
2928
use PHPStan\Type\TypeCombinator;
3029
use PHPStan\Type\TypeUtils;
@@ -37,69 +36,18 @@
3736
*/
3837
final class ArrayMergeTypeOperandBenevolentUnionType extends BenevolentUnionType
3938
{
39+
use ArrayMergeTypeOperandUnionTraversalTrait;
40+
4041
public const PHPDOC_TYPE_NAME = '__array_merge_benevolent';
4142

42-
public function traverse(callable $cb): Type
43+
/** @return list<Type> */
44+
protected function getOperandTypes(): array
4345
{
44-
$types = [];
45-
$replace = false;
46-
47-
foreach ($this->getTypes() as $type) {
48-
$newType = $cb($type);
49-
$types[] = $newType;
50-
if ($newType !== $type) {
51-
$replace = true;
52-
}
53-
}
54-
55-
if (!$replace) {
56-
return $this;
57-
}
58-
59-
return $this->recombineTypes($types);
60-
}
61-
62-
public function traverseSimultaneously(Type $right, callable $cb): Type
63-
{
64-
// PHPStan uses this traversal for too-wide diagnostics. Skipping an unresolved
65-
// right side is conservative; traversing it can discard later specialization.
66-
if (TypeUtils::containsTemplateType($right)) {
67-
return $this;
68-
}
69-
70-
$rightTypes = TypeUtils::flattenTypes($right);
71-
$types = [];
72-
$replace = false;
73-
74-
foreach ($this->getTypes() as $type) {
75-
$candidates = [];
76-
77-
foreach ($rightTypes as $i => $rightType) {
78-
if (!$type->isSuperTypeOf($rightType)->yes()) {
79-
continue;
80-
}
81-
82-
$candidates[] = $rightType;
83-
unset($rightTypes[$i]);
84-
}
85-
86-
if ([] === $candidates) {
87-
$types[] = $type;
88-
continue;
89-
}
90-
91-
$newType = $cb($type, TypeCombinator::union(...$candidates));
92-
$types[] = $newType;
93-
if ($newType !== $type) {
94-
$replace = true;
95-
}
96-
}
97-
98-
return $replace ? $this->recombineTypes($types) : $this;
46+
return $this->getTypes();
9947
}
10048

10149
/** @param list<Type> $types */
102-
private function recombineTypes(array $types): Type
50+
protected function recombineTypes(array $types): Type
10351
{
10452
if ([] === $types) {
10553
return $this;
@@ -136,25 +84,4 @@ public function toPhpDocNode(): TypeNode
13684
))],
13785
);
13886
}
139-
140-
/**
141-
* @param list<Type> $types
142-
* @return list<Type>
143-
*/
144-
private static function flattenOrdinaryUnions(array $types): array
145-
{
146-
$flattenedTypes = [];
147-
148-
foreach ($types as $type) {
149-
if ($type instanceof UnionType && !($type instanceof TemplateType)) {
150-
foreach ($type->getTypes() as $innerType) {
151-
$flattenedTypes[] = $innerType;
152-
}
153-
} else {
154-
$flattenedTypes[] = $type;
155-
}
156-
}
157-
158-
return $flattenedTypes;
159-
}
16087
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
/**
3+
* Copyright (c) anno Domini nostri Jesu Christi MMXXVI John Boehr & contributors
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
declare(strict_types=1);
19+
20+
namespace jbboehr\PHPStan\ArrayMerge;
21+
22+
use PHPStan\Type\Generic\TemplateType;
23+
use PHPStan\Type\Type;
24+
use PHPStan\Type\TypeCombinator;
25+
use PHPStan\Type\TypeUtils;
26+
use PHPStan\Type\UnionType;
27+
28+
/**
29+
* @internal
30+
*/
31+
trait ArrayMergeTypeOperandUnionTraversalTrait
32+
{
33+
/** @return list<Type> */
34+
abstract protected function getOperandTypes(): array;
35+
36+
/** @param list<Type> $types */
37+
abstract protected function recombineTypes(array $types): Type;
38+
39+
public function traverse(callable $cb): Type
40+
{
41+
$types = [];
42+
$replace = false;
43+
44+
foreach ($this->getOperandTypes() as $type) {
45+
$newType = $cb($type);
46+
$types[] = $newType;
47+
if ($newType !== $type) {
48+
$replace = true;
49+
}
50+
}
51+
52+
return $replace ? $this->recombineTypes($types) : $this;
53+
}
54+
55+
public function traverseSimultaneously(Type $right, callable $cb): Type
56+
{
57+
// PHPStan uses this traversal for too-wide diagnostics. Skipping an unresolved
58+
// right side is conservative; traversing it can discard later specialization.
59+
if (TypeUtils::containsTemplateType($right)) {
60+
return $this;
61+
}
62+
63+
$rightTypes = TypeUtils::flattenTypes($right);
64+
$types = [];
65+
$replace = false;
66+
67+
foreach ($this->getOperandTypes() as $type) {
68+
$candidates = [];
69+
70+
foreach ($rightTypes as $i => $rightType) {
71+
if (!$type->isSuperTypeOf($rightType)->yes()) {
72+
continue;
73+
}
74+
75+
$candidates[] = $rightType;
76+
unset($rightTypes[$i]);
77+
}
78+
79+
if ([] === $candidates) {
80+
$types[] = $type;
81+
continue;
82+
}
83+
84+
$newType = $cb($type, TypeCombinator::union(...$candidates));
85+
$types[] = $newType;
86+
if ($newType !== $type) {
87+
$replace = true;
88+
}
89+
}
90+
91+
return $replace ? $this->recombineTypes($types) : $this;
92+
}
93+
94+
/**
95+
* @param list<Type> $types
96+
* @return list<Type>
97+
*/
98+
private static function flattenOrdinaryUnions(array $types): array
99+
{
100+
$flattenedTypes = [];
101+
102+
foreach ($types as $type) {
103+
if ($type instanceof UnionType && !($type instanceof TemplateType)) {
104+
foreach ($type->getTypes() as $innerType) {
105+
$flattenedTypes[] = $innerType;
106+
}
107+
} else {
108+
$flattenedTypes[] = $type;
109+
}
110+
}
111+
112+
return $flattenedTypes;
113+
}
114+
}

src/ArrayMergeTypeOperandUnionType.php

Lines changed: 8 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
*/
4040
final class ArrayMergeTypeOperandUnionType extends UnionType
4141
{
42+
use ArrayMergeTypeOperandUnionTraversalTrait;
43+
4244
/** @var non-empty-list<Type> */
4345
private array $sourceTypes;
4446

@@ -77,70 +79,19 @@ public function equals(Type $type): bool
7779
return [] === $otherSourceTypes;
7880
}
7981

80-
public function traverse(callable $cb): Type
82+
/** @return list<Type> */
83+
protected function getOperandTypes(): array
8184
{
82-
$types = [];
83-
$replace = false;
84-
85-
foreach ($this->sourceTypes as $type) {
86-
$newType = $cb($type);
87-
$types[] = $newType;
88-
if ($newType !== $type) {
89-
$replace = true;
90-
}
91-
}
92-
93-
if (!$replace) {
94-
return $this;
95-
}
96-
97-
return $this->recombineTypes($types);
85+
return $this->sourceTypes;
9886
}
9987

100-
public function traverseSimultaneously(Type $right, callable $cb): Type
88+
/** @param list<Type> $types */
89+
protected function recombineTypes(array $types): Type
10190
{
102-
// PHPStan uses this traversal for too-wide diagnostics. Skipping an unresolved
103-
// right side is conservative; traversing it can discard later specialization.
104-
if (TypeUtils::containsTemplateType($right)) {
91+
if ([] === $types) {
10592
return $this;
10693
}
10794

108-
$rightTypes = TypeUtils::flattenTypes($right);
109-
$types = [];
110-
$replace = false;
111-
112-
foreach ($this->sourceTypes as $type) {
113-
$candidates = [];
114-
115-
foreach ($rightTypes as $i => $rightType) {
116-
if (!$type->isSuperTypeOf($rightType)->yes()) {
117-
continue;
118-
}
119-
120-
$candidates[] = $rightType;
121-
unset($rightTypes[$i]);
122-
}
123-
124-
if ([] === $candidates) {
125-
$types[] = $type;
126-
continue;
127-
}
128-
129-
$newType = $cb($type, TypeCombinator::union(...$candidates));
130-
$types[] = $newType;
131-
if ($newType !== $type) {
132-
$replace = true;
133-
}
134-
}
135-
136-
return $replace ? $this->recombineTypes($types) : $this;
137-
}
138-
139-
/**
140-
* @param non-empty-list<Type> $types
141-
*/
142-
private function recombineTypes(array $types): Type
143-
{
14495
$types = array_map(
14596
static fn(Type $type): Type => ArrayMergeType::normalizeUninhabitedArrays($type),
14697
$types,
@@ -214,25 +165,4 @@ private static function getCoveringBenevolentUnion(array $types): ?BenevolentUni
214165

215166
return null;
216167
}
217-
218-
/**
219-
* @param list<Type> $types
220-
* @return list<Type>
221-
*/
222-
private static function flattenOrdinaryUnions(array $types): array
223-
{
224-
$flattenedTypes = [];
225-
226-
foreach ($types as $type) {
227-
if ($type instanceof UnionType && !($type instanceof TemplateType)) {
228-
foreach ($type->getTypes() as $innerType) {
229-
$flattenedTypes[] = $innerType;
230-
}
231-
} else {
232-
$flattenedTypes[] = $type;
233-
}
234-
}
235-
236-
return $flattenedTypes;
237-
}
238168
}

0 commit comments

Comments
 (0)