DocParser::PlainValue()   B
last analyzed

Complexity

Conditions 10
Paths 10

Size

Total Lines 47
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 27
c 1
b 0
f 0
dl 0
loc 47
rs 7.6666
cc 10
nc 10
nop 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Doctrine\Common\Annotations;
4
5
use Doctrine\Common\Annotations\Annotation\Attribute;
6
use Doctrine\Common\Annotations\Annotation\Attributes;
7
use Doctrine\Common\Annotations\Annotation\Enum;
8
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
9
use Doctrine\Common\Annotations\Annotation\Target;
10
use ReflectionClass;
11
use ReflectionException;
12
use ReflectionProperty;
13
use RuntimeException;
14
use stdClass;
15
16
use function array_keys;
17
use function array_map;
18
use function array_pop;
19
use function array_values;
20
use function class_exists;
21
use function constant;
22
use function count;
23
use function defined;
24
use function explode;
25
use function gettype;
26
use function implode;
27
use function in_array;
28
use function interface_exists;
29
use function is_array;
30
use function is_object;
31
use function json_encode;
32
use function ltrim;
33
use function preg_match;
34
use function reset;
35
use function rtrim;
36
use function sprintf;
37
use function stripos;
38
use function strlen;
39
use function strpos;
40
use function strrpos;
41
use function strtolower;
42
use function substr;
43
use function trim;
44
45
use const PHP_VERSION_ID;
46
47
/**
48
 * A parser for docblock annotations.
49
 *
50
 * It is strongly discouraged to change the default annotation parsing process.
51
 */
52
final class DocParser
53
{
54
    /**
55
     * An array of all valid tokens for a class name.
56
     *
57
     * @phpstan-var list<int>
58
     */
59
    private static $classIdentifiers = [
60
        DocLexer::T_IDENTIFIER,
61
        DocLexer::T_TRUE,
62
        DocLexer::T_FALSE,
63
        DocLexer::T_NULL,
64
    ];
65
66
    /**
67
     * The lexer.
68
     *
69
     * @var DocLexer
70
     */
71
    private $lexer;
72
73
    /**
74
     * Current target context.
75
     *
76
     * @var int
77
     */
78
    private $target;
79
80
    /**
81
     * Doc parser used to collect annotation target.
82
     *
83
     * @var DocParser
84
     */
85
    private static $metadataParser;
86
87
    /**
88
     * Flag to control if the current annotation is nested or not.
89
     *
90
     * @var bool
91
     */
92
    private $isNestedAnnotation = false;
93
94
    /**
95
     * Hashmap containing all use-statements that are to be used when parsing
96
     * the given doc block.
97
     *
98
     * @var array<string, class-string>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, class-string> at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in array<string, class-string>.
Loading history...
99
     */
100
    private $imports = [];
101
102
    /**
103
     * This hashmap is used internally to cache results of class_exists()
104
     * look-ups.
105
     *
106
     * @var array<class-string, bool>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, bool> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, bool>.
Loading history...
107
     */
108
    private $classExists = [];
109
110
    /**
111
     * Whether annotations that have not been imported should be ignored.
112
     *
113
     * @var bool
114
     */
115
    private $ignoreNotImportedAnnotations = false;
116
117
    /**
118
     * An array of default namespaces if operating in simple mode.
119
     *
120
     * @var string[]
121
     */
122
    private $namespaces = [];
123
124
    /**
125
     * A list with annotations that are not causing exceptions when not resolved to an annotation class.
126
     *
127
     * The names must be the raw names as used in the class, not the fully qualified
128
     *
129
     * @var bool[] indexed by annotation name
130
     */
131
    private $ignoredAnnotationNames = [];
132
133
    /**
134
     * A list with annotations in namespaced format
135
     * that are not causing exceptions when not resolved to an annotation class.
136
     *
137
     * @var bool[] indexed by namespace name
138
     */
139
    private $ignoredAnnotationNamespaces = [];
140
141
    /** @var string */
142
    private $context = '';
143
144
    /**
145
     * Hash-map for caching annotation metadata.
146
     *
147
     * @var array<class-string, mixed[]>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, mixed[]> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, mixed[]>.
Loading history...
148
     */
149
    private static $annotationMetadata = [
150
        Annotation\Target::class => [
151
            'is_annotation'                  => true,
152
            'has_constructor'                => true,
153
            'has_named_argument_constructor' => false,
154
            'properties'                     => [],
155
            'targets_literal'                => 'ANNOTATION_CLASS',
156
            'targets'                        => Target::TARGET_CLASS,
157
            'default_property'               => 'value',
158
            'attribute_types'                => [
159
                'value'  => [
160
                    'required'   => false,
161
                    'type'       => 'array',
162
                    'array_type' => 'string',
163
                    'value'      => 'array<string>',
164
                ],
165
            ],
166
        ],
167
        Annotation\Attribute::class => [
168
            'is_annotation'                  => true,
169
            'has_constructor'                => false,
170
            'has_named_argument_constructor' => false,
171
            'targets_literal'                => 'ANNOTATION_ANNOTATION',
172
            'targets'                        => Target::TARGET_ANNOTATION,
173
            'default_property'               => 'name',
174
            'properties'                     => [
175
                'name'      => 'name',
176
                'type'      => 'type',
177
                'required'  => 'required',
178
            ],
179
            'attribute_types'                => [
180
                'value'  => [
181
                    'required'  => true,
182
                    'type'      => 'string',
183
                    'value'     => 'string',
184
                ],
185
                'type'  => [
186
                    'required'  => true,
187
                    'type'      => 'string',
188
                    'value'     => 'string',
189
                ],
190
                'required'  => [
191
                    'required'  => false,
192
                    'type'      => 'boolean',
193
                    'value'     => 'boolean',
194
                ],
195
            ],
196
        ],
197
        Annotation\Attributes::class => [
198
            'is_annotation'                  => true,
199
            'has_constructor'                => false,
200
            'has_named_argument_constructor' => false,
201
            'targets_literal'                => 'ANNOTATION_CLASS',
202
            'targets'                        => Target::TARGET_CLASS,
203
            'default_property'               => 'value',
204
            'properties'                     => ['value' => 'value'],
205
            'attribute_types'                => [
206
                'value' => [
207
                    'type'      => 'array',
208
                    'required'  => true,
209
                    'array_type' => Annotation\Attribute::class,
210
                    'value'     => 'array<' . Annotation\Attribute::class . '>',
211
                ],
212
            ],
213
        ],
214
        Annotation\Enum::class => [
215
            'is_annotation'                  => true,
216
            'has_constructor'                => true,
217
            'has_named_argument_constructor' => false,
218
            'targets_literal'                => 'ANNOTATION_PROPERTY',
219
            'targets'                        => Target::TARGET_PROPERTY,
220
            'default_property'               => 'value',
221
            'properties'                     => ['value' => 'value'],
222
            'attribute_types'                => [
223
                'value' => [
224
                    'type'      => 'array',
225
                    'required'  => true,
226
                ],
227
                'literal' => [
228
                    'type'      => 'array',
229
                    'required'  => false,
230
                ],
231
            ],
232
        ],
233
        Annotation\NamedArgumentConstructor::class => [
234
            'is_annotation'                  => true,
235
            'has_constructor'                => false,
236
            'has_named_argument_constructor' => false,
237
            'targets_literal'                => 'ANNOTATION_CLASS',
238
            'targets'                        => Target::TARGET_CLASS,
239
            'default_property'               => null,
240
            'properties'                     => [],
241
            'attribute_types'                => [],
242
        ],
243
    ];
244
245
    /**
246
     * Hash-map for handle types declaration.
247
     *
248
     * @var array<string, string>
249
     */
250
    private static $typeMap = [
251
        'float'     => 'double',
252
        'bool'      => 'boolean',
253
        // allow uppercase Boolean in honor of George Boole
254
        'Boolean'   => 'boolean',
255
        'int'       => 'integer',
256
    ];
257
258
    /**
259
     * Constructs a new DocParser.
260
     */
261
    public function __construct()
262
    {
263
        $this->lexer = new DocLexer();
264
    }
265
266
    /**
267
     * Sets the annotation names that are ignored during the parsing process.
268
     *
269
     * The names are supposed to be the raw names as used in the class, not the
270
     * fully qualified class names.
271
     *
272
     * @param bool[] $names indexed by annotation name
273
     *
274
     * @return void
275
     */
276
    public function setIgnoredAnnotationNames(array $names)
277
    {
278
        $this->ignoredAnnotationNames = $names;
279
    }
280
281
    /**
282
     * Sets the annotation namespaces that are ignored during the parsing process.
283
     *
284
     * @param bool[] $ignoredAnnotationNamespaces indexed by annotation namespace name
285
     *
286
     * @return void
287
     */
288
    public function setIgnoredAnnotationNamespaces($ignoredAnnotationNamespaces)
289
    {
290
        $this->ignoredAnnotationNamespaces = $ignoredAnnotationNamespaces;
291
    }
292
293
    /**
294
     * Sets ignore on not-imported annotations.
295
     *
296
     * @param bool $bool
297
     *
298
     * @return void
299
     */
300
    public function setIgnoreNotImportedAnnotations($bool)
301
    {
302
        $this->ignoreNotImportedAnnotations = (bool) $bool;
303
    }
304
305
    /**
306
     * Sets the default namespaces.
307
     *
308
     * @param string $namespace
309
     *
310
     * @return void
311
     *
312
     * @throws RuntimeException
313
     */
314
    public function addNamespace($namespace)
315
    {
316
        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...
317
            throw new RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
318
        }
319
320
        $this->namespaces[] = $namespace;
321
    }
