Completed
Pull Request — master (#142)
by Mike
04:49 queued 02:17
created

DocParser   F

Complexity

Total Complexity 171

Size/Duplication

Total Lines 1141
Duplicated Lines 0 %

Test Coverage

Coverage 93.02%

Importance

Changes 0
Metric Value
wmc 171
dl 0
loc 1141
ccs 360
cts 387
cp 0.9302
rs 0.6314
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
D Values() 0 38 10
D collectAnnotationMetadata() 0 94 17
A setImports() 0 7 2
A MethodCall() 0 17 3
A setTarget() 0 3 1
A setIgnoreNotImportedAnnotations() 0 3 1
A match() 0 7 2
A setIgnoredAnnotationNames() 0 3 1
A setIgnoredAnnotationNamespaces() 0 3 1
A parse() 0 13 2
A syntaxError() 0 18 4
B collectAttributeTypeMetadata() 0 40 6
A addNamespace() 0 7 2
A __construct() 0 3 1
A matchAny() 0 7 2
A classExists() 0 8 2
D Annotations() 0 32 10
B findInitialTokenPosition() 0 17 6
F Annotation() 0 159 46
A ArrayEntry() 0 20 4
D PlainValue() 0 41 10
B isIgnoredAnnotation() 0 15 5
A Identifier() 0 21 4
B Arrayx() 0 39 6
C Constant() 0 63 20
A FieldAssignment() 0 12 1
A Value() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like DocParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocParser, and based on these observations, apply Extract Interface, too.

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 330
    public function __construct()
226
    {
227 330
        $this->lexer = new DocLexer;
228 330
    }
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 86
    public function setIgnoredAnnotationNames(array $names)
241
    {
242 86
        $this->ignoredAnnotationNames = $names;
243 86
    }
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 97
    public function setIgnoredAnnotationNamespaces($ignoredAnnotationNamespaces)
253
    {
254 97
        $this->ignoredAnnotationNamespaces = $ignoredAnnotationNamespaces;
255 97
    }
256
257
    /**
258
     * Sets ignore on not-imported annotations.
259
     *
260
     * @param boolean $bool
261
     *
262
     * @return void
263
     */
264 310
    public function setIgnoreNotImportedAnnotations($bool)
265
    {
266 310
        $this->ignoreNotImportedAnnotations = (boolean) $bool;
267 310
    }
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 309
    public function setImports(array $imports)
297
    {
298 309
        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 309
        $this->imports = $imports;
303 309
    }
304
305
    /**
306
     * Sets current target context as bitmask.
307
     *
308
     * @param integer $target
309
     *
310
     * @return void
311
     */
312 270
    public function setTarget($target)
313
    {
314 270
        $this->target = $target;
315 270
    }
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 329
    public function parse($input, $context = '')
326
    {
327 329
        $pos = $this->findInitialTokenPosition($input);
328 329
        if ($pos === null) {
329 32
            return [];
330
        }
331
332 328
        $this->context = $context;
333
334 328
        $this->lexer->setInput(trim(substr($input, $pos), '* /'));
335 328
        $this->lexer->moveNext();
336
337 328
        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 329
    private function findInitialTokenPosition($input)
348
    {
349 329
        $pos = 0;
350
351
        // search for first valid annotation
352 329
        while (($pos = strpos($input, '@', $pos)) !== false) {
353 329
            $preceding = substr($input, $pos - 1, 1);
354
355
            // if the @ is preceded by a space, a tab or * it is valid
356 329
            if ($pos === 0 || $preceding === ' ' || $preceding === '*' || $preceding === "\t") {
357 328
                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 328
    private function match($token)
375
    {
376 328
        if ( ! $this->lexer->isNextToken($token) ) {
377
            $this->syntaxError($this->lexer->getLiteral($token));
378
        }
379
380 328
        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 327
    private function classExists($fqcn)
440
    {
441 327
        if (isset($this->classExists[$fqcn])) {
442 238
            return $this->classExists[$fqcn];
443
        }
444
445
        // final check, does this class exist?
446 327
        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 42
    private function collectAnnotationMetadata($name)
457
    {
458 42
        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 42
        $class      = new \ReflectionClass($name);
472 42
        $docComment = $class->getDocComment();
473
474
        // Sets default values for annotation metadata
475
        $metadata = [
476 42
            'default_property' => null,
477 42
            '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 42
            'is_annotation'    => false !== strpos($docComment, '@Annotation'),
484
        ];
485
486
        // verify that the class is really meant to be an annotation
487 42
        if ($metadata['is_annotation']) {
488 39
            self::$metadataParser->setTarget(Target::TARGET_CLASS);
489
490 39
            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 30
            if (false === $metadata['has_constructor']) {
507
                // collect all public properties
508 21
                foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
509 14
                    $metadata['properties'][$property->name] = $property->name;
510
511 14
                    if (false === ($propertyComment = $property->getDocComment())) {
512 6
                        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 19
                $metadata['default_property'] = reset($metadata['properties']);
546
            }
547
        }
548
549 31
        self::$annotationMetadata[$name] = $metadata;
550 31
    }
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, '<')):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
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, '[')):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
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 328
    private function Annotations()
608
    {
609 328
        $annotations = [];
610
611 328
        while (null !== $this->lexer->lookahead) {
612 328
            if (DocLexer::T_AT !== $this->lexer->lookahead['type']) {
613 56
                $this->lexer->moveNext();
614 56
                continue;
615
            }
616
617
            // make sure the @ is preceded by non-catchable pattern
618 328
            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 328
            if ((null === $peek = $this->lexer->glimpse())
626 328
                || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true))
627 328
                || $peek['position'] !== $this->lexer->lookahead['position'] + 1) {
628 2
                $this->lexer->moveNext();
629 2
                continue;
630
            }
631
632 328
            $this->isNestedAnnotation = false;
633 328
            if (false !== $annot = $this->Annotation()) {
634 153
                $annotations[] = $annot;
635
            }
636
        }
637
638 198
        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 328
    private function Annotation()
653
    {
654 328
        $this->match(DocLexer::T_AT);
655
656
        // check if we have an annotation
657 328
        $name = $this->Identifier();
658
659
        // only process names which are not fully qualified, yet
660
        // fully qualified names must start with a \
661 327
        $originalName = $name;
662
663 327
        if ('\\' !== $name[0]) {
664 323
            $pos = strpos($name, '\\');
665 323
            $alias = (false === $pos)? $name : substr($name, 0, $pos);
666 323
            $found = false;
667 323
            $loweredAlias = strtolower($alias);
668
669 323
            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 322
            } 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 284
            } elseif ( ! isset($this->ignoredAnnotationNames[$name])
683 284
                && isset($this->imports['__NAMESPACE__'])
684 284
                && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name)
685
            ) {
686 47
                $name  = $this->imports['__NAMESPACE__'].'\\'.$name;
687 47
                $found = true;
688 272
            } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) {
689 182
                $found = true;
690
            }
691
692 323
            if ( ! $found) {
693 102
                if ($this->isIgnoredAnnotation($name)) {
694 98
                    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 299
        $name = ltrim($name,'\\');
702
703 299
        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 299
        if ( ! isset(self::$annotationMetadata[$name])) {
714 42
            $this->collectAnnotationMetadata($name);
715
        }
716
717
        // verify that the class is really meant to be an annotation and not just any ordinary class
718 299
        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 294
        $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target;
728
729
        // Next will be nested
730 294
        $this->isNestedAnnotation = true;
731
732
        //if annotation does not support current target
733 294
        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 286
        $values = $this->MethodCall();
741
742 269
        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 268
        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 172
        if (self::$annotationMetadata[$name]['has_constructor'] === true) {
790 83
            return new $name($values);
791
        }
792
793 109
        $instance = new $name();
794
795 109
        foreach ($values as $property => $value) {
796 77
            if (!isset(self::$annotationMetadata[$name]['properties'][$property])) {
797 22
                if ('value' !== $property) {
798 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'])));
799
                }
800
801
                // handle the case if the property has no annotations
802 21
                if ( ! $property = self::$annotationMetadata[$name]['default_property']) {
803 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)));
804
                }
805
            }
806
807 74
            $instance->{$property} = $value;
808
        }
