Completed
Pull Request — master (#211)
by
unknown
02:33
created

DocParser::setIgnoreNotImportedAnnotations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace Doctrine\Annotations;
4
5
use Doctrine\Annotations\Annotation\Attribute;
6
use ReflectionClass;
7
use Doctrine\Annotations\Annotation\Enum;
8
use Doctrine\Annotations\Annotation\Target;
9
use Doctrine\Annotations\Annotation\Attributes;
10
11
/**
12
 * A parser for docblock annotations.
13
 *
14
 * It is strongly discouraged to change the default annotation parsing process.
15
 *
16
 * @author Benjamin Eberlei <[email protected]>
17
 * @author Guilherme Blanco <[email protected]>
18
 * @author Jonathan Wage <[email protected]>
19
 * @author Roman Borschel <[email protected]>
20
 * @author Johannes M. Schmitt <[email protected]>
21
 * @author Fabio B. Silva <[email protected]>
22
 */
23
final class DocParser
24
{
25
    /**
26
     * An array of all valid tokens for a class name.
27
     *
28
     * @var array
29
     */
30
    private static $classIdentifiers = [
31
        DocLexer::T_IDENTIFIER,
32
        DocLexer::T_TRUE,
33
        DocLexer::T_FALSE,
34
        DocLexer::T_NULL
35
    ];
36
37
    /**
38
     * The lexer.
39
     *
40
     * @var \Doctrine\Annotations\DocLexer
41
     */
42
    private $lexer;
43
44
    /**
45
     * Current target context.
46
     *
47
     * @var integer
48
     */
49
    private $target;
50
51
    /**
52
     * Doc parser used to collect annotation target.
53
     *
54
     * @var \Doctrine\Annotations\DocParser
55
     */
56
    private static $metadataParser;
57
58
    /**
59
     * Flag to control if the current annotation is nested or not.
60
     *
61
     * @var boolean
62
     */
63
    private $isNestedAnnotation = false;
64
65
    /**
66
     * Hashmap containing all use-statements that are to be used when parsing
67
     * the given doc block.
68
     *
69
     * @var array
70
     */
71
    private $imports = [];
72
73
    /**
74
     * This hashmap is used internally to cache results of class_exists()
75
     * look-ups.
76
     *
77
     * @var array
78
     */
79
    private $classExists = [];
80
81
    /**
82
     * Whether annotations that have not been imported should be ignored.
83
     *
84
     * @var boolean
85
     */
86
    private $ignoreNotImportedAnnotations = false;
87
88
    /**
89
     * An array of default namespaces if operating in simple mode.
90
     *
91
     * @var string[]
92
     */
93
    private $namespaces = [];
94
95
    /**
96
     * A list with annotations that are not causing exceptions when not resolved to an annotation class.
97
     *
98
     * The names must be the raw names as used in the class, not the fully qualified
99
     * class names.
100
     *
101
     * @var bool[] indexed by annotation name
102
     */
103
    private $ignoredAnnotationNames = [];
104
105
    /**
106
     * A list with annotations in namespaced format
107
     * that are not causing exceptions when not resolved to an annotation class.
108
     *
109
     * @var bool[] indexed by namespace name
110
     */
111
    private $ignoredAnnotationNamespaces = [];
112
113
    /**
114
     * @var string
115
     */
116
    private $context = '';
117
118
    /**
119
     * Hash-map for caching annotation metadata.
120
     *
121
     * @var array
122
     */
123
    private static $annotationMetadata = [
124
        'Doctrine\Annotations\Annotation\Target' => [
125
            'is_annotation'    => true,
126
            'has_constructor'  => true,
127
            'properties'       => [],
128
            'targets_literal'  => 'ANNOTATION_CLASS',
129
            'targets'          => Target::TARGET_CLASS,
130
            'default_property' => 'value',
131
            'attribute_types'  => [
132
                'value'  => [
133
                    'required'  => false,
134
                    'type'      =>'array',
135
                    'array_type'=>'string',
136
                    'value'     =>'array<string>'
137
                ]
138
             ],
139
        ],
140
        'Doctrine\Annotations\Annotation\Attribute' => [
141
            'is_annotation'    => true,
142
            'has_constructor'  => false,
143
            'targets_literal'  => 'ANNOTATION_ANNOTATION',
144
            'targets'          => Target::TARGET_ANNOTATION,
145
            'default_property' => 'name',
146
            'properties'       => [
147
                'name'      => 'name',
148
                'type'      => 'type',
149
                'required'  => 'required'
150
            ],
151
            'attribute_types'  => [
152
                'value'  => [
153
                    'required'  => true,
154
                    'type'      =>'string',
155
                    'value'     =>'string'
156
                ],
157
                'type'  => [
158
                    'required'  =>true,
159
                    'type'      =>'string',
160
                    'value'     =>'string'
161
                ],
162
                'required'  => [
163
                    'required'  =>false,
164
                    'type'      =>'boolean',
165
                    'value'     =>'boolean'
166
                ]
167
             ],
168
        ],
169
        'Doctrine\Annotations\Annotation\Attributes' => [
170
            'is_annotation'    => true,
171
            'has_constructor'  => false,
172
            'targets_literal'  => 'ANNOTATION_CLASS',
173
            'targets'          => Target::TARGET_CLASS,
174
            'default_property' => 'value',
175
            'properties'       => [
176
                'value' => 'value'
177
            ],
178
            'attribute_types'  => [
179
                'value' => [
180
                    'type'      =>'array',
181
                    'required'  =>true,
182
                    'array_type'=>'Doctrine\Annotations\Annotation\Attribute',
183
                    'value'     =>'array<Doctrine\Annotations\Annotation\Attribute>'
184
                ]
185
             ],
186
        ],
187
        'Doctrine\Annotations\Annotation\Enum' => [
188
            'is_annotation'    => true,
189
            'has_constructor'  => true,
190
            'targets_literal'  => 'ANNOTATION_PROPERTY',
191
            'targets'          => Target::TARGET_PROPERTY,
192
            'default_property' => 'value',
193
            'properties'       => [
194
                'value' => 'value'
195
            ],
196
            'attribute_types'  => [
197
                'value' => [
198
                    'type'      => 'array',
199
                    'required'  => true,
200
                ],
201
                'literal' => [
202
                    'type'      => 'array',
203
                    'required'  => false,
204
                ],
205
             ],
206
        ],
207
    ];
208
209
    /**
210
     * Hash-map for handle types declaration.
211
     *
212
     * @var array
213
     */
214
    private static $typeMap = [
215
        'float'     => 'double',
216
        'bool'      => 'boolean',
217
        // allow uppercase Boolean in honor of George Boole
218
        'Boolean'   => 'boolean',
219
        'int'       => 'integer',
220
    ];
221
222
    /**
223
     * Constructs a new DocParser.
224
     */
225 329
    public function __construct()
226
    {
227 329
        $this->lexer = new DocLexer;
228 329
    }
229
230
    /**
231
     * Sets the annotation names that are ignored during the parsing process.
232
     *
233
     * The names are supposed to be the raw names as used in the class, not the
234
     * fully qualified class names.
235
     *
236
     * @param bool[] $names indexed by annotation name
237
     *
238
     * @return void
239
     */
240 85
    public function setIgnoredAnnotationNames(array $names)
241
    {
242 85
        $this->ignoredAnnotationNames = $names;
243 85
    }
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 3
    public function addNamespace($namespace)
279
    {
280 3
        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 3
        $this->namespaces[] = $namespace;
285 3
    }
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 328
    public function parse($input, $context = '')
326
    {
327 328
        $pos = $this->findInitialTokenPosition($input);
328 328
        if ($pos === null) {
329 32
            return [];
330
        }
331
332 327
        $this->context = $context;
333
334 327
        $this->lexer->setInput(trim(substr($input, $pos), '* /'));
335 327
        $this->lexer->moveNext();
336
337 327
        return $this->Annotations();
338
    }
339
340
    /**
341
     * Finds the first valid annotation
342
     *
343
     * @param string $input The docblock string to parse
344
     *
345
     * @return int|null
346
     */
347 328
    private function findInitialTokenPosition($input)
348
    {
349 328
        $pos = 0;
350
351
        // search for first valid annotation
352 328
        while (($pos = strpos($input, '@', $pos)) !== false) {
353 328
            $preceding = substr($input, $pos - 1, 1);
354
355
            // if the @ is preceded by a space, a tab or * it is valid
356 328
            if ($pos === 0 || $preceding === ' ' || $preceding === '*' || $preceding === "\t") {
357 327
                return $pos;
358
            }
359
360 3
            $pos++;
361
        }
362
363 32
        return null;
364
    }
365
366
    /**
367
     * Attempts to match the given token with the current lookahead token.
368
     * If they match, updates the lookahead token; otherwise raises a syntax error.
369
     *
370
     * @param integer $token Type of token.
371
     *
372
     * @return boolean True if tokens match; false otherwise.
373
     */
374 327
    private function match($token)
375
    {
376 327
        if ( ! $this->lexer->isNextToken($token) ) {
377
            $this->syntaxError($this->lexer->getLiteral($token));
378
        }
379
380 327
        return $this->lexer->moveNext();
381
    }
382
383
    /**
384
     * Attempts to match the current lookahead token with any of the given tokens.
385
     *
386
     * If any of them matches, this method updates the lookahead token; otherwise
387
     * a syntax error is raised.
388
     *
389
     * @param array $tokens
390
     *
391
     * @return boolean
392
     */
393 14
    private function matchAny(array $tokens)
394
    {
395 14
        if ( ! $this->lexer->isNextTokenAny($tokens)) {
396 1
            $this->syntaxError(implode(' or ', array_map([$this->lexer, 'getLiteral'], $tokens)));
397
        }
398
399 13
        return $this->lexer->moveNext();
400
    }
401
402
    /**
403
     * Generates a new syntax error.
404
     *
405
     * @param string     $expected Expected string.
406
     * @param array|null $token    Optional token.
407
     *
408
     * @return void
409
     *
410
     * @throws AnnotationException
411
     */
412 17
    private function syntaxError($expected, $token = null)
413
    {
414 17
        if ($token === null) {
415 17
            $token = $this->lexer->lookahead;
416
        }
417
418 17
        $message  = sprintf('Expected %s, got ', $expected);
419 17
        $message .= ($this->lexer->lookahead === null)
420
            ? 'end of string'
421 17
            : sprintf("'%s' at position %s", $token['value'], $token['position']);
422
423 17
        if (strlen($this->context)) {
424 14
            $message .= ' in ' . $this->context;
425
        }
426
427 17
        $message .= '.';
428
429 17
        throw AnnotationException::syntaxError($message);
430
    }
431
432
    /**
433
     * Attempts to check if a class exists or not. This always uses PHP autoloading mechanism.
434
     *
435
     * @param string $fqcn
436
     *
437
     * @return boolean
438
     */
439 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 327
    private function Annotations()
608
    {
609 327
        $annotations = [];
610
611 327
        while (null !== $this->lexer->lookahead) {
612 327
            if (DocLexer::T_AT !== $this->lexer->lookahead['type']) {
613 54
                $this->lexer->moveNext();
614 54
                continue;
615
            }
616
617
            // make sure the @ is preceded by non-catchable pattern
618 327
            if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) {
619 9
                $this->lexer->moveNext();
620 9
                continue;
621
            }
622
623
            // make sure the @ is followed by either a namespace separator, or
624
            // an identifier token
625 327
            if ((null === $peek = $this->lexer->glimpse())
626 327
                || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true))
627 327
                || $peek['position'] !== $this->lexer->lookahead['position'] + 1) {
628 2
                $this->lexer->moveNext();
629 2
                continue;
630
            }
631
632 327
            $this->isNestedAnnotation = false;
633 327
            if (false !== $annot = $this->Annotation()) {
634 151
                $annotations[] = $annot;
635
            }
636
        }
