Completed
Push — develop ( ab0bed...ddf638 )
by Jaap
09:44 queued 06:11
created

ApiContext::findMagicMethodResponse()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 2
dl 0
loc 17
rs 9.4285
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-2017 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\MethodDescriptor;
25
use phpDocumentor\Descriptor\Tag\ParamDescriptor;
26
use phpDocumentor\Descriptor\Tag\ReturnDescriptor;
27
use phpDocumentor\Descriptor\Tag\VersionDescriptor;
28
use phpDocumentor\Descriptor\TraitDescriptor;
29
use PHPUnit\Framework\Assert;
30
31
class ApiContext extends BaseContext implements Context
32
{
33
    /**
34
     * @Then /^the AST has a class named "([^"]*)" in file "([^"]*)"$/
35
     * @throws \Exception
36
     */
37
    public function theASTHasAclassNamedInFile($class, $file)
38
    {
39
        $ast = $this->getAst();
40
41
        $file = $this->processFilePath($file);
42
        /** @var FileDescriptor $fileDescriptor */
43
        $fileDescriptor = $ast->getFiles()->get($file);
44
45
        /** @var ClassDescriptor $classDescriptor */
46
        foreach ($fileDescriptor->getClasses() as $classDescriptor) {
47
            if ($classDescriptor->getName() === $class) {
48
                return;
49
            }
50
        }
51
52
        throw new \Exception(sprintf('Didn\'t find expected class "%s" in "%s"', $class, $file));
53
    }
54
55
    /**
56
     * @Then /^the class named "([^"]*)" is in the default package$/
57
     * @throws \Exception
58
     */
59
    public function theASTHasAClassInDefaultPackage($class)
60
    {
61
        $class = $this->findClassByName($class);
62
63
        Assert::assertEquals('Default', $class->getPackage()->getName());
64
    }
65
66
    /**
67
     * @Then /^the AST has a trait named "([^"]*)" in file "([^"]*)"$/
68
     * @throws \Exception
69
     */
70
    public function theASTHasATraitNamedInFile($trait, $file)
71
    {
72
        $ast = $this->getAst();
73
74
        $file = $this->processFilePath($file);
75
        /** @var FileDescriptor $fileDescriptor */
76
        $fileDescriptor = $ast->getFiles()->get($file);
77
78
        /** @var TraitDescriptor $classDescriptor */
79
        foreach ($fileDescriptor->getTraits() as $classDescriptor) {
80
            if ($classDescriptor->getName() === $trait) {
81
                return;
82
            }
83
        }
84
85
        throw new \Exception(sprintf('Didn\'t find expected trait "%s" in "%s"', $trait, $file));
86
    }
87
88
    /**
89
     * @param $class
90
     * @param $expectedContent
91
     * @Then the class named ":class" has docblock with content:
92
     */
93
    public function classHasDocblockWithContent($class, PyStringNode $expectedContent)
94
    {
95
        $class = $this->findClassByName($class);
96
97
        Assert::assertEquals($expectedContent->getRaw(), $class->getDescription());
98
    }
99
100
    /**
101
     * @param $classFqsen
102
     * @param $docElement
103
     * @param $value
104
     * @Then class ":classFqsen" has :docElement:
105
     * @throws \Exception
106
     */
107
    public function classHasDocblockContent($classFqsen, $docElement, PyStringNode $value)
108
    {
109
        $class = $this->findClassByFqsen($classFqsen);
110
111
        $method = 'get' . $docElement;
112
113
        Assert::assertEquals($value->getRaw(), $class->$method());
114
    }
115
116
    /**
117
     * @param $classFqsen
118
     * @param $elementType
119
     * @param $elementName
120
     * @param $docElement
121
     * @param PyStringNode $value
122
     * @Then class ":classFqsen" has :elementType :elementName with :docElement:
123
     */
124
    public function classHasElementWithDocblockContent($classFqsen, $elementType, $elementName, $docElement, PyStringNode $value)
125
    {
126
        $class = $this->findClassByFqsen($classFqsen);
127
128
        switch ($elementType) {
129
            case 'method':
130
            case 'constant':
131
                $method = $method = 'get' . $elementType . 's';
132
                break;
133
            case 'property':
134
                $method = 'getProperties';
135
                break;
136
            default:
137
                $method = 'get' . $elementType;
138
                break;
139
        }
140
141
        $element = $class-> $method()->get($elementName);
142
143
        $method = 'get' . $docElement;
144
        $actual = $element->$method();
145
146
        Assert::assertEquals($value->getRaw(), $actual,  sprintf('"%s" does not match "%s"', $actual, $value->getRaw()));
0 ignored issues
show
Coding Style introduced by
Expected 1 space instead of 2 after comma in function call.
Loading history...
147
    }
148
149
    /**
150
     * @param $classFqsen
151
     * @param $value
152
     * @Then class ":classFqsen" has version :value
153
     */
154
    public function classHasVersion($classFqsen, $value)
155
    {
156
        $class = $this->findClassByFqsen($classFqsen);
157
158
        /** @var VersionDescriptor $tag */
159
        foreach ($class->getVersion() as $tag) {
160
            if ($tag->getVersion() === $value) {
161
                return;
162
            }
163
        }
164
165
        Assert::fail(sprintf('Didn\'t find expected version "%s"', $value));
166
    }
167
168
    /**
169
     * @param $classFqsen
170
     * @param $tagName
171
     * @Then class ":classFqsen" without tag :tagName
172
     */
173
    public function classWithoutTag($classFqsen, $tagName)
174
    {
175
        $this->classHasTag($classFqsen, $tagName, 0);
176
    }
177
178
    /**
179
     * @param string $classFqsen
180
     * @param string $tagName
181
     * @param int $expectedNumber
182
     * @Then class ":classFqsen" has exactly :expectedNumber tag :tagName
183
     */
184
    public function classHasTag($classFqsen, $tagName, $expectedNumber)
185
    {
186
        $class = $this->findClassByFqsen($classFqsen);
187
        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...
188
    }
189
190
    /**
191
     * @param string $classFqsen
192
     * @param string $tagName
193
     * @param string $method
194
     * @Then class ":classFqsen" has a method named :method without tag :tagName
195
     */
196
    public function classHasMethodWithoutTag($classFqsen, $tagName, $method)
197
    {
198
        $this->classHasMethodWithExpectedCountTag($classFqsen, $tagName, $method, 0);
199
    }
200
201
    /**
202
     * @param string $classFqsen
203
     * @param string $tagName
204
     * @param string $methodName
205
     * @Then class ":classFqsen" has a method named :method with exactly :expected tag :tagName
206
     */
207
    public function classHasMethodWithExpectedCountTag($classFqsen, $tagName, $methodName, $expectedCount)
208
    {
209
        $class = $this->findClassByFqsen($classFqsen);
210
        $method = $class->getMethods()->get($methodName);
211
212
        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...
213
    }
214
215
    /**
216
     * @param string $classFqsen
217
     * @param string $tagName
0 ignored issues
show
Bug introduced by
There is no parameter named $tagName. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
218
     * @param string $methodName
219
     * @Then class ":classFqsen" has a method :method with argument ":argument is variadic
220
     */
221
    public function classHasMethodWithAgumentVariadic($classFqsen, $methodName, $argument)
222
    {
223
        $class = $this->findClassByFqsen($classFqsen);
224
        /** @var MethodDescriptor $method */
225
        $method = $class->getMethods()->get($methodName);
226
        Assert::assertArrayHasKey($argument, $method->getArguments());
227
        /** @var ArgumentDescriptor $argumentD */
228
        $argumentD = $method->getArguments()[$argument];
0 ignored issues
show
Unused Code introduced by
$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...
229
230
        //TODO: enable this check when we support variadic arguments.
231
        //Assert::assertTrue($argumentD->isVariadic(), 'Expected argument to be variadic');
232
    }
233
234
    /**
235
     * @param string $classFqsen
236
     * @param string $tagName
0 ignored issues
show
Bug introduced by
There is no parameter named $tagName. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

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