809
810 106
        return $instance;
811
    }
812
813
    /**
814
     * MethodCall ::= ["(" [Values] ")"]
815
     *
816
     * @return array
817
     */
818 286
    private function MethodCall()
819
    {
820 286
        $values = [];
821
822 286
        if ( ! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
823 62
            return $values;
824
        }
825
826 267
        $this->match(DocLexer::T_OPEN_PARENTHESIS);
827
828 267
        if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
829 267
            $values = $this->Values();
830
        }
831
832 250
        $this->match(DocLexer::T_CLOSE_PARENTHESIS);
833
834 250
        return $values;
835
    }
836
837
    /**
838
     * Values ::= Array | Value {"," Value}* [","]
839
     *
840
     * @return array
841
     */
842 267
    private function Values()
843
    {
844 267
        $values = [$this->Value()];
845
846 250
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
847 15
            $this->match(DocLexer::T_COMMA);
848
849 15
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
850 2
                break;
851
            }
852
853 13
            $token = $this->lexer->lookahead;
854 13
            $value = $this->Value();
855
856 13
            if ( ! is_object($value) && ! is_array($value)) {
857
                $this->syntaxError('Value', $token);
858
            }
859
860 13
            $values[] = $value;
861
        }
862
863 250
        foreach ($values as $k => $value) {
864 250
            if (is_object($value) && $value instanceof \stdClass) {
865 204
                $values[$value->name] = $value->value;
866 62
            } else if ( ! isset($values['value'])){
867 62
                $values['value'] = $value;
868
            } else {
869 1
                if ( ! is_array($values['value'])) {
870 1
                    $values['value'] = [$values['value']];
871
                }
872
873 1
                $values['value'][] = $value;
874
            }
875
876 250
            unset($values[$k]);
877
        }