637
638 197
        return $annotations;
639
    }
640
641
    /**
642
     * Annotation     ::= "@" AnnotationName MethodCall
643
     * AnnotationName ::= QualifiedName | SimpleName
644
     * QualifiedName  ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName
645
     * NameSpacePart  ::= identifier | null | false | true
646
     * SimpleName     ::= identifier | null | false | true
647
     *
648
     * @return mixed False if it is not a valid annotation.
649
     *
650
     * @throws AnnotationException
651
     */
652 327
    private function Annotation()
653
    {
654 327
        $this->match(DocLexer::T_AT);
655
656
        // check if we have an annotation
657 327
        $name = $this->Identifier();
658
659
        // only process names which are not fully qualified, yet
660
        // fully qualified names must start with a \
661 326
        $originalName = $name;
662
663 326
        if ('\\' !== $name[0]) {
664 322
            $pos = strpos($name, '\\');
665 322
            $alias = (false === $pos)? $name : substr($name, 0, $pos);
666 322
            $found = false;
667 322
            $loweredAlias = strtolower($alias);
668
669 322
            if ($this->namespaces) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namespaces of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
670 3
                if (isset($this->ignoredAnnotationNames[$name])) {
671 1
                    return false;
672
                }
673 2
                foreach ($this->namespaces as $namespace) {
674 2
                    if ($this->classExists($namespace.'\\'.$name)) {
675 2
                        $name = $namespace.'\\'.$name;
676 2
                        $found = true;
677 2
                        break;
678
                    }
679
                }
680 320
            } elseif (isset($this->imports[$loweredAlias])) {
681 96
                $found = true;
682 96
                $name  = (false !== $pos)
683 1
                    ? $this->imports[$loweredAlias] . substr($name, $pos)
684 96
                    : $this->imports[$loweredAlias];
685 282
            } elseif ( ! isset($this->ignoredAnnotationNames[$name])
686 282
                && isset($this->imports['__NAMESPACE__'])
687 282
                && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name)
688
            ) {
689 45
                $name  = $this->imports['__NAMESPACE__'].'\\'.$name;
690 45
                $found = true;
691 270
            } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) {
692 182
                $found = true;
693
            }