322
323
    /**
324
     * Sets the imports.
325
     *
326
     * @param array<string, class-string> $imports
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, class-string> at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in array<string, class-string>.
Loading history...
327
     *
328
     * @return void
329
     *
330
     * @throws RuntimeException
331
     */
332
    public function setImports(array $imports)
333
    {
334
        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...
335
            throw new RuntimeException('You must either use addNamespace(), or setImports(), but not both.');
336
        }
337
338
        $this->imports = $imports;
339
    }
340
341
    /**
342
     * Sets current target context as bitmask.
343
     *
344
     * @param int $target
345
     *
346
     * @return void
347
     */
348
    public function setTarget($target)
349
    {
350
        $this->target = $target;
351
    }
352
353
    /**
354
     * Parses the given docblock string for annotations.
355
     *
356
     * @param string $input   The docblock string to parse.
357
     * @param string $context The parsing context.
358
     *
359
     * @throws AnnotationException
360
     * @throws ReflectionException
361
     *
362
     * @phpstan-return list<object> Array of annotations. If no annotations are found, an empty array is returned.
363
     */
364
    public function parse($input, $context = '')
365
    {
366
        $pos = $this->findInitialTokenPosition($input);
367
        if ($pos === null) {
368
            return [];
369
        }
370
371
        $this->context = $context;
372
373
        $this->lexer->setInput(trim(substr($input, $pos), '* /'));
374
        $this->lexer->moveNext();
375
376
        return $this->Annotations();
377
    }
378
379
    /**
380
     * Finds the first valid annotation
381
     *
382
     * @param string $input The docblock string to parse
383
     */
384
    private function findInitialTokenPosition($input): ?int
385
    {
386
        $pos = 0;
387
388
        // search for first valid annotation
389
        while (($pos = strpos($input, '@', $pos)) !== false) {
390
            $preceding = substr($input, $pos - 1, 1);
391
392
            // if the @ is preceded by a space, a tab or * it is valid
393
            if ($pos === 0 || $preceding === ' ' || $preceding === '*' || $preceding === "\t") {
394
                return $pos;
395
            }
396
397
            $pos++;
398
        }
399
400
        return null;
401
    }
