classHasMethodWithReturnTypeAndDescription()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 4
dl 0
loc 7
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
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
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
232
        /** @var ArgumentDescriptor $argumentD */
233
        $argumentD = $method->getArguments()[$argument];
234
        Assert::true($argumentD->isVariadic(), 'Expected argument to be variadic');
235
    }
236
237
    /**
238
     * @param string $classFqsen
239
     * @param string $methodName
240
     * @Then class ":classFqsen" has a method :method
241
     */
242
    public function classHasMethod($classFqsen, $methodName)
243
    {
244
        $class = $this->findClassByFqsen($classFqsen);
245
        /** @var MethodDescriptor $method */
246
        $method = $class->getMethods()->get($methodName, null);
247
        $methodNames = implode(', ', array_keys($class->getMethods()->getAll()));
248
249
        $visibilityLevel = $this->getAst()->getSettings()->getVisibility();
250
        Assert::isInstanceOf(
251
            $method,
252
            MethodDescriptor::class,
253
            "Class $classFqsen does not have a method $methodName, it does have the methods: $methodNames "
254
            . "(visibility level: $visibilityLevel})"
255
        );
256
        Assert::eq($methodName, $method->getName());
257
    }
258
259
    /**
260
     * @param string $classFqsen
261
     * @param string $propertyName
262
     * @Then class ":classFqsen" has a property :property
263
     */
264
    public function classHasProperty($classFqsen, $propertyName)
265
    {
266
        $class = $this->findClassByFqsen($classFqsen);
267
        /** @var PropertyDescriptor $property */
268
        $property = $class->getProperties()->get($propertyName, null);
269
        Assert::isInstanceOf($property, PropertyDescriptor::class);
270
        Assert::eq($propertyName, $property->getName());
271
    }
272
273
    /**
274
     * @param string $classFqsen
275
     * @param string $methodName
276
     * @param string $argument
277
     * @param string $type
278
     * @Then class ":classFqsen" has a method :method with argument :argument of type ":type"
279
     */
280
    public function classHasMethodWithArgumentOfType($classFqsen, $methodName, $argument, $type)
281
    {
282
        $class = $this->findClassByFqsen($classFqsen);
283
        /** @var MethodDescriptor $method */
284
        $method = $class->getMethods()->get($methodName);
285
        Assert::keyExists($method->getArguments()->getAll(), $argument);
286
        /** @var ArgumentDescriptor $argumentDescriptor */
287
        $argumentDescriptor = $method->getArguments()[$argument];
288
289
        Assert::eq($type, (string) $argumentDescriptor->getType());
290
    }
291
292
    /**
293
     * @param string $classFqsen
294
     * @param string $methodName
295
     * @param string $param
296
     * @param string $type
297
     * @Then class ":classFqsen" has a method :method with param :param of type ":type"
298
     */
299
    public function classHasMethodWithParamOfType($classFqsen, $methodName, $param, $type)
300
    {
301
        $class = $this->findClassByFqsen($classFqsen);
302
        /** @var MethodDescriptor $method */
303
        $method = $class->getMethods()->get($methodName);
304
        /** @var ParamDescriptor $paramDescriptor */
305
        foreach ($method->getParam() as $paramDescriptor) {
306
            if ($paramDescriptor->getName() === $param) {
307
                Assert::eq($type, (string) $paramDescriptor->getType());
308
            }
309
        }
310
    }
311
312
    /**
313
     * @param string $classFqsen
314
     * @param string $constantName
315
     * @Then class ":classFqsen" has a constant :constantName
316
     */
317
    public function classHasConstant($classFqsen, $constantName)
318
    {
319
        /** @var ClassDescriptor $class */
320
        $class = $this->findClassByFqsen($classFqsen);
321
        $constant = $class->getConstants()->get($constantName);
322
        Assert::isInstanceOf($constant, ConstantDescriptor::class);
323
    }
324
325
    /**
326
     * @param string $className
327
     * @return ClassDescriptor
328
     * @throws \Exception
329
     */
330
    private function findClassByName($className)
331
    {
332
        $ast = $this->getAst();
333
        foreach ($ast->getFiles() as $file) {
334
            foreach ($file->getClasses() as $classDescriptor) {
335
                if ($classDescriptor->getName() === $className) {
336
                    return $classDescriptor;
337
                }
338
            }
339
        }
340
341
        throw new \Exception(sprintf('Didn\'t find expected class "%s"', $className));
342
    }
343
344
    /**
345
     * @param string $tagName
346
     * @param int $expectedNumber
347
     * @param DescriptorAbstract $element
348
     */
349
    private static function AssertTagCount($element, $tagName, $expectedNumber)
350
    {
351
        /** @var Collection $tagCollection */
352
        $tagCollection = $element->getTags()->get($tagName, new Collection());
353
354
        Assert::eq((int) $expectedNumber, $tagCollection->count());
355
        if ($expectedNumber > 0) {
356
            Assert::eq($tagName, $tagCollection[0]->getName());
357
        }
358
    }