694
695 321
            if ( ! $found) {
696 100
                if ($this->isIgnoredAnnotation($name)) {
697 96
                    return false;
698
                }
699
700 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));
701
            }
702
        }
703
704 297
        $name = ltrim($name,'\\');
705
706 297
        if ( ! $this->classExists($name)) {
707
            throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context));
708
        }
709
710
        // at this point, $name contains the fully qualified class name of the
711
        // annotation, and it is also guaranteed that this class exists, and
712
        // that it is loaded
713
714
715
        // collects the metadata annotation only if there is not yet
716 297
        if ( ! isset(self::$annotationMetadata[$name])) {
717 41
            $this->collectAnnotationMetadata($name);
718
        }
719
720
        // verify that the class is really meant to be an annotation and not just any ordinary class
721 297
        if (self::$annotationMetadata[$name]['is_annotation'] === false) {
722 5
            if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$originalName])) {
723 2
                return false;
724
            }
725
726 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));
727
        }
728
729
        //if target is nested annotation
730 292
        $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target;
731
732
        // Next will be nested
733 292
        $this->isNestedAnnotation = true;
734
735
        //if annotation does not support current target
736 292
        if (0 === (self::$annotationMetadata[$name]['targets'] & $target) && $target) {
737 9
            throw AnnotationException::semanticalError(
738 9
                sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.',
739 9
                     $originalName, $this->context, self::$annotationMetadata[$name]['targets_literal'])
740
            );
