Passed
Push — metadata-1.x ( 3d1ee0 )
by Michael
03:04
created

DocParser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\Common\Annotations;
21
22
use Doctrine\Common\Annotations\Annotation\Attribute;
23
use Doctrine\Common\Annotations\Annotation\Attributes;
24
use Doctrine\Common\Annotations\Annotation\Enum;
25
use Doctrine\Common\Annotations\Annotation\Target;
26
use Doctrine\Annotations\Metadata\AnnotationTarget;
27
use Doctrine\Annotations\Metadata\Builder\AnnotationMetadataBuilder;
28
use Doctrine\Annotations\Metadata\Builder\PropertyMetadataBuilder;
29
use Doctrine\Annotations\Metadata\InternalAnnotations;
30
use Doctrine\Annotations\Metadata\MetadataCollection;
31
use ReflectionClass;
32
use function array_key_exists;
33
use function array_keys;
34
35
/**
36
 * A parser for docblock annotations.
37
 *
38
 * It is strongly discouraged to change the default annotation parsing process.
39
 *
40
 * @author Benjamin Eberlei <[email protected]>
41
 * @author Guilherme Blanco <[email protected]>
42
 * @author Jonathan Wage <[email protected]>
43
 * @author Roman Borschel <[email protected]>
44
 * @author Johannes M. Schmitt <[email protected]>
45
 * @author Fabio B. Silva <[email protected]>
46
 */
47
final class DocParser
48
{
49
    /**
50
     * An array of all valid tokens for a class name.
51
     *
52
     * @var array
53
     */
54
    private static $classIdentifiers = [
55
        DocLexer::T_IDENTIFIER,
56
        DocLexer::T_TRUE,
57
        DocLexer::T_FALSE,
58
        DocLexer::T_NULL
59
    ];
60
61
    /**
62
     * The lexer.
63
     *
64
     * @var \Doctrine\Common\Annotations\DocLexer
65
     */
66
    private $lexer;
67
68
    /**
69
     * Current target context.
70
     *
71
     * @var integer
72
     */
73
    private $target;
74
75
    /**
76
     * Doc parser used to collect annotation target.
77
     *
78
     * @var \Doctrine\Common\Annotations\DocParser
79
     */
80
    private static $metadataParser;
81
82
    /**
83
     * Flag to control if the current annotation is nested or not.
84
     *
85
     * @var boolean
86
     */
87
    private $isNestedAnnotation = false;
88
89
    /**
90
     * Hashmap containing all use-statements that are to be used when parsing
91
     * the given doc block.
92
     *
93
     * @var array
94
     */
95
    private $imports = [];
96
97
    /**
98
     * This hashmap is used internally to cache results of class_exists()
99
     * look-ups.
100
     *
101
     * @var array
102
     */
103
    private $classExists = [];
104
105
    /**
106
     * Whether annotations that have not been imported should be ignored.
107
     *
108
     * @var boolean
109
     */
110
    private $ignoreNotImportedAnnotations = false;
111
112
    /**
113
     * An array of default namespaces if operating in simple mode.
114
     *
115
     * @var string[]
116
     */
117
    private $namespaces = [];
118
119
    /**
120
     * A list with annotations that are not causing exceptions when not resolved to an annotation class.
121
     *
122
     * The names must be the raw names as used in the class, not the fully qualified
123
     * class names.
124
     *
125
     * @var bool[] indexed by annotation name
126
     */
127
    private $ignoredAnnotationNames = [];
128
129
    /**
130
     * A list with annotations in namespaced format
131
     * that are not causing exceptions when not resolved to an annotation class.
132
     *
133
     * @var bool[] indexed by namespace name
134
     */
135
    private $ignoredAnnotationNamespaces = [];
136
137
    /**
138
     * @var string
139
     */
140
    private $context = '';
141
142
    /**
143
     * Hash-map for caching annotation metadata.
144
     *
145
     * @var MetadataCollection
146
     */
147
    private $annotationMetadata;
148
149
    /** @var array<string, bool> */
150
    private $nonAnnotationClasses = [];
151
152
    /**
153
     * Hash-map for handle types declaration.
154
     *
155
     * @var array
156
     */
157
    private static $typeMap = [
158
        'float'     => 'double',
159
        'bool'      => 'boolean',
160
        // allow uppercase Boolean in honor of George Boole
161
        'Boolean'   => 'boolean',
162
        'int'       => 'integer',
163
    ];
164
165
    /**
166
     * Constructs a new DocParser.
167
     */
168
    public function __construct()
169
    {
170
        $this->lexer              = new DocLexer;
171
        $this->annotationMetadata = InternalAnnotations::createMetadata();
172
    }
173
174
    /**
175
     * Sets the annotation names that are ignored during the parsing process.
176
     *
177
     * The names are supposed to be the raw names as used in the class, not the
178
     * fully qualified class names.
179
     *
180
     * @param bool[] $names indexed by annotation name
181
     *
182
     * @return void
183
     */
184
    public function setIgnoredAnnotationNames(array $names)
185
    {
186
        $this->ignoredAnnotationNames = $names;
187
    }
188
189
    /**
190
     * Sets the annotation namespaces that are ignored during the parsing process.
191
     *
192
     * @param bool[] $ignoredAnnotationNamespaces indexed by annotation namespace name
193
     *
194
     * @return void
195
     */
196
    public function setIgnoredAnnotationNamespaces($ignoredAnnotationNamespaces)
197
    {
198
        $this->ignoredAnnotationNamespaces = $ignoredAnnotationNamespaces;
199
    }
200
201
    /**
202
     * Sets ignore on not-imported annotations.
203
     *
204
     * @param boolean $bool
205
     *
206
     * @return void
207
     */
208
    public function setIgnoreNotImportedAnnotations($bool)
209
    {
210
        $this->ignoreNotImportedAnnotations = (boolean) $bool;
211
    }
212
213
    /**
214
     * Sets the default namespaces.
215
     *
216
     * @param string $namespace
217
     *
218
     * @return void
219
     *
220
     * @throws \RuntimeException
221
     */
222
    public function addNamespace($namespace)
223
    {
224
        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...
225
            throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
226
        }
227
228
        $this->namespaces[] = $namespace;
229
    }
