DocParser::Annotation()   F
last analyzed

Complexity

Conditions 46
Paths 15623

Size

Total Lines 156
Code Lines 76

Duplication

Lines 6
Ratio 3.85 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
dl 6
loc 156
rs 2
c 4
b 0
f 0
cc 46
eloc 76
nc 15623
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\Common\Annotations;
21
22
use Doctrine\Common\Annotations\Annotation\Attribute;
23
use ReflectionClass;
24
use Doctrine\Common\Annotations\Annotation\Enum;
25
use Doctrine\Common\Annotations\Annotation\Target;
26
use Doctrine\Common\Annotations\Annotation\Attributes;
27
28
/**
29
 * A parser for docblock annotations.
30
 *
31
 * It is strongly discouraged to change the default annotation parsing process.
32
 *
33
 * @author Benjamin Eberlei <[email protected]>
34
 * @author Guilherme Blanco <[email protected]>
35
 * @author Jonathan Wage <[email protected]>
36
 * @author Roman Borschel <[email protected]>
37
 * @author Johannes M. Schmitt <[email protected]>
38
 * @author Fabio B. Silva <[email protected]>
39
 */
40
final class DocParser
41
{
42
    /**
43
     * An array of all valid tokens for a class name.
44
     *
45
     * @var array
46
     */
47
    private static $classIdentifiers = array(
48
        DocLexer::T_IDENTIFIER,
49
        DocLexer::T_TRUE,
50
        DocLexer::T_FALSE,
51
        DocLexer::T_NULL
52
    );
53
54
    /**
55
     * The lexer.
56
     *
57
     * @var \Doctrine\Common\Annotations\DocLexer
58
     */
59
    private $lexer;
60
61
    /**
62
     * Current target context.
63
     *
64
     * @var string
65
     */
66
    private $target;
67
68
    /**
69
     * Doc parser used to collect annotation target.
70
     *
71
     * @var \Doctrine\Common\Annotations\DocParser
72
     */
73
    private static $metadataParser;
74
75
    /**
76
     * Flag to control if the current annotation is nested or not.
77
     *
78
     * @var boolean
79
     */
80
    private $isNestedAnnotation = false;
81
82
    /**
83
     * Hashmap containing all use-statements that are to be used when parsing
84
     * the given doc block.
85
     *
86
     * @var array
87
     */
88
    private $imports = array();
89
90
    /**
91
     * This hashmap is used internally to cache results of class_exists()
92
     * look-ups.
93
     *
94
     * @var array
95
     */
96
    private $classExists = array();
97
98
    /**
99
     * Whether annotations that have not been imported should be ignored.
100
     *
101
     * @var boolean
102
     */
103
    private $ignoreNotImportedAnnotations = false;
104
105
    /**
106
     * An array of default namespaces if operating in simple mode.
107
     *
108
     * @var array
109
     */
110
    private $namespaces = array();
111
112
    /**
113
     * A list with annotations that are not causing exceptions when not resolved to an annotation class.
114
     *
115
     * The names must be the raw names as used in the class, not the fully qualified
116
     * class names.
117
     *
118
     * @var array
119
     */
120
    private $ignoredAnnotationNames = array();
121
122
    /**
123
     * @var string
124
     */
125
    private $context = '';
126
127
    /**
128
     * Hash-map for caching annotation metadata.
129
     *
130
     * @var array
131
     */
132
    private static $annotationMetadata = array(
133
        'Doctrine\Common\Annotations\Annotation\Target' => array(
134
            'is_annotation'    => true,
135
            'has_constructor'  => true,
136
            'properties'       => array(),
137
            'targets_literal'  => 'ANNOTATION_CLASS',
138
            'targets'          => Target::TARGET_CLASS,
139
            'default_property' => 'value',
140
            'attribute_types'  => array(
141
                'value'  => array(
142
                    'required'  => false,
143
                    'type'      =>'array',
144
                    'array_type'=>'string',
145
                    'value'     =>'array<string>'
146
                )
147
             ),
148
        ),
149
        'Doctrine\Common\Annotations\Annotation\Attribute' => array(
150
            'is_annotation'    => true,
151
            'has_constructor'  => false,
152
            'targets_literal'  => 'ANNOTATION_ANNOTATION',
153
            'targets'          => Target::TARGET_ANNOTATION,
154
            'default_property' => 'name',
155
            'properties'       => array(
156
                'name'      => 'name',
157
                'type'      => 'type',
158
                'required'  => 'required'
159
            ),
160
            'attribute_types'  => array(
161
                'value'  => array(
162
                    'required'  => true,
163
                    'type'      =>'string',
164
                    'value'     =>'string'
165
                ),
166
                'type'  => array(
167
                    'required'  =>true,
168
                    'type'      =>'string',
169
                    'value'     =>'string'
170
                ),
171
                'required'  => array(
172
                    'required'  =>false,
173
                    'type'      =>'boolean',
174
                    'value'     =>'boolean'
175
                )
176
             ),
177
        ),
178
        'Doctrine\Common\Annotations\Annotation\Attributes' => array(
179
            'is_annotation'    => true,
180
            'has_constructor'  => false,
181
            'targets_literal'  => 'ANNOTATION_CLASS',
182
            'targets'          => Target::TARGET_CLASS,
183
            'default_property' => 'value',
184
            'properties'       => array(
185
                'value' => 'value'
186
            ),
187
            'attribute_types'  => array(
188
                'value' => array(
189
                    'type'      =>'array',
190
                    'required'  =>true,
191
                    'array_type'=>'Doctrine\Common\Annotations\Annotation\Attribute',
192
                    'value'     =>'array<Doctrine\Common\Annotations\Annotation\Attribute>'
193
                )
194
             ),
195
        ),
196
        'Doctrine\Common\Annotations\Annotation\Enum' => array(
197
            'is_annotation'    => true,
198
            'has_constructor'  => true,
199
            'targets_literal'  => 'ANNOTATION_PROPERTY',
200
            'targets'          => Target::TARGET_PROPERTY,
201
            'default_property' => 'value',
202
            'properties'       => array(
203
                'value' => 'value'
204
            ),
205
            'attribute_types'  => array(
206
                'value' => array(
207
                    'type'      => 'array',
208
                    'required'  => true,
209
                ),
210
                'literal' => array(
211
                    'type'      => 'array',
212
                    'required'  => false,
213
                ),
214
             ),
215
        ),
216
    );
217
218
    /**
219
     * Hash-map for handle types declaration.
220
     *
221
     * @var array
222
     */
223
    private static $typeMap = array(
224
        'float'     => 'double',
225
        'bool'      => 'boolean',
226
        // allow uppercase Boolean in honor of George Boole
227
        'Boolean'   => 'boolean',
228
        'int'       => 'integer',
229
    );
230
231
    /**
232
     * Constructs a new DocParser.
233
     */
234
    public function __construct()
235
    {
236
        $this->lexer = new DocLexer;
237
    }
238
239
    /**
240
     * Sets the annotation names that are ignored during the parsing process.
241
     *
242
     * The names are supposed to be the raw names as used in the class, not the
243
     * fully qualified class names.
244
     *
245
     * @param array $names
246
     *
247
     * @return void
248
     */
249
    public function setIgnoredAnnotationNames(array $names)
250
    {
251
        $this->ignoredAnnotationNames = $names;
252
    }
253
254
    /**
255
     * Sets ignore on not-imported annotations.
256
     *
257
     * @param boolean $bool
258
     *
259
     * @return void
260
     */
261
    public function setIgnoreNotImportedAnnotations($bool)
262
    {
263
        $this->ignoreNotImportedAnnotations = (boolean) $bool;
264
    }
265
266
    /**
267
     * Sets the default namespaces.
268
     *
269
     * @param array $namespace
270
     *
271
     * @return void
272
     *
273
     * @throws \RuntimeException
274
     */
275
    public function addNamespace($namespace)
276
    {
277
        if ($this->imports) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->imports of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
278
            throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
279
        }
280
281
        $this->namespaces[] = $namespace;
282
    }
