Completed
Pull Request — master (#167)
by Grégoire
03:31
created

DocParser::Annotation()   F

Complexity

Conditions 47
Paths 17527

Size

Total Lines 166
Code Lines 82

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 82
CRAP Score 47.0038

Importance

Changes 0
Metric Value
eloc 82
dl 0
loc 166
ccs 82
cts 83
cp 0.988
rs 0
c 0
b 0
f 0
cc 47
nc 17527
nop 0
crap 47.0038

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
namespace Doctrine\Annotations;
4
5
use Doctrine\Annotations\Annotation\Attribute;
6
use ReflectionClass;
7
use Doctrine\Annotations\Annotation\Enum;
8
use Doctrine\Annotations\Annotation\Target;
9
use Doctrine\Annotations\Annotation\Attributes;
10
11
/**
12
 * A parser for docblock annotations.
13
 *
14
 * It is strongly discouraged to change the default annotation parsing process.
15
 *
16
 * @author Benjamin Eberlei <[email protected]>
17
 * @author Guilherme Blanco <[email protected]>
18
 * @author Jonathan Wage <[email protected]>
19
 * @author Roman Borschel <[email protected]>
20
 * @author Johannes M. Schmitt <[email protected]>
21
 * @author Fabio B. Silva <[email protected]>
22
 */
23
final class DocParser
24
{
25
    /**
26
     * An array of all valid tokens for a class name.
27
     *
28
     * @var array
29
     */
30
    private static $classIdentifiers = [
31
        DocLexer::T_IDENTIFIER,
32
        DocLexer::T_TRUE,
33
        DocLexer::T_FALSE,
34
        DocLexer::T_NULL
35
    ];
36
37
    /**
38
     * The lexer.
39
     *
40
     * @var \Doctrine\Annotations\DocLexer
41
     */
42
    private $lexer;
43
44
    /**
45
     * Current target context.
46
     *
47
     * @var integer
48
     */
49
    private $target;
50
51
    /**
52
     * Doc parser used to collect annotation target.
53
     *
54
     * @var \Doctrine\Annotations\DocParser
55
     */
56
    private static $metadataParser;
57
58
    /**
59
     * Flag to control if the current annotation is nested or not.
60
     *
61
     * @var boolean
62
     */
63
    private $isNestedAnnotation = false;
64
65
    /**
66
     * Hashmap containing all use-statements that are to be used when parsing
67
     * the given doc block.
68
     *
69
     * @var array
70
     */
71
    private $imports = [];
72
73
    /**
74
     * This hashmap is used internally to cache results of class_exists()
75
     * look-ups.
76
     *
77
     * @var array
78
     */
79
    private $classExists = [];
80
81
    /**
82
     * Whether annotations that have not been imported should be ignored.
83
     *
84
     * @var boolean
85
     */
86
    private $ignoreNotImportedAnnotations = false;
87
88
    /**
89
     * An array of default namespaces if operating in simple mode.
90
     *
91
     * @var string[]
92
     */
93
    private $namespaces = [];
94
95
    /**
96
     * A list with annotations that are not causing exceptions when not resolved to an annotation class.
97
     *
98
     * The names must be the raw names as used in the class, not the fully qualified
99
     * class names.
100
     *
101
     * @var bool[] indexed by annotation name
102
     */
103
    private $ignoredAnnotationNames = [];
104
105
    /**
106
     * A list with annotations in namespaced format
107
     * that are not causing exceptions when not resolved to an annotation class.
108
     *
109
     * @var bool[] indexed by namespace name
110
     */
111
    private $ignoredAnnotationNamespaces = [];
112
113
    /**
114
     * @var string
115
     */
116
    private $context = '';
117
118
    /**
119
     * Hash-map for caching annotation metadata.
120
     *
121
     * @var array
122
     */
123
    private static $annotationMetadata = [
124
        'Doctrine\Annotations\Annotation\Target' => [
125
            'is_annotation'    => true,
126
            'has_constructor'  => true,
127
            'properties'       => [],
128
            'targets_literal'  => 'ANNOTATION_CLASS',
129
            'targets'          => Target::TARGET_CLASS,
130
            'default_property' => 'value',
131
            'attribute_types'  => [
132
                'value'  => [
133
                    'required'  => false,
134
                    'type'      =>'array',
135
                    'array_type'=>'string',
136
                    'value'     =>'array<string>'
137
                ]
138
             ],
139
        ],
140
        'Doctrine\Annotations\Annotation\Attribute' => [
141
            'is_annotation'    => true,
142
            'has_constructor'  => false,
143
            'targets_literal'  => 'ANNOTATION_ANNOTATION',
144
            'targets'          => Target::TARGET_ANNOTATION,
145
            'default_property' => 'name',
146
            'properties'       => [
147
                'name'      => 'name',
148
                'type'      => 'type',
149
                'required'  => 'required'
150
            ],
151
            'attribute_types'  => [
152
                'value'  => [
153
                    'required'  => true,
154
                    'type'      =>'string',
155
                    'value'     =>'string'
156
                ],
157
                'type'  => [
158
                    'required'  =>true,
159
                    'type'      =>'string',
160
                    'value'     =>'string'
161
                ],
162
                'required'  => [
163
                    'required'  =>false,
164
                    'type'      =>'boolean',
165
                    'value'     =>'boolean'
166
                ]
167
             ],
168
        ],
169
        'Doctrine\Annotations\Annotation\Attributes' => [
170
            'is_annotation'    => true,
171
            'has_constructor'  => false,
172
            'targets_literal'  => 'ANNOTATION_CLASS',
173
            'targets'          => Target::TARGET_CLASS,
174
            'default_property' => 'value',
175
            'properties'       => [
176
                'value' => 'value'
177
            ],
178
            'attribute_types'  => [
179
                'value' => [
180
                    'type'      =>'array',
181
                    'required'  =>true,
182
                    'array_type'=>'Doctrine\Annotations\Annotation\Attribute',
183
                    'value'     =>'array<Doctrine\Annotations\Annotation\Attribute>'
184
                ]
185
             ],
186
        ],
187
        'Doctrine\Annotations\Annotation\Enum' => [
188
            'is_annotation'    => true,
189
            'has_constructor'  => true,
190
            'targets_literal'  => 'ANNOTATION_PROPERTY',
191
            'targets'          => Target::TARGET_PROPERTY,
192
            'default_property' => 'value',
193
            'properties'       => [
194
                'value' => 'value'
195
            ],
196
            'attribute_types'  => [
197
                'value' => [
198
                    'type'      => 'array',
199
                    'required'  => true,
200
                ],
201
                'literal' => [
202
                    'type'      => 'array',
203
                    'required'  => false,
204
                ],
205
             ],
206
        ],
207
    ];
208
209
    /**
210
     * Hash-map for handle types declaration.
211
     *
212
     * @var array
213
     */
214
    private static $typeMap = [
215
        'float'     => 'double',
216
        'bool'      => 'boolean',
217
        // allow uppercase Boolean in honor of George Boole
218
        'Boolean'   => 'boolean',
219
        'int'       => 'integer',
220
    ];
221
222
    /**
223
     * Constructs a new DocParser.
224
     */
225 329
    public function __construct()
226
    {
227 329
        $this->lexer = new DocLexer;
228 329
    }
229
230
    /**
231
     * Sets the annotation names that are ignored during the parsing process.
232
     *
233
     * The names are supposed to be the raw names as used in the class, not the
234
     * fully qualified class names.
235
     *
236
     * @param bool[] $names indexed by annotation name
237
     *
238
     * @return void
239
     */
240 84
    public function setIgnoredAnnotationNames(array $names)
241
    {
242 84
        $this->ignoredAnnotationNames = $names;
243 84
    }
244
245
    /**
246
     * Sets the annotation namespaces that are ignored during the parsing process.
247
     *
248
     * @param bool[] $ignoredAnnotationNamespaces indexed by annotation namespace name
249
     *
250
     * @return void
251
     */
252 95
    public function setIgnoredAnnotationNamespaces($ignoredAnnotationNamespaces)
253
    {
254 95
        $this->ignoredAnnotationNamespaces = $ignoredAnnotationNamespaces;
255 95
    }
256
257
    /**
258
     * Sets ignore on not-imported annotations.
259
     *
260
     * @param boolean $bool
261
     *
262
     * @return void
263
     */
264 309
    public function setIgnoreNotImportedAnnotations($bool)
265
    {
266 309
        $this->ignoreNotImportedAnnotations = (boolean) $bool;
267 309
    }
268
269
    /**
270
     * Sets the default namespaces.
271
     *
272
     * @param string $namespace
273
     *
274
     * @return void
275
     *
276
     * @throws \RuntimeException
277
     */
278 2
    public function addNamespace($namespace)
279
    {
280 2
        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...
281
            throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
282
        }
283
284 2
        $this->namespaces[] = $namespace;
285 2
    }
286
287
    /**
288
     * Sets the imports.
289
     *
290
     * @param array $imports
291
     *
292
     * @return void
293
     *
294
     * @throws \RuntimeException
295
     */
296 308
    public function setImports(array $imports)
297
    {
298 308
        if ($this->namespaces) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namespaces of type string[] 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...
299
            throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
300
        }
301
302 308
        $this->imports = $imports;
303 308
    }
