Passed
Push — master ( c2f53d...1e149c )
by Edward
02:16
created

QueryCallbackBuilder::setReference()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 7
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace Remorhaz\JSON\Path\Query;
5
6
use function array_map;
7
use PhpParser\BuilderFactory;
8
use PhpParser\Node\Arg;
9
use PhpParser\Node\Expr;
10
use PhpParser\Node\Expr\Assign;
11
use PhpParser\Node\Stmt\Expression;
12
use PhpParser\Node\Stmt\Return_;
13
use PhpParser\PrettyPrinter\Standard;
14
use Remorhaz\JSON\Data\Value\NodeValueInterface;
15
use Remorhaz\JSON\Path\Value\ValueListInterface;
16
use Remorhaz\JSON\Path\Runtime\RuntimeInterface;
17
use Remorhaz\UniLex\AST\AbstractTranslatorListener;
18
use Remorhaz\UniLex\AST\Node;
19
use Remorhaz\UniLex\Exception as UniLexException;
20
use Remorhaz\UniLex\Stack\PushInterface;
21
22
final class QueryCallbackBuilder extends AbstractTranslatorListener
23
{
24
25
    private $php;
26
27
    private $references = [];
28
29
    private $runtime;
30
31
    private $input;
32
33
    private $stmts = [];
34
35
    private $queryCallback;
36
37
    private $isDefinite;
38
39
    public function __construct()
40
    {
41
        $this->php = new BuilderFactory;
42
    }
43
44
    public function getQueryCallback(): callable
45
    {
46
        if (isset($this->queryCallback)) {
47
            return $this->queryCallback;
48
        }
49
50
        throw new Exception\QueryCallbackNotFoundException;
51
    }
52
53
    public function isDefinite(): bool
54
    {
55
        if (isset($this->isDefinite)) {
56
            return $this->isDefinite;
57
        }
58
59
        throw new Exception\IsDefiniteFlagNotFoundException;
60
    }
61
62
    public function onStart(Node $node): void
63
    {
64
        $this->runtime = $this->php->var('runtime');
65
        $this->input = $this->php->var('input');
66
    }
67
68
    public function onFinish(): void
69
    {
70
        $runtimeParam = $this
71
            ->php
72
            ->param('runtime')
73
            ->setType(RuntimeInterface::class)
74
            ->getNode();
75
        $inputParam = $this
76
            ->php
77
            ->param('input')
78
            ->setType(NodeValueInterface::class)
79
            ->getNode();
80
        $stmts = array_map(
81
            function (\PhpParser\Node $stmt): \PhpParser\Node {
82
                return $stmt instanceof Expr ? new Expression($stmt): $stmt;
83
            },
84
            $this->stmts
85
        );
86
87
        $closure = new Expr\Closure(
88
            [
89
                'stmts' => $stmts,
90
                'returnType' => ValueListInterface::class,
91
                'params' => [$runtimeParam, $inputParam],
92
            ]
93
        );
94
        $return = new Return_($closure);
95
96
        $callbackCode = (new Standard)->prettyPrint([$return]);
97
        $this->queryCallback = eval($callbackCode);
1 ignored issue
show
introduced by
The use of eval() is discouraged.
Loading history...
98
    }
99
100
    public function onBeginProduction(Node $node, PushInterface $stack): void
101
    {
102
        $stack->push(...$node->getChildList());
103
    }
104
105
    /**
106
     * @param Node $node
107
     * @throws UniLexException
108
     */
109
    public function onFinishProduction(Node $node): void
110
    {
111
        if ($this->hasReference($node)) {
112
            return;
113
        }
114
        switch ($node->getName()) {
115
            case QueryAstNodeType::GET_INPUT:
116
                $this->addMethodCall(
117
                    $node,
118
                    'getInput',
119
                    $this->input
120
                );
121
                break;
122
123
            case QueryAstNodeType::SET_OUTPUT:
124
                $this->isDefinite = $node->getAttribute('is_definite');
125
                $this->stmts[] = new Return_($this->getReference($node->getChild(0)));
126
                break;
127
128
            case QueryAstNodeType::CREATE_FILTER_CONTEXT:
129
                $this->addMethodCall(
130
                    $node,
131
                    'createFilterContext',
132
                    $this->getReference($node->getChild(0))
133
                );
134
                break;
135
136
            case QueryAstNodeType::SPLIT:
137
                $this->addMethodCall(
138
                    $node,
139
                    'split',
140
                    $this->getReference($node->getChild(0))
141
                );
142
                break;
143
144
            case QueryAstNodeType::EVALUATE:
145
                $this->addMethodCall(
146
                    $node,
147
                    'evaluate',
148
                    $this->getReference($node->getChild(0)),
149
                    $this->getReference($node->getChild(1))
150
                );
151
                break;
152
153
            case QueryAstNodeType::FILTER:
154
                $this->addMethodCall(
155
                    $node,
156
                    'filter',
157
                    $this->getReference($node->getChild(0)),
158
                    $this->getReference($node->getChild(1))
159
                );
160
                break;
161
162
            case QueryAstNodeType::EVALUATE_LOGICAL_OR:
163
                $this->addMethodCall(
164
                    $node,
165
                    'evaluateLogicalOr',
166
                    $this->getReference($node->getChild(0)),
167
                    $this->getReference($node->getChild(1))
168
                );
169
                break;
170
171
            case QueryAstNodeType::EVALUATE_LOGICAL_AND:
172
                $this->addMethodCall(
173
                    $node,
174
                    'evaluateLogicalAnd',
175
                    $this->getReference($node->getChild(0)),
176
                    $this->getReference($node->getChild(1))
177
                );
178
                break;
179
180
            case QueryAstNodeType::EVALUATE_LOGICAL_NOT:
181
                $this->addMethodCall(
182
                    $node,
183
                    'evaluateLogicalNot',
184
                    $this->getReference($node->getChild(0))
185
                );
186
                break;
187
188
            case QueryAstNodeType::CALCULATE_IS_EQUAL:
189
                $this->addMethodCall(
190
                    $node,
191
                    'calculateIsEqual',
192
                    $this->getReference($node->getChild(0)),
193
                    $this->getReference($node->getChild(1))
194
                );
195
                break;
196
197
            case QueryAstNodeType::CALCULATE_IS_GREATER:
198
                $this->addMethodCall(
199
                    $node,
200
                    'calculateIsGreater',
201
                    $this->getReference($node->getChild(0)),
202
                    $this->getReference($node->getChild(1))
203
                );
204
                break;
205
206
            case QueryAstNodeType::CALCULATE_IS_REGEXP:
207
                $this->addMethodCall(
208
                    $node,
209
                    'calculateIsRegExp',
210
                    $this->php->val($node->getAttribute('pattern')),
211
                    $this->getReference($node->getChild(0))
212
                );
213
                break;
214
215
            case QueryAstNodeType::FETCH_CHILDREN:
216
                $this->addMethodCall(
217
                    $node,
218
                    'fetchChildren',
219
                    $this->getReference($node->getChild(0)),
220
                    new Arg(
221
                        $this->getReference($node->getChild(1)),
222
                        false,
223
                        true
224
                    )
225
                );
226
                break;
227
228
            case QueryAstNodeType::FETCH_CHILDREN_DEEP:
229
                $this->addMethodCall(
230
                    $node,
231
                    'fetchChildrenDeep',
232
                    $this->getReference($node->getChild(0)),
233
                    new Arg(
234
                        $this->getReference($node->getChild(1)),
235
                        false,
236
                        true
237
                    )
238
                );
239
                break;
240
241
            case QueryAstNodeType::MATCH_ANY_CHILD:
242
                $this->addMethodCall(
243
                    $node,
244
                    'matchAnyChild',
245
                    $this->getReference($node->getChild(0))
246
                );
247
                break;
248
249
            case QueryAstNodeType::MATCH_PROPERTY_STRICTLY:
250
                $this->addMethodCall(
251
                    $node,
252
                    'matchPropertyStrictly',
253
                    $this->getReference($node->getChild(0)),
254
                );
255
                break;
256
257
            case QueryAstNodeType::MATCH_ELEMENT_STRICTLY:
258
                $this->addMethodCall(
259
                    $node,
260
                    'matchElementStrictly',
261
                    $this->getReference($node->getChild(0)),
262
                );
263
                break;
264
265
            case QueryAstNodeType::AGGREGATE:
266
                $this->addMethodCall(
267
                    $node,
268
                    'aggregate',
269
                    $this->php->val($node->getAttribute('name')),
270
                    $this->getReference($node->getChild(0)),
271
                );
272
                break;
273
274
            case QueryAstNodeType::POPULATE_LITERAL:
275
                $this->addMethodCall(
276
                    $node,
277
                    'populateLiteral',
278
                    $this->getReference($node->getChild(0)),
279
                    $this->getReference($node->getChild(1))
280
                );
281
                break;
282
283
            case QueryAstNodeType::POPULATE_LITERAL_ARRAY:
284
                $this->addMethodCall(
285
                    $node,
286
                    'populateLiteralArray',
287
                    $this->getReference($node->getChild(0)),
288
                    new Arg(
289
                        $this->getReference($node->getChild(1)),
290
                        false,
291
                        true
292
                    )
293
                );
294
                break;
295
296
            case QueryAstNodeType::POPULATE_INDEX_LIST:
297
                $this->addMethodCall(
298
                    $node,
299
                    'populateIndexList',
300
                    $this->getReference($node->getChild(0)),
301
                    ...array_map(
302
                        [$this->php, 'val'],
303
                        $node->getAttribute('indexList')
304
                    )
305
                );
306
                break;
307
308
            case QueryAstNodeType::POPULATE_INDEX_SLICE:
309
                $attributes = $node->getAttributeList();
310
                $this->addMethodCall(
311
                    $node,
312
                    'populateIndexSlice',
313
                    $this->getReference($node->getChild(0)),
314
                    $this->php->val($attributes['start'] ?? null),
315
                    $this->php->val($attributes['end'] ?? null),
316
                    $this->php->val($attributes['step'] ?? null)
317
                );
318
319
                break;
320
321
            case QueryAstNodeType::POPULATE_NAME_LIST:
322
                $this->addMethodCall(
323
                    $node,
324
                    'populateNameList',
325
                    $this->getReference($node->getChild(0)),
326
                    ...array_map(
327
                        [$this->php, 'val'],
328
                        $node->getAttribute('nameList')
329
                    )
330
                );
331
                break;
332
333
            case QueryAstNodeType::CREATE_SCALAR:
334
                $attributes = $node->getAttributeList();
335
                // TODO: allow accessing null attributes
336
                $value = $this->php->val($attributes['value'] ?? null);
337
                $this->addMethodCall(
338
                    $node,
339
                    'createScalar',
340
                    $value
341
                );
342
                break;
343
344
            case QueryAstNodeType::CREATE_ARRAY:
345
                // [ X:APPEND_TO_ARRAY ]
346
                $items = [];
347
                foreach ($node->getChildList() as $child) {
348
                    $items[] = $this->getReference($child);
349
                }
350
                $this->stmts[] = new Assign(
351
                    $this->createReference($node),
352
                    $this->php->val($items)
353
                );
354
                break;
355
356
            case QueryAstNodeType::APPEND_TO_ARRAY:
357
                // [ 0:<value> ]
358
                $this->setReference(
359
                    $node,
360
                    $this->getReference($node->getChild(0))
361
                );
362
                break;
363
        }
364
    }
365
366
    private function getVarName(Node $node): string
367
    {
368
        return "var{$node->getId()}";
369
    }
370
371
    private function createReference(Node $node): Expr
372
    {
373
        $reference = $this->php->var($this->getVarName($node));
374
        $this->setReference($node, $reference);
375
376
        return $reference;
377
    }
378
379
    private function setReference(Node $node, Expr $expr): void
380
    {
381
        if (isset($this->references[$node->getId()])) {
382
            throw new Exception\ReferenceAlreadyExistsException($node->getId());
383
        }
384
385
        $this->references[$node->getId()] = $expr;
386
    }
387
388
    private function hasReference(Node $node): bool
389
    {
390
        return isset($this->references[$node->getId()]);
391
    }
392
393
    private function getReference(Node $node): Expr
394
    {
395
        if (!isset($this->references[$node->getId()])) {
396
            throw new Exception\ReferenceNotFoundException($node->getId());
397
        }
398
399
        return $this->references[$node->getId()];
400
    }
401
402
    private function addMethodCall(Node $node, string $method, \PhpParser\Node ...$args): void
403
    {
404
        $methodCall = $this
405
            ->php
406
            ->methodCall($this->runtime, $method, $args);
407
        $this->stmts[] = new Assign($this->createReference($node), $methodCall);
408
    }
409
}
410