TypeIsCovariantTest::testCovariance()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 3
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace RoaveTest\BackwardCompatibility\DetectChanges\Variance;
6
7
use PHPUnit\Framework\TestCase;
8
use Roave\BackwardCompatibility\DetectChanges\Variance\TypeIsCovariant;
9
use Roave\BetterReflection\BetterReflection;
10
use Roave\BetterReflection\Reflection\ReflectionType;
11
use Roave\BetterReflection\Reflector\ClassReflector;
12
use Roave\BetterReflection\SourceLocator\Type\StringSourceLocator;
13
use function array_map;
14
use function array_merge;
15
16
final class TypeIsCovariantTest extends TestCase
17
{
18
    /**
19
     * @dataProvider checkedTypes
20
     */
21
    public function testCovariance(
22
        ?ReflectionType $type,
23
        ?ReflectionType $newType,
24
        bool $expectedToBeContravariant
25
    ) : void {
26
        self::assertSame(
27
            $expectedToBeContravariant,
28
            (new TypeIsCovariant())
29
                ->__invoke($type, $newType)
30
        );
31
    }
32
33
    /**
34
     * @return array<string, array<int, bool|ReflectionType|null>>
35
     *
36
     * @psalm-return array<string, array{0: ReflectionType|null, 1: ReflectionType|null, 2: bool}>
37
     */
38
    public function checkedTypes() : array
39
    {
40
        $reflector = new ClassReflector(new StringSourceLocator(
41
            <<<'PHP'
42
<?php
43
44
interface Traversable {}
45
interface Iterator extends Traversable {}
46
interface AnInterface {}
47
interface AnotherInterface {}
48
class AnotherClass implements AnInterface {}
49
class AnotherClassWithMultipleInterfaces implements AnInterface, AnotherInterface {}
50
class AClass {}
51
class BClass extends AClass {}
52
class CClass extends BClass {}
53
PHP
54
            ,
55
            (new BetterReflection())->astLocator()
56
        ));
57
58
        return [
59
            'no type to void type is covariant'                    => [
60
                null,
61
                ReflectionType::createFromTypeAndReflector('void', false, $reflector),
62
                true,
63
            ],
64
            'void type to no type is not covariant'                => [
65
                ReflectionType::createFromTypeAndReflector('void', false, $reflector),
66
                null,
67
                false,
68
            ],
69
            'void type to scalar type is not covariant'            => [
70
                ReflectionType::createFromTypeAndReflector('void', false, $reflector),
71
                ReflectionType::createFromTypeAndReflector('string', false, $reflector),
72
                false,
73
            ],
74
            'void type to class type is covariant'                 => [
75
                ReflectionType::createFromTypeAndReflector('void', false, $reflector),
76
                ReflectionType::createFromTypeAndReflector('AClass', false, $reflector),
77
                false,
78
            ],
79
            'scalar type to no type is not covariant'              => [
80
                ReflectionType::createFromTypeAndReflector('string', false, $reflector),
81
                null,
82
                false,
83
            ],
84
            'no type to scalar type is covariant'                  => [
85
                null,
86
                ReflectionType::createFromTypeAndReflector('string', false, $reflector),
87
                true,
88
            ],
89
            'class type to no type is not covariant'               => [
90
                ReflectionType::createFromTypeAndReflector('AClass', false, $reflector),
91
                null,
92
                false,
93
            ],
94
            'no type to class type is not contravariant'           => [
95
                ReflectionType::createFromTypeAndReflector('AClass', false, $reflector),
96
                null,
97
                false,
98
            ],
99
            'iterable to array is covariant' => [
100
                ReflectionType::createFromTypeAndReflector('iterable', false, $reflector),
101
                ReflectionType::createFromTypeAndReflector('array', false, $reflector),
102
                true,
103
            ],
104
            'iterable to scalar is not covariant' => [
105
                ReflectionType::createFromTypeAndReflector('iterable', false, $reflector),
106
                ReflectionType::createFromTypeAndReflector('int', false, $reflector),
107
                false,
108
            ],
109
            'scalar to iterable is not covariant' => [
110
                ReflectionType::createFromTypeAndReflector('iterable', false, $reflector),
111
                ReflectionType::createFromTypeAndReflector('int', false, $reflector),
112
                false,
113
            ],
114
            'array to iterable is not covariant'         => [
115
                ReflectionType::createFromTypeAndReflector('array', false, $reflector),
116
                ReflectionType::createFromTypeAndReflector('iterable', false, $reflector),
117
                false,
118
            ],
119
            'iterable to non-iterable class type is not covariant' => [
120
                ReflectionType::createFromTypeAndReflector('iterable', false, $reflector),
121
                ReflectionType::createFromTypeAndReflector('AnotherClassWithMultipleInterfaces', false, $reflector),
122
                false,
123
            ],
124
            'iterable to iterable class type is covariant'         => [
125
                ReflectionType::createFromTypeAndReflector('iterable', false, $reflector),
126
                ReflectionType::createFromTypeAndReflector('Iterator', false, $reflector),
127
                true,
128
            ],
129
            'non-iterable class to iterable type is not covariant' => [
130
                ReflectionType::createFromTypeAndReflector('iterable', false, $reflector),
131
                ReflectionType::createFromTypeAndReflector('AnotherClassWithMultipleInterfaces', false, $reflector),
132
                false,
133
            ],
134
            'iterable class type to iterable is not covariant'     => [
135
                ReflectionType::createFromTypeAndReflector('Iterator', false, $reflector),
136
                ReflectionType::createFromTypeAndReflector('iterable', false, $reflector),
137
                false,
138
            ],
139
            'object to class type is covariant'                    => [
140
                ReflectionType::createFromTypeAndReflector('object', false, $reflector),
141
                ReflectionType::createFromTypeAndReflector('AClass', false, $reflector),
142
                true,
143
            ],
144
            'class type to object is not covariant'                => [
145
                ReflectionType::createFromTypeAndReflector('AClass', false, $reflector),
146
                ReflectionType::createFromTypeAndReflector('object', false, $reflector),
147
                false,
148
            ],
149
150
            'class type to scalar type is not covariant'                           => [
151
                ReflectionType::createFromTypeAndReflector('AClass', false, $reflector),
152
                ReflectionType::createFromTypeAndReflector('string', false, $reflector),
153
                false,
154
            ],
155
            'scalar type to class type is not covariant'                           => [
156
                ReflectionType::createFromTypeAndReflector('string', false, $reflector),
157
                ReflectionType::createFromTypeAndReflector('AClass', false, $reflector),
158
                false,
159
            ],
160
            'scalar type (string) to different scalar type (int) is not covariant' => [
161
                ReflectionType::createFromTypeAndReflector('string', false, $reflector),
162
                ReflectionType::createFromTypeAndReflector('int', false, $reflector),
163
                false,
164
            ],
165
            'scalar type (int) to different scalar type (float) is not covariant'  => [
166
                ReflectionType::createFromTypeAndReflector('int', false, $reflector),
167
                ReflectionType::createFromTypeAndReflector('float', false, $reflector),
168
                false,
169
            ],
170
            'object type to scalar type is not contravariant'                      => [
171
                ReflectionType::createFromTypeAndReflector('object', false, $reflector),
172
                ReflectionType::createFromTypeAndReflector('string', false, $reflector),
173
                false,
174
            ],
175
            'scalar type to object type is not covariant'                          => [
176
                ReflectionType::createFromTypeAndReflector('string', false, $reflector),
177
                ReflectionType::createFromTypeAndReflector('object', false, $reflector),
178
                false,
179
            ],
180
            'class to superclass is not covariant'                                 => [
181
                ReflectionType::createFromTypeAndReflector('BClass', false, $reflector),
182
                ReflectionType::createFromTypeAndReflector('AClass', false, $reflector),
183
                false,
184
            ],
185
            'class to subclass is covariant'                                       => [
186
                ReflectionType::createFromTypeAndReflector('BClass', false, $reflector),
187
                ReflectionType::createFromTypeAndReflector('CClass', false, $reflector),
188
                true,
189
            ],
190
            'class to implemented interface is not covariant'                      => [
191
                ReflectionType::createFromTypeAndReflector('AnotherClassWithMultipleInterfaces', false, $reflector),
192
                ReflectionType::createFromTypeAndReflector('AnInterface', false, $reflector),
193
                false,
194
            ],
195
            'interface to implementing class is covariant'                         => [
196
                ReflectionType::createFromTypeAndReflector('AnInterface', false, $reflector),
197
                ReflectionType::createFromTypeAndReflector('AnotherClassWithMultipleInterfaces', false, $reflector),
198
                true,
199
            ],
200
            'class to not implemented interface is not covariant'                  => [
201
                ReflectionType::createFromTypeAndReflector('AnotherClassWithMultipleInterfaces', false, $reflector),
202
                ReflectionType::createFromTypeAndReflector('Traversable', false, $reflector),
203
                false,
204
            ],
205
            'interface to parent interface is not covariant'                       => [
206
                ReflectionType::createFromTypeAndReflector('Iterator', false, $reflector),
207
                ReflectionType::createFromTypeAndReflector('Traversable', false, $reflector),
208
                false,
209
            ],
210
            'interface to child interface is covariant'                            => [
211
                ReflectionType::createFromTypeAndReflector('Traversable', false, $reflector),
212
                ReflectionType::createFromTypeAndReflector('Iterator', false, $reflector),
213
                true,
214
            ],
215
        ];
216
    }
217
218
    /**
219
     * @dataProvider existingTypes
220
     */
221
    public function testCovarianceConsidersSameTypeAlwaysCovariant(?ReflectionType $type) : void
222
    {
223
        self::assertTrue(
224
            (new TypeIsCovariant())
225
                ->__invoke($type, $type)
226
        );
227
    }
228
229
    /** @return (ReflectionType|null)[][] */
230
    public function existingTypes() : array
231
    {
232
        $reflector = new ClassReflector(new StringSourceLocator(
233
            <<<'PHP'
234
<?php
235
236
interface Traversable {}
237
class AClass {}
238
PHP
239
            ,
240
            (new BetterReflection())->astLocator()
241
        ));
242
243
        return array_merge(
244
            [[null]],
245
            array_merge(...array_map(
246
                static function (string $type) use ($reflector) : array {
247
                    return [
248
                        [ReflectionType::createFromTypeAndReflector($type, false, $reflector)],
249
                        [ReflectionType::createFromTypeAndReflector($type, true, $reflector)],
250
                    ];
251
                },
252
                [
253
                    'int',
254
                    'string',
255
                    'float',
256
                    'bool',
257
                    'array',
258
                    'iterable',
259
                    'callable',
260
                    'Traversable',
261
                    'AClass',
262
                ]
263
            ))
264
        );
265
    }
266
267
    /**
268
     * @dataProvider existingNullableTypeStrings
269
     */
270
    public function testCovarianceConsidersNullability(string $type) : void
271
    {
272
        $reflector   = new ClassReflector(new StringSourceLocator(
273
            <<<'PHP'
274
<?php
275
276
interface Traversable {}
277
class AClass {}
278
PHP
279
            ,
280
            (new BetterReflection())->astLocator()
281
        ));
282
        $nullable    = ReflectionType::createFromTypeAndReflector($type, true, $reflector);
283
        $notNullable = ReflectionType::createFromTypeAndReflector($type, false, $reflector);
284
285
        $isCovariant = new TypeIsCovariant();
286
287
        self::assertTrue($isCovariant->__invoke($nullable, $notNullable));
288
        self::assertFalse($isCovariant->__invoke($notNullable, $nullable));
289
    }
290
291
    /** @return string[][] */
292
    public function existingNullableTypeStrings() : array
293
    {
294
        return [
295
            ['int'],
296
            ['string'],
297
            ['float'],
298
            ['bool'],
299
            ['array'],
300
            ['iterable'],
301
            ['callable'],
302
            ['Traversable'],
303
            ['AClass'],
304
        ];
305
    }
306
}
307