402
403
    /**
404
     * Attempts to match the given token with the current lookahead token.
405
     * If they match, updates the lookahead token; otherwise raises a syntax error.
406
     *
407
     * @param int $token Type of token.
408
     *
409
     * @return bool True if tokens match; false otherwise.
410
     *
411
     * @throws AnnotationException
412
     */
413
    private function match(int $token): bool
414
    {
415
        if (! $this->lexer->isNextToken($token)) {
416
            throw $this->syntaxError($this->lexer->getLiteral($token));
417
        }
418
419
        return $this->lexer->moveNext();
420
    }
421
422
    /**
423
     * Attempts to match the current lookahead token with any of the given tokens.
424
     *
425
     * If any of them matches, this method updates the lookahead token; otherwise
426
     * a syntax error is raised.
427
     *
428
     * @throws AnnotationException
429
     *
430
     * @phpstan-param list<mixed[]> $tokens
431
     */
432
    private function matchAny(array $tokens): bool
433
    {
434
        if (! $this->lexer->isNextTokenAny($tokens)) {
435
            throw $this->syntaxError(implode(' or ', array_map([$this->lexer, 'getLiteral'], $tokens)));
436
        }
437
438
        return $this->lexer->moveNext();
439
    }
440
441
    /**
442
     * Generates a new syntax error.
443
     *
444
     * @param string       $expected Expected string.
445
     * @param mixed[]|null $token    Optional token.
446
     */
447
    private function syntaxError(string $expected, ?array $token = null): AnnotationException
448
    {
449
        if ($token === null) {
450
            $token = $this->lexer->lookahead;
451
        }
452
453
        $message  = sprintf('Expected %s, got ', $expected);
454
        $message .= $this->lexer->lookahead === null
455
            ? 'end of string'
456
            : sprintf("'%s' at position %s", $token['value'], $token['position']);
457
458
        if (strlen($this->context)) {
459
            $message .= ' in ' . $this->context;
460
        }
461
462
        $message .= '.';
463
464
        return AnnotationException::syntaxError($message);
465
    }
466
467
    /**
468
     * Attempts to check if a class exists or not. This never goes through the PHP autoloading mechanism
469
     * but uses the {@link AnnotationRegistry} to load classes.
470
     *
471
     * @param class-string $fqcn
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
472
     */
473
    private function classExists(string $fqcn): bool
474
    {
475
        if (isset($this->classExists[$fqcn])) {
476
            return $this->classExists[$fqcn];
477
        }
478
479
        // first check if the class already exists, maybe loaded through another AnnotationReader
480
        if (class_exists($fqcn, false)) {
481
            return $this->classExists[$fqcn] = true;
482
        }
483
484
        // final check, does this class exist?
485
        return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn);
486
    }
487
488
    /**
489
     * Collects parsing metadata for a given annotation class
490
     *
491
     * @param class-string $name The annotation name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
492
     *
493
     * @throws AnnotationException
494
     * @throws ReflectionException
495
     */
496
    private function collectAnnotationMetadata(string $name): void
497
    {
498
        if (self::$metadataParser === null) {
499
            self::$metadataParser = new self();
500
501
            self::$metadataParser->setIgnoreNotImportedAnnotations(true);
502
            self::$metadataParser->setIgnoredAnnotationNames($this->ignoredAnnotationNames);
503
            self::$metadataParser->setImports([
504
                'enum'                     => Enum::class,
505
                'target'                   => Target::class,
506
                'attribute'                => Attribute::class,
507
                'attributes'               => Attributes::class,
508
                'namedargumentconstructor' => NamedArgumentConstructor::class,
509
            ]);
510
511
            // Make sure that annotations from metadata are loaded
512
            class_exists(Enum::class);
513
            class_exists(Target::class);
514
            class_exists(Attribute::class);
515
            class_exists(Attributes::class);
516
            class_exists(NamedArgumentConstructor::class);
517
        }
518
519
        $class      = new ReflectionClass($name);
520
        $docComment = $class->getDocComment();
521
522
        // Sets default values for annotation metadata
523
        $constructor = $class->getConstructor();
524
        $metadata    = [
525
            'default_property' => null,
526
            'has_constructor'  => $constructor !== null && $constructor->getNumberOfParameters() > 0,
527
            'constructor_args' => [],
528
            'properties'       => [],
529
            'property_types'   => [],
530
            'attribute_types'  => [],
531
            'targets_literal'  => null,
532
            'targets'          => Target::TARGET_ALL,
533
            'is_annotation'    => strpos($docComment, '@Annotation') !== false,
534
        ];
535
536
        $metadata['has_named_argument_constructor'] = $metadata['has_constructor']
537
            && $class->implementsInterface(NamedArgumentConstructorAnnotation::class);
538
539
        // verify that the class is really meant to be an annotation
540
        if ($metadata['is_annotation']) {
541
            self::$metadataParser->setTarget(Target::TARGET_CLASS);
542
543
            foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) {
544
                if ($annotation instanceof Target) {
545
                    $metadata['targets']         = $annotation->targets;
546
                    $metadata['targets_literal'] = $annotation->literal;
547
548
                    continue;
549
                }
550
551
                if ($annotation instanceof NamedArgumentConstructor) {
552
                    $metadata['has_named_argument_constructor'] = $metadata['has_constructor'];
553
                    if ($metadata['has_named_argument_constructor']) {
554
                        // choose the first argument as the default property
555
                        $metadata['default_property'] = $constructor->getParameters()[0]->getName();
556
                    }
557
                }
558
559
                if (! ($annotation instanceof Attributes)) {
560
                    continue;
561
                }
562
563
                foreach ($annotation->value as $attribute) {
564
                    $this->collectAttributeTypeMetadata($metadata, $attribute);
565
                }
566
            }
567
568
            // if not has a constructor will inject values into public properties
569
            if ($metadata['has_constructor'] === false) {
570
                // collect all public properties
571
                foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
572
                    $metadata['properties'][$property->name] = $property->name;
573
574
                    $propertyComment = $property->getDocComment();
575
                    if ($propertyComment === false) {
576
                        continue;
577
                    }
578
579
                    $attribute = new Attribute();
580
581
                    $attribute->required = (strpos($propertyComment, '@Required') !== false);
582
                    $attribute->name     = $property->name;
583
                    $attribute->type     = (strpos($propertyComment, '@var') !== false &&
584
                        preg_match('/@var\s+([^\s]+)/', $propertyComment, $matches))
585
                        ? $matches[1]
586
                        : 'mixed';
587
588
                    $this->collectAttributeTypeMetadata($metadata, $attribute);
589
590
                    // checks if the property has @Enum
591
                    if (strpos($propertyComment, '@Enum') === false) {
592
                        continue;
593
                    }
594
595
                    $context = 'property ' . $class->name . '::$' . $property->name;
596
597
                    self::$metadataParser->setTarget(Target::TARGET_PROPERTY);
598
599
                    foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) {
600
                        if (! $annotation instanceof Enum) {
601
                            continue;
602
                        }
603
604
                        $metadata['enum'][$property->name]['value']   = $annotation->value;
605
                        $metadata['enum'][$property->name]['literal'] = (! empty($annotation->literal))
606
                            ? $annotation->literal
607
                            : $annotation->value;
608
                    }
609
                }
