Failed Conditions
Push — annotation-metadata ( 414255 )
by Michael
02:42
created

DocParser   F

Complexity

Total Complexity 172

Size/Duplication

Total Lines 1083
Duplicated Lines 0 %

Test Coverage

Coverage 92.82%

Importance

Changes 0
Metric Value
wmc 172
eloc 400
dl 0
loc 1083
ccs 375
cts 404
cp 0.9282
rs 2
c 0
b 0
f 0

27 Methods

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

455
                $attribute->required = (false !== strpos(/** @scrutinizer ignore-type */ $propertyComment, '@Required'));
Loading history...
456 123
                $attribute->name     = $property->name;
457 123
                $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

457
                $attribute->type     = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',/** @scrutinizer ignore-type */ $propertyComment, $matches))
Loading history...
458 123
                    ? $matches[1]
459
                    : 'mixed';
460
461 123
                $propertyBuilder = $this->collectAttributeTypeMetadata($propertyBuilder, $attribute);
462
463
                // checks if the property has @Enum
464 123
                if (false !== strpos($propertyComment, '@Enum')) {
465 6
                    $context = 'property ' . $class->name . "::\$" . $property->name;
466
467 6
                    self::$metadataParser->setTarget(Target::TARGET_PROPERTY);
468
469 6
                    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

469
                    foreach (self::$metadataParser->parse(/** @scrutinizer ignore-type */ $propertyComment, $context) as $annotation) {
Loading history...
470 4
                        if ( ! $annotation instanceof Enum) {
471
                            continue;
472
                        }
473
474 4
                        $propertyBuilder = $propertyBuilder->withEnum([
475 4
                            'value'   => $annotation->value,
476 4
                            'literal' => ( ! empty($annotation->literal))
477 1
                                ? $annotation->literal
478 4
                                : $annotation->value,
479
                        ]);
480
                    }
481
                }
482
483 121
                $annotationBuilder = $annotationBuilder->withProperty($propertyBuilder->build());
484
            }
485
        }
486
487 231
        $this->annotationMetadata[] = $annotationBuilder->build();
488 231
    }
489
490
    /**
491
     * Collects parsing metadata for a given attribute.
492
     *
493
     * @param array     $metadata
494
     * @param Attribute $attribute
495
     */
496 157
    private function collectAttributeTypeMetadata(
497
        PropertyMetadataBuilder $metadata,
498
        Attribute $attribute
499
    ) : PropertyMetadataBuilder
500
    {
501
        // handle internal type declaration
502 157
        $type = self::$typeMap[$attribute->type] ?? $attribute->type;
503
504
        // handle the case if the property type is mixed
505 157
        if ('mixed' === $type) {
506 114
            return $metadata;
507
        }
508
509 132
        if ($attribute->required) {
510 8
            $metadata = $metadata->withBeingRequired();
511
        }
512
513
        // Evaluate type
514
515
        // Checks if the property has array<type>
516 132
        if (false !== $pos = strpos($type, '<')) {
517 89
            $arrayType = substr($type, $pos + 1, -1);
518
519 89
            return $metadata->withType([
520 89
                'type' => 'array',
521 89
                'attribute_types' => self::$typeMap[$arrayType] ?? $arrayType,
522
            ]);
523
        }
524
525
            // Checks if the property has type[]
526 132
         if (false !== $pos = strrpos($type, '[')) {
527 89
            $arrayType  = substr($type, 0, $pos);
528
529 89
            return $metadata->withType([
530 89
                'type'            => 'array',
531 89
                'attribute_types' => self::$typeMap[$arrayType] ?? $arrayType,
532
            ]);
533
        }
534
535 132
        return $metadata->withType([
536 132
            'type'  => $type,
537 132
            'value' => $attribute->type,
538
        ]);
539
    }
540
541
    /**
542
     * Annotations ::= Annotation {[ "*" ]* [Annotation]}*
543
     *
544
     * @return array
545
     */
546 276
    private function Annotations()
547
    {
548 276
        $annotations = [];
549
550 276
        while (null !== $this->lexer->lookahead) {
551 276
            if (DocLexer::T_AT !== $this->lexer->lookahead['type']) {
552 58
                $this->lexer->moveNext();
553 58
                continue;
554
            }
555
556
            // make sure the @ is preceded by non-catchable pattern
557 276
            if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) {
558 16
                $this->lexer->moveNext();
559 16
                continue;
560
            }
561
562
            // make sure the @ is followed by either a namespace separator, or
563
            // an identifier token
564 276
            if ((null === $peek = $this->lexer->glimpse())
565 276
                || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true))