878
879 250
        return $values;
880
    }
881
882
    /**
883
     * Constant ::= integer | string | float | boolean
884
     *
885
     * @return mixed
886
     *
887
     * @throws AnnotationException
888
     */
889 77
    private function Constant()
890
    {
891 77
        $identifier = $this->Identifier();
892
893 77
        if ( ! defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) {
894 18
            list($className, $const) = explode('::', $identifier);
895
896 18
            $pos = strpos($className, '\\');
897 18
            $alias = (false === $pos) ? $className : substr($className, 0, $pos);
898 18
            $found = false;
899 18
            $loweredAlias = strtolower($alias);
900
901
            switch (true) {
902 18
                case !empty ($this->namespaces):
903
                    foreach ($this->namespaces as $ns) {
904
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
905
                             $className = $ns.'\\'.$className;
906
                             $found = true;
907
                             break;
908
                        }
909
                    }
910
                    break;
911
912 18
                case isset($this->imports[$loweredAlias]):
913 15
                    $found     = true;
914 15
                    $className = (false !== $pos)
915
                        ? $this->imports[$loweredAlias] . substr($className, $pos)
916 15
                        : $this->imports[$loweredAlias];
917 15
                    break;
918
919
                default:
920 3
                    if(isset($this->imports['__NAMESPACE__'])) {
921 1
                        $ns = $this->imports['__NAMESPACE__'];
922
923 1
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
924
                            $className = $ns.'\\'.$className;
925
                            $found = true;
926
                        }
927
                    }
928 3
                    break;
929
            }
930
931 18
            if ($found) {
932 15
                 $identifier = $className . '::' . $const;
933
            }
934
        }
935
936
        /* checks if identifier ends with ::class, \strlen('::class') === 7
937
         * and remove the leading backslash if it exists.
938
         */
939 77
        $classPos = stripos($identifier, '::class');
940 77
        if ($classPos === strlen($identifier) - 7 && '\\' !== $identifier[0]) {
941 5
            return substr($identifier, 0, $classPos);
942
        }
943 72
        if ($classPos === strlen($identifier) - 7 && '\\' === $identifier[0]) {
944 1
            return substr($identifier, 1, $classPos - 1);
945
        }
946
947 71
        if (!defined($identifier)) {
948 1
            throw AnnotationException::semanticalErrorConstants($identifier, $this->context);
949
        }
950
951 70
        return constant($identifier);
952
    }
953
954
    /**
955
     * Identifier ::= string
956
     *
957
     * @return string
958
     */
959 328
    private function Identifier()
960
    {
961
        // check if we have an annotation
962 328
        if ( ! $this->lexer->isNextTokenAny(self::$classIdentifiers)) {
963 14
            $this->syntaxError('namespace separator or identifier');
964
        }
965
966 328
        $this->lexer->moveNext();
967
968 328
        $className = $this->lexer->token['value'];
969
970 328
        while ($this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value']))
971 328
                && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) {
972
973 1
            $this->match(DocLexer::T_NAMESPACE_SEPARATOR);
974 1
            $this->matchAny(self::$classIdentifiers);
975
976
            $className .= '\\' . $this->lexer->token['value'];
977
        }
978
979 327
        return $className;
980
    }
981
982
    /**
983
     * Value ::= PlainValue | FieldAssignment
984
     *
985
     * @return mixed
986
     */
987 267
    private function Value()
988
    {
989 267
        $peek = $this->lexer->glimpse();
990
991 267
        if (DocLexer::T_EQUALS === $peek['type']) {
992 206
            return $this->FieldAssignment();
993
        }
994
995 134
        return $this->PlainValue();
996
    }
997
998
    /**
999
     * PlainValue ::= integer | string | float | boolean | Array | Annotation
1000
     *
1001
     * @return mixed
1002
     */
1003 267
    private function PlainValue()
1004
    {
1005 267
        if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
1006 79
            return $this->Arrayx();
1007
        }
1008
1009 266
        if ($this->lexer->isNextToken(DocLexer::T_AT)) {
1010 55
            return $this->Annotation();
1011
        }
1012
1013 230
        if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1014 77
            return $this->Constant();
1015
        }