741
        }
742
743 284
        $values = $this->MethodCall();
744
745 267
        if (isset(self::$annotationMetadata[$name]['enum'])) {
746
            // checks all declared attributes
747 4
            foreach (self::$annotationMetadata[$name]['enum'] as $property => $enum) {
748
                // checks if the attribute is a valid enumerator
749 4
                if (isset($values[$property]) && ! in_array($values[$property], $enum['value'])) {
750 4
                    throw AnnotationException::enumeratorError($property, $name, $this->context, $enum['literal'], $values[$property]);
751
                }
752
            }
753
        }
754
755
        // checks all declared attributes
756 266
        foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) {
757 190
            if ($property === self::$annotationMetadata[$name]['default_property']
758 190
                && !isset($values[$property]) && isset($values['value'])) {
759 7
                $property = 'value';
760
            }
761
762
            // handle a not given attribute or null value
763 190
            if (!isset($values[$property])) {
764 168
                if ($type['required']) {
765 2
                    throw AnnotationException::requiredError($property, $originalName, $this->context, 'a(n) '.$type['value']);
766
                }
767
768 166
                continue;
769
            }
770
771 177
            if ($type['type'] === 'array') {
772
                // handle the case of a single value
773 63
                if ( ! is_array($values[$property])) {
774 29
                    $values[$property] = [$values[$property]];
775
                }
776
777
                // checks if the attribute has array type declaration, such as "array<string>"
778 63
                if (isset($type['array_type'])) {
779 59
                    foreach ($values[$property] as $item) {
780 59
                        if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) {
781 63
                            throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item);
782
                        }
783
                    }
784
                }
