Passed
Push — master ( c94a81...b2c847 )
by Kirill
04:43
created

AttributeParser::evaluator()   B

Complexity

Conditions 9
Paths 1

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 9
eloc 26
c 3
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 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
use PhpParser\PrettyPrinter\Standard;
25
26
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
    private function resolveNames(array $ast): array
0 ignored issues
show
Unused Code introduced by
The method resolveNames() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

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