Completed
Push — develop ( 84e8c9...0030f8 )
by Mike
09:36
created

ApiContext::classHasProperty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of phpDocumentor.
4
 *
5
 *  For the full copyright and license information, please view the LICENSE
6
 *  file that was distributed with this source code.
7
 *
8
 * @copyright 2010-2018 Mike van Riel<[email protected]>
9
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
10
 * @link      http://phpdoc.org
11
 */
12
13
namespace phpDocumentor\Behat\Contexts\Ast;
14
15
use Behat\Behat\Context\Context;
16
use Behat\Behat\Tester\Exception\PendingException;
17
use Behat\Gherkin\Node\PyStringNode;
18
use phpDocumentor\Descriptor\ArgumentDescriptor;
19
use phpDocumentor\Descriptor\ClassDescriptor;
20
use phpDocumentor\Descriptor\Collection;
21
use phpDocumentor\Descriptor\ConstantDescriptor;
22
use phpDocumentor\Descriptor\DescriptorAbstract;
23
use phpDocumentor\Descriptor\FileDescriptor;
24
use phpDocumentor\Descriptor\FunctionDescriptor;
25
use phpDocumentor\Descriptor\MethodDescriptor;
26
use phpDocumentor\Descriptor\NamespaceDescriptor;
27
use phpDocumentor\Descriptor\PropertyDescriptor;
28
use phpDocumentor\Descriptor\Tag\ParamDescriptor;
29
use phpDocumentor\Descriptor\Tag\ReturnDescriptor;
30
use phpDocumentor\Descriptor\Tag\VersionDescriptor;
31
use phpDocumentor\Descriptor\TraitDescriptor;
32
use phpDocumentor\Reflection\Php\File;
33
use Webmozart\Assert\Assert;
34
35
class ApiContext extends BaseContext implements Context
36
{
37
    /**
38
     * @Then /^the AST has a class named "([^"]*)" in file "([^"]*)"$/
39
     * @throws \Exception
40
     */
41
    public function theASTHasAclassNamedInFile($class, $file)
42
    {
43
        $ast = $this->getAst();
44
45
        $file = $this->processFilePath($file);
46
        /** @var FileDescriptor $fileDescriptor */
47
        $fileDescriptor = $ast->getFiles()->get($file);
48
49
        /** @var ClassDescriptor $classDescriptor */
50
        foreach ($fileDescriptor->getClasses() as $classDescriptor) {
51
            if ($classDescriptor->getName() === $class) {
52
                return;
53
            }
54
        }
55
56
        throw new \Exception(sprintf('Didn\'t find expected class "%s" in "%s"', $class, $file));
57
    }
58
59
    /**
60
     * @Then /^the AST doesn't have a class "([^"]*)"$/
61
     * @throws \Exception
62
     */
63
    public function theASTDoesnTHaveAClass($className)
64
    {
65
        $ast = $this->getAst();
66
        foreach ($ast->getFiles() as $file) {
67
            foreach ($file->getClasses() as $classDescriptor) {
68
                if ($classDescriptor->getName() === $className) {
69
                    throw new \Exception('Found unexpected class');
70
                }
71
            }
72
        }
73
    }
74
75
    /**
76
     * @Then /^the class named "([^"]*)" is in the default package$/
77
     * @throws \Exception
78
     */
79
    public function theASTHasAClassInDefaultPackage($class)
80
    {
81
        $class = $this->findClassByName($class);
82
83
        Assert::eq('Default', $class->getPackage()->getName());
84
    }
85
86
    /**
87
     * @Then /^the AST has a trait named "([^"]*)" in file "([^"]*)"$/
88
     * @throws \Exception
89
     */
90
    public function theASTHasATraitNamedInFile($trait, $file)
91
    {
92
        $ast = $this->getAst();
93
94
        $file = $this->processFilePath($file);
95
        /** @var FileDescriptor $fileDescriptor */
96
        $fileDescriptor = $ast->getFiles()->get($file);
97
98
        /** @var TraitDescriptor $classDescriptor */
99
        foreach ($fileDescriptor->getTraits() as $classDescriptor) {
100
            if ($classDescriptor->getName() === $trait) {
101
                return;
102
            }
103
        }
104
105
        throw new \Exception(sprintf('Didn\'t find expected trait "%s" in "%s"', $trait, $file));
106
    }
107
108
    /**
109
     * @Then the class named ":class" has docblock with content:
110
     */
111
    public function classHasDocblockWithContent($class, PyStringNode $expectedContent)
112
    {
113
        $class = $this->findClassByName($class);
114
115
        Assert::eq($expectedContent->getRaw(), $class->getDescription());
116
    }
117
118
    /**
119
     * @Then class ":classFqsen" has :docElement:
120
     * @throws Exception
121
     */
122
    public function classHasDocblockContent($classFqsen, $docElement, PyStringNode $value)
123
    {
124
        $class = $this->findClassByFqsen($classFqsen);
125
126
        $method = 'get' . $docElement;
127
128
        Assert::eq($value->getRaw(), $class->{$method}());
129
    }
130
131
    /**
132
     * @Then class ":classFqsen" has :elementType :elementName with :docElement:
133
     */
134
    public function classHasElementWithDocblockContent($classFqsen, $elementType, $elementName, $docElement, PyStringNode $value)
135
    {
136
        $class = $this->findClassByFqsen($classFqsen);
137
138
        switch ($elementType) {
139
            case 'method':
140
            case 'constant':
141
                $method = $method = 'get' . $elementType . 's';
142
                break;
143
            case 'property':
144
                $method = 'getProperties';
145
                break;
146
            default:
147
                $method = 'get' . $elementType;
148
                break;
149
        }
150
151
        $element = $class-> {$method}()->get($elementName);
152
        $method = 'get' . $docElement;
153
        $actual = $element->{$method}();
154
155
        Assert::eq($value->getRaw(), $actual, sprintf('"%s" does not match "%s"', $actual, $value->getRaw()));
156
    }
157
158
    /**
159
     * @Then class ":classFqsen" has version :value
160
     */
161
    public function classHasVersion($classFqsen, $value)
162
    {
163
        $class = $this->findClassByFqsen($classFqsen);
164
165
        /** @var VersionDescriptor $tag */
166
        foreach ($class->getVersion() as $tag) {
167
            if ($tag->getVersion() === $value) {
168
                return;
169
            }
170
        }
171
172
        Assert::false(true, sprintf('Didn\'t find expected version "%s"', $value));
173
    }
174
175
    /**
176
     * @Then class ":classFqsen" without tag :tagName
177
     */
178
    public function classWithoutTag($classFqsen, $tagName)
179
    {
180
        $this->classHasTag($classFqsen, $tagName, 0);
181
    }
182
183
    /**
184
     * @param string $classFqsen
185
     * @param string $tagName
186
     * @param int $expectedNumber
187
     * @Then class ":classFqsen" has exactly :expectedNumber tag :tagName
188
     */
189
    public function classHasTag($classFqsen, $tagName, $expectedNumber)
190
    {
191
        $class = $this->findClassByFqsen($classFqsen);
192
        static::AssertTagCount($class, $tagName, $expectedNumber);
0 ignored issues
show
Bug introduced by Jaapio
Since AssertTagCount() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of AssertTagCount() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
193
    }
194
195
    /**
196
     * @param string $classFqsen
197
     * @param string $tagName
198
     * @param string $method
199
     * @Then class ":classFqsen" has a method named :method without tag :tagName
200
     */
201
    public function classHasMethodWithoutTag($classFqsen, $tagName, $method)
202
    {
203
        $this->classHasMethodWithExpectedCountTag($classFqsen, $tagName, $method, 0);
204
    }
205
206
    /**
207
     * @param string $classFqsen
208
     * @param string $tagName
209
     * @param string $methodName
210
     * @Then class ":classFqsen" has a method named :method with exactly :expected tag :tagName
211
     */
212
    public function classHasMethodWithExpectedCountTag($classFqsen, $tagName, $methodName, $expectedCount)
213
    {
214
        $class = $this->findClassByFqsen($classFqsen);
215
        $method = $class->getMethods()->get($methodName);
216
217
        static::AssertTagCount($method, $tagName, $expectedCount);
0 ignored issues
show
Bug introduced by Jaapio
Since AssertTagCount() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of AssertTagCount() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
218
    }
219
220
    /**
221
     * @param string $classFqsen
222
     * @param string $methodName
223
     * @Then class ":classFqsen" has a method :method with argument ":argument is variadic
224
     */
225
    public function classHasMethodWithArgumentVariadic($classFqsen, $methodName, $argument)
226
    {
227
        $class = $this->findClassByFqsen($classFqsen);
228
        /** @var MethodDescriptor $method */
229
        $method = $class->getMethods()->get($methodName);
230
        Assert::keyExists($method->getArguments()->getAll(), $argument);
231
        /** @var ArgumentDescriptor $argumentD */
232
        $argumentD = $method->getArguments()[$argument];
0 ignored issues
show
Unused Code introduced by Jaapio
$argumentD is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
233
234
        //TODO: enable this check when we support variadic arguments.
235
        //Assert::true($argumentD->isVariadic(), 'Expected argument to be variadic');
0 ignored issues
show
Unused Code Comprehensibility introduced by Jaapio
72% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
236
    }
237
238
    /**
239
     * @param string $classFqsen
240
     * @param string $methodName
241
     * @Then class ":classFqsen" has a method :method
242
     */
243
    public function classHasMethod($classFqsen, $methodName)
244
    {
245
        $class = $this->findClassByFqsen($classFqsen);
246
        /** @var MethodDescriptor $method */
247
        $method = $class->getMethods()->get($methodName, null);
248
        Assert::isInstanceOf($method, MethodDescriptor::class);
249
        Assert::eq($methodName, $method->getName());
250
    }
251
252
    /**
253
     * @param string $classFqsen
254
     * @param string $propertyName
255
     * @Then class ":classFqsen" has a property :property
256
     */
257
    public function classHasProperty($classFqsen, $propertyName)
258
    {
259
        $class = $this->findClassByFqsen($classFqsen);
260
        /** @var PropertyDescriptor $property */
261
        $property = $class->getProperties()->get($propertyName, null);
262
        Assert::isInstanceOf($property, PropertyDescriptor::class);
263
        Assert::eq($propertyName, $property->getName());
264
    }
265
266
    /**
267
     * @param string $classFqsen
268
     * @param string $methodName
269
     * @param string $argument
270
     * @param string $type
271
     * @Then class ":classFqsen" has a method :method with argument :argument of type ":type"
272
     */
273
    public function classHasMethodWithArgumentOfType($classFqsen, $methodName, $argument, $type)
274
    {
275
        $class = $this->findClassByFqsen($classFqsen);
276
        /** @var MethodDescriptor $method */
277
        $method = $class->getMethods()->get($methodName);
278
        Assert::keyExists($method->getArguments()->getAll(), $argument);
279
        /** @var ArgumentDescriptor $argumentDescriptor */
280
        $argumentDescriptor = $method->getArguments()[$argument];
281
282
        Assert::eq($type, (string) $argumentDescriptor->getType());
283
    }
284
285
    /**
286
     * @param string $classFqsen
287
     * @param string $methodName
288
     * @param string $param
289
     * @param string $type
290
     * @Then class ":classFqsen" has a method :method with param :param of type ":type"
291
     */
292
    public function classHasMethodWithParamOfType($classFqsen, $methodName, $param, $type)
293
    {
294
        $class = $this->findClassByFqsen($classFqsen);
295
        /** @var MethodDescriptor $method */
296
        $method = $class->getMethods()->get($methodName);
297
        /** @var ParamDescriptor $paramDescriptor */
298
        foreach ($method->getParam() as $paramDescriptor) {
299
            if ($paramDescriptor->getName() === $param) {
300
                Assert::eq($type, (string) $paramDescriptor->getType());
301
            }
302
        }
303
    }
304
305
    /**
306
     * @param string $classFqsen
307
     * @param string $constantName
308
     * @Then class ":classFqsen" has a constant :constantName
309
     */
310
    public function classHasConstant($classFqsen, $constantName)
311
    {
312
        /** @var ClassDescriptor $class */
313
        $class = $this->findClassByFqsen($classFqsen);
314
        $constant = $class->getConstants()->get($constantName);
315
        Assert::isInstanceOf($constant, ConstantDescriptor::class);
316
    }
317
318
    /**
319
     * @param string $className
320
     * @return ClassDescriptor
321
     * @throws \Exception
322
     */
323
    private function findClassByName($className)
324
    {
325
        $ast = $this->getAst();
326
        foreach ($ast->getFiles() as $file) {
327
            foreach ($file->getClasses() as $classDescriptor) {
328
                if ($classDescriptor->getName() === $className) {
329
                    return $classDescriptor;
330
                }
331
            }
332
        }
333
334
        throw new \Exception(sprintf('Didn\'t find expected class "%s"', $className));
335
    }
336
337
    /**
338
     * @param string $tagName
339
     * @param int $expectedNumber
340
     * @param DescriptorAbstract $element
341
     */
342
    private static function AssertTagCount($element, $tagName, $expectedNumber)
343
    {
344
        /** @var Collection $tagCollection */
345
        $tagCollection = $element->getTags()->get($tagName, new Collection());
346
347
        Assert::eq((int) $expectedNumber, $tagCollection->count());
348
        if ($expectedNumber > 0) {
349
            Assert::eq($tagName, $tagCollection[0]->getName());
350
        }
351
    }
352
353
    /**
354
     * @Then /^the ast has a file named "([^"]*)" with a summary:$/
355
     * @throws \Exception
356
     */
357
    public function theAstHasAFileNamedWithASummary(string $fileName, PyStringNode $string)
358
    {
359
        $ast = $this->getAst();
360
        /** @var FileDescriptor $file */
361
        $file = $ast->getFiles()->get($fileName);
362
363
        Assert::eq($string->getRaw(), $file->getSummary());
364
    }
365
366
    /**
367
     * @param string $classFqsen
368
     * @param string $methodName
369
     * @throws Exception
370
     * @Then class ":classFqsen" has a method :method with returntype :returnType
371
     * @Then class ":classFqsen" has a method :method with returntype :returnType without description
372
     */
373
    public function classHasMethodWithReturnType($classFqsen, $methodName, $returnType)
374
    {
375
        $response = $this->findMethodResponse($classFqsen, $methodName);
376
377
        Assert::eq((string) $response->getType(), $returnType);
378
        Assert::eq((string) $response->getDescription(), '');
379
    }
380
381
    /**
382
     * @param string $classFqsen
383
     * @param string $methodName
384
     * @throws Exception
385
     * @Then class ":classFqsen" has a magic method :method with returntype :returnType
386
     * @Then class ":classFqsen" has a magic method :method with returntype :returnType without description
387
     */
388
    public function classHasMagicMethodWithReturnType($classFqsen, $methodName, $returnType)
389
    {
390
        $response = $this->findMagicMethodResponse($classFqsen, $methodName);
391
392
        Assert::eq((string) $response->getType(), $returnType);
393
        Assert::eq((string) $response->getDescription(), '');
394
    }
395
396
    /**
397
     * @param string $classFqsen
398
     * @param string $methodName
399
     * @throws Exception
400
     * @Then class ":classFqsen" has a method :method with returntype :returnType with description:
401
     */
402
    public function classHasMethodWithReturnTypeAndDescription($classFqsen, $methodName, $returnType, PyStringNode $description)
403
    {
404
        $response = $this->findMethodResponse($classFqsen, $methodName);
405
406
        Assert::eq($returnType, (string) $response->getType());
407
        Assert::eq($description, (string) $response->getDescription());
408
    }
409
410
    /**
411
     * @Then class ":classFqsen" has a method ":method" without returntype
412
     * @throws \Exception
413
     */
414
    public function classReturnTaggetReturnWithoutAnyWithoutReturntype($classFqsen, $methodName)
415
    {
416
        $response = $this->findMethodResponse($classFqsen, $methodName);
417
        Assert::eq('mixed', (string) $response->getType());
418
        Assert::eq('', $response->getDescription());
419
    }
420
421
    /**
422
     * @throws Exception
423
     * @Then has function :fqsen with returntype :returnType
424
     * @Then has function :fqsen with returntype :returnType without description
425
     */
426
    public function functionWithReturnType($fqsen, $returnType)
427
    {
428
        $response = $this->findFunctionResponse($fqsen);
429
430
        Assert::eq($returnType, (string) $response->getType());
431
        Assert::eq('', (string) $response->getDescription());
432
    }
433
434
    /**
435
     * @throws Exception
436
     * @Then has function :fqsen with returntype :returnType with description:
437
     */
438
    public function functionWithReturnTypeAndDescription($fqsen, $returnType, PyStringNode $description)
439
    {
440
        $response = $this->findFunctionResponse($fqsen);
441
442
        Assert::eq($returnType, (string) $response->getType());
443
        Assert::eq($description, (string) $response->getDescription());
444
    }
445
446
    /**
447
     * @Then has function :fqsen without returntype
448
     * @throws \Exception
449
     */
450
    public function functionWithoutReturntype($fqsen)
451
    {
452
        $response = $this->findFunctionResponse($fqsen);
453
        Assert::eq('mixed', (string) $response->getType());
454
        Assert::eq('', $response->getDescription());
455
    }
456
457
    /**
458
     * @throws Exception
459
     */
460
    private function findMethodResponse($classFqsen, $methodName): ReturnDescriptor
461
    {
462
        $class = $this->findClassByFqsen($classFqsen);
463
        /** @var MethodDescriptor $method */
464
        $method = $class->getMethods()->get($methodName, null);
465
        Assert::isInstanceOf($method, MethodDescriptor::class);
466
        Assert::eq($methodName, $method->getName());
467
468
        return $method->getResponse();
469
    }
470
471
    /**
472
     * @throws Exception
473
     */
474
    private function findMagicMethodResponse($classFqsen, $methodName): ReturnDescriptor
475
    {
476
        $class = $this->findClassByFqsen($classFqsen);
477
        $match = null;
478
479
        /** @var MethodDescriptor $method */
480
        foreach ($class->getMagicMethods() as $method) {
481
            if ($method->getName() === $methodName) {
482
                $match = $method;
483
            }
484
        }
485
486
        Assert::isInstanceOf($match, MethodDescriptor::class);
487
        Assert::eq($methodName, $match->getName());
488
489
        return $match->getResponse();
490
    }
491
492
    /**
493
     * @throws Exception
494
     */
495
    private function findFunctionResponse(string $fqsen): ReturnDescriptor
496
    {
497
        $function = $this->findFunctionByFqsen($fqsen);
498
        return $function->getResponse();
499
    }
500
501
    /**
502
     * @Then class ":classFqsen" has a magic method :method with argument ":argument" of type :type
503
     */
504
    public function classHasMagicMethodWithArgument($classFqsen, $methodName, $argument, $type)
0 ignored issues
show
Unused Code introduced by Jaapio
The parameter $type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
505
    {
506
        $class = $this->findClassByFqsen($classFqsen);
507
        $match = null;
508
509
        /** @var MethodDescriptor $method */
510
        foreach ($class->getMagicMethods() as $method) {
511
            if ($method->getName() === $methodName) {
512
                $match = $method;
513
            }
514
        }
515
516
        Assert::isInstanceOf($match, MethodDescriptor::class);
517
        Assert::notNull($match->getArguments()->get($argument));
518
    }
519
520
    /**
521
     * @Then /^(\d+) files should be parsed$/
522
     */
523
    public function filesShouldBeParsed($count)
524
    {
525
        Assert::same((int) $count, $this->getAst()->getFiles()->count());
526
    }
527
528
    /**
529
     * @Then /^the ast has a function named "([^"]*)"$/
530
     */
531
    public function theAstHasAFunctionNamed($functionName)
532
    {
533
        Assert::isInstanceOf(
534
            $this->getAst()->getIndexes()->get('functions')->get($functionName . '()'),
535
            FunctionDescriptor::class
536
        );
537
    }
538
539
    /**
540
     * @Then argument :argument of function ":functionName" has no defined type and description is:
541
     */
542
    public function argumentOfFunctionHasNoTypeAndHasDescripion($argument, $functionName, PyStringNode $description)
543
    {
544
        /** @var FunctionDescriptor $functionDescriptor */
545
        $functionDescriptor = $this->getAst()->getIndexes()->get('functions')->get($functionName . '()');
546
        Assert::isInstanceOf(
547
            $functionDescriptor,
548
            FunctionDescriptor::class
549
        );
550
551
        /** @var ArgumentDescriptor $argumentDescriptor */
552
        $argumentDescriptor = $functionDescriptor->getArguments()->get($argument);
553
554
        Assert::isInstanceOf($argumentDescriptor, ArgumentDescriptor::class);
555
556
        Assert::same($description->getRaw(), (string) $argumentDescriptor->getDescription());
557
    }
558
559
    /**
560
     * @Given the namespace ':namespace' has a function named ':functionName'
561
     */
562
    public function theNamespaceFoo(string $namespace, string $functionName)
563
    {
564
        /** @var NamespaceDescriptor $namespace */
565
        $namespace = $this->getAst()->getIndexes()->get('namespaces')->get($namespace);
566
        Assert::isInstanceOf($namespace, NamespaceDescriptor::class);
567
        $function = $this->findFunctionInNamespace($namespace, $functionName);
568
        Assert::isInstanceOf($function, FunctionDescriptor::class);
569
    }
570
571
    private function findFunctionInNamespace(NamespaceDescriptor $namespace, string $functionName)
572
    {
573
        foreach ($namespace->getFunctions()->getAll() as $key => $function) {
574
            if ($function->getName() === $functionName) {
575
                return $function;
576
            }
577
        }
578
579
        return null;
580
    }
581
582
    /**
583
     * @Then /^file "([^"]*)" must contain a marker$/
584
     */
585
    public function fileMustContainAMarker($filename)
586
    {
587
        $ast = $this->getAst();
588
589
        /** @var FileDescriptor $file */
590
        $file = $ast->getFiles()->get($filename);
591
592
        Assert::count($file->getMarkers(), 1);
593
    }
594
595
    /**
596
     * @Then class ":className" must have magic property ":propertyName" of type :type
597
     */
598
    public function classMustHaveMagicPropertyOfType($className, $propertyName, $type)
599
    {
600
        $classDescriptor = $this->findClassByFqsen($className);
601
        /** @var PropertyDescriptor $propertyDescriptor */
602
        $propertyDescriptor = null;
603
        foreach ($classDescriptor->getMagicProperties() as $property) {
604
            if ($property->getName() === $propertyName) {
605
                $propertyDescriptor = $property;
606
                break;
607
            }
608
        }
609
610
        Assert::isInstanceOf($propertyDescriptor, PropertyDescriptor::class);
611
        Assert::eq($type, (string) $propertyDescriptor->getType());
612
    }
613
}
614