566 276
                || $peek['position'] !== $this->lexer->lookahead['position'] + 1) {
567 2
                $this->lexer->moveNext();
568 2
                continue;
569
            }
570
571 276
            $this->isNestedAnnotation = false;
572 276
            if (false !== $annot = $this->Annotation()) {
573 220
                $annotations[] = $annot;
574
            }
575
        }
576
577 268
        return $annotations;
578
    }
579
580
    /**
581
     * Annotation     ::= "@" AnnotationName MethodCall
582
     * AnnotationName ::= QualifiedName | SimpleName
583
     * QualifiedName  ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName
584
     * NameSpacePart  ::= identifier | null | false | true
585
     * SimpleName     ::= identifier | null | false | true
586
     *
587
     * @return mixed False if it is not a valid annotation.
588
     *
589
     * @throws AnnotationException
590
     */
591 276
    private function Annotation()
592
    {
593 276
        $this->match(DocLexer::T_AT);
594
595
        // check if we have an annotation
596 276
        $name = $this->Identifier();
597
598
        // only process names which are not fully qualified, yet
599
        // fully qualified names must start with a \
600 275
        $originalName = $name;
601
602 275
        if ('\\' !== $name[0]) {
603 273
            $pos = strpos($name, '\\');
604 273
            $alias = (false === $pos)? $name : substr($name, 0, $pos);
605 273
            $found = false;
606 273
            $loweredAlias = strtolower($alias);
607
608 273
            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...
609 2
                foreach ($this->namespaces as $namespace) {
610 2
                    if ($this->classExists($namespace.'\\'.$name)) {
611 2
                        $name = $namespace.'\\'.$name;
612 2
                        $found = true;
613 2
                        break;
614
                    }
615
                }
616 273
            } elseif (isset($this->imports[$loweredAlias])) {
617 211
                $found = true;
618 211
                $name  = (false !== $pos)
619 1
                    ? $this->imports[$loweredAlias] . substr($name, $pos)
620 211
                    : $this->imports[$loweredAlias];
621 273
            } elseif ( ! isset($this->ignoredAnnotationNames[$name])
622 273
                && isset($this->imports['__NAMESPACE__'])
623 273
                && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name)
624
            ) {
625 43
                $name  = $this->imports['__NAMESPACE__'].'\\'.$name;
626 43
                $found = true;
627 273
            } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) {
628 132
                $found = true;
629
            }
630
631 273
            if ( ! $found) {
632 273
                if ($this->isIgnoredAnnotation($name)) {
633 269
                    return false;
634
                }
635
636 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));
637
            }
638
        }
639
640 247
        $name = ltrim($name,'\\');
641
642 247
        if ( ! $this->classExists($name)) {
643
            throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context));
644
        }
645
646
        // at this point, $name contains the fully qualified class name of the
647
        // annotation, and it is also guaranteed that this class exists, and
648
        // that it is loaded
649
650
651
        // collects the metadata annotation only if there is not yet
652 247
        if (! isset($this->annotationMetadata[$name]) && ! array_key_exists($name, $this->nonAnnotationClasses)) {
653 247
            $this->collectAnnotationMetadata($name);
654
        }
655
656
        // verify that the class is really meant to be an annotation and not just any ordinary class
657 247
        if (array_key_exists($name, $this->nonAnnotationClasses)) {
658 5
            if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$originalName])) {
659 2
                return false;
660
            }
661
662 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));
663
        }
664
665
        //if target is nested annotation
666 242
        $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target;
667
668
        // Next will be nested
669 242
        $this->isNestedAnnotation = true;
670
671
        //if annotation does not support current target
672 242
        if (($this->annotationMetadata[$name]->getTarget()->unwrap() & $target) === 0 && $target) {
673 9
            throw AnnotationException::semanticalError(
674 9
                sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.',
675 9
                     $originalName, $this->context, $this->annotationMetadata[$name]->getTarget()->describe())
676
            );
677
        }
678
679 242
        $values = $this->MethodCall();
680
681
        // checks all declared attributes for enums
682 225
        foreach ($this->annotationMetadata[$name]->getProperties() as $property) {
683 179
            $enum = $property->getEnum();
684
685 179
            if ($enum === null) {
686 179
                continue;
687
            }
688
689 4
            $propertyName = $property->getName();
690
691
            // checks if the attribute is a valid enumerator
692 4
            if (isset($values[$propertyName]) && ! in_array($values[$propertyName], $enum['value'])) {
693 4
                throw AnnotationException::enumeratorError($propertyName, $name, $this->context, $enum['literal'], $values[$propertyName]);
694
            }
695
        }
