Completed
Push — develop ( e58221...7a35e1 )
by Jaap
08:17 queued 01:05
created

ApiContext::functionWithReturnTypeAndDescription()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 3
dl 0
loc 7
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 method :method with returntype :returnType with description:
371
     */
372
    public function classHasMethodWithReturnTypeAndDescription($classFqsen, $methodName, $returnType, PyStringNode $description)
373
    {
374
        $response = $this->findMethodResponse($classFqsen, $methodName);
375
376
        Assert::assertEquals($returnType, (string)$response->getTypes());
377
        Assert::assertEquals($description, (string)$response->getDescription());
378
    }
379
380
    /**
381
     * @Then class ":classFqsen" has a method ":method" without returntype
382
     * @throws \Exception
383
     */
384
    public function classReturnTaggetReturnWithoutAnyWithoutReturntype($classFqsen, $methodName)
385
    {
386
        $response = $this->findMethodResponse($classFqsen, $methodName);
387
        Assert::assertEquals('mixed', (string)$response->getTypes());
388
        Assert::assertEquals('', $response->getDescription());
389
    }
390
391
    /**
392
     * @param $fqsen
393
     * @param $returnType
394
     * @throws \Exception
395
     * @Then has function :fqsen with returntype :returnType
396
     * @Then has function :fqsen with returntype :returnType without description
397
     */
398
    public function functionWithReturnType($fqsen, $returnType)
399
    {
400
        $response = $this->findFunctionResponse($fqsen);
401
402
        Assert::assertEquals($returnType, (string)$response->getTypes());
403
        Assert::assertEquals('', (string)$response->getDescription());
404
    }
405
406
    /**
407
     * @param $fqsen
408
     * @param $returnType
409
     * @param PyStringNode $description
410
     * @throws \Exception
411
     * @Then has function :fqsen with returntype :returnType with description:
412
     */
413
    public function functionWithReturnTypeAndDescription($fqsen, $returnType, PyStringNode $description)
414
    {
415
        $response = $this->findFunctionResponse($fqsen);
416
417
        Assert::assertEquals($returnType, (string)$response->getTypes());
418
        Assert::assertEquals($description, (string)$response->getDescription());
419
    }
420
421
    /**
422
     * @Then has function :fqsen without returntype
423
     * @throws \Exception
424
     */
425
    public function functionWithoutReturntype($fqsen)
426
    {
427
        $response = $this->findFunctionResponse($fqsen);
428
        Assert::assertEquals('mixed', (string)$response->getTypes());
429
        Assert::assertEquals('', $response->getDescription());
430
    }
431
432
    /**
433
     * @param $classFqsen
434
     * @param $methodName
435
     * @return ReturnDescriptor
436
     * @throws \Exception
437
     */
438
    private function findMethodResponse($classFqsen, $methodName): ReturnDescriptor
439
    {
440
        $class = $this->findClassByFqsen($classFqsen);
441
        /** @var MethodDescriptor $method */
442
        $method = $class->getMethods()->get($methodName, null);
443
        Assert::assertInstanceOf(MethodDescriptor::class, $method);
444
        Assert::assertEquals($methodName, $method->getName());
445
446
        $response = $method->getResponse();
447
        return $response;
448
    }
449
450
    /**
451
     * @param string $fqsen
452
     * @return ReturnDescriptor
453
     * @throws \Exception
454
     */
455
    private function findFunctionResponse(string $fqsen): ReturnDescriptor
456
    {
457
        $function = $this->findFunctionByFqsen($fqsen);
458
        return $function->getResponse();
459
    }
460
}
461