785 118
            } elseif (gettype($values[$property]) !== $type['type'] && !$values[$property] instanceof $type['type']) {
786 141
                throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'a(n) '.$type['value'], $values[$property]);
787
            }
788
        }
789
790
        // check if the annotation expects values via the constructor,
791
        // or directly injected into public properties
792 170
        if (self::$annotationMetadata[$name]['has_constructor'] === true) {
793 83
            return new $name($values);
794
        }
795
796 107
        $instance = new $name();
797
798 107
        foreach ($values as $property => $value) {
799 75
            if (!isset(self::$annotationMetadata[$name]['properties'][$property])) {
800 20
                if ('value' !== $property) {
801 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'])));
802
                }
803
804
                // handle the case if the property has no annotations
805 19
                if ( ! $property = self::$annotationMetadata[$name]['default_property']) {
806 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)));
807
                }
808
            }
809
810 72
            $instance->{$property} = $value;
811
        }
812
813 104
        return $instance;
814
    }
815
816
    /**
817
     * MethodCall ::= ["(" [Values] ")"]
818
     *
819
     * @return array
820
     */
821 284
    private function MethodCall()
822
    {
823 284
        $values = [];
824
825 284
        if ( ! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
826 62
            return $values;
827
        }
828
829 265
        $this->match(DocLexer::T_OPEN_PARENTHESIS);
830
831 265
        if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
832 265
            $values = $this->Values();
833
        }
834
835 248
        $this->match(DocLexer::T_CLOSE_PARENTHESIS);
836
837 248
        return $values;
838
    }
839
840
    /**
841
     * Values ::= Array | Value {"," Value}* [","]
842
     *
843
     * @return array
844
     */
845 265
    private function Values()
846
    {
847 265
        $values = [$this->Value()];
848
849 248
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
850 15
            $this->match(DocLexer::T_COMMA);
851
852 15
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
853 2
                break;
854
            }
855
856 13
            $token = $this->lexer->lookahead;
857 13
            $value = $this->Value();
858
859 13
            if ( ! is_object($value) && ! is_array($value)) {
860
                $this->syntaxError('Value', $token);
861
            }
862
863 13
            $values[] = $value;
864
        }
865
866 248
        foreach ($values as $k => $value) {
867 248
            if (is_object($value) && $value instanceof \stdClass) {
868 204
                $values[$value->name] = $value->value;
869 60
            } else if ( ! isset($values['value'])){
870 60
                $values['value'] = $value;
871
            } else {
872 1
                if ( ! is_array($values['value'])) {
873 1
                    $values['value'] = [$values['value']];
874
                }
875
876 1
                $values['value'][] = $value;
877
            }
878
879 248
            unset($values[$k]);
880
        }
881
882 248
        return $values;
883
    }
884
885
    /**
886
     * Constant ::= integer | string | float | boolean
887
     *
888
     * @return mixed
889
     *
890
     * @throws AnnotationException
891
     */
892 75
    private function Constant()