610
611
                // choose the first property as default property
612
                $metadata['default_property'] = reset($metadata['properties']);
613
            } elseif ($metadata['has_named_argument_constructor']) {
614
                foreach ($constructor->getParameters() as $parameter) {
615
                    $metadata['constructor_args'][$parameter->getName()] = [
616
                        'position' => $parameter->getPosition(),
617
                        'default' => $parameter->isOptional() ? $parameter->getDefaultValue() : null,
618
                    ];
619
                }
620
            }
621
        }
622
623
        self::$annotationMetadata[$name] = $metadata;
624
    }
625
626
    /**
627
     * Collects parsing metadata for a given attribute.
628
     *
629
     * @param mixed[] $metadata
630
     */
631
    private function collectAttributeTypeMetadata(array &$metadata, Attribute $attribute): void
632
    {
633
        // handle internal type declaration
634
        $type = self::$typeMap[$attribute->type] ?? $attribute->type;
635
636
        // handle the case if the property type is mixed
637
        if ($type === 'mixed') {
638
            return;
639
        }
640
641
        // Evaluate type
642
        $pos = strpos($type, '<');
643
        if ($pos !== false) {
644
            // Checks if the property has array<type>
645
            $arrayType = substr($type, $pos + 1, -1);
646
            $type      = 'array';
647
648
            if (isset(self::$typeMap[$arrayType])) {
649
                $arrayType = self::$typeMap[$arrayType];
650
            }
651
652
            $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType;
653
        } else {
654
            // Checks if the property has type[]
655
            $pos = strrpos($type, '[');
656
            if ($pos !== false) {
657
                $arrayType = substr($type, 0, $pos);
658
                $type      = 'array';
659
660
                if (isset(self::$typeMap[$arrayType])) {
661
                    $arrayType = self::$typeMap[$arrayType];
662
                }
663
664
                $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType;
665
            }
666
        }
667
668
        $metadata['attribute_types'][$attribute->name]['type']     = $type;
669
        $metadata['attribute_types'][$attribute->name]['value']    = $attribute->type;
670
        $metadata['attribute_types'][$attribute->name]['required'] = $attribute->required;
671
    }
672
673
    /**
674
     * Annotations ::= Annotation {[ "*" ]* [Annotation]}*
675
     *
676
     * @throws AnnotationException
677
     * @throws ReflectionException
678
     *
679
     * @phpstan-return list<object>
680
     */
681
    private function Annotations(): array
682
    {
683
        $annotations = [];
684
685
        while ($this->lexer->lookahead !== null) {
686
            if ($this->lexer->lookahead['type'] !== DocLexer::T_AT) {
687
                $this->lexer->moveNext();
688
                continue;
689
            }
690
691
            // make sure the @ is preceded by non-catchable pattern
692
            if (
693
                $this->lexer->token !== null &&
694
                $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen(
695
                    $this->lexer->token['value']
696
                )
697
            ) {
698
                $this->lexer->moveNext();
699
                continue;
700
            }
701
702
            // make sure the @ is followed by either a namespace separator, or
703
            // an identifier token
704
            $peek = $this->lexer->glimpse();
705
            if (
706
                ($peek === null)
707
                || ($peek['type'] !== DocLexer::T_NAMESPACE_SEPARATOR && ! in_array(
708
                    $peek['type'],
709
                    self::$classIdentifiers,
710
                    true
711
                ))
712
                || $peek['position'] !== $this->lexer->lookahead['position'] + 1
713
            ) {
714
                $this->lexer->moveNext();
715
                continue;
716
            }
717
718
            $this->isNestedAnnotation = false;
719
            $annot                    = $this->Annotation();
720
            if ($annot === false) {
721
                continue;
722
            }
723
724
            $annotations[] = $annot;
725
        }
726
727
        return $annotations;
728
    }
729
730
    /**
731
     * Annotation     ::= "@" AnnotationName MethodCall
732
     * AnnotationName ::= QualifiedName | SimpleName
733
     * QualifiedName  ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName
734
     * NameSpacePart  ::= identifier | null | false | true
735
     * SimpleName     ::= identifier | null | false | true
736
     *
737
     * @return object|false False if it is not a valid annotation.
738
     *
739
     * @throws AnnotationException
740
     * @throws ReflectionException
741
     */
742
    private function Annotation()