304
305
    /**
306
     * Sets current target context as bitmask.
307
     *
308
     * @param integer $target
309
     *
310
     * @return void
311
     */
312 268
    public function setTarget($target)
313
    {
314 268
        $this->target = $target;
315 268
    }
316
317
    /**
318
     * Parses the given docblock string for annotations.
319
     *
320
     * @param string $input   The docblock string to parse.
321
     * @param string $context The parsing context.
322
     *
323
     * @return array Array of annotations. If no annotations are found, an empty array is returned.
324
     */
325 328
    public function parse($input, $context = '')
326
    {
327 328
        $pos = $this->findInitialTokenPosition($input);
328 328
        if ($pos === null) {
329 32
            return [];
330
        }
331
332 327
        $this->context = $context;
333
334 327
        $this->lexer->setInput(trim(substr($input, $pos), '* /'));
335 327
        $this->lexer->moveNext();
336
337 327
        return $this->Annotations();
338
    }
339
340
    /**
341
     * Finds the first valid annotation
342
     *
343
     * @param string $input The docblock string to parse
344
     *
345
     * @return int|null
346
     */
347 328
    private function findInitialTokenPosition($input)
348
    {
349 328
        $pos = 0;
350
351
        // search for first valid annotation
352 328
        while (($pos = strpos($input, '@', $pos)) !== false) {
353 328
            $preceding = substr($input, $pos - 1, 1);
354
355
            // if the @ is preceded by a space, a tab or * it is valid
356 328
            if ($pos === 0 || $preceding === ' ' || $preceding === '*' || $preceding === "\t") {
357 327
                return $pos;
358
            }
359
360 3
            $pos++;
361
        }
362
363 32
        return null;
364
    }
