Passed
Push — master ( b5c7df...649c38 )
by Kirill
04:24
created

AttributeParser::evaluator()   B

Complexity

Conditions 8
Paths 1

Size

Total Lines 36
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 8
eloc 22
c 2
b 0
f 0
nc 1
nop 2
dl 0
loc 36
rs 8.4444
1
<?php
2
3
/**
4
 * This file is part of Attributes 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\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
25
class AttributeParser
26
{
27
    /**
28
     * @var string
29
     */
30
    private const ERROR_NAMED_ARGUMENTS_ORDER = 'Cannot use positional argument after named argument';
31
32
    /**
33
     * @var string
34
     */
35
    private const ERROR_BAD_CONSTANT_EXPRESSION = 'Constant expression contains invalid operations';
36
37
    /**
38
     * @var string
39
     */
40
    private const ERROR_BAD_CONSTANT = 'Undefined constant %s';
41
42
    /**
43
     * @var string
44
     */
45
    public const CTX_FUNCTION = '__FUNCTION__';
46
47
    /**
48
     * @var string
49
     */
50
    public const CTX_NAMESPACE = '__NAMESPACE__';
51
52
    /**
53
     * @var string
54
     */
55
    public const CTX_CLASS = '__CLASS__';
56
57
    /**
58
     * @var string
59
     */
60
    public const CTX_TRAIT = '__TRAIT__';
61
62
    /**
63
     * @var Parser
64
     */
65
    private $parser;
66
67
    /**
68
     * @param Parser|null $parser
69
     */
70
    public function __construct(Parser $parser = null)
71
    {
72
        $this->parser = $parser ?? $this->createParser();
73
    }
74
75
    /**
76
     * @return Parser
77
     */
78
    private function createParser(): Parser
79
    {
80
        $factory = new ParserFactory();
81
82
        return $factory->create(ParserFactory::ONLY_PHP7);
83
    }
84
85
    /**
86
     * @param string $file
87
     * @return AttributeFinderVisitor
88
     */
89
    public function parse(string $file): AttributeFinderVisitor
90
    {
91
        $ast = $this->parser->parse($this->read($file));
92
93
        $finder = new AttributeFinderVisitor($file, $this);
94
95
        $traverser = new NodeTraverser();
96
        $traverser->addVisitor(new NameResolver());
97
        $traverser->addVisitor($finder);
98
        $traverser->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

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