893
    {
894 75
        $identifier = $this->Identifier();
895
896 75
        if ( ! defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) {
897 17
            list($className, $const) = explode('::', $identifier);
898
899 17
            $pos = strpos($className, '\\');
900 17
            $alias = (false === $pos) ? $className : substr($className, 0, $pos);
901 17
            $found = false;
902 17
            $loweredAlias = strtolower($alias);
903
904
            switch (true) {
905 17
                case !empty ($this->namespaces):
906
                    foreach ($this->namespaces as $ns) {
907
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
908
                             $className = $ns.'\\'.$className;
909
                             $found = true;
910
                             break;
911
                        }
912
                    }
913
                    break;
914
915 17
                case isset($this->imports[$loweredAlias]):
916 15
                    $found     = true;
917 15
                    $className = (false !== $pos)
918
                        ? $this->imports[$loweredAlias] . substr($className, $pos)
919 15
                        : $this->imports[$loweredAlias];
920 15
                    break;
921
922
                default:
923 2
                    if(isset($this->imports['__NAMESPACE__'])) {
924
                        $ns = $this->imports['__NAMESPACE__'];
925
926
                        if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) {
927
                            $className = $ns.'\\'.$className;
928
                            $found = true;
929
                        }
930
                    }
931 2
                    break;
932
            }
933
934 17
            if ($found) {
935 15
                 $identifier = $className . '::' . $const;
936
            }
937
        }
938
939
        // checks if identifier ends with ::class, \strlen('::class') === 7
940 75
        $classPos = stripos($identifier, '::class');
941 75
        if ($classPos === strlen($identifier) - 7) {
942 4
            return substr($identifier, 0, $classPos);
943
        }
944
945 71
        if (!defined($identifier)) {
946 1
            throw AnnotationException::semanticalErrorConstants($identifier, $this->context);
947
        }
948
949 70
        return constant($identifier);
950
    }
951
952
    /**
953
     * Identifier ::= string
954
     *
955
     * @return string
956
     */
957 327
    private function Identifier()
958
    {
959
        // check if we have an annotation
960 327
        if ( ! $this->lexer->isNextTokenAny(self::$classIdentifiers)) {
961 14
            $this->syntaxError('namespace separator or identifier');
962
        }
963
964 327
        $this->lexer->moveNext();
965
966 327
        $className = $this->lexer->token['value'];
967
968 327
        while ($this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value']))
969 327
                && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) {
970
971 1
            $this->match(DocLexer::T_NAMESPACE_SEPARATOR);
972 1
            $this->matchAny(self::$classIdentifiers);
973
974
            $className .= '\\' . $this->lexer->token['value'];
975
        }
976
977 326
        return $className;
978
    }
979
980
    /**
981
     * Value ::= PlainValue | FieldAssignment
982
     *
983
     * @return mixed
984
     */
985 265
    private function Value()
986
    {
987 265
        $peek = $this->lexer->glimpse();
988
989 265
        if (DocLexer::T_EQUALS === $peek['type']) {
990 206
            return $this->FieldAssignment();
991
        }
992
993 132
        return $this->PlainValue();
994
    }
995
996
    /**
997
     * PlainValue ::= integer | string | float | boolean | Array | Annotation
998
     *
999
     * @return mixed
1000
     */
1001 265
    private function PlainValue()
1002
    {
1003 265
        if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
1004 79
            return $this->Arrayx();
1005
        }
1006
1007 264
        if ($this->lexer->isNextToken(DocLexer::T_AT)) {
1008 55
            return $this->Annotation();
1009
        }
1010
1011 228
        if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1012 75
            return $this->Constant();
1013
        }
1014
1015 161
        switch ($this->lexer->lookahead['type']) {
1016 161
            case DocLexer::T_STRING:
1017 88
                $this->match(DocLexer::T_STRING);
1018 88
                return $this->lexer->token['value'];
1019
1020 84
            case DocLexer::T_INTEGER:
1021 56
                $this->match(DocLexer::T_INTEGER);
1022 56
                return (int)$this->lexer->token['value'];
1023
1024 30
            case DocLexer::T_FLOAT:
1025 28
                $this->match(DocLexer::T_FLOAT);
1026 28
                return (float)$this->lexer->token['value'];
1027
1028 2
            case DocLexer::T_TRUE:
1029
                $this->match(DocLexer::T_TRUE);
1030
                return true;
1031
1032 2
            case DocLexer::T_FALSE:
1033
                $this->match(DocLexer::T_FALSE);
1034
                return false;
1035
1036 2
            case DocLexer::T_NULL:
1037
                $this->match(DocLexer::T_NULL);
1038
                return null;
1039
1040
            default:
1041 2
                $this->syntaxError('PlainValue');
1042
        }