365
366
    /**
367
     * Attempts to match the given token with the current lookahead token.
368
     * If they match, updates the lookahead token; otherwise raises a syntax error.
369
     *
370
     * @param integer $token Type of token.
371
     *
372
     * @return boolean True if tokens match; false otherwise.
373
     */
374 327
    private function match($token)
375
    {
376 327
        if ( ! $this->lexer->isNextToken($token) ) {
377
            $this->syntaxError($this->lexer->getLiteral($token));
378
        }
379
380 327
        return $this->lexer->moveNext();
381
    }
382
383
    /**
384
     * Attempts to match the current lookahead token with any of the given tokens.
385
     *
386
     * If any of them matches, this method updates the lookahead token; otherwise
387
     * a syntax error is raised.
388
     *
389
     * @param array $tokens
390
     *
391
     * @return boolean
392
     */
393 14
    private function matchAny(array $tokens)
394
    {
395 14
        if ( ! $this->lexer->isNextTokenAny($tokens)) {
396 1
            $this->syntaxError(implode(' or ', array_map([$this->lexer, 'getLiteral'], $tokens)));
397
        }
398
399 13
        return $this->lexer->moveNext();
400
    }
401
402
    /**
403
     * Generates a new syntax error.
404
     *
405
     * @param string     $expected Expected string.
406
     * @param array|null $token    Optional token.
407
     *
408
     * @return void
409
     *
410
     * @throws AnnotationException
411
     */
412 17
    private function syntaxError($expected, $token = null)
413
    {
414 17
        if ($token === null) {
415 17
            $token = $this->lexer->lookahead;
416
        }
417
418 17
        $message  = sprintf('Expected %s, got ', $expected);
419 17
        $message .= ($this->lexer->lookahead === null)
420
            ? 'end of string'
421 17
            : sprintf("'%s' at position %s", $token['value'], $token['position']);
422
423 17
        if (strlen($this->context)) {
424 14
            $message .= ' in ' . $this->context;
425
        }
426
427 17
        $message .= '.';
428
429 17
        throw AnnotationException::syntaxError($message);
430
    }
431
432
    /**
433
     * Attempts to check if a class exists or not. This always uses PHP autoloading mechanism.
434
     *
435
     * @param string $fqcn
436
     *
437
     * @return boolean
438
     */
439 326
    private function classExists($fqcn)
440
    {
441 326
        if (isset($this->classExists[$fqcn])) {
442 237
            return $this->classExists[$fqcn];
443
        }
444
445
        // final check, does this class exist?
446 326
        return $this->classExists[$fqcn] = class_exists($fqcn);
447
    }
448
449
    /**
450
     * Collects parsing metadata for a given annotation class
451
     *
452
     * @param string $name The annotation name
453
     *
454
     * @return void
455
     */
456 41
    private function collectAnnotationMetadata($name)