283
284
    /**
285
     * Sets the imports.
286
     *
287
     * @param array $imports
288
     *
289
     * @return void
290
     *
291
     * @throws \RuntimeException
292
     */
293
    public function setImports(array $imports)
294
    {
295
        if ($this->namespaces) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namespaces of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
296
            throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
297
        }
298
299
        $this->imports = $imports;
300
    }
301
302
    /**
303
     * Sets current target context as bitmask.
304
     *
305
     * @param integer $target
306
     *
307
     * @return void
308
     */
309
    public function setTarget($target)
310
    {
311
        $this->target = $target;
0 ignored issues
show
Documentation Bug introduced by
The property $target was declared of type string, but $target is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
312
    }
313
314
    /**
315
     * Parses the given docblock string for annotations.
316
     *
317
     * @param string $input   The docblock string to parse.
318
     * @param string $context The parsing context.
319
     *
320
     * @return array Array of annotations. If no annotations are found, an empty array is returned.
321
     */
322
    public function parse($input, $context = '')
323
    {
324
        $pos = $this->findInitialTokenPosition($input);
325
        if ($pos === null) {
326
            return array();
327
        }
328
329
        $this->context = $context;
330
331
        $this->lexer->setInput(trim(substr($input, $pos), '* /'));
332
        $this->lexer->moveNext();
333
334
        return $this->Annotations();
335
    }