696
697
        // checks all declared attributes
698 225
        foreach ($this->annotationMetadata[$name]->getProperties() as $property) {
699 179
            $propertyName = $property->getName();
700 179
            $valueName    = $propertyName;
701 179
            $type         = $property->getType();
702
703 179
            if ($property->isDefault() && !isset($values[$propertyName]) && isset($values['value'])) {
704 49
                $valueName = 'value';
705
            }
706
707
            // handle a not given attribute or null value
708 179
            if (! isset($values[$valueName])) {
709 148
                if ($property->isRequired()) {
710
                    throw AnnotationException::requiredError($propertyName, $originalName, $this->context, 'a(n) ' . $type['value']);
711
                }
712
713 148
                continue;
714
            }
715
716 174
            var_dump($type['type']);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($type['type']) looks like debug code. Are you sure you do not want to remove it?
Loading history...
717 174
            if ($type !== null && $type['type'] === 'array') {
718
                // handle the case of a single value
719 165
                if ( ! is_array($values[$valueName])) {
720 159
                    $values[$valueName] = [$values[$valueName]];
721
                }
722
723
                // checks if the attribute has array type declaration, such as "array<string>"
724 165
                if (isset($type['array_type'])) {
725 165
                    foreach ($values[$valueName] as $item) {
726 165
                        if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) {
727
                            throw AnnotationException::attributeTypeError($propertyName, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item);
728
                        }
729
                    }
730
                }
731 135
            } elseif ($type !== null && gettype($values[$valueName]) !== $type['type'] && !$values[$valueName] instanceof $type['type']) {
732 39
                throw AnnotationException::attributeTypeError($propertyName, $originalName, $this->context, 'a(n) '.$type['value'], $values[$valueName]);
733
            }
734
        }
735
736
        // check if the annotation expects values via the constructor,
737
        // or directly injected into public properties
738 225
        if ($this->annotationMetadata[$name]->hasConstructor()) {
739 204
            return new $name($values);
740
        }
741
742 141
        $instance = new $name();
743
744 141
        foreach ($values as $property => $value) {
745 125
            if (! isset($this->annotationMetadata[$name]->getProperties()[$property])) {
746 52
                if ('value' !== $property) {
747 1
                    throw AnnotationException::creationError(
748 1
                        sprintf(
749 1
                            'The annotation @%s declared on %s does not have a property named "%s". Available properties: %s',
750 1
                            $originalName,
751 1
                            $this->context,
752 1
                            $property,
753 1
                            implode(', ', array_keys($this->annotationMetadata[$name]->getProperties()))
754
                        )
755
                    );
756
                }
757
758
                // handle the case if the property has no annotations
759 51
                if ($this->annotationMetadata[$name]->getDefaultProperty() === null) {
760 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)));
761
                }
762
            }
763
764 122
            $instance->{$this->annotationMetadata[$name]->getDefaultProperty()->getName()} = $value;
765
        }
766
767 138
        return $instance;
768
    }
769
770
    /**
771
     * MethodCall ::= ["(" [Values] ")"]
772
     *
773
     * @return array
774
     */
775 242
    private function MethodCall()
776
    {
777 242
        $values = [];
778
779 242
        if ( ! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
780 58
            return $values;
781
        }
782
783 223
        $this->match(DocLexer::T_OPEN_PARENTHESIS);
784
785 223
        if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
786 223
            $values = $this->Values();
787
        }
788
789 206
        $this->match(DocLexer::T_CLOSE_PARENTHESIS);
790
791 206
        return $values;
792
    }
793
794
    /**
795
     * Values ::= Array | Value {"," Value}* [","]
796
     *
797
     * @return array
798
     */
799 223
    private function Values()
800
    {
801 223
        $values = [$this->Value()];
802
803 206
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
804 47
            $this->match(DocLexer::T_COMMA);
805
806 47
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
807 2
                break;
808
            }
809
810 45
            $token = $this->lexer->lookahead;
811 45
            $value = $this->Value();
812
813 45
            if ( ! is_object($value) && ! is_array($value)) {
814
                $this->syntaxError('Value', $token);
815
            }
816
817 45
            $values[] = $value;
818
        }
