|
14 | 14 | use PHPStan\Type\ErrorType; |
15 | 15 | use PHPStan\Type\Generic\GenericObjectType; |
16 | 16 | use PHPStan\Type\Generic\TemplateMixedType; |
| 17 | +use PHPStan\Type\Generic\TemplateType; |
| 18 | +use PHPStan\Type\Generic\TemplateTypeVariance; |
17 | 19 | use PHPStan\Type\IntersectionType; |
18 | 20 | use PHPStan\Type\MixedType; |
19 | 21 | use PHPStan\Type\NeverType; |
@@ -86,24 +88,81 @@ private function transformCommonType(Type $type): Type |
86 | 88 | return $type; |
87 | 89 | } |
88 | 90 |
|
89 | | - return TypeTraverser::map($type, function (Type $type, callable $traverse) { |
90 | | - if ($type instanceof TemplateMixedType) { |
91 | | - if ($this->checkExplicitMixed) { |
92 | | - return $type->toStrictMixedType(); |
93 | | - } |
| 91 | + return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { |
| 92 | + if ($type instanceof GenericObjectType && !$type instanceof TemplateType) { |
| 93 | + return $this->transformGenericObjectTypeArguments($type, $traverse); |
| 94 | + } |
| 95 | + |
| 96 | + return $this->transformMixedType($type) ?? $traverse($type); |
| 97 | + }); |
| 98 | + } |
| 99 | + |
| 100 | + private function transformMixedType(Type $type): ?Type |
| 101 | + { |
| 102 | + if ($type instanceof TemplateMixedType) { |
| 103 | + if ($this->checkExplicitMixed) { |
| 104 | + return $type->toStrictMixedType(); |
94 | 105 | } |
| 106 | + } |
| 107 | + if ( |
| 108 | + $type instanceof MixedType |
| 109 | + && ( |
| 110 | + ($type->isExplicitMixed() && $this->checkExplicitMixed) |
| 111 | + || (!$type->isExplicitMixed() && $this->checkImplicitMixed) |
| 112 | + ) |
| 113 | + ) { |
| 114 | + return new StrictMixedType(); |
| 115 | + } |
| 116 | + |
| 117 | + return null; |
| 118 | + } |
| 119 | + |
| 120 | + /** |
| 121 | + * Invariant type arguments are compared with equals(), so mapping mixed nested in them |
| 122 | + * only changes the outcome when one side is mapped and the other is not - like the |
| 123 | + * type arguments an accepted object type inherits through @extends, which a traversal |
| 124 | + * does not reach. A top-level mixed is still mapped, as variance checks treat it leniently. |
| 125 | + * |
| 126 | + * @param callable(Type): Type $traverse |
| 127 | + */ |
| 128 | + private function transformGenericObjectTypeArguments(GenericObjectType $type, callable $traverse): Type |
| 129 | + { |
| 130 | + $classReflection = $type->getClassReflection(); |
| 131 | + if ($classReflection === null) { |
| 132 | + return $traverse($type); |
| 133 | + } |
| 134 | + |
| 135 | + $templateTypes = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()); |
| 136 | + $variances = $type->getVariances(); |
| 137 | + $typesChanged = false; |
| 138 | + $types = []; |
| 139 | + foreach ($type->getTypes() as $i => $argument) { |
| 140 | + $templateType = $templateTypes[$i] ?? null; |
95 | 141 | if ( |
96 | | - $type instanceof MixedType |
97 | | - && ( |
98 | | - ($type->isExplicitMixed() && $this->checkExplicitMixed) |
99 | | - || (!$type->isExplicitMixed() && $this->checkImplicitMixed) |
100 | | - ) |
| 142 | + ($variances[$i] ?? TemplateTypeVariance::createInvariant())->invariant() |
| 143 | + && $templateType instanceof TemplateType |
| 144 | + && $templateType->getVariance()->invariant() |
101 | 145 | ) { |
102 | | - return new StrictMixedType(); |
| 146 | + $newArgument = $this->transformMixedType($argument) ?? $argument; |
| 147 | + } else { |
| 148 | + $newArgument = $traverse($argument); |
103 | 149 | } |
104 | 150 |
|
105 | | - return $traverse($type); |
106 | | - }); |
| 151 | + $types[] = $newArgument; |
| 152 | + if ($newArgument === $argument) { |
| 153 | + continue; |
| 154 | + } |
| 155 | + |
| 156 | + $typesChanged = true; |
| 157 | + } |
| 158 | + |
| 159 | + $subtractedType = $type->getSubtractedType(); |
| 160 | + $newSubtractedType = $subtractedType !== null ? $traverse($subtractedType) : null; |
| 161 | + if (!$typesChanged && $newSubtractedType === $subtractedType) { |
| 162 | + return $type; |
| 163 | + } |
| 164 | + |
| 165 | + return new GenericObjectType($type->getClassName(), $types, $newSubtractedType, variances: $variances); |
107 | 166 | } |
108 | 167 |
|
109 | 168 | /** |
|
0 commit comments