1016
1017 161
        switch ($this->lexer->lookahead['type']) {
1018 161
            case DocLexer::T_STRING:
1019 88
                $this->match(DocLexer::T_STRING);
1020 88
                return $this->lexer->token['value'];
1021
1022 84
            case DocLexer::T_INTEGER:
1023 56
                $this->match(DocLexer::T_INTEGER);
1024 56
                return (int)$this->lexer->token['value'];
1025
1026 30
            case DocLexer::T_FLOAT:
1027 28
                $this->match(DocLexer::T_FLOAT);
1028 28
                return (float)$this->lexer->token['value'];
1029
1030 2
            case DocLexer::T_TRUE:
1031
                $this->match(DocLexer::T_TRUE);
1032
                return true;
1033
1034 2
            case DocLexer::T_FALSE:
1035
                $this->match(DocLexer::T_FALSE);
1036
                return false;
1037
1038 2
            case DocLexer::T_NULL:
1039
                $this->match(DocLexer::T_NULL);
1040
                return null;
1041
1042
            default:
1043 2
                $this->syntaxError('PlainValue');
1044
        }
1045
    }
1046
1047
    /**
1048
     * FieldAssignment ::= FieldName "=" PlainValue
1049
     * FieldName ::= identifier
1050
     *
1051
     * @return \stdClass
1052
     */
1053 206
    private function FieldAssignment()
1054
    {
1055 206
        $this->match(DocLexer::T_IDENTIFIER);
1056 206
        $fieldName = $this->lexer->token['value'];
1057
1058 206
        $this->match(DocLexer::T_EQUALS);
1059
1060 206
        $item = new \stdClass();
1061 206
        $item->name  = $fieldName;
1062 206
        $item->value = $this->PlainValue();
1063
1064 204
        return $item;
1065
    }
1066
1067
    /**
1068
     * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}"
1069
     *
1070
     * @return array
1071
     */
1072 79
    private function Arrayx()
1073
    {
1074 79
        $array = $values = [];
1075
1076 79
        $this->match(DocLexer::T_OPEN_CURLY_BRACES);
1077
1078
        // If the array is empty, stop parsing and return.
1079 79
        if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1080 1
            $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1081
1082 1
            return $array;
1083
        }
1084
1085 79
        $values[] = $this->ArrayEntry();
1086
1087 79
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
1088 54
            $this->match(DocLexer::T_COMMA);
1089
1090
            // optional trailing comma
1091 54
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1092 4
                break;
1093
            }
1094
1095 54
            $values[] = $this->ArrayEntry();
1096
        }
1097
1098 79
        $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1099
1100 79
        foreach ($values as $value) {
1101 79
            list ($key, $val) = $value;
1102
1103 79
            if ($key !== null) {
1104 13
                $array[$key] = $val;
1105
            } else {
1106 79
                $array[] = $val;
1107
            }
1108
        }
1109
1110 79
        return $array;
1111
    }
1112
1113
    /**
1114
     * ArrayEntry ::= Value | KeyValuePair
1115
     * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant
1116
     * Key ::= string | integer | Constant
1117
     *
1118
     * @return array
1119
     */
1120 79
    private function ArrayEntry()
1121
    {
1122 79
        $peek = $this->lexer->glimpse();
1123
1124 79
        if (DocLexer::T_EQUALS === $peek['type']
1125 79
                || DocLexer::T_COLON === $peek['type']) {
1126
1127 13
            if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1128 5
                $key = $this->Constant();
1129
            } else {
1130 8
                $this->matchAny([DocLexer::T_INTEGER, DocLexer::T_STRING]);
1131 8
                $key = $this->lexer->token['value'];
1132
            }
1133
1134 13
            $this->matchAny([DocLexer::T_EQUALS, DocLexer::T_COLON]);
1135
1136 13
            return [$key, $this->PlainValue()];
1137
        }
1138
1139 70
        return [null, $this->Value()];
1140
    }
1141
1142
    /**
1143
     * Checks whether the given $name matches any ignored annotation name or namespace
1144
     *
1145
     * @param string $name
1146
     *
1147
     * @return bool
1148
     */
1149 102
    private function isIgnoredAnnotation($name)
1150
    {
1151 102
        if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
1152 84
            return true;
1153
        }
1154
1155 19
        foreach (array_keys($this->ignoredAnnotationNamespaces) as $ignoredAnnotationNamespace) {
1156 15
            $ignoredAnnotationNamespace = rtrim($ignoredAnnotationNamespace, '\\') . '\\';
1157
1158 15
            if (0 === stripos(rtrim($name, '\\') . '\\', $ignoredAnnotationNamespace)) {
1159 15
                return true;
1160
            }
1161
        }
1162
1163 4
        return false;
1164
    }
1165
}
1166