Passed
Push — master ( 55fca5...85d75f )
by Kirill
04:44 queued 10s
created

FallbackAttributeReader::getPropertyAttributes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 2
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Spiral Framework package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Attributes\Internal;
13
14
use PhpParser\Parser;
15
16
/**
17
 * @internal FallbackAttributeReader is an internal library class, please do not use it in your code.
18
 * @psalm-internal Spiral\Attributes
19
 */
20
final class FallbackAttributeReader extends AttributeReader
21
{
22
    /**
23
     * @var int
24
     */
25
    private const KEY_CLASSES = 0x00;
26
27
    /**
28
     * @var int
29
     */
30
    private const KEY_FUNCTIONS = 0x01;
31
32
    /**
33
     * @var int
34
     */
35
    private const KEY_CONSTANTS = 0x02;
36
37
    /**
38
     * @var int
39
     */
40
    private const KEY_PROPERTIES = 0x03;
41
42
    /**
43
     * @var int
44
     */
45
    private const KEY_PARAMETERS = 0x04;
46
47
    /**
48
     * @var AttributeParser
49
     */
50
    private $parser;
51
52
    /**
53
     * @var array
54
     */
55
    private $attributes = [];
56
57
    /**
58
     * @param Parser|null $parser
59
     */
60
    public function __construct(Parser $parser = null)
61
    {
62
        $this->parser = new AttributeParser($parser);
63
64
        parent::__construct();
65
    }
66
67
    /**
68
     * {@inheritDoc}
69
     */
70
    protected function getClassAttributes(\ReflectionClass $class, ?string $name): iterable
71
    {
72
        // 1) Can not parse internal classes
73
        // 2) Anonymous classes don't support attributes (PHP semantic)
74
        if ($class->isInternal() || $class->isAnonymous()) {
75
            return [];
76
        }
77
78
        $attributes = $this->parseAttributes($class->getFileName(), self::KEY_CLASSES);
79
80
        return $this->format($attributes[$class->getName()] ?? [], $name);
81
    }
82
83
    /**
84
     * {@inheritDoc}
85
     */
86
    protected function getFunctionAttributes(\ReflectionFunctionAbstract $function, ?string $name): iterable
87
    {
88
        // Can not parse internal functions
89
        if ($function->isInternal()) {
90
            return [];
91
        }
92
93
        $attributes = $this->parseAttributes($function->getFileName(), self::KEY_FUNCTIONS);
94
        $attributes = $this->extractFunctionAttributes($attributes, $function);
95
96
        return $this->format($attributes, $name);
97
    }
98
99
    /**
100
     * {@inheritDoc}
101
     */
102
    protected function getPropertyAttributes(\ReflectionProperty $property, ?string $name): iterable
103
    {
104
        $class = $property->getDeclaringClass();
105
106
        // Can not parse property of internal class
107
        if ($class->isInternal()) {
108
            return [];
109
        }
110
111
        $attributes = $this->parseAttributes($class->getFileName(), self::KEY_PROPERTIES);
112
113
        return $this->format($attributes[$class->getName()][$property->getName()] ?? [], $name);
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119
    protected function getConstantAttributes(\ReflectionClassConstant $const, ?string $name): iterable
120
    {
121
        $class = $const->getDeclaringClass();
122
123
        // Can not parse internal classes
124
        if ($class->isInternal()) {
125
            return [];
126
        }
127
128
        $attributes = $this->parseAttributes($class->getFileName(), self::KEY_CONSTANTS);
129
130
        return $this->format($attributes[$class->getName()][$const->getName()] ?? [], $name);
131
    }
132
133
    /**
134
     * {@inheritDoc}
135
     */
136
    protected function getParameterAttributes(\ReflectionParameter $param, ?string $name): iterable
137
    {
138
        $function = $param->getDeclaringFunction();
139
140
        // Can not parse parameter of internal function
141
        if ($function->isInternal()) {
142
            return [];
143
        }
144
145
        $attributes = $this->parseAttributes($function->getFileName(), self::KEY_PARAMETERS);
146
        $attributes = $this->extractFunctionAttributes($attributes, $function);
147
148
        return $this->format($attributes[$param->getName()] ?? [], $name);
149
    }
150
151
    /**
152
     * @param array $attributes
153
     * @param \ReflectionFunctionAbstract $function
154
     * @return array
155
     */
156
    private function extractFunctionAttributes(array $attributes, \ReflectionFunctionAbstract $function): array
157
    {
158
        /**
159
         * We cannot use the function start line because it is different for
160
         * the PHP and nikic/php-parser AST.
161
         *
162
         * For example:
163
         * <code>
164
         *  1. | #[ExampleAttribute]
165
         *  2. | #[ExampleAttribute]
166
         *  3. | function example() { ... }
167
         * </code>
168
         *
169
         * In this case, the PHP {@see \ReflectionFunction} will return:
170
         * <code>
171
         *  $reflection->getStartLine(); // 3 (real start of function)
172
         * </code>
173
         *
174
         * However, nikic/php-parser returns:
175
         * <code>
176
         *  $ast->getStartLine(); // 1 (the line starts from the first attribute)
177
         * </code>
178
         */
179
        $line = $function->getEndLine();
180
181
        if ($result = $attributes[$line] ?? null) {
182
            return $result;
183
        }
184
185
        /**
186
         * Workaround for those cases when the ";" is on a new line
187
         * (nikic/php-parser bug related to terminal line).
188
         *
189
         * For example:
190
         * <code>
191
         *  1. | $function = #[ExampleAttribute]
192
         *  2. |     fn() => 42
193
         *  3. | ;
194
         * </code>
195
         *
196
         * In this case, the PHP {@see \ReflectionFunction} will return:
197
         * <code>
198
         *  $reflection->getEndLine(); // 3 (real end of function)
199
         * </code>
200
         *
201
         * However, nikic/php-parser returns:
202
         * <code>
203
         *  $ast->getEndLine(); // 2 (last significant character of a function)
204
         * </code>
205
         */
206
        while ($line-- > 0) {
207
            if ($result = $attributes[$line] ?? null) {
208
                return $result;
209
            }
210
        }
211
212
        return [];
213
    }
214
215
    /**
216
     * @param AttributePrototype[] $attributes
217
     * @param class-string|null $name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|null at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|null.
Loading history...
218
     * @return iterable<\ReflectionClass, array>
219
     */
220
    private function format(iterable $attributes, ?string $name): iterable
221
    {
222
        foreach ($attributes as $prototype) {
223
            if ($name !== null && !\is_subclass_of($prototype->name, $name) && $prototype->name !== $name) {
224
                continue;
225
            }
226
227
            yield new \ReflectionClass($prototype->name) => $prototype->params;
228
        }
229
    }
230
231
    /**
232
     * @psalm-type Context = FallbackAttributeReader::KEY_*
233
     *
234
     * @param string $file
235
     * @param Context $context
236
     * @return array
237
     */
238
    private function parseAttributes(string $file, int $context): array
239
    {
240
        if (!isset($this->attributes[$file])) {
241
            $found = $this->parser->parse($file);
242
243
            $this->attributes[$file] = [
244
                self::KEY_CLASSES    => $found->getClasses(),
245
                self::KEY_FUNCTIONS  => $found->getFunctions(),
246
                self::KEY_CONSTANTS  => $found->getConstants(),
247
                self::KEY_PROPERTIES => $found->getProperties(),
248
                self::KEY_PARAMETERS => $found->getParameters(),
249
            ];
250
        }
251
252
        return $this->attributes[$file][$context];
253
    }
254
}
255