1043
    }
1044
1045
    /**
1046
     * FieldAssignment ::= FieldName "=" PlainValue
1047
     * FieldName ::= identifier
1048
     *
1049
     * @return \stdClass
1050
     */
1051 206
    private function FieldAssignment()
1052
    {
1053 206
        $this->match(DocLexer::T_IDENTIFIER);
1054 206
        $fieldName = $this->lexer->token['value'];
1055
1056 206
        $this->match(DocLexer::T_EQUALS);
1057
1058 206
        $item = new \stdClass();
1059 206
        $item->name  = $fieldName;
1060 206
        $item->value = $this->PlainValue();
1061
1062 204
        return $item;
1063
    }
1064
1065
    /**
1066
     * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}"
1067
     *
1068
     * @return array
1069
     */
1070 79
    private function Arrayx()
1071
    {
1072 79
        $array = $values = [];
1073
1074 79
        $this->match(DocLexer::T_OPEN_CURLY_BRACES);
1075
1076
        // If the array is empty, stop parsing and return.
1077 79
        if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1078 1
            $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1079
1080 1
            return $array;
1081
        }
1082
1083 79
        $values[] = $this->ArrayEntry();
1084
1085 79
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
1086 54
            $this->match(DocLexer::T_COMMA);
1087
1088
            // optional trailing comma
1089 54
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1090 4
                break;
1091
            }
1092
1093 54
            $values[] = $this->ArrayEntry();
1094
        }
1095
1096 79
        $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1097
1098 79
        foreach ($values as $value) {
1099 79
            list ($key, $val) = $value;
1100
1101 79
            if ($key !== null) {
1102 13
                $array[$key] = $val;
1103
            } else {
1104 79
                $array[] = $val;
1105
            }
1106
        }
1107
1108 79
        return $array;
1109
    }
1110
1111
    /**
1112
     * ArrayEntry ::= Value | KeyValuePair
1113
     * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant
1114
     * Key ::= string | integer | Constant
1115
     *
1116
     * @return array
1117
     */
1118 79
    private function ArrayEntry()
1119
    {
1120 79
        $peek = $this->lexer->glimpse();
1121
1122 79
        if (DocLexer::T_EQUALS === $peek['type']
1123 79
                || DocLexer::T_COLON === $peek['type']) {
1124
1125 13
            if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1126 5
                $key = $this->Constant();
1127
            } else {
1128 8
                $this->matchAny([DocLexer::T_INTEGER, DocLexer::T_STRING]);
1129 8
                $key = $this->lexer->token['value'];
1130
            }
1131
1132 13
            $this->matchAny([DocLexer::T_EQUALS, DocLexer::T_COLON]);
1133
1134 13
            return [$key, $this->PlainValue()];
1135
        }
1136
1137 70
        return [null, $this->Value()];
1138
    }
1139
1140
    /**
1141
     * Checks whether the given $name matches any ignored annotation name or namespace
1142
     *
1143
     * @param string $name
1144
     *
1145
     * @return bool
1146
     */
1147 100
    private function isIgnoredAnnotation($name)
1148
    {
1149 100
        if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
1150 82
            return true;
1151
        }
1152
1153 19
        foreach (array_keys($this->ignoredAnnotationNamespaces) as $ignoredAnnotationNamespace) {
1154 15
            $ignoredAnnotationNamespace = rtrim($ignoredAnnotationNamespace, '\\') . '\\';
1155
1156 15
            if (0 === stripos(rtrim($name, '\\') . '\\', $ignoredAnnotationNamespace)) {
1157 15
                return true;
1158
            }
1159
        }
1160
1161 4
        return false;
1162
    }
1163
}
1164