336
337
    /**
338
     * Finds the first valid annotation
339
     *
340
     * @param string $input The docblock string to parse
341
     *
342
     * @return int|null
343
     */
344
    private function findInitialTokenPosition($input)
345
    {
346
        $pos = 0;
347
348
        // search for first valid annotation
349
        while (($pos = strpos($input, '@', $pos)) !== false) {
350
            // if the @ is preceded by a space or * it is valid
351
            if ($pos === 0 || $input[$pos - 1] === ' ' || $input[$pos - 1] === '*') {
352
                return $pos;
353
            }
354
355
            $pos++;
356
        }
357
358
        return null;
359
    }
360
361
    /**
362
     * Attempts to match the given token with the current lookahead token.
363
     * If they match, updates the lookahead token; otherwise raises a syntax error.
364
     *
365
     * @param integer $token Type of token.
366
     *
367
     * @return boolean True if tokens match; false otherwise.
368
     */
369
    private function match($token)
370
    {
371
        if ( ! $this->lexer->isNextToken($token) ) {
372
            $this->syntaxError($this->lexer->getLiteral($token));
373
        }
374
375
        return $this->lexer->moveNext();
376
    }
377
378
    /**
379
     * Attempts to match the current lookahead token with any of the given tokens.
380
     *
381
     * If any of them matches, this method updates the lookahead token; otherwise
382
     * a syntax error is raised.
383
     *
384
     * @param array $tokens
385
     *
386
     * @return boolean
387
     */
388
    private function matchAny(array $tokens)
389
    {
390
        if ( ! $this->lexer->isNextTokenAny($tokens)) {
391
            $this->syntaxError(implode(' or ', array_map(array($this->lexer, 'getLiteral'), $tokens)));
392
        }
393
394
        return $this->lexer->moveNext();
395
    }
396
397
    /**
398
     * Generates a new syntax error.
399
     *
400
     * @param string     $expected Expected string.
401
     * @param array|null $token    Optional token.
402
     *
403
     * @return void
404
     *
405
     * @throws AnnotationException
406
     */
407
    private function syntaxError($expected, $token = null)
408
    {
409
        if ($token === null) {
410
            $token = $this->lexer->lookahead;
411
        }
412
413
        $message  = sprintf('Expected %s, got ', $expected);
414
        $message .= ($this->lexer->lookahead === null)
415
            ? 'end of string'
416
            : sprintf("'%s' at position %s", $token['value'], $token['position']);
417
418
        if (strlen($this->context)) {
419
            $message .= ' in ' . $this->context;
420
        }
421
422
        $message .= '.';
423
424
        throw AnnotationException::syntaxError($message);
425
    }
426
427
    /**
428
     * Attempts to check if a class exists or not. This never goes through the PHP autoloading mechanism
429
     * but uses the {@link AnnotationRegistry} to load classes.
430
     *
431
     * @param string $fqcn
432
     *
433
     * @return boolean
434
     */
435
    private function classExists($fqcn)
436
    {
437
        if (isset($this->classExists[$fqcn])) {
438
            return $this->classExists[$fqcn];
439
        }
440
441
        // first check if the class already exists, maybe loaded through another AnnotationReader
442
        if (class_exists($fqcn, false)) {
443
            return $this->classExists[$fqcn] = true;
444
        }
445
446
        // final check, does this class exist?
447
        return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn);
448
    }
449
450
    /**
451
     * Collects parsing metadata for a given annotation class
452
     *
453
     * @param string $name The annotation name
454
     *
455
     * @return void
456
     */
457
    private function collectAnnotationMetadata($name)