230
231
    /**
232
     * Sets the imports.
233
     *
234
     * @param array $imports
235
     *
236
     * @return void
237
     *
238
     * @throws \RuntimeException
239
     */
240
    public function setImports(array $imports)
241
    {
242
        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...
243
            throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
244
        }
245
246
        $this->imports = $imports;
247
    }
248
249
    /**
250
     * Sets current target context as bitmask.
251
     *
252
     * @param integer $target
253
     *
254
     * @return void
255
     */
256
    public function setTarget($target)
257
    {
258
        $this->target = $target;
259
    }
260
261
    /**
262
     * Parses the given docblock string for annotations.
263
     *
264
     * @param string $input   The docblock string to parse.
265
     * @param string $context The parsing context.
266
     *
267
     * @return array Array of annotations. If no annotations are found, an empty array is returned.
268
     */
269
    public function parse($input, $context = '')
270
    {
271
        $pos = $this->findInitialTokenPosition($input);
272
        if ($pos === null) {
273
            return [];
274
        }
275
276
        $this->context = $context;
277
278
        $this->lexer->setInput(trim(substr($input, $pos), '* /'));
279
        $this->lexer->moveNext();
280
281
        return $this->Annotations();
282
    }
283
284
    /**
285
     * Finds the first valid annotation
286
     *
287
     * @param string $input The docblock string to parse
288
     *
289
     * @return int|null
290
     */
291
    private function findInitialTokenPosition($input)
292
    {
293
        $pos = 0;
294
295
        // search for first valid annotation
296
        while (($pos = strpos($input, '@', $pos)) !== false) {
297
            $preceding = substr($input, $pos - 1, 1);
298
299
            // if the @ is preceded by a space, a tab or * it is valid
300
            if ($pos === 0 || $preceding === ' ' || $preceding === '*' || $preceding === "\t") {
301
                return $pos;
302
            }
303
304
            $pos++;
305
        }
306
307
        return null;
308
    }
309
310
    /**
311
     * Attempts to match the given token with the current lookahead token.
312
     * If they match, updates the lookahead token; otherwise raises a syntax error.
313
     *
314
     * @param integer $token Type of token.
315
     *
316
     * @return boolean True if tokens match; false otherwise.
317
     */
318
    private function match($token)
319
    {
320
        if ( ! $this->lexer->isNextToken($token) ) {
321
            $this->syntaxError($this->lexer->getLiteral($token));
322
        }
323
324
        return $this->lexer->moveNext();
325
    }
326
327
    /**
328
     * Attempts to match the current lookahead token with any of the given tokens.
329
     *
330
     * If any of them matches, this method updates the lookahead token; otherwise
331
     * a syntax error is raised.
332
     *
333
     * @param array $tokens
334
     *
335
     * @return boolean
336
     */
337
    private function matchAny(array $tokens)
338
    {
339
        if ( ! $this->lexer->isNextTokenAny($tokens)) {
340
            $this->syntaxError(implode(' or ', array_map([$this->lexer, 'getLiteral'], $tokens)));
341
        }
342
343
        return $this->lexer->moveNext();
344
    }
345
346
    /**
347
     * Generates a new syntax error.
348
     *
349
     * @param string     $expected Expected string.
350
     * @param array|null $token    Optional token.
351
     *
352
     * @return void
353
     *
354
     * @throws AnnotationException
355
     */
356
    private function syntaxError($expected, $token = null)
357
    {
358
        if ($token === null) {
359
            $token = $this->lexer->lookahead;
360
        }
361
362
        $message  = sprintf('Expected %s, got ', $expected);
363
        $message .= ($this->lexer->lookahead === null)
364
            ? 'end of string'
365
            : sprintf("'%s' at position %s", $token['value'], $token['position']);
366
367
        if (strlen($this->context)) {
368
            $message .= ' in ' . $this->context;
369
        }
370
371
        $message .= '.';
372
373
        throw AnnotationException::syntaxError($message);
374
    }
