Completed
Push — master ( 6366df...fbe022 )
by Kirill
23s queued 19s
created

extractFunctionAttributes()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 59
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 8
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 59
rs 9.6111

How to fix   Long Method   

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