Failed Conditions
Push — master ( bac869...b5d5f9 )
by Michael
10s
created

DocParser   F

Complexity

Total Complexity 168

Size/Duplication

Total Lines 1136
Duplicated Lines 0 %

Test Coverage

Coverage 92.47%

Importance

Changes 0
Metric Value
wmc 168
dl 0
loc 1136
ccs 356
cts 385
cp 0.9247
rs 0.6314
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
D Values() 0 38 10
A ArrayEntry() 0 20 4
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
D PlainValue() 0 41 10
A addNamespace() 0 7 2
A __construct() 0 3 1
B isIgnoredAnnotation() 0 15 5
A matchAny() 0 7 2
A classExists() 0 8 2
A Identifier() 0 21 4
D Annotations() 0 32 10
B findInitialTokenPosition() 0 17 6
B Arrayx() 0 39 6
F Annotation() 0 159 46
A FieldAssignment() 0 12 1
A Value() 0 9 2
C Constant() 0 58 17

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 328
    public function __construct()
226
    {
227 328
        $this->lexer = new DocLexer;
228 328
    }
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 308
    public function setIgnoreNotImportedAnnotations($bool)
265
    {
266 308
        $this->ignoreNotImportedAnnotations = (boolean) $bool;
267 308
    }
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 307
    public function setImports(array $imports)
297
    {
298 307
        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 307
        $this->imports = $imports;
303 307
    }
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 327
    public function parse($input, $context = '')