375
376
    /**
377
     * Attempts to check if a class exists or not. This never goes through the PHP autoloading mechanism
378
     * but uses the {@link AnnotationRegistry} to load classes.
379
     *
380
     * @param string $fqcn
381
     *
382
     * @return boolean
383
     */
384
    private function classExists($fqcn)
385
    {
386
        if (isset($this->classExists[$fqcn])) {
387
            return $this->classExists[$fqcn];
388
        }
389
390
        // first check if the class already exists, maybe loaded through another AnnotationReader
391
        if (class_exists($fqcn, false)) {
392
            return $this->classExists[$fqcn] = true;
393
        }
394
395
        // final check, does this class exist?
396
        return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn);
397
    }
398
399
    /**
400
     * Collects parsing metadata for a given annotation class
401
     *
402
     * @param string $name The annotation name
403
     *
404
     * @return void
405
     */
406
    private function collectAnnotationMetadata($name)
407
    {
408
        if (self::$metadataParser === null) {
409
            self::$metadataParser = new self();
410
411
            self::$metadataParser->setIgnoreNotImportedAnnotations(true);
412
            self::$metadataParser->setIgnoredAnnotationNames($this->ignoredAnnotationNames);
413
            self::$metadataParser->setImports([
414
                'enum'          => 'Doctrine\Common\Annotations\Annotation\Enum',
415
                'target'        => 'Doctrine\Common\Annotations\Annotation\Target',
416
                'attribute'     => 'Doctrine\Common\Annotations\Annotation\Attribute',
417
                'attributes'    => 'Doctrine\Common\Annotations\Annotation\Attributes'
418
            ]);
419
420
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Enum.php');
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Common\Annotati...egistry::registerFile() has been deprecated: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists') ( Ignorable by Annotation )

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

420
            /** @scrutinizer ignore-deprecated */ AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Enum.php');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
421
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Target.php');
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Common\Annotati...egistry::registerFile() has been deprecated: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists') ( Ignorable by Annotation )

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

421
            /** @scrutinizer ignore-deprecated */ AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Target.php');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
422
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attribute.php');
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Common\Annotati...egistry::registerFile() has been deprecated: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists') ( Ignorable by Annotation )

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

422
            /** @scrutinizer ignore-deprecated */ AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attribute.php');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
423
            AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attributes.php');
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Common\Annotati...egistry::registerFile() has been deprecated: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists') ( Ignorable by Annotation )

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

423
            /** @scrutinizer ignore-deprecated */ AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attributes.php');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
424
        }
425
426
        $class          = new \ReflectionClass($name);
427
        $docComment     = $class->getDocComment();
428
        $constructor    = $class->getConstructor();
429
        $useConstructor = $constructor !== null && $constructor->getNumberOfParameters() > 0;
430
431
        // verify that the class is really meant to be an annotation
432
        if (strpos($docComment, '@Annotation') === false) {
433
            $this->nonAnnotationClasses[$name] = true;
434
            return;
435
        }
436
437
        $annotationBuilder = new AnnotationMetadataBuilder($name);
438
439
        if ($useConstructor) {
440
            $annotationBuilder = $annotationBuilder->withUsingConstructor();
441
        }
442
443
        self::$metadataParser->setTarget(Target::TARGET_CLASS);
444
445
        foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) {
446
            if ($annotation instanceof Target) {
447
                $annotationBuilder = $annotationBuilder->withTarget(AnnotationTarget::fromAnnotation($annotation));
448
449
                continue;
450
            }
451
452
            if ($annotation instanceof Attributes) {
453
                foreach ($annotation->value as $attribute) {
454
                    $annotationBuilder = $annotationBuilder->withProperty(
455
                        $this->collectAttributeTypeMetadata(new PropertyMetadataBuilder($attribute->name), $attribute)->build()
456
                    );
457
                }
458
            }
459
        }
460
461
        // if there is no constructor we will inject values into public properties
462
        if (! $useConstructor) {
463
            // collect all public properties
464
            foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $i => $property) {
465
                $propertyBuilder = new PropertyMetadataBuilder($property->getName());
466
                $propertyComment = $property->getDocComment();
467
468
                if ($i === 0) {
469
                    $propertyBuilder = $propertyBuilder->withBeingDefault();
470
                }
471
472
473
                if ($propertyComment === false) {
474
                    $annotationBuilder = $annotationBuilder->withProperty($propertyBuilder->build());
475
476
                    continue;
477
                }
478
479
                $attribute           = new Attribute();
480
                $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

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

482
                $attribute->type     = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',/** @scrutinizer ignore-type */ $propertyComment, $matches))
Loading history...
483
                    ? $matches[1]
484
                    : 'mixed';
485
486
                $propertyBuilder = $this->collectAttributeTypeMetadata($propertyBuilder, $attribute);
487
488
                // checks if the property has @Enum
489
                if (false !== strpos($propertyComment, '@Enum')) {
490
                    $context = 'property ' . $class->name . "::\$" . $property->name;
491
492
                    self::$metadataParser->setTarget(Target::TARGET_PROPERTY);
493
494
                    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\Common\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

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