359
360
    /**
361
     * @Then /^the ast has a file named "([^"]*)" with a summary:$/
362
     * @throws \Exception
363
     */
364
    public function theAstHasAFileNamedWithASummary(string $fileName, PyStringNode $string)
365
    {
366
        $ast = $this->getAst();
367
        /** @var FileDescriptor $file */
368
        $file = $ast->getFiles()->get($fileName);
369
370
        Assert::eq($string->getRaw(), $file->getSummary());
371
    }
372
373
    /**
374
     * @param string $classFqsen
375
     * @param string $methodName
376
     * @throws Exception
377
     * @Then class ":classFqsen" has a method :method with returntype :returnType
378
     * @Then class ":classFqsen" has a method :method with returntype :returnType without description
379
     */
380
    public function classHasMethodWithReturnType($classFqsen, $methodName, $returnType)
381
    {
382
        $response = $this->findMethodResponse($classFqsen, $methodName);
383
384
        Assert::eq((string) $response->getType(), $returnType);
385
        Assert::eq((string) $response->getDescription(), '');
386
    }
387
388
    /**
389
     * @param string $classFqsen
390
     * @param string $methodName
391
     * @throws Exception
392
     * @Then class ":classFqsen" has a magic method :method with returntype :returnType
393
     * @Then class ":classFqsen" has a magic method :method with returntype :returnType without description
394
     */
395
    public function classHasMagicMethodWithReturnType($classFqsen, $methodName, $returnType)
396
    {
397
        $response = $this->findMagicMethodResponse($classFqsen, $methodName);
398
399
        Assert::eq((string) $response->getType(), $returnType);
400
        Assert::eq((string) $response->getDescription(), '');
401
    }
402
403
    /**
404
     * @param string $classFqsen
405
     * @param string $methodName
406
     * @throws Exception
407
     * @Then class ":classFqsen" has a method :method with returntype :returnType with description:
408
     */
409
    public function classHasMethodWithReturnTypeAndDescription($classFqsen, $methodName, $returnType, PyStringNode $description)
410
    {
411
        $response = $this->findMethodResponse($classFqsen, $methodName);
412
413
        Assert::eq($returnType, (string) $response->getType());
414
        Assert::eq($description, (string) $response->getDescription());
415
    }
416
417
    /**
418
     * @Then class ":classFqsen" has a method ":method" without returntype
419
     * @throws \Exception
420
     */
421
    public function classReturnTaggetReturnWithoutAnyWithoutReturntype($classFqsen, $methodName)
422
    {
423
        $response = $this->findMethodResponse($classFqsen, $methodName);
424
        Assert::eq('mixed', (string) $response->getType());
425
        Assert::eq('', $response->getDescription());
426
    }
427
428
    /**
429
     * @throws Exception
430
     * @Then has function :fqsen with returntype :returnType
431
     * @Then has function :fqsen with returntype :returnType without description
432
     */
433
    public function functionWithReturnType($fqsen, $returnType)
434
    {
435
        $response = $this->findFunctionResponse($fqsen);
436
437
        Assert::eq($returnType, (string) $response->getType());
438
        Assert::eq('', (string) $response->getDescription());
439
    }
440
441
    /**
442
     * @throws Exception
443
     * @Then has function :fqsen with returntype :returnType with description:
444
     */
445
    public function functionWithReturnTypeAndDescription($fqsen, $returnType, PyStringNode $description)
446
    {
447
        $response = $this->findFunctionResponse($fqsen);
448
449
        Assert::eq($returnType, (string) $response->getType());
450
        Assert::eq($description, (string) $response->getDescription());
451
    }
452
453
    /**
454
     * @Then has function :fqsen without returntype
455
     * @throws \Exception
456
     */
457
    public function functionWithoutReturntype($fqsen)
458
    {
459
        $response = $this->findFunctionResponse($fqsen);
460
        Assert::eq('mixed', (string) $response->getType());
461
        Assert::eq('', $response->getDescription());
462
    }
463
464
    /**
465
     * @throws Exception
466
     */
467
    private function findMethodResponse($classFqsen, $methodName): ReturnDescriptor
468
    {
469
        $class = $this->findClassByFqsen($classFqsen);
470
        /** @var MethodDescriptor $method */
471
        $method = $class->getMethods()->get($methodName, null);
472
        Assert::isInstanceOf($method, MethodDescriptor::class);
473
        Assert::eq($methodName, $method->getName());
474
475
        return $method->getResponse();
476
    }
477
478
    /**
479
     * @throws Exception
480
     */
481
    private function findMagicMethodResponse($classFqsen, $methodName): ReturnDescriptor