326
    {
327 327
        $pos = $this->findInitialTokenPosition($input);
328 327
        if ($pos === null) {
329 32
            return [];
330
        }
331
332 326
        $this->context = $context;
333
334 326
        $this->lexer->setInput(trim(substr($input, $pos), '* /'));
335 326
        $this->lexer->moveNext();
336
337 326
        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 327
    private function findInitialTokenPosition($input)
348
    {
349 327
        $pos = 0;
350
351
        // search for first valid annotation
352 327
        while (($pos = strpos($input, '@', $pos)) !== false) {
353 327
            $preceding = substr($input, $pos - 1, 1);
354
355
            // if the @ is preceded by a space, a tab or * it is valid
356 327
            if ($pos === 0 || $preceding === ' ' || $preceding === '*' || $preceding === "\t") {
357 326
                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 326
    private function match($token)
375
    {
376 326
        if ( ! $this->lexer->isNextToken($token) ) {
377
            $this->syntaxError($this->lexer->getLiteral($token));
378
        }
379
380 326
        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 325
    private function classExists($fqcn)
440
    {
441 325
        if (isset($this->classExists[$fqcn])) {
442 236
            return $this->classExists[$fqcn];
443
        }
444
445
        // final check, does this class exist?
446 325
        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, '<')):
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 326
    private function Annotations()
608
    {
609 326
        $annotations = [];
610
611 326
        while (null !== $this->lexer->lookahead) {
612 326
            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 326
            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 326
            if ((null === $peek = $this->lexer->glimpse())
626 326
                || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true))
627 326
                || $peek['position'] !== $this->lexer->lookahead['position'] + 1) {
628 2
                $this->lexer->moveNext();
629 2
                continue;
630
            }
631
632 326
            $this->isNestedAnnotation = false;
633 326
            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 326
    private function Annotation()
653
    {
654 326
        $this->match(DocLexer::T_AT);
655
656
        // check if we have an annotation
657 326
        $name = $this->Identifier();
658
659
        // only process names which are not fully qualified, yet
660
        // fully qualified names must start with a \
661 325
        $originalName = $name;
662
663 325
        if ('\\' !== $name[0]) {
664 321
            $pos = strpos($name, '\\');
665 321
            $alias = (false === $pos)? $name : substr($name, 0, $pos);
666 321
            $found = false;
667 321
            $loweredAlias = strtolower($alias);
668
669 321
            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 320
            } 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 282
            } elseif ( ! isset($this->ignoredAnnotationNames[$name])
683 282
                && isset($this->imports['__NAMESPACE__'])
684 282
                && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name)
685
            ) {
686 45
                $name  = $this->imports['__NAMESPACE__'].'\\'.$name;
687 45
                $found = true;
688 270
            } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) {
689 182
                $found = true;
690
            }
691
692 321
            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 297
        $name = ltrim($name,'\\');
702
703 297
        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 297
        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 297
        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 292
        $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target;
728
729
        // Next will be nested
730 292
        $this->isNestedAnnotation = true;
731
732
        //if annotation does not support current target
733 292
        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 284
        $values = $this->MethodCall();
741
742 267
        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 266
        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 170
        if (self::$annotationMetadata[$name]['has_constructor'] === true) {
790 83
            return new $name($values);
791
        }
792
793 107
        $instance = new $name();
794
795 107
        foreach ($values as $property => $value) {
796 75
            if (!isset(self::$annotationMetadata[$name]['properties'][$property])) {
797 20
                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 19
                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 72
            $instance->{$property} = $value;
808
        }
809
810 104
        return $instance;
811
    }
812
813
    /**
814
     * MethodCall ::= ["(" [Values] ")"]
815
     *
816
     * @return array
817
     */
818 284
    private function MethodCall()
819
    {
820 284
        $values = [];
821
822 284
        if ( ! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
823 62
            return $values;
824
        }
825
826 265
        $this->match(DocLexer::T_OPEN_PARENTHESIS);
827
828 265
        if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
829 265
            $values = $this->Values();
830
        }
831
832 248
        $this->match(DocLexer::T_CLOSE_PARENTHESIS);
833
834 248
        return $values;
835
    }
836
837
    /**
838
     * Values ::= Array | Value {"," Value}* [","]
839
     *
840
     * @return array
841
     */
842 265
    private function Values()
843
    {
844 265
        $values = [$this->Value()];
845
846 248
        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 248
        foreach ($values as $k => $value) {
864 248
            if (is_object($value) && $value instanceof \stdClass) {
865 204
                $values[$value->name] = $value->value;
866 60
            } else if ( ! isset($values['value'])){
867 60
                $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 248
            unset($values[$k]);
877
        }
878
879 248
        return $values;
880
    }
881
882
    /**
883
     * Constant ::= integer | string | float | boolean
884
     *
885
     * @return mixed
886
     *
887
     * @throws AnnotationException
888
     */
889 75
    private function Constant()
890
    {
891 75
        $identifier = $this->Identifier();
892
893 75
        if ( ! defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) {
894 17
            list($className, $const) = explode('::', $identifier);
895
896 17
            $pos = strpos($className, '\\');
897 17
            $alias = (false === $pos) ? $className : substr($className, 0, $pos);
898 17
            $found = false;
899 17
            $loweredAlias = strtolower($alias);
900
901
            switch (true) {
902 17
                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 17
                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 2
                    if(isset($this->imports['__NAMESPACE__'])) {
921
                        $ns = $this->imports['__NAMESPACE__'];
922
923
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
924
                            $className = $ns.'\\'.$className;
925
                            $found = true;
926
                        }
927
                    }
928 2
                    break;
929
            }
930
931 17
            if ($found) {
932 15
                 $identifier = $className . '::' . $const;
933
            }
934
        }
935
936
        // checks if identifier ends with ::class, \strlen('::class') === 7
937 75
        $classPos = stripos($identifier, '::class');
938 75
        if ($classPos === strlen($identifier) - 7) {
939 4
            return substr($identifier, 0, $classPos);
940
        }
941
942 71
        if (!defined($identifier)) {
943 1
            throw AnnotationException::semanticalErrorConstants($identifier, $this->context);
944
        }
945
946 70
        return constant($identifier);
947
    }
948
949
    /**
950
     * Identifier ::= string
951
     *
952
     * @return string
953
     */
954 326
    private function Identifier()
955
    {
956
        // check if we have an annotation
957 326
        if ( ! $this->lexer->isNextTokenAny(self::$classIdentifiers)) {
958 14
            $this->syntaxError('namespace separator or identifier');
959
        }
960
961 326
        $this->lexer->moveNext();
962
963 326
        $className = $this->lexer->token['value'];
964
965 326
        while ($this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value']))
966 326
                && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) {
967
968 1
            $this->match(DocLexer::T_NAMESPACE_SEPARATOR);
969 1
            $this->matchAny(self::$classIdentifiers);
970
971
            $className .= '\\' . $this->lexer->token['value'];
972
        }
973
974 325
        return $className;
975
    }
976
977
    /**
978
     * Value ::= PlainValue | FieldAssignment
979
     *
980
     * @return mixed
981
     */
982 265
    private function Value()
983
    {
984 265
        $peek = $this->lexer->glimpse();
985
986 265
        if (DocLexer::T_EQUALS === $peek['type']) {
987 206
            return $this->FieldAssignment();
988
        }
989
990 132
        return $this->PlainValue();
991
    }
992
993
    /**
994
     * PlainValue ::= integer | string | float | boolean | Array | Annotation
995
     *
996
     * @return mixed
997
     */
998 265
    private function PlainValue()
999
    {
1000 265
        if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
1001 79
            return $this->Arrayx();
1002
        }
1003
1004 264
        if ($this->lexer->isNextToken(DocLexer::T_AT)) {
1005 55
            return $this->Annotation();
1006
        }
1007
1008 228
        if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1009 75
            return $this->Constant();
1010
        }