743
    {
744
        $this->match(DocLexer::T_AT);
745
746
        // check if we have an annotation
747
        $name = $this->Identifier();
748
749
        if (
750
            $this->lexer->isNextToken(DocLexer::T_MINUS)
751
            && $this->lexer->nextTokenIsAdjacent()
752
        ) {
753
            // Annotations with dashes, such as "@foo-" or "@foo-bar", are to be discarded
754
            return false;
755
        }
756
757
        // only process names which are not fully qualified, yet
758
        // fully qualified names must start with a \
759
        $originalName = $name;
760
761
        if ($name[0] !== '\\') {
762
            $pos          = strpos($name, '\\');
763
            $alias        = ($pos === false) ? $name : substr($name, 0, $pos);
764
            $found        = false;
765
            $loweredAlias = strtolower($alias);
766
767
            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...
768
                foreach ($this->namespaces as $namespace) {
769
                    if ($this->classExists($namespace . '\\' . $name)) {
770
                        $name  = $namespace . '\\' . $name;
771
                        $found = true;
772
                        break;
773
                    }
774
                }
775
            } elseif (isset($this->imports[$loweredAlias])) {
776
                $namespace = ltrim($this->imports[$loweredAlias], '\\');
777
                $name      = ($pos !== false)
778
                    ? $namespace . substr($name, $pos)
779
                    : $namespace;
780
                $found     = $this->classExists($name);
781
            } elseif (
782
                ! isset($this->ignoredAnnotationNames[$name])
783
                && isset($this->imports['__NAMESPACE__'])
784
                && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name)
785
            ) {
786
                $name  = $this->imports['__NAMESPACE__'] . '\\' . $name;
787
                $found = true;
788
            } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) {
789
                $found = true;
790
            }
791
792
            if (! $found) {
793
                if ($this->isIgnoredAnnotation($name)) {
794
                    return false;
795
                }
796
797
                throw AnnotationException::semanticalError(sprintf(
798
                    <<<'EXCEPTION'
799
The annotation "@%s" in %s was never imported. Did you maybe forget to add a "use" statement for this annotation?
800
EXCEPTION
801
                    ,
802
                    $name,
803
                    $this->context
804
                ));
805
            }
806
        }
807
808
        $name = ltrim($name, '\\');
809
810
        if (! $this->classExists($name)) {
811
            throw AnnotationException::semanticalError(sprintf(
812
                'The annotation "@%s" in %s does not exist, or could not be auto-loaded.',
813
                $name,
814
                $this->context
815
            ));
816
        }
817
818
        // at this point, $name contains the fully qualified class name of the
819
        // annotation, and it is also guaranteed that this class exists, and
820
        // that it is loaded
821
822
        // collects the metadata annotation only if there is not yet
823
        if (! isset(self::$annotationMetadata[$name])) {
824
            $this->collectAnnotationMetadata($name);
825
        }
826
827
        // verify that the class is really meant to be an annotation and not just any ordinary class
828
        if (self::$annotationMetadata[$name]['is_annotation'] === false) {
829
            if ($this->isIgnoredAnnotation($originalName) || $this->isIgnoredAnnotation($name)) {
830
                return false;
831
            }
832
833
            throw AnnotationException::semanticalError(sprintf(
834
                <<<'EXCEPTION'
835
The class "%s" is not annotated with @Annotation.
836
Are you sure this class can be used as annotation?
837
If so, then you need to add @Annotation to the _class_ doc comment of "%s".
838
If it is indeed no annotation, then you need to add @IgnoreAnnotation("%s") to the _class_ doc comment of %s.
839
EXCEPTION
840
                ,
841
                $name,
842
                $name,
843
                $originalName,
844
                $this->context
845
            ));
846
        }
847
848
        //if target is nested annotation
849
        $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target;
850
851
        // Next will be nested
852
        $this->isNestedAnnotation = true;
853
854
        //if annotation does not support current target
855
        if ((self::$annotationMetadata[$name]['targets'] & $target) === 0 && $target) {
856
            throw AnnotationException::semanticalError(
857
                sprintf(
858
                    <<<'EXCEPTION'
859
Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.
860
EXCEPTION
861
                    ,
862
                    $originalName,
863
                    $this->context,
864
                    self::$annotationMetadata[$name]['targets_literal']
865
                )
866
            );
867
        }
868
869
        $arguments = $this->MethodCall();
870
        $values    = $this->resolvePositionalValues($arguments, $name);
871
872
        if (isset(self::$annotationMetadata[$name]['enum'])) {
873
            // checks all declared attributes
874
            foreach (self::$annotationMetadata[$name]['enum'] as $property => $enum) {
875
                // checks if the attribute is a valid enumerator
876
                if (isset($values[$property]) && ! in_array($values[$property], $enum['value'])) {
877
                    throw AnnotationException::enumeratorError(
878
                        $property,
879
                        $name,
880
                        $this->context,
881
                        $enum['literal'],
882
                        $values[$property]
883
                    );
884
                }
885
            }
886
        }
887
888
        // checks all declared attributes
889
        foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) {
890
            if (
891
                $property === self::$annotationMetadata[$name]['default_property']
892
                && ! isset($values[$property]) && isset($values['value'])
893
            ) {
894
                $property = 'value';
895
            }
896
897
            // handle a not given attribute or null value
898
            if (! isset($values[$property])) {
899
                if ($type['required']) {
900
                    throw AnnotationException::requiredError(
901
                        $property,
902
                        $originalName,
903
                        $this->context,
904
                        'a(n) ' . $type['value']
905
                    );
906
                }
907
908
                continue;
909
            }
910
911
            if ($type['type'] === 'array') {
912
                // handle the case of a single value
913
                if (! is_array($values[$property])) {
914
                    $values[$property] = [$values[$property]];
915
                }
916
917
                // checks if the attribute has array type declaration, such as "array<string>"
918
                if (isset($type['array_type'])) {
919
                    foreach ($values[$property] as $item) {
920
                        if (gettype($item) !== $type['array_type'] && ! $item instanceof $type['array_type']) {
921
                            throw AnnotationException::attributeTypeError(
922
                                $property,
923
                                $originalName,
924
                                $this->context,
925
                                'either a(n) ' . $type['array_type'] . ', or an array of ' . $type['array_type'] . 's',
926
                                $item
927
                            );
928
                        }
929
                    }
930
                }
931
            } elseif (gettype($values[$property]) !== $type['type'] && ! $values[$property] instanceof $type['type']) {
932
                throw AnnotationException::attributeTypeError(
933
                    $property,
934
                    $originalName,
935
                    $this->context,
936
                    'a(n) ' . $type['value'],
937
                    $values[$property]
938
                );
939
            }
940
        }