458
    {
459
        if (self::$metadataParser === null) {
460
            self::$metadataParser = new self();
461
462
            self::$metadataParser->setIgnoreNotImportedAnnotations(true);
463
            self::$metadataParser->setIgnoredAnnotationNames($this->ignoredAnnotationNames);
464
            self::$metadataParser->setImports(array(
465
                'enum'          => 'Doctrine\Common\Annotations\Annotation\Enum',
466
                'target'        => 'Doctrine\Common\Annotations\Annotation\Target',
467
                'attribute'     => 'Doctrine\Common\Annotations\Annotation\Attribute',
468
                'attributes'    => 'Doctrine\Common\Annotations\Annotation\Attributes'
469
            ));
470
471
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Enum.php');
472
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Target.php');
473
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attribute.php');
474
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attributes.php');
475
        }
476
477
        $class      = new \ReflectionClass($name);
478
        $docComment = $class->getDocComment();
479
480
        // Sets default values for annotation metadata
481
        $metadata = array(
482
            'default_property' => null,
483
            'has_constructor'  => (null !== $constructor = $class->getConstructor()) && $constructor->getNumberOfParameters() > 0,
484
            'properties'       => array(),
485
            'property_types'   => array(),
486
            'attribute_types'  => array(),
487
            'targets_literal'  => null,
488
            'targets'          => Target::TARGET_ALL,
489
            'is_annotation'    => false !== strpos($docComment, '@Annotation'),
490
        );
491
492
        // verify that the class is really meant to be an annotation
493
        if ($metadata['is_annotation']) {
494
            self::$metadataParser->setTarget(Target::TARGET_CLASS);
495
496
            foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) {
497
                if ($annotation instanceof Target) {
498
                    $metadata['targets']         = $annotation->targets;
499
                    $metadata['targets_literal'] = $annotation->literal;
500
501
                    continue;
502
                }
503
504
                if ($annotation instanceof Attributes) {
505
                    foreach ($annotation->value as $attribute) {
506
                        $this->collectAttributeTypeMetadata($metadata, $attribute);
507
                    }
508
                }
509
            }
510
511
            // if not has a constructor will inject values into public properties
512
            if (false === $metadata['has_constructor']) {
513
                // collect all public properties
514
                foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
515
                    $metadata['properties'][$property->name] = $property->name;
516
517
                    if (false === ($propertyComment = $property->getDocComment())) {
518
                        continue;
519
                    }
520
521
                    $attribute = new Attribute();
522
523
                    $attribute->required = (false !== strpos($propertyComment, '@Required'));
524
                    $attribute->name     = $property->name;
525
                    $attribute->type     = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',$propertyComment, $matches))
526
                        ? $matches[1]
0 ignored issues
show
Bug introduced by
The variable $matches does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
527
                        : 'mixed';
528
529
                    $this->collectAttributeTypeMetadata($metadata, $attribute);
530
531
                    // checks if the property has @Enum
532
                    if (false !== strpos($propertyComment, '@Enum')) {
533
                        $context = 'property ' . $class->name . "::\$" . $property->name;
534
535
                        self::$metadataParser->setTarget(Target::TARGET_PROPERTY);
536
537
                        foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) {
538
                            if ( ! $annotation instanceof Enum) {
539
                                continue;
540
                            }
541
542
                            $metadata['enum'][$property->name]['value']   = $annotation->value;
543
                            $metadata['enum'][$property->name]['literal'] = ( ! empty($annotation->literal))
544
                                ? $annotation->literal
545
                                : $annotation->value;
546
                        }
547
                    }
548
                }
549
550
                // choose the first property as default property
551
                $metadata['default_property'] = reset($metadata['properties']);
552
            }
553
        }
554
555
        self::$annotationMetadata[$name] = $metadata;
556
    }
557
558
    /**
559
     * Collects parsing metadata for a given attribute.
560
     *
561
     * @param array     $metadata
562
     * @param Attribute $attribute
563
     *
564
     * @return void
565
     */
566
    private function collectAttributeTypeMetadata(&$metadata, Attribute $attribute)