457
    {
458 41
        if (self::$metadataParser === null) {
459 1
            self::$metadataParser = new self();
460
461 1
            self::$metadataParser->setIgnoreNotImportedAnnotations(true);
462 1
            self::$metadataParser->setIgnoredAnnotationNames($this->ignoredAnnotationNames);
463 1
            self::$metadataParser->setImports([
464 1
                'enum'          => 'Doctrine\Annotations\Annotation\Enum',
465
                'target'        => 'Doctrine\Annotations\Annotation\Target',
466
                'attribute'     => 'Doctrine\Annotations\Annotation\Attribute',
467
                'attributes'    => 'Doctrine\Annotations\Annotation\Attributes'
468
            ]);
469
        }
470
471 41
        $class      = new \ReflectionClass($name);
472 41
        $docComment = $class->getDocComment();
473
474
        // Sets default values for annotation metadata
475
        $metadata = [
476 41
            'default_property' => null,
477 41
            'has_constructor'  => (null !== $constructor = $class->getConstructor()) && $constructor->getNumberOfParameters() > 0,
478
            'properties'       => [],
479
            'property_types'   => [],
480
            'attribute_types'  => [],
481
            'targets_literal'  => null,
482
            'targets'          => Target::TARGET_ALL,
483 41
            'is_annotation'    => false !== strpos($docComment, '@Annotation'),
484
        ];
485
486
        // verify that the class is really meant to be an annotation
487 41
        if ($metadata['is_annotation']) {
488 38
            self::$metadataParser->setTarget(Target::TARGET_CLASS);
489
490 38
            foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) {
491 10
                if ($annotation instanceof Target) {
492 10
                    $metadata['targets']         = $annotation->targets;
493 10
                    $metadata['targets_literal'] = $annotation->literal;
494
495 10
                    continue;
496
                }
497
498 2
                if ($annotation instanceof Attributes) {
499 2
                    foreach ($annotation->value as $attribute) {
500 2
                        $this->collectAttributeTypeMetadata($metadata, $attribute);
501
                    }
502
                }
503
            }
504
505
            // if not has a constructor will inject values into public properties
506 29
            if (false === $metadata['has_constructor']) {
507
                // collect all public properties
508 20
                foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
509 13
                    $metadata['properties'][$property->name] = $property->name;
510
511 13
                    if (false === ($propertyComment = $property->getDocComment())) {
512 5
                        continue;
513
                    }
514
515 9
                    $attribute = new Attribute();
516
517 9
                    $attribute->required = (false !== strpos($propertyComment, '@Required'));
0 ignored issues
show
Bug introduced by
It seems like $propertyComment can also be of type true; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

517
                    $attribute->required = (false !== strpos(/** @scrutinizer ignore-type */ $propertyComment, '@Required'));
Loading history...
518 9
                    $attribute->name     = $property->name;
519 9
                    $attribute->type     = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',$propertyComment, $matches))
0 ignored issues
show
Bug introduced by
It seems like $propertyComment can also be of type true; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

519
                    $attribute->type     = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',/** @scrutinizer ignore-type */ $propertyComment, $matches))
Loading history...
520 9
                        ? $matches[1]
521
                        : 'mixed';
522
523 9
                    $this->collectAttributeTypeMetadata($metadata, $attribute);
524
525
                    // checks if the property has @Enum
526 9
                    if (false !== strpos($propertyComment, '@Enum')) {
527 4
                        $context = 'property ' . $class->name . "::\$" . $property->name;
528
529 4
                        self::$metadataParser->setTarget(Target::TARGET_PROPERTY);
530
531 4
                        foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) {
0 ignored issues
show
Bug introduced by
It seems like $propertyComment can also be of type true; however, parameter $input of Doctrine\Annotations\DocParser::parse() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

531
                        foreach (self::$metadataParser->parse(/** @scrutinizer ignore-type */ $propertyComment, $context) as $annotation) {
Loading history...
532 2
                            if ( ! $annotation instanceof Enum) {
533
                                continue;
534
                            }
535
536 2
                            $metadata['enum'][$property->name]['value']   = $annotation->value;
537 2
                            $metadata['enum'][$property->name]['literal'] = ( ! empty($annotation->literal))
538 1
                                ? $annotation->literal
539 7
                                : $annotation->value;
540
                        }
541
                    }
542
                }
543
544
                // choose the first property as default property
545 18
                $metadata['default_property'] = reset($metadata['properties']);
546
            }
547
        }
548
549 30
        self::$annotationMetadata[$name] = $metadata;
550 30
    }
551
552
    /**
553
     * Collects parsing metadata for a given attribute.
554
     *
555
     * @param array     $metadata
556
     * @param Attribute $attribute
557
     *
558
     * @return void
559
     */
560 11
    private function collectAttributeTypeMetadata(&$metadata, Attribute $attribute)