941
942
        if (self::$annotationMetadata[$name]['has_named_argument_constructor']) {
943
            if (PHP_VERSION_ID >= 80000) {
944
                return new $name(...$values);
945
            }
946
947
            $positionalValues = [];
948
            foreach (self::$annotationMetadata[$name]['constructor_args'] as $property => $parameter) {
949
                $positionalValues[$parameter['position']] = $parameter['default'];
950
            }
951
952
            foreach ($values as $property => $value) {
953
                if (! isset(self::$annotationMetadata[$name]['constructor_args'][$property])) {
954
                    throw AnnotationException::creationError(sprintf(
955
                        <<<'EXCEPTION'
956
The annotation @%s declared on %s does not have a property named "%s"
957
that can be set through its named arguments constructor.
958
Available named arguments: %s
959
EXCEPTION
960
                        ,
961
                        $originalName,
962
                        $this->context,
963
                        $property,
964
                        implode(', ', array_keys(self::$annotationMetadata[$name]['constructor_args']))
965
                    ));
966
                }
967
968
                $positionalValues[self::$annotationMetadata[$name]['constructor_args'][$property]['position']] = $value;
969
            }
970
971
            return new $name(...$positionalValues);
972
        }
973
974
        // check if the annotation expects values via the constructor,
975
        // or directly injected into public properties
976
        if (self::$annotationMetadata[$name]['has_constructor'] === true) {
977
            return new $name($values);
978
        }
979
980
        $instance = new $name();
981
982
        foreach ($values as $property => $value) {
983
            if (! isset(self::$annotationMetadata[$name]['properties'][$property])) {
984
                if ($property !== 'value') {
985
                    throw AnnotationException::creationError(sprintf(
986
                        <<<'EXCEPTION'
987
The annotation @%s declared on %s does not have a property named "%s".
988
Available properties: %s
989
EXCEPTION
990
                        ,
991
                        $originalName,
992
                        $this->context,
993
                        $property,
994
                        implode(', ', self::$annotationMetadata[$name]['properties'])
995
                    ));
996
                }
997
998
                // handle the case if the property has no annotations
999
                $property = self::$annotationMetadata[$name]['default_property'];
1000
                if (! $property) {
1001
                    throw AnnotationException::creationError(sprintf(
1002
                        'The annotation @%s declared on %s does not accept any values, but got %s.',
1003
                        $originalName,
1004
                        $this->context,
1005
                        json_encode($values)
1006
                    ));
1007
                }
1008
            }
1009
1010
            $instance->{$property} = $value;
1011
        }
1012
1013
        return $instance;
1014
    }
1015
1016
    /**
1017
     * MethodCall ::= ["(" [Values] ")"]
1018
     *
1019
     * @return mixed[]
1020
     *
1021
     * @throws AnnotationException
1022
     * @throws ReflectionException
1023
     */
1024
    private function MethodCall(): array
1025
    {
1026
        $values = [];
1027
1028
        if (! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) {
1029
            return $values;
1030
        }
1031
1032
        $this->match(DocLexer::T_OPEN_PARENTHESIS);
1033
1034
        if (! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
1035
            $values = $this->Values();
1036
        }
1037
1038
        $this->match(DocLexer::T_CLOSE_PARENTHESIS);
1039
1040
        return $values;
1041
    }
1042
1043
    /**
1044
     * Values ::= Array | Value {"," Value}* [","]
1045
     *
1046
     * @return mixed[]
1047
     *
1048
     * @throws AnnotationException
1049
     * @throws ReflectionException
1050
     */
1051
    private function Values(): array
1052
    {
1053
        $values = [$this->Value()];
1054
1055
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
1056
            $this->match(DocLexer::T_COMMA);
1057
1058
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) {
1059
                break;
1060
            }
1061
1062
            $token = $this->lexer->lookahead;
0 ignored issues
show
Unused Code introduced by
The assignment to $token is dead and can be removed.
Loading history...
1063
            $value = $this->Value();
1064
1065
            $values[] = $value;
1066
        }
1067
1068
        $namedArguments      = [];
1069
        $positionalArguments = [];
1070
        foreach ($values as $k => $value) {
1071
            if (is_object($value) && $value instanceof stdClass) {
1072
                $namedArguments[$value->name] = $value->value;
1073
            } else {
1074
                $positionalArguments[$k] = $value;
1075
            }
1076
        }
1077
1078
        return ['named_arguments' => $namedArguments, 'positional_arguments' => $positionalArguments];
1079
    }
1080
1081
    /**
1082
     * Constant ::= integer | string | float | boolean
1083
     *
1084
     * @return mixed
1085
     *
1086
     * @throws AnnotationException
1087
     */
1088
    private function Constant()