819
820 206
        foreach ($values as $k => $value) {
821 206
            if (is_object($value) && $value instanceof \stdClass) {
822 154
                $values[$value->name] = $value->value;
823 188
            } else if ( ! isset($values['value'])){
824 188
                $values['value'] = $value;
825
            } else {
826 1
                if ( ! is_array($values['value'])) {
827 1
                    $values['value'] = [$values['value']];
828
                }
829
830 1
                $values['value'][] = $value;
831
            }
832
833 206
            unset($values[$k]);
834
        }
835
836 206
        return $values;
837
    }
838
839
    /**
840
     * Constant ::= integer | string | float | boolean
841
     *
842
     * @return mixed
843
     *
844
     * @throws AnnotationException
845
     */
846 63
    private function Constant()
847
    {
848 63
        $identifier = $this->Identifier();
849
850 63
        if ( ! defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) {
851 17
            list($className, $const) = explode('::', $identifier);
852
853 17
            $pos = strpos($className, '\\');
854 17
            $alias = (false === $pos) ? $className : substr($className, 0, $pos);
855 17
            $found = false;
856 17
            $loweredAlias = strtolower($alias);
857
858
            switch (true) {
859 17
                case !empty ($this->namespaces):
860
                    foreach ($this->namespaces as $ns) {
861
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
862
                             $className = $ns.'\\'.$className;
863
                             $found = true;
864
                             break;
865
                        }
866
                    }
867
                    break;
868
869 17
                case isset($this->imports[$loweredAlias]):
870 15
                    $found     = true;
871 15
                    $className = (false !== $pos)
872
                        ? $this->imports[$loweredAlias] . substr($className, $pos)
873 15
                        : $this->imports[$loweredAlias];
874 15
                    break;
875
876
                default:
877 2
                    if(isset($this->imports['__NAMESPACE__'])) {
878
                        $ns = $this->imports['__NAMESPACE__'];
879
880
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
881
                            $className = $ns.'\\'.$className;
882
                            $found = true;
883
                        }
884
                    }
885 2
                    break;
886
            }
887
888 17
            if ($found) {
889 15
                 $identifier = $className . '::' . $const;
890
            }
891
        }
892
893
        // checks if identifier ends with ::class, \strlen('::class') === 7
894 63
        $classPos = stripos($identifier, '::class');
895 63
        if ($classPos === strlen($identifier) - 7) {
896 4
            return substr($identifier, 0, $classPos);
897
        }
898
899 59
        if (!defined($identifier)) {
900 1
            throw AnnotationException::semanticalErrorConstants($identifier, $this->context);
901
        }
902
903 58
        return constant($identifier);
904
    }
905
906
    /**
907
     * Identifier ::= string
908
     *
909
     * @return string
910
     */
911 276
    private function Identifier()
912
    {
913
        // check if we have an annotation
914 276
        if ( ! $this->lexer->isNextTokenAny(self::$classIdentifiers)) {
915 14
            $this->syntaxError('namespace separator or identifier');
916
        }
917
918 276
        $this->lexer->moveNext();
919
920 276
        $className = $this->lexer->token['value'];
921
922 276
        while ($this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value']))
923 276
                && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) {
924
925 1
            $this->match(DocLexer::T_NAMESPACE_SEPARATOR);
926 1
            $this->matchAny(self::$classIdentifiers);
927
928
            $className .= '\\' . $this->lexer->token['value'];
929
        }
930
931 275
        return $className;
932
    }
933
934
    /**
935
     * Value ::= PlainValue | FieldAssignment
936
     *
937
     * @return mixed
938
     */
939 223
    private function Value()
940
    {
941 223
        $peek = $this->lexer->glimpse();
942
943 223
        if (DocLexer::T_EQUALS === $peek['type']) {
944 156
            return $this->FieldAssignment();
945
        }
946
947 204
        return $this->PlainValue();
948
    }
949
950
    /**
951
     * PlainValue ::= integer | string | float | boolean | Array | Annotation
952
     *
953
     * @return mixed
954
     */
955 223
    private function PlainValue()
956
    {
957 223
        if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
958 96
            return $this->Arrayx();
959
        }
960
961 222
        if ($this->lexer->isNextToken(DocLexer::T_AT)) {
962 73
            return $this->Annotation();
963
        }
964
965 208
        if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
966 63
            return $this->Constant();
967
        }