567
    {
568
        // handle internal type declaration
569
        $type = isset(self::$typeMap[$attribute->type])
570
            ? self::$typeMap[$attribute->type]
571
            : $attribute->type;
572
573
        // handle the case if the property type is mixed
574
        if ('mixed' === $type) {
575
            return;
576
        }
577
578
        // Evaluate type
579
        switch (true) {
580
            // Checks if the property has array<type>
581 View Code Duplication
            case (false !== $pos = strpos($type, '<')):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
582
                $arrayType  = substr($type, $pos + 1, -1);
0 ignored issues
show
Bug introduced by
The variable $pos seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
583
                $type       = 'array';
584
585
                if (isset(self::$typeMap[$arrayType])) {
586
                    $arrayType = self::$typeMap[$arrayType];
587
                }
588
589
                $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType;
590
                break;
591
592
            // Checks if the property has type[]
593 View Code Duplication
            case (false !== $pos = strrpos($type, '[')):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
594
                $arrayType  = substr($type, 0, $pos);
0 ignored issues
show
Bug introduced by
The variable $pos seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
595
                $type       = 'array';
596
597
                if (isset(self::$typeMap[$arrayType])) {
598
                    $arrayType = self::$typeMap[$arrayType];
599
                }
600
601
                $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType;
602
                break;
603
        }
604
605
        $metadata['attribute_types'][$attribute->name]['type']     = $type;
606
        $metadata['attribute_types'][$attribute->name]['value']    = $attribute->type;
607
        $metadata['attribute_types'][$attribute->name]['required'] = $attribute->required;
608
    }
609
610
    /**
611
     * Annotations ::= Annotation {[ "*" ]* [Annotation]}*
612
     *
613
     * @return array
614
     */
615
    private function Annotations()
616
    {
617
        $annotations = array();
618
619
        while (null !== $this->lexer->lookahead) {
620
            if (DocLexer::T_AT !== $this->lexer->lookahead['type']) {
621
                $this->lexer->moveNext();
622
                continue;
623
            }
624
625
            // make sure the @ is preceded by non-catchable pattern
626
            if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) {
627
                $this->lexer->moveNext();
628
                continue;
629
            }
630
631
            // make sure the @ is followed by either a namespace separator, or
632
            // an identifier token
633
            if ((null === $peek = $this->lexer->glimpse())
634
                || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true))
635
                || $peek['position'] !== $this->lexer->lookahead['position'] + 1) {
636
                $this->lexer->moveNext();
637
                continue;
638
            }
639
640
            $this->isNestedAnnotation = false;
641
            if (false !== $annot = $this->Annotation()) {
642
                $annotations[] = $annot;
643
            }
644
        }
645
646
        return $annotations;
647
    }
648
649
    /**
650
     * Annotation     ::= "@" AnnotationName MethodCall
651
     * AnnotationName ::= QualifiedName | SimpleName
652
     * QualifiedName  ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName
653
     * NameSpacePart  ::= identifier | null | false | true
654
     * SimpleName     ::= identifier | null | false | true
655
     *
656
     * @return mixed False if it is not a valid annotation.
657
     *
658
     * @throws AnnotationException
659
     */
660
    private function Annotation()
661
    {
662
        $this->match(DocLexer::T_AT);
663
664
        // check if we have an annotation
665
        $name = $this->Identifier();
666
667
        // only process names which are not fully qualified, yet
668
        // fully qualified names must start with a \
669
        $originalName = $name;
670
671
        if ('\\' !== $name[0]) {
672
            $alias = (false === $pos = strpos($name, '\\'))? $name : substr($name, 0, $pos);
673
            $found = false;
674
675
            if ($this->namespaces) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namespaces of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
676
                foreach ($this->namespaces as $namespace) {
677
                    if ($this->classExists($namespace.'\\'.$name)) {
678
                        $name = $namespace.'\\'.$name;
679
                        $found = true;
680
                        break;
681
                    }
682
                }
683
            } elseif (isset($this->imports[$loweredAlias = strtolower($alias)])) {
684
                $found = true;
685
                $name  = (false !== $pos)
686
                    ? $this->imports[$loweredAlias] . substr($name, $pos)
687
                    : $this->imports[$loweredAlias];
688
            } elseif ( ! isset($this->ignoredAnnotationNames[$name])
689
                && isset($this->imports['__NAMESPACE__'])
690
                && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name)
691
            ) {
692
                $name  = $this->imports['__NAMESPACE__'].'\\'.$name;
693
                $found = true;
694
            } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) {
695
                $found = true;
696
            }
697
698
            if ( ! $found) {
699
                if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
700
                    return false;
701
                }
702
703
                throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s was never imported. Did you maybe forget to add a "use" statement for this annotation?', $name, $this->context));
704
            }
705
        }
706
707
        if ( ! $this->classExists($name)) {
708
            throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context));
