Passed
Push — master ( a9f91d...44a018 )
by Kirill
04:46
created

FallbackAttributeReader::normalize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
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;
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()] ?? [], $name);
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, $name);
96
    }
97
98
    /**
99
     * {@inheritDoc}
100
     */
101
    protected function getPropertyAttributes(\ReflectionProperty $property, ?string $name): iterable
102
    {
103
        $class = $property->getDeclaringClass();
104
105
        // Can not parse property of internal class
106
        if ($class->isInternal()) {
107
            return [];
108
        }
109
110
        $attributes = $this->parseAttributes($class->getFileName(), self::KEY_PROPERTIES);
111
112
        return $this->format($attributes[$class->getName()][$property->getName()] ?? [], $name);
113
    }
114
115
    /**
116
     * {@inheritDoc}
117
     */
118
    protected function getConstantAttributes(\ReflectionClassConstant $const, ?string $name): iterable
119
    {
120
        $class = $const->getDeclaringClass();
121
122
        // Can not parse internal classes
123
        if ($class->isInternal()) {
124
            return [];
125
        }
126
127
        $attributes = $this->parseAttributes($class->getFileName(), self::KEY_CONSTANTS);
128
129
        return $this->format($attributes[$class->getName()][$const->getName()] ?? [], $name);
130
    }
131
132
    /**
133
     * {@inheritDoc}
134
     */
135
    protected function getParameterAttributes(\ReflectionParameter $param, ?string $name): iterable
136
    {
137
        $function = $param->getDeclaringFunction();
138
139
        // Can not parse parameter of internal function
140
        if ($function->isInternal()) {
141
            return [];
142
        }
143
144
        $attributes = $this->parseAttributes($function->getFileName(), self::KEY_PARAMETERS);
145
        $attributes = $this->extractFunctionAttributes($attributes, $function);
146
147
        return $this->format($attributes[$param->getName()] ?? [], $name);
148
    }
149
150
    /**
151
     * @param array $attributes
152
     * @param \ReflectionFunctionAbstract $function
153
     * @return array
154
     */
155
    private function extractFunctionAttributes(array $attributes, \ReflectionFunctionAbstract $function): array
156
    {
157
        /**
158
         * We cannot use the function start line because it is different for
159
         * the PHP and nikic/php-parser AST.
160
         *
161
         * For example:
162
         * <code>
163
         *  1. | #[ExampleAttribute]
164
         *  2. | #[ExampleAttribute]
165
         *  3. | function example() { ... }
166
         * </code>
167
         *
168
         * In this case, the PHP {@see \ReflectionFunction} will return:
169
         * <code>
170
         *  $reflection->getStartLine(); // 3 (real start of function)
171
         * </code>
172
         *
173
         * However, nikic/php-parser returns:
174
         * <code>
175
         *  $ast->getStartLine(); // 1 (the line starts from the first attribute)
176
         * </code>
177
         */
178
        $line = $function->getEndLine();
179
180
        if ($result = $attributes[$line] ?? null) {
181
            return $result;
182
        }
183
184
        /**
185
         * Workaround for those cases when the ";" is on a new line
186
         * (nikic/php-parser bug related to terminal line).
187
         *
188
         * For example:
189
         * <code>
190
         *  1. | $function = #[ExampleAttribute]
191
         *  2. |     fn() => 42
192
         *  3. | ;
193
         * </code>
194
         *
195
         * In this case, the PHP {@see \ReflectionFunction} will return:
196
         * <code>
197
         *  $reflection->getEndLine(); // 3 (real end of function)
198
         * </code>
199
         *
200
         * However, nikic/php-parser returns:
201
         * <code>
202
         *  $ast->getEndLine(); // 2 (last significant character of a function)
203
         * </code>
204
         */
205
        while ($line-- > 0) {
206
            if ($result = $attributes[$line] ?? null) {
207
                return $result;
208
            }
209
        }
210
211
        return [];
212
    }
213
214
    /**
215
     * @param AttributePrototype[] $attributes
216
     * @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...
217
     * @return iterable<\ReflectionClass, array>
218
     */
219
    private function format(iterable $attributes, ?string $name): iterable
220
    {
221
        foreach ($attributes as $prototype) {
222
            if ($name !== null && ! \is_subclass_of($prototype->name, $name)) {
223
                continue;
224
            }
225
226
            yield new \ReflectionClass($prototype->name) => $prototype->params;
227
        }
228
    }
229
230
    /**
231
     * @param string $name
232
     * @return string
233
     */
234
    private function normalize(string $name): string
235
    {
236
        return \strtolower(\trim($name, '\\'));
237
    }
238
239
    /**
240
     * @psalm-type Context = FallbackAttributeReader::KEY_*
241
     *
242
     * @param string $file
243
     * @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...
244
     * @return array
245
     */
246
    private function parseAttributes(string $file, int $context): array
247
    {
248
        if (!isset($this->attributes[$file])) {
249
            $found = $this->parser->parse($file);
250
251
            $this->attributes[$file] = [
252
                self::KEY_CLASSES    => $found->getClasses(),
253
                self::KEY_FUNCTIONS  => $found->getFunctions(),
254
                self::KEY_CONSTANTS  => $found->getConstants(),
255
                self::KEY_PROPERTIES => $found->getProperties(),
256
                self::KEY_PARAMETERS => $found->getParameters(),
257
            ];
258
        }
259
260
        return $this->attributes[$file][$context];
261
    }
262
}
263