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

AttributeParser::evaluator()   B

Complexity

Conditions 9
Paths 1

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 26
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 43
rs 8.0555
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\FallbackAttributeReader;
13
14
use PhpParser\ConstExprEvaluationException;
15
use PhpParser\ConstExprEvaluator;
16
use PhpParser\Node\Attribute;
17
use PhpParser\Node\AttributeGroup;
18
use PhpParser\Node\Expr;
19
use PhpParser\Node\Scalar;
20
use PhpParser\NodeTraverser;
21
use PhpParser\NodeVisitor\NameResolver;
22
use PhpParser\Parser;
23
use PhpParser\ParserFactory;
24
use Spiral\Attributes\Internal\Exception;
25
26
final class AttributeParser
27
{
28
    /**
29
     * @var string
30
     */
31
    public const CTX_FUNCTION = '__FUNCTION__';
32
33
    /**
34
     * @var string
35
     */
36
    public const CTX_NAMESPACE = '__NAMESPACE__';
37
38
    /**
39
     * @var string
40
     */
41
    public const CTX_CLASS = '__CLASS__';
42
43
    /**
44
     * @var string
45
     */
46
    public const CTX_TRAIT = '__TRAIT__';
47
    /**
48
     * @var string
49
     */
50
    private const ERROR_NAMED_ARGUMENTS_ORDER = 'Cannot use positional argument after named argument';
51
52
    /**
53
     * @var string
54
     */
55
    private const ERROR_BAD_CONSTANT_EXPRESSION = 'Constant expression contains invalid operations';
56
57
    /**
58
     * @var string
59
     */
60
    private const ERROR_BAD_CONSTANT = 'Undefined constant %s';
61
62
    /**
63
     * @var Parser
64
     */
65
    private $parser;
66
67
    /**
68
     * @var NodeTraverser
69
     */
70
    private $resolver;
71
72
    /**
73
     * @param Parser|null $parser
74
     */
75
    public function __construct(Parser $parser = null)
76
    {
77
        $this->parser = $parser ?? $this->createParser();
78
79
        $this->resolver = new NodeTraverser();
80
        $this->resolver->addVisitor(new NameResolver());
81
    }
82
83
    /**
84
     * @param string $file
85
     * @return AttributeFinderVisitor
86
     */
87
    public function parse(string $file): AttributeFinderVisitor
88
    {
89
        $ast = $this->parser->parse($this->read($file));
90
91
        $finder = new AttributeFinderVisitor($file, $this);
92
93
        $traverser = new NodeTraverser();
94
        $traverser->addVisitor($finder);
95
96
        $traverser->traverse(
97
            $this->resolver->traverse($ast)
0 ignored issues
show
Bug introduced by
It seems like $ast can also be of type null; however, parameter $nodes of PhpParser\NodeTraverser::traverse() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
            $this->resolver->traverse(/** @scrutinizer ignore-type */ $ast)
Loading history...
98
        );
99
100
        return $finder;
101
    }
102
103
    /**
104
     * @param string $file
105
     * @param AttributeGroup[] $groups
106
     * @param array $context
107
     * @return \Traversable<AttributePrototype>
108
     * @throws \Throwable
109
     */
110
    public function parseAttributes(string $file, array $groups, array $context): \Traversable
111
    {
112
        $eval = new ConstExprEvaluator($this->evaluator($file, $context));
113
114
        foreach ($groups as $group) {
115
            foreach ($group->attrs as $attr) {
116
                $arguments = $this->parseAttributeArguments($attr, $file, $eval);
117
118
                yield new AttributePrototype($attr->name->toString(), $arguments);
119
            }
120
        }
121
    }
122
123
    /**
124
     * @return Parser
125
     */
126
    private function createParser(): Parser
127
    {
128
        $factory = new ParserFactory();
129
130
        return $factory->create(ParserFactory::ONLY_PHP7);
131
    }
132
133
    /**
134
     * @param string $file
135
     * @return string
136
     */
137
    private function read(string $file): string
138
    {
139
        if (!\is_readable($file)) {
140
            throw new \InvalidArgumentException('Unable to read file "' . $file . '"');
141
        }
142
143
        return \file_get_contents($file);
144
    }
145
146
    /**
147
     * @param string $file
148
     * @param array $context
149
     * @return \Closure
150
     */
151
    private function evaluator(string $file, array $context): \Closure
152
    {
153
        return static function (Expr $expr) use ($file, $context) {
154
            switch (\get_class($expr)) {
155
                case Scalar\MagicConst\File::class:
156
                    return $file;
157
158
                case Scalar\MagicConst\Dir::class:
159
                    return \dirname($file);
160
161
                case Scalar\MagicConst\Line::class:
162
                    return $expr->getStartLine();
163
164
                case Scalar\MagicConst\Method::class:
165
                    $namespace = $context[self::CTX_NAMESPACE] ?? '';
166
                    $function = $context[self::CTX_FUNCTION] ?? '';
167
168
                    return \ltrim($namespace . '\\' . $function, '\\');
169
170
                case Expr\ClassConstFetch::class:
171
                    $constant = $expr->name->toString();
172
                    $class = $expr->class->toString();
173
174
                    if (\strtolower($constant) === 'class') {
175
                        return $class;
176
                    }
177
178
                    $definition = $class . '::' . $constant;
179
180
                    if (!\defined($definition)) {
181
                        $exception = new \ParseError(\sprintf(self::ERROR_BAD_CONSTANT, $definition));
182
                        throw Exception::withLocation($exception, $file, $expr->getStartLine());
183
                    }
184
185
                    return \constant($definition);
186
            }
187
188
            if ($expr instanceof Scalar\MagicConst) {
189
                return $context[$expr->getName()] ?? '';
190
            }
191
192
            $exception = new \ParseError(self::ERROR_BAD_CONSTANT_EXPRESSION);
193
            throw Exception::withLocation($exception, $file, $expr->getStartLine());
194
        };
195
    }
196
197
    /**
198
     * @param Attribute $attr
199
     * @param string $file
200
     * @param ConstExprEvaluator $eval
201
     * @return array
202
     * @throws ConstExprEvaluationException
203
     * @throws \Throwable
204
     */
205
    private function parseAttributeArguments(Attribute $attr, string $file, ConstExprEvaluator $eval): array
206
    {
207
        $hasNamedArguments = false;
208
        $arguments = [];
209
210
        foreach ($attr->args as $argument) {
211
            $value = $eval->evaluateDirectly($argument->value);
212
213
            if ($argument->name === null) {
214
                $arguments[] = $value;
215
216
                if ($hasNamedArguments) {
217
                    $exception = new \ParseError(self::ERROR_NAMED_ARGUMENTS_ORDER);
218
                    throw Exception::withLocation($exception, $file, $argument->getStartLine());
219
                }
220
221
                continue;
222
            }
223
224
            $hasNamedArguments = true;
225
            $arguments[$argument->name->toString()] = $value;
226
        }
227
228
        return $arguments;
229
    }
230
}
231