561
    {
562
        // handle internal type declaration
563 11
        $type = self::$typeMap[$attribute->type] ?? $attribute->type;
564
565
        // handle the case if the property type is mixed
566 11
        if ('mixed' === $type) {
567 7
            return;
568
        }
569
570
        // Evaluate type
571
        switch (true) {
572
            // Checks if the property has array<type>
573 6
            case (false !== $pos = strpos($type, '<')):
574 2
                $arrayType  = substr($type, $pos + 1, -1);
575 2
                $type       = 'array';
576
577 2
                if (isset(self::$typeMap[$arrayType])) {
578
                    $arrayType = self::$typeMap[$arrayType];
579
                }
580
581 2
                $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType;
582 2
                break;
583
584
            // Checks if the property has type[]
585 6
            case (false !== $pos = strrpos($type, '[')):
586 2
                $arrayType  = substr($type, 0, $pos);
587 2
                $type       = 'array';
588
589 2
                if (isset(self::$typeMap[$arrayType])) {
590
                    $arrayType = self::$typeMap[$arrayType];
591
                }
592
593 2
                $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType;
594 2
                break;
595
        }
596
597 6
        $metadata['attribute_types'][$attribute->name]['type']     = $type;
598 6
        $metadata['attribute_types'][$attribute->name]['value']    = $attribute->type;
599 6
        $metadata['attribute_types'][$attribute->name]['required'] = $attribute->required;
600 6
    }
601
602
    /**
603
     * Annotations ::= Annotation {[ "*" ]* [Annotation]}*
604
     *
605
     * @return array
606
     */
607 327
    private function Annotations()
608
    {
609 327
        $annotations = [];
610
611 327
        while (null !== $this->lexer->lookahead) {
612 327
            if (DocLexer::T_AT !== $this->lexer->lookahead['type']) {
613 54
                $this->lexer->moveNext();
614 54
                continue;
615
            }
616
617
            // make sure the @ is preceded by non-catchable pattern
618 327
            if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) {
619 9
                $this->lexer->moveNext();
620 9
                continue;
621
            }
622
623
            // make sure the @ is followed by either a namespace separator, or
624
            // an identifier token
625 327
            if ((null === $peek = $this->lexer->glimpse())
626 327
                || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true))
627 327
                || $peek['position'] !== $this->lexer->lookahead['position'] + 1) {
628 2
                $this->lexer->moveNext();
629 2
                continue;
630
            }
631
632 327
            $this->isNestedAnnotation = false;
633 327
            if (false !== $annot = $this->Annotation()) {
634 151
                $annotations[] = $annot;
635
            }
636
        }
637
638 196
        return $annotations;
639
    }
640
641
    /**
642
     * Annotation     ::= "@" AnnotationName MethodCall
643
     * AnnotationName ::= QualifiedName | SimpleName
644
     * QualifiedName  ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName
645
     * NameSpacePart  ::= identifier | null | false | true
646
     * SimpleName     ::= identifier | null | false | true
647
     *
648
     * @return mixed False if it is not a valid annotation.
649
     *
650
     * @throws AnnotationException
651
     */
652 327
    private function Annotation()
653
    {
654 327
        $this->match(DocLexer::T_AT);
655
656
        // check if we have an annotation
657 327
        $name = $this->Identifier();
658
659
        // only process names which are not fully qualified, yet
660
        // fully qualified names must start with a \
661 326
        $originalName = $name;
662
663 326
        if ('\\' !== $name[0]) {
664 322
            $pos = strpos($name, '\\');
665 322
            $alias = (false === $pos)? $name : substr($name, 0, $pos);
666 322
            $found = false;
667 322
            $loweredAlias = strtolower($alias);
668
669 322
            if ($this->namespaces) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namespaces of type string[] 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...
670 2
                foreach ($this->namespaces as $namespace) {
671 2
                    if ($this->classExists($namespace.'\\'.$name)) {
672 2
                        $name = $namespace.'\\'.$name;
673 2
                        $found = true;
674 2
                        break;
675
                    }
676
                }
677 321
            } elseif (isset($this->imports[$loweredAlias])) {
678 96
                $found = true;
679 96
                $name  = (false !== $pos)
680 1
                    ? $this->imports[$loweredAlias] . substr($name, $pos)
681 96
                    : $this->imports[$loweredAlias];
682 283
            } elseif ( ! isset($this->ignoredAnnotationNames[$name])
683 283
                && isset($this->imports['__NAMESPACE__'])
684 283
                && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name)
685
            ) {
686 46
                $name  = $this->imports['__NAMESPACE__'].'\\'.$name;
687 46
                $found = true;
688 270
            } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) {
689 182
                $found = true;
690
            }
691
692 322
            if ( ! $found) {
693 100
                if ($this->isIgnoredAnnotation($name)) {
694 96
                    return false;
695
                }
696
697 4
                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));
698
            }
699
        }
700
701 298
        $name = ltrim($name,'\\');
702
703 298
        if ( ! $this->classExists($name)) {
704
            throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context));
705
        }
706
707
        // at this point, $name contains the fully qualified class name of the
