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

AttributeParser::createParser()   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 0
dl 0
loc 5
rs 10
c 1
b 0
f 0
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
    public const CTX_FUNCTION = '__FUNCTION__';
41
42
    /**
43
     * @var string
44
     */
45
    public const CTX_NAMESPACE = '__NAMESPACE__';
46
47
    /**
48
     * @var string
49
     */
50
    public const CTX_CLASS = '__CLASS__';
51
52
    /**
53
     * @var string
54
     */
55
    public const CTX_TRAIT = '__TRAIT__';
56
57
    /**
58
     * @var Parser
59
     */
60
    private $parser;
61
62
    /**
63
     * @param Parser|null $parser
64
     */
65
    public function __construct(Parser $parser = null)
66
    {
67
        $this->parser = $parser ?? $this->createParser();
68
    }
69
70
    /**
71
     * @return Parser
72
     */
73
    private function createParser(): Parser
74
    {
75
        $factory = new ParserFactory();
76
77
        return $factory->create(ParserFactory::ONLY_PHP7);
78
    }
79
80
    /**
81
     * @param string $file
82
     * @return AttributeFinderVisitor
83
     */
84
    public function parse(string $file): AttributeFinderVisitor
85
    {
86
        $ast = $this->parser->parse($this->read($file));
87
88
        $finder = new AttributeFinderVisitor($file, $this);
89
90
        $traverser = new NodeTraverser();
91
        $traverser->addVisitor(new NameResolver());
92
        $traverser->addVisitor($finder);
93
        $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

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