1089
    {
1090
        $identifier = $this->Identifier();
1091
1092
        if (! defined($identifier) && strpos($identifier, '::') !== false && $identifier[0] !== '\\') {
1093
            [$className, $const] = explode('::', $identifier);
1094
1095
            $pos          = strpos($className, '\\');
1096
            $alias        = ($pos === false) ? $className : substr($className, 0, $pos);
1097
            $found        = false;
1098
            $loweredAlias = strtolower($alias);
1099
1100
            switch (true) {
1101
                case ! empty($this->namespaces):
1102
                    foreach ($this->namespaces as $ns) {
1103
                        if (class_exists($ns . '\\' . $className) || interface_exists($ns . '\\' . $className)) {
1104
                            $className = $ns . '\\' . $className;
1105
                            $found     = true;
1106
                            break;
1107
                        }
1108
                    }
1109
1110
                    break;
1111
1112
                case isset($this->imports[$loweredAlias]):
1113
                    $found     = true;
1114
                    $className = ($pos !== false)
1115
                        ? $this->imports[$loweredAlias] . substr($className, $pos)
1116
                        : $this->imports[$loweredAlias];
1117
                    break;
1118
1119
                default:
1120
                    if (isset($this->imports['__NAMESPACE__'])) {
1121
                        $ns = $this->imports['__NAMESPACE__'];
1122
1123
                        if (class_exists($ns . '\\' . $className) || interface_exists($ns . '\\' . $className)) {
1124
                            $className = $ns . '\\' . $className;
1125
                            $found     = true;
1126
                        }
1127
                    }
1128
1129
                    break;
1130
            }
1131
1132
            if ($found) {
1133
                $identifier = $className . '::' . $const;
1134
            }
1135
        }
1136
1137
        /**
1138
         * Checks if identifier ends with ::class and remove the leading backslash if it exists.
1139
         */
1140
        if (
1141
            $this->identifierEndsWithClassConstant($identifier) &&
1142
            ! $this->identifierStartsWithBackslash($identifier)
1143
        ) {
1144
            return substr($identifier, 0, $this->getClassConstantPositionInIdentifier($identifier));
1145
        }
1146
1147
        if ($this->identifierEndsWithClassConstant($identifier) && $this->identifierStartsWithBackslash($identifier)) {
1148
            return substr($identifier, 1, $this->getClassConstantPositionInIdentifier($identifier) - 1);
1149
        }
1150
1151
        if (! defined($identifier)) {
1152
            throw AnnotationException::semanticalErrorConstants($identifier, $this->context);
1153
        }
1154
1155
        return constant($identifier);
1156
    }
1157
1158
    private function identifierStartsWithBackslash(string $identifier): bool
1159
    {
1160
        return $identifier[0] === '\\';
1161
    }
1162
1163
    private function identifierEndsWithClassConstant(string $identifier): bool
1164
    {
1165
        return $this->getClassConstantPositionInIdentifier($identifier) === strlen($identifier) - strlen('::class');
1166
    }
1167
1168
    /**
1169
     * @return int|false
1170
     */
1171
    private function getClassConstantPositionInIdentifier(string $identifier)
1172
    {
1173
        return stripos($identifier, '::class');
1174
    }
1175
1176
    /**
1177
     * Identifier ::= string
1178
     *
1179
     * @throws AnnotationException
1180
     */
1181
    private function Identifier(): string
1182
    {
1183
        // check if we have an annotation
1184
        if (! $this->lexer->isNextTokenAny(self::$classIdentifiers)) {
1185
            throw $this->syntaxError('namespace separator or identifier');
1186
        }
1187
1188
        $this->lexer->moveNext();
1189
1190
        $className = $this->lexer->token['value'];
1191
1192
        while (
1193
            $this->lexer->lookahead !== null &&
1194
            $this->lexer->lookahead['position'] === ($this->lexer->token['position'] +
1195
            strlen($this->lexer->token['value'])) &&
1196
            $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)
1197
        ) {
1198
            $this->match(DocLexer::T_NAMESPACE_SEPARATOR);
1199
            $this->matchAny(self::$classIdentifiers);
1200
1201
            $className .= '\\' . $this->lexer->token['value'];
1202
        }
1203
1204
        return $className;
1205
    }
1206
1207
    /**
1208
     * Value ::= PlainValue | FieldAssignment
1209
     *
1210
     * @return mixed
1211
     *
1212
     * @throws AnnotationException
1213
     * @throws ReflectionException
1214
     */
1215
    private function Value()
1216
    {
1217
        $peek = $this->lexer->glimpse();
1218
1219
        if ($peek['type'] === DocLexer::T_EQUALS) {
1220
            return $this->FieldAssignment();
1221
        }
1222
1223
        return $this->PlainValue();
1224
    }
1225
1226
    /**
1227
     * PlainValue ::= integer | string | float | boolean | Array | Annotation
1228
     *
1229
     * @return mixed
1230
     *
1231
     * @throws AnnotationException
1232
     * @throws ReflectionException
1233
     */
1234
    private function PlainValue()
1235
    {
1236
        if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) {
1237
            return $this->Arrayx();
1238
        }
1239
1240
        if ($this->lexer->isNextToken(DocLexer::T_AT)) {
1241
            return $this->Annotation();
1242
        }
1243
1244
        if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1245
            return $this->Constant();
1246
        }
1247
1248
        switch ($this->lexer->lookahead['type']) {
1249
            case DocLexer::T_STRING:
1250
                $this->match(DocLexer::T_STRING);
1251
1252
                return $this->lexer->token['value'];
1253
1254
            case DocLexer::T_INTEGER:
1255
                $this->match(DocLexer::T_INTEGER);
1256
1257
                return (int) $this->lexer->token['value'];
1258
1259
            case DocLexer::T_FLOAT:
1260
                $this->match(DocLexer::T_FLOAT);
1261
1262
                return (float) $this->lexer->token['value'];
1263
1264
            case DocLexer::T_TRUE:
1265
                $this->match(DocLexer::T_TRUE);
1266
1267
                return true;
1268
1269
            case DocLexer::T_FALSE:
1270
                $this->match(DocLexer::T_FALSE);
1271
1272
                return false;
1273
1274
            case DocLexer::T_NULL:
1275
                $this->match(DocLexer::T_NULL);
1276
1277
                return null;
1278
1279
            default:
1280
                throw $this->syntaxError('PlainValue');
1281
        }