709
        }
710
711
        // at this point, $name contains the fully qualified class name of the
712
        // annotation, and it is also guaranteed that this class exists, and
713
        // that it is loaded
714
715
716
        // collects the metadata annotation only if there is not yet
717
        if ( ! isset(self::$annotationMetadata[$name])) {
718
            $this->collectAnnotationMetadata($name);
719
        }
720
721
        // verify that the class is really meant to be an annotation and not just any ordinary class
722
        if (self::$annotationMetadata[$name]['is_annotation'] === false) {
723
            if (isset($this->ignoredAnnotationNames[$originalName])) {
724
                return false;
725
            }
726
727
            throw AnnotationException::semanticalError(sprintf('The class "%s" is not annotated with @Annotation. Are you sure this class can be used as annotation? If so, then you need to add @Annotation to the _class_ doc comment of "%s". If it is indeed no annotation, then you need to add @IgnoreAnnotation("%s") to the _class_ doc comment of %s.', $name, $name, $originalName, $this->context));
728
        }
729
730
        //if target is nested annotation
731
        $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target;
732
733
        // Next will be nested
734
        $this->isNestedAnnotation = true;
735
736
        //if annotation does not support current target
737
        if (0 === (self::$annotationMetadata[$name]['targets'] & $target) && $target) {
738
            throw AnnotationException::semanticalError(
739
                sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.',
740
                     $originalName, $this->context, self::$annotationMetadata[$name]['targets_literal'])
741
            );
742
        }
743
744
        $values = $this->MethodCall();
745
746
        if (isset(self::$annotationMetadata[$name]['enum'])) {
747
            // checks all declared attributes
748
            foreach (self::$annotationMetadata[$name]['enum'] as $property => $enum) {
749
                // checks if the attribute is a valid enumerator
750
                if (isset($values[$property]) && ! in_array($values[$property], $enum['value'])) {
751
                    throw AnnotationException::enumeratorError($property, $name, $this->context, $enum['literal'], $values[$property]);
752
                }
753
            }
754
        }
755
756
        // checks all declared attributes
757
        foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) {
758
            if ($property === self::$annotationMetadata[$name]['default_property']
759
                && !isset($values[$property]) && isset($values['value'])) {
760
                $property = 'value';
761
            }
762
763
            // handle a not given attribute or null value
764
            if (!isset($values[$property])) {
765
                if ($type['required']) {
766
                    throw AnnotationException::requiredError($property, $originalName, $this->context, 'a(n) '.$type['value']);
767
                }
768
769
                continue;
770
            }
771
772
            if ($type['type'] === 'array') {
773
                // handle the case of a single value
774
                if ( ! is_array($values[$property])) {
775
                    $values[$property] = array($values[$property]);
776
                }
777
778
                // checks if the attribute has array type declaration, such as "array<string>"
779
                if (isset($type['array_type'])) {
780
                    foreach ($values[$property] as $item) {
781
                        if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) {
782
                            throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item);
783
                        }
784
                    }
785
                }
786
            } elseif (gettype($values[$property]) !== $type['type'] && !$values[$property] instanceof $type['type']) {
787
                throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'a(n) '.$type['value'], $values[$property]);
788
            }
789
        }
790
791
        // check if the annotation expects values via the constructor,
792
        // or directly injected into public properties
793
        if (self::$annotationMetadata[$name]['has_constructor'] === true) {
794
            return new $name($values);
795
        }
796
797
        $instance = new $name();
798
799
        foreach ($values as $property => $value) {
800
            if (!isset(self::$annotationMetadata[$name]['properties'][$property])) {
801 View Code Duplication
                if ('value' !== $property) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
802
                    throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not have a property named "%s". Available properties: %s', $originalName, $this->context, $property, implode(', ', self::$annotationMetadata[$name]['properties'])));
803
                }
804
805
                // handle the case if the property has no annotations
806 View Code Duplication
                if ( ! $property = self::$annotationMetadata[$name]['default_property']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
807
                    throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not accept any values, but got %s.', $originalName, $this->context, json_encode($values)));
808
                }
809
            }
810
811
            $instance->{$property} = $value;
812
        }
813
814
        return $instance;
815
    }
816
817
    /**
818
     * MethodCall ::= ["(" [Values] ")"]
819
     *
820
     * @return array
821
     */
822
    private function MethodCall()
