Completed
Push — develop ( 54507c...0b78c8 )
by Jaap
12:15 queued 08:32
created

ApiContext::classHasDocblockContent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 3
dl 0
loc 8
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-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\Gherkin\Node\PyStringNode;
17
use phpDocumentor\Descriptor\ArgumentDescriptor;
18
use phpDocumentor\Descriptor\ClassDescriptor;
19
use phpDocumentor\Descriptor\Collection;
20
use phpDocumentor\Descriptor\ConstantDescriptor;
21
use phpDocumentor\Descriptor\DescriptorAbstract;
22
use phpDocumentor\Descriptor\FileDescriptor;
23
use phpDocumentor\Descriptor\FunctionDescriptor;
24
use phpDocumentor\Descriptor\MethodDescriptor;
25
use phpDocumentor\Descriptor\NamespaceDescriptor;
26
use phpDocumentor\Descriptor\Tag\ParamDescriptor;
27
use phpDocumentor\Descriptor\Tag\ReturnDescriptor;
28
use phpDocumentor\Descriptor\Tag\VersionDescriptor;
29
use phpDocumentor\Descriptor\TraitDescriptor;
30
use phpDocumentor\Reflection\Php\File;
31
use PHPUnit\Framework\Assert;
32
33
class ApiContext extends BaseContext implements Context
34
{
35
    /**
36
     * @Then /^the AST has a class named "([^"]*)" in file "([^"]*)"$/
37
     * @throws \Exception
38
     */
39
    public function theASTHasAclassNamedInFile($class, $file)
40
    {
41
        $ast = $this->getAst();
42
43
        $file = $this->processFilePath($file);
44
        /** @var FileDescriptor $fileDescriptor */
45
        $fileDescriptor = $ast->getFiles()->get($file);
46
47
        /** @var ClassDescriptor $classDescriptor */
48
        foreach ($fileDescriptor->getClasses() as $classDescriptor) {
49
            if ($classDescriptor->getName() === $class) {
50
                return;
51
            }
52
        }
53
54
        throw new \Exception(sprintf('Didn\'t find expected class "%s" in "%s"', $class, $file));
55
    }
56
57
    /**
58
     * @Then /^the AST doesn't have a class "([^"]*)"$/
59
     * @throws \Exception
60
     */
61
    public function theASTDoesnTHaveAClass($className)
62
    {
63
        $ast = $this->getAst();
64
        foreach ($ast->getFiles() as $file) {
65
            foreach ($file->getClasses() as $classDescriptor) {
66
                if ($classDescriptor->getName() === $className) {
67
                    throw new \Exception('Found unexpected class');
68
                }
69
            }
70
        }
71
    }
72
73
    /**
74
     * @Then /^the class named "([^"]*)" is in the default package$/
75
     * @throws \Exception
76
     */
77
    public function theASTHasAClassInDefaultPackage($class)
78
    {
79
        $class = $this->findClassByName($class);
80
81
        Assert::assertEquals('Default', $class->getPackage()->getName());
82
    }
83
84
    /**
85
     * @Then /^the AST has a trait named "([^"]*)" in file "([^"]*)"$/
86
     * @throws \Exception
87
     */
88
    public function theASTHasATraitNamedInFile($trait, $file)
89
    {
90
        $ast = $this->getAst();
91
92
        $file = $this->processFilePath($file);
93
        /** @var FileDescriptor $fileDescriptor */
94
        $fileDescriptor = $ast->getFiles()->get($file);
95
96
        /** @var TraitDescriptor $classDescriptor */
97
        foreach ($fileDescriptor->getTraits() as $classDescriptor) {
98
            if ($classDescriptor->getName() === $trait) {
99
                return;
100
            }
101
        }
102
103
        throw new \Exception(sprintf('Didn\'t find expected trait "%s" in "%s"', $trait, $file));
104
    }
105
106
    /**
107
     * @Then the class named ":class" has docblock with content:
108
     */
109
    public function classHasDocblockWithContent($class, PyStringNode $expectedContent)
110
    {
111
        $class = $this->findClassByName($class);
112
113
        Assert::assertEquals($expectedContent->getRaw(), $class->getDescription());
114
    }
115
116
    /**
117
     * @Then class ":classFqsen" has :docElement:
118
     * @throws Exception
119
     */
120
    public function classHasDocblockContent($classFqsen, $docElement, PyStringNode $value)
121
    {
122
        $class = $this->findClassByFqsen($classFqsen);
123
124
        $method = 'get' . $docElement;
125
126
        Assert::assertEquals($value->getRaw(), $class->{$method}());
127
    }
128
129
    /**
130
     * @Then class ":classFqsen" has :elementType :elementName with :docElement:
131
     */
132
    public function classHasElementWithDocblockContent($classFqsen, $elementType, $elementName, $docElement, PyStringNode $value)
133
    {
134
        $class = $this->findClassByFqsen($classFqsen);
135
136
        switch ($elementType) {
137
            case 'method':
138
            case 'constant':
139
                $method = $method = 'get' . $elementType . 's';
140
                break;
141
            case 'property':
142
                $method = 'getProperties';
143
                break;
144
            default:
145
                $method = 'get' . $elementType;
146
                break;
147
        }
148
149
        $element = $class-> {$method}()->get($elementName);
150
        $method = 'get' . $docElement;
151
        $actual = $element->{$method}();
152
153
        Assert::assertEquals($value->getRaw(), $actual, sprintf('"%s" does not match "%s"', $actual, $value->getRaw()));
154
    }
155
156
    /**
157
     * @Then class ":classFqsen" has version :value
158
     */
159
    public function classHasVersion($classFqsen, $value)
160
    {
161
        $class = $this->findClassByFqsen($classFqsen);
162
163
        /** @var VersionDescriptor $tag */
164
        foreach ($class->getVersion() as $tag) {
165
            if ($tag->getVersion() === $value) {
166
                return;
167
            }
168
        }
169
170
        Assert::fail(sprintf('Didn\'t find expected version "%s"', $value));
171
    }
172
173
    /**
174
     * @Then class ":classFqsen" without tag :tagName
175
     */
176
    public function classWithoutTag($classFqsen, $tagName)
177
    {
178
        $this->classHasTag($classFqsen, $tagName, 0);
179
    }
180
181
    /**
182
     * @param string $classFqsen
183
     * @param string $tagName
184
     * @param int $expectedNumber
185
     * @Then class ":classFqsen" has exactly :expectedNumber tag :tagName
186
     */
187
    public function classHasTag($classFqsen, $tagName, $expectedNumber)
188
    {
189
        $class = $this->findClassByFqsen($classFqsen);
190
        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...
191
    }
192
193
    /**
194
     * @param string $classFqsen
195
     * @param string $tagName
196
     * @param string $method
197
     * @Then class ":classFqsen" has a method named :method without tag :tagName
198
     */
199
    public function classHasMethodWithoutTag($classFqsen, $tagName, $method)
200
    {
201
        $this->classHasMethodWithExpectedCountTag($classFqsen, $tagName, $method, 0);
202
    }
203
204
    /**
205
     * @param string $classFqsen
206
     * @param string $tagName
207
     * @param string $methodName
208
     * @Then class ":classFqsen" has a method named :method with exactly :expected tag :tagName
209
     */
210
    public function classHasMethodWithExpectedCountTag($classFqsen, $tagName, $methodName, $expectedCount)
211
    {
212
        $class = $this->findClassByFqsen($classFqsen);
213
        $method = $class->getMethods()->get($methodName);
214
215
        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...
216
    }
217
218
    /**
219
     * @param string $classFqsen
220
     * @param string $methodName
221
     * @Then class ":classFqsen" has a method :method with argument ":argument is variadic
222
     */
223
    public function classHasMethodWithAgumentVariadic($classFqsen, $methodName, $argument)
224
    {
225
        $class = $this->findClassByFqsen($classFqsen);
226
        /** @var MethodDescriptor $method */
227
        $method = $class->getMethods()->get($methodName);
228
        Assert::assertArrayHasKey($argument, $method->getArguments());
229
        /** @var ArgumentDescriptor $argumentD */
230
        $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...
231
232
        //TODO: enable this check when we support variadic arguments.
233
        //Assert::assertTrue($argumentD->isVariadic(), 'Expected argument to be variadic');
0 ignored issues
show
Unused Code Comprehensibility introduced by
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...
234
    }
235
236
    /**
237
     * @param string $classFqsen
238
     * @param string $methodName
239
     * @Then class ":classFqsen" has a method :method
240
     */
241
    public function classHasMethod($classFqsen, $methodName)
242
    {
243
        $class = $this->findClassByFqsen($classFqsen);
244
        /** @var MethodDescriptor $method */
245
        $method = $class->getMethods()->get($methodName, null);
246
        Assert::assertInstanceOf(MethodDescriptor::class, $method);
247
        Assert::assertEquals($methodName, $method->getName());
248
    }
249
250
    /**
251
     * @param string $classFqsen
252
     * @param string $methodName
253
     * @param string $argument
254
     * @param string $type
255
     * @Then class ":classFqsen" has a method :method with argument :argument of type ":type"
256
     */
257
    public function classHasMethodWithAgumentOfType($classFqsen, $methodName, $argument, $type)
258
    {
259
        $class = $this->findClassByFqsen($classFqsen);
260
        /** @var MethodDescriptor $method */
261
        $method = $class->getMethods()->get($methodName);
262
        Assert::assertArrayHasKey($argument, $method->getArguments());
263
        /** @var ArgumentDescriptor $argumentDescriptor */
264
        $argumentDescriptor = $method->getArguments()[$argument];
265
266
        Assert::assertEquals($type, (string) $argumentDescriptor->getTypes());
267
    }
268
269
    /**
270
     * @param string $classFqsen
271
     * @param string $methodName
272
     * @param string $param
273
     * @param string $type
274
     * @Then class ":classFqsen" has a method :method with param :param of type ":type"
275
     */
276
    public function classHasMethodWithParamOfType($classFqsen, $methodName, $param, $type)
277
    {
278
        $class = $this->findClassByFqsen($classFqsen);
279
        /** @var MethodDescriptor $method */
280
        $method = $class->getMethods()->get($methodName);
281
        /** @var ParamDescriptor $paramDescriptor */
282
        foreach ($method->getParam() as $paramDescriptor) {
283
            if ($paramDescriptor->getName() === $param) {
284
                Assert::assertEquals($type, (string) $paramDescriptor->getTypes());
285
            }
286
        }
287
    }
288
289
    /**
290
     * @param string $classFqsen
291
     * @param string $constantName
292
     * @Then class ":classFqsen" has a constant :constantName
293
     */
294
    public function classHasConstant($classFqsen, $constantName)
295
    {
296
        /** @var ClassDescriptor $class */
297
        $class = $this->findClassByFqsen($classFqsen);
298
        $constant = $class->getConstants()->get($constantName);
299
        Assert::assertInstanceOf(ConstantDescriptor::class, $constant);
300
    }
301
302
    /**
303
     * @param string $className
304
     * @return ClassDescriptor
305
     * @throws \Exception
306
     */
307
    private function findClassByName($className)
308
    {
309
        $ast = $this->getAst();
310
        foreach ($ast->getFiles() as $file) {
311
            foreach ($file->getClasses() as $classDescriptor) {
312
                if ($classDescriptor->getName() === $className) {
313
                    return $classDescriptor;
314
                }
315
            }
316
        }
317
318
        throw new \Exception(sprintf('Didn\'t find expected class "%s"', $className));
319
    }
320
321
    /**
322
     * @param string $tagName
323
     * @param int $expectedNumber
324
     * @param DescriptorAbstract $element
325
     */
326
    private static function AssertTagCount($element, $tagName, $expectedNumber)
327
    {
328
        /** @var Collection $tagCollection */
329
        $tagCollection = $element->getTags()->get($tagName, new Collection());
330
331
        Assert::assertEquals((int) $expectedNumber, $tagCollection->count());
332
        if ($expectedNumber > 0) {
333
            Assert::assertEquals($tagName, $tagCollection[0]->getName());
334
        }
335
    }
336
337
    /**
338
     * @Then /^the ast has a file named "([^"]*)" with a summary:$/
339
     * @throws \Exception
340
     */
341
    public function theAstHasAFileNamedWithASummary(string $fileName, PyStringNode $string)
342
    {
343
        $ast = $this->getAst();
344
        /** @var FileDescriptor $file */
345
        $file = $ast->getFiles()->get($fileName);
346
347
        Assert::assertEquals($string->getRaw(), $file->getSummary());
348
    }
349
350
    /**
351
     * @param string $classFqsen
352
     * @param string $methodName
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
     * @throws Exception
369
     * @Then class ":classFqsen" has a magic method :method with returntype :returnType
370
     * @Then class ":classFqsen" has a magic method :method with returntype :returnType without description
371
     */
372
    public function classHasMagicMethodWithReturnType($classFqsen, $methodName, $returnType)
373
    {
374
        $response = $this->findMagicMethodResponse($classFqsen, $methodName);
375
376
        Assert::assertEquals($returnType, (string) $response->getTypes());
377
        Assert::assertEquals('', (string) $response->getDescription());
378
    }
379
380
    /**
381
     * @param string $classFqsen
382
     * @param string $methodName
383
     * @throws Exception
384
     * @Then class ":classFqsen" has a method :method with returntype :returnType with description:
385
     */
386
    public function classHasMethodWithReturnTypeAndDescription($classFqsen, $methodName, $returnType, PyStringNode $description)
387
    {
388
        $response = $this->findMethodResponse($classFqsen, $methodName);
389
390
        Assert::assertEquals($returnType, (string) $response->getTypes());
391
        Assert::assertEquals($description, (string) $response->getDescription());
392
    }
393
394
    /**
395
     * @Then class ":classFqsen" has a method ":method" without returntype
396
     * @throws \Exception
397
     */
398
    public function classReturnTaggetReturnWithoutAnyWithoutReturntype($classFqsen, $methodName)
399
    {
400
        $response = $this->findMethodResponse($classFqsen, $methodName);
401
        Assert::assertEquals('mixed', (string) $response->getTypes());
402
        Assert::assertEquals('', $response->getDescription());
403
    }
404
405
    /**
406
     * @throws Exception
407
     * @Then has function :fqsen with returntype :returnType
408
     * @Then has function :fqsen with returntype :returnType without description
409
     */
410
    public function functionWithReturnType($fqsen, $returnType)
411
    {
412
        $response = $this->findFunctionResponse($fqsen);
413
414
        Assert::assertEquals($returnType, (string) $response->getTypes());
415
        Assert::assertEquals('', (string) $response->getDescription());
416
    }
417
418
    /**
419
     * @throws Exception
420
     * @Then has function :fqsen with returntype :returnType with description:
421
     */
422
    public function functionWithReturnTypeAndDescription($fqsen, $returnType, PyStringNode $description)
423
    {
424
        $response = $this->findFunctionResponse($fqsen);
425
426
        Assert::assertEquals($returnType, (string) $response->getTypes());
427
        Assert::assertEquals($description, (string) $response->getDescription());
428
    }
429
430
    /**
431
     * @Then has function :fqsen without returntype
432
     * @throws \Exception
433
     */
434
    public function functionWithoutReturntype($fqsen)
435
    {
436
        $response = $this->findFunctionResponse($fqsen);
437
        Assert::assertEquals('mixed', (string) $response->getTypes());
438
        Assert::assertEquals('', $response->getDescription());
439
    }
440
441
    /**
442
     * @throws Exception
443
     */
444
    private function findMethodResponse($classFqsen, $methodName): ReturnDescriptor
445
    {
446
        $class = $this->findClassByFqsen($classFqsen);
447
        /** @var MethodDescriptor $method */
448
        $method = $class->getMethods()->get($methodName, null);
449
        Assert::assertInstanceOf(MethodDescriptor::class, $method);
450
        Assert::assertEquals($methodName, $method->getName());
451
452
        return $method->getResponse();
453
    }
454
455
    /**
456
     * @throws Exception
457
     */
458
    private function findMagicMethodResponse($classFqsen, $methodName): ReturnDescriptor
459
    {
460
        $class = $this->findClassByFqsen($classFqsen);
461
        $match = null;
462
463
        /** @var MethodDescriptor $method */
464
        foreach ($class->getMagicMethods() as $method) {
465
            if ($method->getName() === $methodName) {
466
                $match = $method;
467
            }
468
        }
469
470
        Assert::assertInstanceOf(MethodDescriptor::class, $match);
471
        Assert::assertEquals($methodName, $match->getName());
472
473
        return $match->getResponse();
474
    }
475
476
    /**
477
     * @throws Exception
478
     */
479
    private function findFunctionResponse(string $fqsen): ReturnDescriptor
480
    {
481
        $function = $this->findFunctionByFqsen($fqsen);
482
        return $function->getResponse();
483
    }
484
485
    /**
486
     * @Then class ":classFqsen" has a magic method :method with argument ":argument" of type :type
487
     */
488
    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...
489
    {
490
        $class = $this->findClassByFqsen($classFqsen);
491
        $match = null;
492
493
        /** @var MethodDescriptor $method */
494
        foreach ($class->getMagicMethods() as $method) {
495
            if ($method->getName() === $methodName) {
496
                $match = $method;
497
            }
498
        }
499
500
        Assert::assertInstanceOf(MethodDescriptor::class, $match);
501
        Assert::assertNotNull($match->getArguments()->get($argument));
502
    }
503
504
    /**
505
     * @Then /^(\d+) files should be parsed$/
506
     */
507
    public function filesShouldBeParsed($count)
508
    {
509
        \PHPUnit\Framework\Assert::assertSame((int) $count, $this->getAst()->getFiles()->count());
510
    }
511
512
    /**
513
     * @Given /^the ast has a function named "([^"]*)"$/
514
     */
515
    public function theAstHasAFunctionNamed($functionName)
516
    {
517
        Assert::assertInstanceOf(FunctionDescriptor::class, $this->getAst()->getIndexes()->get('functions')->get($functionName . '()'));
518
    }
519
520
    /**
521
     * @Given the namespace ':namespace' has a function named ':functionName'
522
     */
523
    public function theNamespaceFoo(string $namespace, string $functionName)
524
    {
525
        /** @var NamespaceDescriptor $namespace */
526
        $namespace = $this->getAst()->getIndexes()->get('namespaces')->get($namespace);
527
        Assert::assertInstanceOf(NamespaceDescriptor::class, $namespace);
528
        $function = $this->findFunctionInNamespace($namespace, $functionName);
529
        Assert::assertInstanceOf(FunctionDescriptor::class, $function);
530
    }
531
532
    private function findFunctionInNamespace(NamespaceDescriptor $namespace, string $functionName)
533
    {
534
        foreach ($namespace->getFunctions()->getAll() as $key => $function) {
535
            if ($function->getName() === $functionName) {
536
                return $function;
537
            }
538
        }
539
540
        return null;
541
    }
542
}
543