482
    {
483
        $class = $this->findClassByFqsen($classFqsen);
484
        $match = null;
485
486
        /** @var MethodDescriptor $method */
487
        foreach ($class->getMagicMethods() as $method) {
488
            if ($method->getName() === $methodName) {
489
                $match = $method;
490
            }
491
        }
492
493
        Assert::isInstanceOf($match, MethodDescriptor::class);
494
        Assert::eq($methodName, $match->getName());
495
496
        return $match->getResponse();
497
    }
498
499
    /**
500
     * @throws Exception
501
     */
502
    private function findFunctionResponse(string $fqsen): ReturnDescriptor
503
    {
504
        $function = $this->findFunctionByFqsen($fqsen);
505
        return $function->getResponse();
506
    }
507
508
    /**
509
     * @Then class ":classFqsen" has a magic method :method with argument ":argument" of type :type
510
     */
511
    public function classHasMagicMethodWithArgument($classFqsen, $methodName, $argument, $type)
0 ignored issues
show
Unused Code introduced by
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...
512
    {
513
        $class = $this->findClassByFqsen($classFqsen);
514
        $match = null;
515
516
        /** @var MethodDescriptor $method */
517
        foreach ($class->getMagicMethods() as $method) {
518
            if ($method->getName() === $methodName) {
519
                $match = $method;
520
            }
521
        }
522
523
        Assert::isInstanceOf($match, MethodDescriptor::class);
524
        Assert::notNull($match->getArguments()->get($argument));
525
    }
526
527
    /**
528
     * @Then /^(\d+) files should be parsed$/
529
     */
530
    public function filesShouldBeParsed($count)
531
    {
532
        Assert::same((int) $count, $this->getAst()->getFiles()->count());
533
    }
534
535
    /**
536
     * @Then /^the ast has a function named "([^"]*)"$/
537
     */
538
    public function theAstHasAFunctionNamed($functionName)
539
    {
540
        Assert::isInstanceOf(
541
            $this->getAst()->getIndexes()->get('functions')->get($functionName . '()'),
542
            FunctionDescriptor::class
543
        );
544
    }
545
546
    /**
547
     * @Then argument :argument of function ":functionName" has no defined type and description is:
548
     */
549
    public function argumentOfFunctionHasNoTypeAndHasDescripion($argument, $functionName, PyStringNode $description)
550
    {
551
        /** @var FunctionDescriptor $functionDescriptor */
552
        $functionDescriptor = $this->getAst()->getIndexes()->get('functions')->get($functionName . '()');
553
        Assert::isInstanceOf(
554
            $functionDescriptor,
555
            FunctionDescriptor::class
556
        );
557
558
        /** @var ArgumentDescriptor $argumentDescriptor */
559
        $argumentDescriptor = $functionDescriptor->getArguments()->get($argument);
560
561
        Assert::isInstanceOf($argumentDescriptor, ArgumentDescriptor::class);
562
563
        Assert::same($description->getRaw(), (string) $argumentDescriptor->getDescription());
564
    }
565
566
    /**
567
     * @Given the namespace ':namespace' has a function named ':functionName'
568
     */
569
    public function theNamespaceFoo(string $namespace, string $functionName)
570
    {
571
        /** @var NamespaceDescriptor $namespace */
572
        $namespace = $this->getAst()->getIndexes()->get('namespaces')->get($namespace);
573
        Assert::isInstanceOf($namespace, NamespaceDescriptor::class);
574
        $function = $this->findFunctionInNamespace($namespace, $functionName);
575
        Assert::isInstanceOf($function, FunctionDescriptor::class);
576
    }
577
578
    private function findFunctionInNamespace(NamespaceDescriptor $namespace, string $functionName)
579
    {
580
        foreach ($namespace->getFunctions()->getAll() as $key => $function) {
581
            if ($function->getName() === $functionName) {
582
                return $function;
583
            }
584
        }
585
586
        return null;
587
    }
588
589
    /**
590
     * @Then /^file "([^"]*)" must contain a marker$/
591
     */
592
    public function fileMustContainAMarker($filename)
593
    {
594
        $ast = $this->getAst();
595
596
        /** @var FileDescriptor $file */
597
        $file = $ast->getFiles()->get($filename);
598
599
        Assert::count($file->getMarkers(), 1);
600
    }
601
602
    /**
603
     * @Then class ":className" must have magic property ":propertyName" of type :type
604
     */
605
    public function classMustHaveMagicPropertyOfType($className, $propertyName, $type)
606
    {
607
        $classDescriptor = $this->findClassByFqsen($className);
608
        /** @var PropertyDescriptor $propertyDescriptor */
609
        $propertyDescriptor = null;
610
        foreach ($classDescriptor->getMagicProperties() as $property) {
611
            if ($property->getName() === $propertyName) {
612
                $propertyDescriptor = $property;
613
                break;
614
            }
615
        }
616
617
        Assert::isInstanceOf($propertyDescriptor, PropertyDescriptor::class);
618
        Assert::eq($type, (string) $propertyDescriptor->getType());
619
    }
620
}
621