823
    {
824
        $values = array();
825
826
        if ( ! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
827
            return $values;
828
        }
829
830
        $this->match(DocLexer::T_OPEN_PARENTHESIS);
831
832
        if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
833
            $values = $this->Values();
834
        }
835
836
        $this->match(DocLexer::T_CLOSE_PARENTHESIS);
837
838
        return $values;
839
    }
840
841
    /**
842
     * Values ::= Array | Value {"," Value}* [","]
843
     *
844
     * @return array
845
     */
846
    private function Values()
847
    {
848
        $values = array($this->Value());
849
850
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
851
            $this->match(DocLexer::T_COMMA);
852
853
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
854
                break;
855
            }
856
857
            $token = $this->lexer->lookahead;
858
            $value = $this->Value();
859
860
            if ( ! is_object($value) && ! is_array($value)) {
861
                $this->syntaxError('Value', $token);
862
            }
863
864
            $values[] = $value;
865
        }
866
867
        foreach ($values as $k => $value) {
868
            if (is_object($value) && $value instanceof \stdClass) {
869
                $values[$value->name] = $value->value;
870
            } else if ( ! isset($values['value'])){
871
                $values['value'] = $value;
872
            } else {
873
                if ( ! is_array($values['value'])) {
874
                    $values['value'] = array($values['value']);
875
                }
876
877
                $values['value'][] = $value;
878
            }
879
880
            unset($values[$k]);
881
        }
882
883
        return $values;
884
    }
885
886
    /**
887
     * Constant ::= integer | string | float | boolean
888
     *
889
     * @return mixed
890
     *
891
     * @throws AnnotationException
892
     */
893
    private function Constant()
894
    {
895
        $identifier = $this->Identifier();
896
897
        if ( ! defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) {
898
            list($className, $const) = explode('::', $identifier);
899
900
            $alias = (false === $pos = strpos($className, '\\')) ? $className : substr($className, 0, $pos);
901
            $found = false;
902
903
            switch (true) {
904
                case !empty ($this->namespaces):
905
                    foreach ($this->namespaces as $ns) {
906 View Code Duplication
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
907
                             $className = $ns.'\\'.$className;
908
                             $found = true;
909
                             break;
910
                        }
911
                    }
912
                    break;
913
914
                case isset($this->imports[$loweredAlias = strtolower($alias)]):
915
                    $found     = true;
916
                    $className = (false !== $pos)
917
                        ? $this->imports[$loweredAlias] . substr($className, $pos)
0 ignored issues
show
Bug introduced by
The variable $loweredAlias seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
918
                        : $this->imports[$loweredAlias];
0 ignored issues
show
Bug introduced by
The variable $loweredAlias seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
919
                    break;
920
921
                default:
922
                    if(isset($this->imports['__NAMESPACE__'])) {
923
                        $ns = $this->imports['__NAMESPACE__'];
924
925 View Code Duplication
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
926
                            $className = $ns.'\\'.$className;
927
                            $found = true;
928
                        }
929
                    }
930
                    break;
931
            }
932
933
            if ($found) {
934
                 $identifier = $className . '::' . $const;
935
            }
936
        }
937
938
        // checks if identifier ends with ::class, \strlen('::class') === 7
939
        $classPos = stripos($identifier, '::class');
940
        if ($classPos === strlen($identifier) - 7) {
941
            return substr($identifier, 0, $classPos);
942
        }
943
944
        if (!defined($identifier)) {
945
            throw AnnotationException::semanticalErrorConstants($identifier, $this->context);
946
        }
947
948
        return constant($identifier);
949
    }
950
951
    /**
952
     * Identifier ::= string
953
     *
954
     * @return string
955
     */
956
    private function Identifier()
957
    {
958
        // check if we have an annotation
959
        if ( ! $this->lexer->isNextTokenAny(self::$classIdentifiers)) {
960
            $this->syntaxError('namespace separator or identifier');
961
        }
962
963
        $this->lexer->moveNext();
964
965
        $className = $this->lexer->token['value'];
966
967
        while ($this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value']))
968
                && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) {
969
970
            $this->match(DocLexer::T_NAMESPACE_SEPARATOR);
971
            $this->matchAny(self::$classIdentifiers);
972
973
            $className .= '\\' . $this->lexer->token['value'];
974
        }
975
976
        return $className;
977
    }
978
979
    /**
980
     * Value ::= PlainValue | FieldAssignment
981
     *
982
     * @return mixed
983
     */
984
    private function Value()