708
        // annotation, and it is also guaranteed that this class exists, and
709
        // that it is loaded
710
711
712
        // collects the metadata annotation only if there is not yet
713 298
        if ( ! isset(self::$annotationMetadata[$name])) {
714 41
            $this->collectAnnotationMetadata($name);
715
        }
716
717
        // verify that the class is really meant to be an annotation and not just any ordinary class
718 298
        if (self::$annotationMetadata[$name]['is_annotation'] === false) {
719 5
            if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$originalName])) {
720 2
                return false;
721
            }
722
723 3
            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));
724
        }
725
726
        //if target is nested annotation
727 293
        $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target;
728
729
        // Next will be nested
730 293
        $this->isNestedAnnotation = true;
731
732
        //if annotation does not support current target
733 293
        if (0 === (self::$annotationMetadata[$name]['targets'] & $target) && $target) {
734 9
            throw AnnotationException::semanticalError(
735 9
                sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.',
736 9
                     $originalName, $this->context, self::$annotationMetadata[$name]['targets_literal'])
737
            );
738
        }
739
740 285
        $values = $this->MethodCall();
741
742 268
        if (isset(self::$annotationMetadata[$name]['enum'])) {
743
            // checks all declared attributes
744 4
            foreach (self::$annotationMetadata[$name]['enum'] as $property => $enum) {
745
                // checks if the attribute is a valid enumerator
746 4
                if (isset($values[$property]) && ! in_array($values[$property], $enum['value'])) {
747 4
                    throw AnnotationException::enumeratorError($property, $name, $this->context, $enum['literal'], $values[$property]);
748
                }
749
            }
750
        }
751
752
        // checks all declared attributes
753 267
        foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) {
754 190
            if ($property === self::$annotationMetadata[$name]['default_property']
755 190
                && !isset($values[$property]) && isset($values['value'])) {
756 7
                $property = 'value';
757
            }
758
759
            // handle a not given attribute or null value
760 190
            if (!isset($values[$property])) {
761 168
                if ($type['required']) {
762 2
                    throw AnnotationException::requiredError($property, $originalName, $this->context, 'a(n) '.$type['value']);
763
                }
764
765 166
                continue;
766
            }
767
768 177
            if ($type['type'] === 'array') {
769
                // handle the case of a single value
770 63
                if ( ! is_array($values[$property])) {
771 29
                    $values[$property] = [$values[$property]];
772
                }
773
774
                // checks if the attribute has array type declaration, such as "array<string>"
775 63
                if (isset($type['array_type'])) {
776 59
                    foreach ($values[$property] as $item) {
777 59
                        if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) {
778 63
                            throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item);
779
                        }
780
                    }
781
                }
782 118
            } elseif (gettype($values[$property]) !== $type['type'] && !$values[$property] instanceof $type['type']) {
783 141
                throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'a(n) '.$type['value'], $values[$property]);
784
            }
785
        }
786
787
        // check if the annotation expects values via the constructor,
788
        // or directly injected into public properties
789 171
        if (self::$annotationMetadata[$name]['has_constructor'] === true) {
790 83
            return new $name($values);
791
        }
792
793 108
        $instance = new $name();
794
795 108
        foreach ($values as $property => $value) {
796 76
            if (isset(self::$annotationMetadata[$name]['properties'][$property])) {
797 60
                $instance->{$property} = $value;
798
799 60
                continue;
800
            }
801
802 21
            if ('value' !== $property) {
803 2
                if (self::$annotationMetadata[$name]['properties']) {
804 1
                    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'])));
805
                }
806 1
                throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not have a property named "%s". There are no available properties.', $originalName, $this->context, $property));
807
            }
808
809
            // handle the case if the property has no annotations
810 19
            if ( ! $property = self::$annotationMetadata[$name]['default_property']) {
811 2
                throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not accept any values, but got %s.', $originalName, $this->context, json_encode($values)));
812
            }
813
814 17
            $instance->{$property} = $value;
815
        }
816
817 104
        return $instance;
818
    }
819
820
    /**
821
     * MethodCall ::= ["(" [Values] ")"]
822
     *
823
     * @return array
824
     */
825 285
    private function MethodCall()
826
    {
827 285
        $values = [];
828
829 285
        if ( ! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
830 62
            return $values;
831
        }
832
833 266
        $this->match(DocLexer::T_OPEN_PARENTHESIS);
834
835 266
        if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
836 266
            $values = $this->Values();
837
        }
838
839 249
        $this->match(DocLexer::T_CLOSE_PARENTHESIS);
840
841 249
        return $values;
842
    }
843
844
    /**
845
     * Values ::= Array | Value {"," Value}* [","]
846
     *
847
     * @return array
848
     */
849 266
    private function Values()