1282
    }
1283
1284
    /**
1285
     * FieldAssignment ::= FieldName "=" PlainValue
1286
     * FieldName ::= identifier
1287
     *
1288
     * @throws AnnotationException
1289
     * @throws ReflectionException
1290
     */
1291
    private function FieldAssignment(): stdClass
1292
    {
1293
        $this->match(DocLexer::T_IDENTIFIER);
1294
        $fieldName = $this->lexer->token['value'];
1295
1296
        $this->match(DocLexer::T_EQUALS);
1297
1298
        $item        = new stdClass();
1299
        $item->name  = $fieldName;
1300
        $item->value = $this->PlainValue();
1301
1302
        return $item;
1303
    }
1304
1305
    /**
1306
     * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}"
1307
     *
1308
     * @return mixed[]
1309
     *
1310
     * @throws AnnotationException
1311
     * @throws ReflectionException
1312
     */
1313
    private function Arrayx(): array
1314
    {
1315
        $array = $values = [];
1316
1317
        $this->match(DocLexer::T_OPEN_CURLY_BRACES);
1318
1319
        // If the array is empty, stop parsing and return.
1320
        if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1321
            $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1322
1323
            return $array;
1324
        }
1325
1326
        $values[] = $this->ArrayEntry();
1327
1328
        while ($this->lexer->isNextToken(DocLexer::T_COMMA)) {
1329
            $this->match(DocLexer::T_COMMA);
1330
1331
            // optional trailing comma
1332
            if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) {
1333
                break;
1334
            }
1335
1336
            $values[] = $this->ArrayEntry();
1337
        }
1338
1339
        $this->match(DocLexer::T_CLOSE_CURLY_BRACES);
1340
1341
        foreach ($values as $value) {
1342
            [$key, $val] = $value;
1343
1344
            if ($key !== null) {
1345
                $array[$key] = $val;
1346
            } else {
1347
                $array[] = $val;
1348
            }
1349
        }
1350
1351
        return $array;
1352
    }
1353
1354
    /**
1355
     * ArrayEntry ::= Value | KeyValuePair
1356
     * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant
1357
     * Key ::= string | integer | Constant
1358
     *
1359
     * @throws AnnotationException
1360
     * @throws ReflectionException
1361
     *
1362
     * @phpstan-return array{mixed, mixed}
1363
     */
1364
    private function ArrayEntry(): array
1365
    {
1366
        $peek = $this->lexer->glimpse();
1367
1368
        if (
1369
            $peek['type'] === DocLexer::T_EQUALS
1370
                || $peek['type'] === DocLexer::T_COLON
1371
        ) {
1372
            if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) {
1373
                $key = $this->Constant();
1374
            } else {
1375
                $this->matchAny([DocLexer::T_INTEGER, DocLexer::T_STRING]);
1376
                $key = $this->lexer->token['value'];
1377
            }
1378
1379
            $this->matchAny([DocLexer::T_EQUALS, DocLexer::T_COLON]);
1380
1381
            return [$key, $this->PlainValue()];
1382
        }
1383
1384
        return [null, $this->Value()];
1385
    }
1386
1387
    /**
1388
     * Checks whether the given $name matches any ignored annotation name or namespace
1389
     */
1390
    private function isIgnoredAnnotation(string $name): bool
1391
    {
1392
        if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) {
1393
            return true;
1394
        }
1395
1396
        foreach (array_keys($this->ignoredAnnotationNamespaces) as $ignoredAnnotationNamespace) {
1397
            $ignoredAnnotationNamespace = rtrim($ignoredAnnotationNamespace, '\\') . '\\';
1398
1399
            if (stripos(rtrim($name, '\\') . '\\', $ignoredAnnotationNamespace) === 0) {
1400
                return true;
1401
            }
1402
        }
1403
1404
        return false;
1405
    }
1406
1407
    /**
1408
     * Resolve positional arguments (without name) to named ones
1409
     *
1410
     * @param array<string,mixed> $arguments
1411
     *
1412
     * @return array<string,mixed>
1413
     */
1414
    private function resolvePositionalValues(array $arguments, string $name): array
1415
    {
1416
        $positionalArguments = $arguments['positional_arguments'] ?? [];
1417
        $values              = $arguments['named_arguments'] ?? [];
1418
1419
        if (
1420
            self::$annotationMetadata[$name]['has_named_argument_constructor']
1421
            && self::$annotationMetadata[$name]['default_property'] !== null
1422
        ) {
1423
            // We must ensure that we don't have positional arguments after named ones
1424
            $positions    = array_keys($positionalArguments);
1425
            $lastPosition = null;
1426
            foreach ($positions as $position) {
1427
                if (
1428
                    ($lastPosition === null && $position !== 0) ||
1429
                    ($lastPosition !== null && $position !== $lastPosition + 1)
1430
                ) {
1431
                    throw $this->syntaxError('Positional arguments after named arguments is not allowed');
1432
                }
1433
1434
                $lastPosition = $position;
1435
            }
1436
1437
            foreach (self::$annotationMetadata[$name]['constructor_args'] as $property => $parameter) {
1438
                $position = $parameter['position'];
1439
                if (isset($values[$property]) || ! isset($positionalArguments[$position])) {
1440
                    continue;
1441
                }
1442
1443
                $values[$property] = $positionalArguments[$position];
1444
            }
1445
        } else {
1446
            if (count($positionalArguments) > 0 && ! isset($values['value'])) {
1447
                if (count($positionalArguments) === 1) {
1448
                    $value = array_pop($positionalArguments);
1449
                } else {
1450
                    $value = array_values($positionalArguments);
1451
                }
1452
1453
                $values['value'] = $value;
1454
            }
1455
        }
1456
1457
        return $values;
1458
    }
1459
}
1460