985
    {
986
        $peek = $this->lexer->glimpse();
987
988
        if (DocLexer::T_EQUALS === $peek['type']) {
989
            return $this->FieldAssignment();
990
        }
991
992
        return $this->PlainValue();
993
    }
994
995
    /**
996
     * PlainValue ::= integer | string | float | boolean | Array | Annotation
997
     *
998
     * @return mixed
999
     */
1000
    private function PlainValue()
1001
    {
1002
        if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
1003
            return $this->Arrayx();
1004
        }
1005
1006
        if ($this->lexer->isNextToken(DocLexer::T_AT)) {
1007
            return $this->Annotation();
1008
        }
1009
1010
        if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1011
            return $this->Constant();
1012
        }
1013
1014
        switch ($this->lexer->lookahead['type']) {
1015
            case DocLexer::T_STRING:
1016
                $this->match(DocLexer::T_STRING);
1017
                return $this->lexer->token['value'];
1018
1019
            case DocLexer::T_INTEGER:
1020
                $this->match(DocLexer::T_INTEGER);
1021
                return (int)$this->lexer->token['value'];
1022
1023
            case DocLexer::T_FLOAT:
1024
                $this->match(DocLexer::T_FLOAT);
1025
                return (float)$this->lexer->token['value'];
1026
1027
            case DocLexer::T_TRUE:
1028
                $this->match(DocLexer::T_TRUE);
1029
                return true;
1030
1031
            case DocLexer::T_FALSE:
1032
                $this->match(DocLexer::T_FALSE);
1033
                return false;
1034
1035
            case DocLexer::T_NULL:
1036
                $this->match(DocLexer::T_NULL);
1037
                return null;
1038
1039
            default:
1040
                $this->syntaxError('PlainValue');
1041
        }
1042
    }
1043
1044
    /**
1045
     * FieldAssignment ::= FieldName "=" PlainValue
1046
     * FieldName ::= identifier
1047
     *
1048
     * @return array
1049
     */
1050
    private function FieldAssignment()
1051
    {
1052
        $this->match(DocLexer::T_IDENTIFIER);
1053
        $fieldName = $this->lexer->token['value'];
1054
1055
        $this->match(DocLexer::T_EQUALS);
1056
1057
        $item = new \stdClass();
1058
        $item->name  = $fieldName;
1059
        $item->value = $this->PlainValue();
1060
1061
        return $item;
1062
    }
1063
1064
    /**
1065
     * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}"
1066
     *
1067
     * @return array
1068
     */
1069
    private function Arrayx()
1070
    {
1071
        $array = $values = array();
1072
1073
        $this->match(DocLexer::T_OPEN_CURLY_BRACES);
1074
1075
        // If the array is empty, stop parsing and return.
1076
        if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1077
            $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1078
1079
            return $array;
1080
        }
1081
1082
        $values[] = $this->ArrayEntry();
1083
1084
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
1085
            $this->match(DocLexer::T_COMMA);
1086
1087
            // optional trailing comma
1088
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1089
                break;
1090
            }
1091
1092
            $values[] = $this->ArrayEntry();
1093
        }
1094
1095
        $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1096
1097
        foreach ($values as $value) {
1098
            list ($key, $val) = $value;
1099
1100
            if ($key !== null) {
1101
                $array[$key] = $val;
1102
            } else {
1103
                $array[] = $val;
1104
            }
1105
        }
1106
1107
        return $array;
1108
    }
1109
1110
    /**
1111
     * ArrayEntry ::= Value | KeyValuePair
1112
     * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant
1113
     * Key ::= string | integer | Constant
1114
     *
1115
     * @return array
1116
     */
1117
    private function ArrayEntry()
1118
    {
1119
        $peek = $this->lexer->glimpse();
1120
1121
        if (DocLexer::T_EQUALS === $peek['type']
1122
                || DocLexer::T_COLON === $peek['type']) {
1123
1124
            if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1125
                $key = $this->Constant();
1126
            } else {
1127
                $this->matchAny(array(DocLexer::T_INTEGER, DocLexer::T_STRING));
1128
                $key = $this->lexer->token['value'];
1129
            }
1130
1131
            $this->matchAny(array(DocLexer::T_EQUALS, DocLexer::T_COLON));
1132
1133
            return array($key, $this->PlainValue());
1134
        }
1135
1136
        return array(null, $this->Value());
1137
    }
1138
}
1139