850
    {
851 266
        $values = [$this->Value()];
852
853 249
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
854 15
            $this->match(DocLexer::T_COMMA);
855
856 15
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
857 2
                break;
858
            }
859
860 13
            $token = $this->lexer->lookahead;
861 13
            $value = $this->Value();
862
863 13
            if ( ! is_object($value) && ! is_array($value)) {
864
                $this->syntaxError('Value', $token);
865
            }
866
867 13
            $values[] = $value;
868
        }
869
870 249
        foreach ($values as $k => $value) {
871 249
            if (is_object($value) && $value instanceof \stdClass) {
872 205
                $values[$value->name] = $value->value;
873 60
            } else if ( ! isset($values['value'])){
874 60
                $values['value'] = $value;
875
            } else {
876 1
                if ( ! is_array($values['value'])) {
877 1
                    $values['value'] = [$values['value']];
878
                }
879
880 1
                $values['value'][] = $value;
881
            }
882
883 249
            unset($values[$k]);
884
        }
885
886 249
        return $values;
887
    }
888
889
    /**
890
     * Constant ::= integer | string | float | boolean
891
     *
892
     * @return mixed
893
     *
894
     * @throws AnnotationException
895
     */
896 75
    private function Constant()
897
    {
898 75
        $identifier = $this->Identifier();
899
900 75
        if ( ! defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) {
901 17
            list($className, $const) = explode('::', $identifier);
902
903 17
            $pos = strpos($className, '\\');
904 17
            $alias = (false === $pos) ? $className : substr($className, 0, $pos);
905 17
            $found = false;
906 17
            $loweredAlias = strtolower($alias);
907
908
            switch (true) {
909 17
                case !empty ($this->namespaces):
910
                    foreach ($this->namespaces as $ns) {
911
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
912
                             $className = $ns.'\\'.$className;
913
                             $found = true;
914
                             break;
915
                        }
916
                    }
917
                    break;
918
919 17
                case isset($this->imports[$loweredAlias]):
920 15
                    $found     = true;
921 15
                    $className = (false !== $pos)
922
                        ? $this->imports[$loweredAlias] . substr($className, $pos)
923 15
                        : $this->imports[$loweredAlias];
924 15
                    break;
925
926
                default:
927 2
                    if(isset($this->imports['__NAMESPACE__'])) {
928
                        $ns = $this->imports['__NAMESPACE__'];
929
930
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
931
                            $className = $ns.'\\'.$className;
932
                            $found = true;
933
                        }
934
                    }
935 2
                    break;
936
            }
937
938 17
            if ($found) {
939 15
                 $identifier = $className . '::' . $const;
940
            }
941
        }
942
943
        // checks if identifier ends with ::class, \strlen('::class') === 7
944 75
        $classPos = stripos($identifier, '::class');
945 75
        if ($classPos === strlen($identifier) - 7) {
946 4
            return substr($identifier, 0, $classPos);
947
        }
948
949 71
        if (!defined($identifier)) {
950 1
            throw AnnotationException::semanticalErrorConstants($identifier, $this->context);
951
        }
952
953 70
        return constant($identifier);
954
    }
955
956
    /**
957
     * Identifier ::= string
958
     *
959
     * @return string
960
     */
961 327
    private function Identifier()
962
    {
963
        // check if we have an annotation
964 327
        if ( ! $this->lexer->isNextTokenAny(self::$classIdentifiers)) {
965 14
            $this->syntaxError('namespace separator or identifier');
966
        }
967
968 327
        $this->lexer->moveNext();
969
970 327
        $className = $this->lexer->token['value'];
971
972 327
        while ($this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value']))
973 327
                && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) {
974
975 1
            $this->match(DocLexer::T_NAMESPACE_SEPARATOR);
976 1
            $this->matchAny(self::$classIdentifiers);
977
978
            $className .= '\\' . $this->lexer->token['value'];
979
        }
980
981 326
        return $className;
982
    }
983
984
    /**
985
     * Value ::= PlainValue | FieldAssignment
986
     *
987
     * @return mixed
988
     */
989 266
    private function Value()
990
    {
991 266
        $peek = $this->lexer->glimpse();
992
993 266
        if (DocLexer::T_EQUALS === $peek['type']) {
994 207
            return $this->FieldAssignment();
995
        }
996
997 132
        return $this->PlainValue();
998
    }
999
1000
    /**
1001
     * PlainValue ::= integer | string | float | boolean | Array | Annotation
1002
     *
1003
     * @return mixed
1004
     */
1005 266
    private function PlainValue()
1006
    {
1007 266
        if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
1008 79
            return $this->Arrayx();
1009
        }
1010
1011 265
        if ($this->lexer->isNextToken(DocLexer::T_AT)) {
1012 55
            return $this->Annotation();
1013
        }
1014
1015 229
        if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1016 75
            return $this->Constant();