1011
1012 161
        switch ($this->lexer->lookahead['type']) {
1013 161
            case DocLexer::T_STRING:
1014 88
                $this->match(DocLexer::T_STRING);
1015 88
                return $this->lexer->token['value'];
1016
1017 84
            case DocLexer::T_INTEGER:
1018 56
                $this->match(DocLexer::T_INTEGER);
1019 56
                return (int)$this->lexer->token['value'];
1020
1021 30
            case DocLexer::T_FLOAT:
1022 28
                $this->match(DocLexer::T_FLOAT);
1023 28
                return (float)$this->lexer->token['value'];
1024
1025 2
            case DocLexer::T_TRUE:
1026
                $this->match(DocLexer::T_TRUE);
1027
                return true;
1028
1029 2
            case DocLexer::T_FALSE:
1030
                $this->match(DocLexer::T_FALSE);
1031
                return false;
1032
1033 2
            case DocLexer::T_NULL:
1034
                $this->match(DocLexer::T_NULL);
1035
                return null;
1036
1037
            default:
1038 2
                $this->syntaxError('PlainValue');
1039
        }
1040
    }
1041
1042
    /**
1043
     * FieldAssignment ::= FieldName "=" PlainValue
1044
     * FieldName ::= identifier
1045
     *
1046
     * @return \stdClass
1047
     */
1048 206
    private function FieldAssignment()
1049
    {
1050 206
        $this->match(DocLexer::T_IDENTIFIER);
1051 206
        $fieldName = $this->lexer->token['value'];
1052
1053 206
        $this->match(DocLexer::T_EQUALS);
1054
1055 206
        $item = new \stdClass();
1056 206
        $item->name  = $fieldName;
1057 206
        $item->value = $this->PlainValue();
1058
1059 204
        return $item;
1060
    }
1061
1062
    /**
1063
     * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}"
1064
     *
1065
     * @return array
1066
     */
1067 79
    private function Arrayx()
1068
    {
1069 79
        $array = $values = [];
1070
1071 79
        $this->match(DocLexer::T_OPEN_CURLY_BRACES);
1072
1073
        // If the array is empty, stop parsing and return.
1074 79
        if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1075 1
            $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1076
1077 1
            return $array;
1078
        }
1079
1080 79
        $values[] = $this->ArrayEntry();
1081
1082 79
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
1083 54
            $this->match(DocLexer::T_COMMA);
1084
1085
            // optional trailing comma
1086 54
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1087 4
                break;
1088
            }
1089
1090 54
            $values[] = $this->ArrayEntry();
1091
        }
1092
1093 79
        $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1094
1095 79
        foreach ($values as $value) {
1096 79
            list ($key, $val) = $value;
1097
1098 79
            if ($key !== null) {
1099 13
                $array[$key] = $val;
1100
            } else {
1101 79
                $array[] = $val;
1102
            }
1103
        }
1104
1105 79
        return $array;
1106
    }
1107
1108
    /**
1109
     * ArrayEntry ::= Value | KeyValuePair
1110
     * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant
1111
     * Key ::= string | integer | Constant
1112
     *
1113
     * @return array
1114
     */
1115 79
    private function ArrayEntry()
1116
    {
1117 79
        $peek = $this->lexer->glimpse();
1118
1119 79
        if (DocLexer::T_EQUALS === $peek['type']
1120 79
                || DocLexer::T_COLON === $peek['type']) {
1121
1122 13
            if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1123 5
                $key = $this->Constant();
1124
            } else {
1125 8
                $this->matchAny([DocLexer::T_INTEGER, DocLexer::T_STRING]);
1126 8
                $key = $this->lexer->token['value'];
1127
            }
1128
1129 13
            $this->matchAny([DocLexer::T_EQUALS, DocLexer::T_COLON]);
1130
1131 13
            return [$key, $this->PlainValue()];
1132
        }
1133
1134 70
        return [null, $this->Value()];
1135
    }
1136
1137
    /**
1138
     * Checks whether the given $name matches any ignored annotation name or namespace
1139
     *
1140
     * @param string $name
1141
     *
1142
     * @return bool
1143
     */
1144 100
    private function isIgnoredAnnotation($name)
1145
    {
1146 100
        if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
1147 82
            return true;
1148
        }
1149
1150 19
        foreach (array_keys($this->ignoredAnnotationNamespaces) as $ignoredAnnotationNamespace) {
1151 15
            $ignoredAnnotationNamespace = rtrim($ignoredAnnotationNamespace, '\\') . '\\';
1152
1153 15
            if (0 === stripos(rtrim($name, '\\') . '\\', $ignoredAnnotationNamespace)) {
1154 15
                return true;
1155
            }
1156
        }
1157
1158 4
        return false;
1159
    }
1160
}
1161