968
969 207
        switch ($this->lexer->lookahead['type']) {
970 207
            case DocLexer::T_STRING:
971 200
                $this->match(DocLexer::T_STRING);
972 200
                return $this->lexer->token['value'];
973
974 62
            case DocLexer::T_INTEGER:
975 43
                $this->match(DocLexer::T_INTEGER);
976 43
                return (int)$this->lexer->token['value'];
977
978 21
            case DocLexer::T_FLOAT:
979 19
                $this->match(DocLexer::T_FLOAT);
980 19
                return (float)$this->lexer->token['value'];
981
982 2
            case DocLexer::T_TRUE:
983
                $this->match(DocLexer::T_TRUE);
984
                return true;
985
986 2
            case DocLexer::T_FALSE:
987
                $this->match(DocLexer::T_FALSE);
988
                return false;
989
990 2
            case DocLexer::T_NULL:
991
                $this->match(DocLexer::T_NULL);
992
                return null;
993
994
            default:
995 2
                $this->syntaxError('PlainValue');
996
        }
997
    }
998
999
    /**
1000
     * FieldAssignment ::= FieldName "=" PlainValue
1001
     * FieldName ::= identifier
1002
     *
1003
     * @return \stdClass
1004
     */
1005 156
    private function FieldAssignment()
1006
    {
1007 156
        $this->match(DocLexer::T_IDENTIFIER);
1008 156
        $fieldName = $this->lexer->token['value'];
1009
1010 156
        $this->match(DocLexer::T_EQUALS);
1011
1012 156
        $item = new \stdClass();
1013 156
        $item->name  = $fieldName;
1014 156
        $item->value = $this->PlainValue();
1015
1016 154
        return $item;
1017
    }
1018
1019
    /**
1020
     * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}"
1021
     *
1022
     * @return array
1023
     */
1024 96
    private function Arrayx()
1025
    {
1026 96
        $array = $values = [];
1027
1028 96
        $this->match(DocLexer::T_OPEN_CURLY_BRACES);
1029
1030
        // If the array is empty, stop parsing and return.
1031 96
        if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1032 1
            $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1033
1034 1
            return $array;
1035
        }
1036
1037 96
        $values[] = $this->ArrayEntry();
1038
1039 96
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
1040 75
            $this->match(DocLexer::T_COMMA);
1041
1042
            // optional trailing comma
1043 75
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1044 36
                break;
1045
            }
1046
1047 75
            $values[] = $this->ArrayEntry();
1048
        }
1049
1050 96
        $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1051
1052 96
        foreach ($values as $value) {
1053 96
            list ($key, $val) = $value;
1054
1055 96
            if ($key !== null) {
1056 13
                $array[$key] = $val;
1057
            } else {
1058 87
                $array[] = $val;
1059
            }
1060
        }
1061
1062 96
        return $array;
1063
    }
1064
1065
    /**
1066
     * ArrayEntry ::= Value | KeyValuePair
1067
     * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant
1068
     * Key ::= string | integer | Constant
1069
     *
1070
     * @return array
1071
     */
1072 96
    private function ArrayEntry()
1073
    {
1074 96
        $peek = $this->lexer->glimpse();
1075
1076 96
        if (DocLexer::T_EQUALS === $peek['type']
1077 96
                || DocLexer::T_COLON === $peek['type']) {
1078
1079 13
            if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1080 5
                $key = $this->Constant();
1081
            } else {
1082 8
                $this->matchAny([DocLexer::T_INTEGER, DocLexer::T_STRING]);
1083 8
                $key = $this->lexer->token['value'];
1084
            }
1085
1086 13
            $this->matchAny([DocLexer::T_EQUALS, DocLexer::T_COLON]);
1087
1088 13
            return [$key, $this->PlainValue()];
1089
        }
1090
1091 87
        return [null, $this->Value()];
1092
    }
1093
1094
    /**
1095
     * Checks whether the given $name matches any ignored annotation name or namespace
1096
     *
1097
     * @param string $name
1098
     *
1099
     * @return bool
1100
     */
1101 273
    private function isIgnoredAnnotation($name)
1102
    {
1103 273
        if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
1104 255
            return true;
1105
        }
1106
1107 19
        foreach (array_keys($this->ignoredAnnotationNamespaces) as $ignoredAnnotationNamespace) {
1108 15
            $ignoredAnnotationNamespace = rtrim($ignoredAnnotationNamespace, '\\') . '\\';
1109
1110 15
            if (0 === stripos(rtrim($name, '\\') . '\\', $ignoredAnnotationNamespace)) {
1111 15
                return true;
1112
            }
1113
        }
1114
1115 4
        return false;
1116
    }
1117
}
1118