1017
        }
1018
1019 162
        switch ($this->lexer->lookahead['type']) {
1020 162
            case DocLexer::T_STRING:
1021 89
                $this->match(DocLexer::T_STRING);
1022 89
                return $this->lexer->token['value'];
1023
1024 84
            case DocLexer::T_INTEGER:
1025 56
                $this->match(DocLexer::T_INTEGER);
1026 56
                return (int)$this->lexer->token['value'];
1027
1028 30
            case DocLexer::T_FLOAT:
1029 28
                $this->match(DocLexer::T_FLOAT);
1030 28
                return (float)$this->lexer->token['value'];
1031
1032 2
            case DocLexer::T_TRUE:
1033
                $this->match(DocLexer::T_TRUE);
1034
                return true;
1035
1036 2
            case DocLexer::T_FALSE:
1037
                $this->match(DocLexer::T_FALSE);
1038
                return false;
1039
1040 2
            case DocLexer::T_NULL:
1041
                $this->match(DocLexer::T_NULL);
1042
                return null;
1043
1044
            default:
1045 2
                $this->syntaxError('PlainValue');
1046
        }
1047
    }
1048
1049
    /**
1050
     * FieldAssignment ::= FieldName "=" PlainValue
1051
     * FieldName ::= identifier
1052
     *
1053
     * @return \stdClass
1054
     */
1055 207
    private function FieldAssignment()
1056
    {
1057 207
        $this->match(DocLexer::T_IDENTIFIER);
1058 207
        $fieldName = $this->lexer->token['value'];
1059
1060 207
        $this->match(DocLexer::T_EQUALS);
1061
1062 207
        $item = new \stdClass();
1063 207
        $item->name  = $fieldName;
1064 207
        $item->value = $this->PlainValue();
1065
1066 205
        return $item;
1067
    }
1068
1069
    /**
1070
     * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}"
1071
     *
1072
     * @return array
1073
     */
1074 79
    private function Arrayx()
1075
    {
1076 79
        $array = $values = [];
1077
1078 79
        $this->match(DocLexer::T_OPEN_CURLY_BRACES);
1079
1080
        // If the array is empty, stop parsing and return.
1081 79
        if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1082 1
            $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1083
1084 1
            return $array;
1085
        }
1086
1087 79
        $values[] = $this->ArrayEntry();
1088
1089 79
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
1090 54
            $this->match(DocLexer::T_COMMA);
1091
1092
            // optional trailing comma
1093 54
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1094 4
                break;
1095
            }
1096
1097 54
            $values[] = $this->ArrayEntry();
1098
        }
1099
1100 79
        $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1101
1102 79
        foreach ($values as $value) {
1103 79
            list ($key, $val) = $value;
1104
1105 79
            if ($key !== null) {
1106 13
                $array[$key] = $val;
1107
            } else {
1108 79
                $array[] = $val;
1109
            }
1110
        }
1111
1112 79
        return $array;
1113
    }
1114
1115
    /**
1116
     * ArrayEntry ::= Value | KeyValuePair
1117
     * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant
1118
     * Key ::= string | integer | Constant
1119
     *
1120
     * @return array
1121
     */
1122 79
    private function ArrayEntry()
1123
    {
1124 79
        $peek = $this->lexer->glimpse();
1125
1126 79
        if (DocLexer::T_EQUALS === $peek['type']
1127 79
                || DocLexer::T_COLON === $peek['type']) {
1128
1129 13
            if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1130 5
                $key = $this->Constant();
1131
            } else {
1132 8
                $this->matchAny([DocLexer::T_INTEGER, DocLexer::T_STRING]);
1133 8
                $key = $this->lexer->token['value'];
1134
            }
1135
1136 13
            $this->matchAny([DocLexer::T_EQUALS, DocLexer::T_COLON]);
1137
1138 13
            return [$key, $this->PlainValue()];
1139
        }
1140
1141 70
        return [null, $this->Value()];
1142
    }
1143
1144
    /**
1145
     * Checks whether the given $name matches any ignored annotation name or namespace
1146
     *
1147
     * @param string $name
1148
     *
1149
     * @return bool
1150
     */
1151 100
    private function isIgnoredAnnotation($name)
1152
    {
1153 100
        if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
1154 82
            return true;
1155
        }
1156
1157 19
        foreach (array_keys($this->ignoredAnnotationNamespaces) as $ignoredAnnotationNamespace) {
1158 15
            $ignoredAnnotationNamespace = rtrim($ignoredAnnotationNamespace, '\\') . '\\';
1159
1160 15
            if (0 === stripos(rtrim($name, '\\') . '\\', $ignoredAnnotationNamespace)) {
1161 15
                return true;
1162
            }
1163
        }
1164
1165 4
        return false;
1166
    }
1167
}
1168