Passed
Push — master ( b674ac...2bc77e )
by Kirill
04:21
created

FallbackAttributeReader::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
rs 10
c 1
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;
13
14
use PhpParser\Parser;
15
use Spiral\Attributes\Internal\AttributeParser;
16
use Spiral\Attributes\Internal\AttributePrototype;
17
use Spiral\Attributes\Internal\AttributeReader;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Spiral\Attributes\AttributeReader. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
18
19
final class FallbackAttributeReader extends AttributeReader
20
{
21
    /**
22
     * @var int
23
     */
24
    private const KEY_CLASSES = 0x00;
25
26
    /**
27
     * @var int
28
     */
29
    private const KEY_FUNCTIONS = 0x01;
30
31
    /**
32
     * @var int
33
     */
34
    private const KEY_CONSTANTS = 0x02;
35
36
    /**
37
     * @var int
38
     */
39
    private const KEY_PROPERTIES = 0x03;
40
41
    /**
42
     * @var int
43
     */
44
    private const KEY_PARAMETERS = 0x04;
45
46
    /**
47
     * @var AttributeParser
48
     */
49
    private $parser;
50
51
    /**
52
     * @var array
53
     */
54
    private $attributes = [];
55
56
    /**
57
     * @param Parser|null $parser
58
     */
59
    public function __construct(Parser $parser = null)
60
    {
61
        $this->parser = new AttributeParser($parser);
62
63
        parent::__construct();
64
    }
65
66
    /**
67
     * {@inheritDoc}
68
     */
69
    protected function getClassAttributes(\ReflectionClass $class, ?string $name): iterable
70
    {
71
        // 1) Can not parse internal classes
72
        // 2) Anonymous classes don't support attributes (PHP semantic)
73
        if ($class->isInternal() || $class->isAnonymous()) {
74
            return [];
75
        }
76
77
        $attributes = $this->parseAttributes($class->getFileName(), self::KEY_CLASSES);
78
79
        return $this->format($attributes[$class->getName()] ?? []);
80
    }
81
82
    /**
83
     * {@inheritDoc}
84
     */
85
    protected function getFunctionAttributes(\ReflectionFunctionAbstract $function, ?string $name): iterable
86
    {
87
        // Can not parse internal functions
88
        if ($function->isInternal()) {
89
            return [];
90
        }
91
92
        $attributes = $this->parseAttributes($function->getFileName(), self::KEY_FUNCTIONS);
93
        $attributes = $this->extractFunctionAttributes($attributes, $function);
94
95
        return $this->format($attributes);
96
    }
97
98
    /**
99
     * @param array $attributes
100
     * @param \ReflectionFunctionAbstract $function
101
     * @return array
102
     */
103
    private function extractFunctionAttributes(array $attributes, \ReflectionFunctionAbstract $function): array
104
    {
105
        /**
106
         * We cannot use the function start line because it is different for
107
         * the PHP and nikic/php-parser AST.
108
         *
109
         * For example:
110
         * <code>
111
         *  1. | #[ExampleAttribute]
112
         *  2. | #[ExampleAttribute]
113
         *  3. | function example() { ... }
114
         * </code>
115
         *
116
         * In this case, the PHP {@see \ReflectionFunction} will return:
117
         * <code>
118
         *  $reflection->getStartLine(); // 3 (real start of function)
119
         * </code>
120
         *
121
         * However, nikic/php-parser returns:
122
         * <code>
123
         *  $ast->getEndLine(); // 1 (the line starts from the first attribute)
124
         * </code>
125
         */
126
        $line = $function->getEndLine();
127
128
        if ($result = $attributes[$line] ?? null) {
129
            return $result;
130
        }
131
132
        /**
133
         * Workaround for those cases when the ";" is on a new line
134
         * (nikic/php-parser bug related to terminal line).
135
         *
136
         * For example:
137
         * <code>
138
         *  1. | $function = #[ExampleAttribute]
139
         *  2. |     fn() => 42
140
         *  3. | ;
141
         * </code>
142
         *
143
         * In this case, the PHP {@see \ReflectionFunction} will return:
144
         * <code>
145
         *  $reflection->getEndLine(); // 3 (real end of function)
146
         * </code>
147
         *
148
         * However, nikic/php-parser returns:
149
         * <code>
150
         *  $ast->getEndLine(); // 2 (last significant character of a function)
151
         * </code>
152
         */
153
        while ($line-- > 0) {
154
            if ($result = $attributes[$line] ?? null) {
155
                return $result;
156
            }
157
        }
158
159
        return [];
160
    }
161
162
    /**
163
     * {@inheritDoc}
164
     */
165
    protected function getPropertyAttributes(\ReflectionProperty $property, ?string $name): iterable
166
    {
167
        $class = $property->getDeclaringClass();
168
169
        // Can not parse property of internal class
170
        if ($class->isInternal()) {
171
            return [];
172
        }
173
174
        $attributes = $this->parseAttributes($class->getFileName(), self::KEY_PROPERTIES);
175
176
        return $this->format($attributes[$class->getName()][$property->getName()] ?? []);
177
    }
178
179
    /**
180
     * {@inheritDoc}
181
     */
182
    protected function getConstantAttributes(\ReflectionClassConstant $const, ?string $name): iterable
183
    {
184
        $class = $const->getDeclaringClass();
185
186
        // Can not parse internal classes
187
        if ($class->isInternal()) {
188
            return [];
189
        }
190
191
        $attributes = $this->parseAttributes($class->getFileName(), self::KEY_CONSTANTS);
192
193
        return $this->format($attributes[$class->getName()][$const->getName()] ?? []);
194
    }
195
196
    /**
197
     * {@inheritDoc}
198
     */
199
    protected function getParameterAttributes(\ReflectionParameter $param, ?string $name): iterable
200
    {
201
        $function = $param->getDeclaringFunction();
202
203
        // Can not parse parameter of internal function
204
        if ($function->isInternal()) {
205
            return [];
206
        }
207
208
        $attributes = $this->parseAttributes($function->getFileName(), self::KEY_PARAMETERS);
209
        $attributes = $this->extractFunctionAttributes($attributes, $function);
210
211
        return $this->format($attributes[$param->getName()] ?? []);
212
    }
213
214
    /**
215
     * @param AttributePrototype[] $attributes
216
     * @return iterable<\ReflectionClass, array>
217
     */
218
    private function format(iterable $attributes): iterable
219
    {
220
        foreach ($attributes as $prototype) {
221
            yield new \ReflectionClass($prototype->name) => $prototype->params;
222
        }
223
    }
224
225
    /**
226
     * @psalm-type Context = FallbackAttributeReader::KEY_*
227
     *
228
     * @param string $file
229
     * @param Context $context
0 ignored issues
show
Bug introduced by
The type Spiral\Attributes\Context was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
230
     * @return array
231
     */
232
    private function parseAttributes(string $file, int $context): array
233
    {
234
        if (! isset($this->attributes[$file])) {
235
            $found = $this->parser->parse($file);
236
237
            $this->attributes[$file] = [
238
                self::KEY_CLASSES    => $found->getClasses(),
239
                self::KEY_FUNCTIONS  => $found->getFunctions(),
240
                self::KEY_CONSTANTS  => $found->getConstants(),
241
                self::KEY_PROPERTIES => $found->getProperties(),
242
                self::KEY_PARAMETERS => $found->getParameters(),
243
            ];
244
        }
245
246
        return $this->attributes[$file][$context];
247
    }
248
}
249