Completed
Pull Request — master (#457)
by Claus
03:30 queued 37s
created

Sequencer::sequenceInlineNodes()   F

Complexity

Conditions 74
Paths 230

Size

Total Lines 309

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 74
nc 230
nop 2
dl 0
loc 309
rs 2.3666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
declare(strict_types=1);
3
4
namespace TYPO3Fluid\Fluid\Core\Parser;
5
6
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode;
7
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionException;
8
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
9
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode;
10
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
11
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
12
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
13
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition;
14
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperResolver;
15
16
/**
17
 * Sequencer for Fluid syntax
18
 *
19
 * Uses a NoRewindIterator around a sequence of byte values to
20
 * iterate over each syntax-relevant character and determine
21
 * which nodes to create.
22
 *
23
 * Passes the outer iterator between functions that perform the
24
 * iterations. Since the iterator is a NoRewindIterator it will
25
 * not be reset before the start of each loop - meaning that
26
 * when it is passed to a new function, that function continues
27
 * from the last touched index in the byte sequence.
28
 *
29
 * The circumstances around "break or return" in the switches is
30
 * very, very important to understand in context of how iterators
31
 * work. Returning does not advance the iterator like breaking
32
 * would and this causes a different position in the byte sequence
33
 * to be experienced in the method that uses the return value (it
34
 * sees the index of the symbol which terminated the expression,
35
 * not the next symbol after that).
36
 */
37
class Sequencer
38
{
39
    /**
40
     * @var RenderingContextInterface
41
     */
42
    protected $renderingContext;
43
44
    /**
45
     * @var ParsingState
46
     */
47
    protected $state;
48
49
    /**
50
     * @var Contexts
51
     */
52
    protected $contexts;
53
54
    /**
55
     * @var Source
56
     */
57
    protected $source;
58
59
    /**
60
     * @var Splitter
61
     */
62
    protected $splitter;
63
64
    /**
65
     * @var Configuration
66
     */
67
    protected $configuration;
68
69
    /**
70
     * @var ViewHelperResolver
71
     */
72
    protected $resolver;
73
74
    /**
75
     * Whether or not the escaping interceptors are active
76
     *
77
     * @var boolean
78
     */
79
    protected $escapingEnabled = true;
80
81
    public function __construct(
82
        RenderingContextInterface $renderingContext,
83
        ParsingState $state,
84
        Contexts $contexts,
85
        Source $source
86
    ) {
87
        $this->renderingContext = $renderingContext;
88
        $this->resolver = $renderingContext->getViewHelperResolver();
89
        $this->configuration = $renderingContext->buildParserConfiguration();
90
        $this->state = clone $state;
91
        $this->contexts = $contexts;
92
        $this->source = $source;
93
        $this->splitter = new Splitter($this->source, $this->contexts);
94
    }
95
96
    /**
97
     * Creates a dump, starting from the first line break before $position,
98
     * to the next line break from $position, counting the lines and characters
99
     * and inserting a marker pointing to the exact offending character.
100
     *
101
     * Is not very efficient - but adds bug tracing information. Should only
102
     * be called when exceptions are raised during sequencing.
103
     *
104
     * @param Position $position
105
     * @return string
106
     */
107
    public function extractSourceDumpOfLineAtPosition(Position $position): string
108
    {
109
        $lines = $this->splitter->countCharactersMatchingMask(Splitter::MASK_LINEBREAKS, 1, $position->index) + 1;
110
        $offset = $this->splitter->findBytePositionBeforeOffset(Splitter::MASK_LINEBREAKS, $position->index);
111
        $line = substr(
112
            $this->source->source,
113
            $offset,
114
            $this->splitter->findBytePositionAfterOffset(Splitter::MASK_LINEBREAKS, $position->index)
115
        );
116
        $character = $position->index - $offset - 1;
117
        $string = 'Line ' . $lines . ' character ' . $character . PHP_EOL;
118
        $string .= PHP_EOL;
119
        $string .= str_repeat(' ', max($character, 0)) . 'v' . PHP_EOL;
120
        $string .= trim($line) . PHP_EOL;
121
        $string .= str_repeat(' ', max($character, 0)) . '^' . PHP_EOL;
122
        return $string;
123
    }
124
125
    protected function createErrorAtPosition(string $message, int $code): SequencingException
126
    {
127
        $position = new Position($this->splitter->context, $this->splitter->index);
128
        $ascii = (string) $this->source->bytes[$this->splitter->index];
129
        $message .=  ' ASCII: ' . $ascii . ': ' . $this->extractSourceDumpOfLineAtPosition($position);
130
        $error = new SequencingException($message, $code);
131
        return $error;
132
    }
133
134
    protected function createUnsupportedArgumentError(string $argument, array $definitions): SequencingException
135
    {
136
        return $this->createErrorAtPosition(
137
            sprintf(
138
                'Unsupported argument "%s". Supported: ' . implode(', ', array_keys($definitions)),
139
                $argument
140
            ),
141
            1558298976
142
        );
143
    }
144
145
    protected function createIterator(\Generator $generator): \NoRewindIterator
146
    {
147
        return new \NoRewindIterator($generator);
148
    }
149
150
    public function sequence(): ParsingState
151
    {
152
        $split = $this->splitter->parse();
153
        $sequence = $this->createIterator($split);
154
155
        // Please note: repeated calls to $this->getTopmostNodeFromStack() are indeed intentional. That method may
156
        // return different nodes at different times depending on what has occured in other methods! Only the places
157
        // where $node is actually extracted is it (by design) safe to do so. DO NOT REFACTOR!
158
        // It is *also* intentional that this switch has no default case. The root context is very specific and will
159
        // only apply when the splitter is actually in root, which means there is no chance of it yielding an unexpected
160
        // character (because that implies a method called by this method already threw a SequencingException).
161
        foreach ($sequence as $symbol => $captured) {
162
            switch ($symbol) {
163
                case Splitter::BYTE_INLINE:
164
                    $node = $this->state->getNodeFromStack();
165
                    if ($this->splitter->index > 1 && $this->source->bytes[$this->splitter->index - 1] === Splitter::BYTE_BACKSLASH) {
166
                        $node->addChildNode(new TextNode(substr($captured, 0, -1) . '{'));
167
                        break;
168
                    }
169
                    if ($captured !== null) {
170
                        $node->addChildNode(new TextNode($captured));
171
                    }
172
                    $node->addChildNode($this->sequenceInlineNodes($sequence, false));
173
                    $this->splitter->switch($this->contexts->root);
174
                    break;
175
176
                case Splitter::BYTE_TAG:
177
                    if ($captured !== null) {
178
                        $this->state->getNodeFromStack()->addChildNode(new TextNode($captured));
179
                    }
180
181
                    $childNode = $this->sequenceTagNode($sequence);
182
                    $this->splitter->switch($this->contexts->root);
183
                    if ($childNode) {
184
                        $this->state->getNodeFromStack()->addChildNode($childNode);
185
                    }
186
                    break;
187
188
                case Splitter::BYTE_NULL:
189
                    if ($captured !== null) {
190
                        $this->state->getNodeFromStack()->addChildNode(new TextNode($captured));
191
                    }
192
                    break;
193
            }
194
        }
195
196
        return $this->state;
197
    }
198
199
    /**
200
     * @param \Iterator|string[]|null[] $sequence
201
     * @return NodeInterface|null
202
     */
203
    protected function sequenceTagNode(\Iterator $sequence): ?NodeInterface
204
    {
205
        $arguments = [];
206
        $definitions = null;
207
        $text = '<';
208
        $namespace = null;
209
        $method = null;
210
        $bytes = &$this->source->bytes;
211
        $node = new RootNode();
212
        $selfClosing = false;
213
        $closing = false;
214
        #$escapingEnabledBackup = $this->escapingEnabled;
215
216
        $interceptionPoint = InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER;
217
218
        $this->splitter->switch($this->contexts->tag);
219
        $sequence->next();
220
        foreach ($sequence as $symbol => $captured) {
221
            $text .= $captured;
222
            switch ($symbol) {
223
                case Splitter::BYTE_INLINE:
224
                    $contextBefore = $this->splitter->context;
225
                    $collected = $this->sequenceInlineNodes($sequence, isset($namespace) && isset($method));
226
                    $node->addChildNode(new TextNode($text));
227
                    $node->addChildNode($collected);
228
                    $text = '';
229
                    $this->splitter->switch($contextBefore);
230
                    break;
231
232
                case Splitter::BYTE_SEPARATOR_EQUALS:
233
                    $key = $captured;
234
                    if ($definitions !== null && !isset($definitions[$key])) {
235
                        throw $this->createUnsupportedArgumentError($key, $definitions);
236
                    }
237
                    break;
238
239
                case Splitter::BYTE_QUOTE_DOUBLE:
240
                case Splitter::BYTE_QUOTE_SINGLE:
241
                    $text .= chr($symbol);
242
                    if (!isset($key)) {
243
                        throw $this->createErrorAtPosition('Quoted value without a key is not allowed in tags', 1558952412);
244
                    } else {
245
                        $arguments[$key] = $this->sequenceQuotedNode($sequence, 0, isset($namespace) && isset($method))->flatten(true);
246
                        $key = null;
247
                    }
248
                    break;
249
250
                case Splitter::BYTE_TAG_CLOSE:
251
                    $method = $method ?? $captured;
252
                    $text .= '/';
253
                    $closing = true;
254
                    $selfClosing = $bytes[$this->splitter->index - 1] !== Splitter::BYTE_TAG;
255
                    $interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER;
256
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES && $captured !== null) {
257
                        // We are still capturing arguments and the last yield contained a value. Null-coalesce key
258
                        // with captured string so object accessor becomes key name (ECMA shorthand literal)
259
                        $arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
260
                        $key = null;
261
                    }
262
                    break;
263
264
                case Splitter::BYTE_SEPARATOR_COLON:
265
                    $text .= ':';
266
                    $namespace = $namespace ?? $captured;
267
                    break;
268
269
                case Splitter::BYTE_TAG_END:
270
                    $text .= '>';
271
                    $method = $method ?? $captured;
272
273
                    if (!isset($namespace) || !isset($method) || $this->splitter->context->context === Context::CONTEXT_DEAD || $this->resolver->isNamespaceIgnored($namespace)) {
274
                        return $node->addChildNode(new TextNode($text))->flatten();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $node->addChildNo...ode($text))->flatten(); (null|integer|double|stri...yntaxTree\NodeInterface) is incompatible with the return type documented by TYPO3Fluid\Fluid\Core\Pa...uencer::sequenceTagNode of type TYPO3Fluid\Fluid\Core\Pa...Tree\NodeInterface|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
275
                    }
276
277
                    try {
278
                        $expectedClass = $this->resolver->resolveViewHelperClassName($namespace, $method);
279
                    } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
280
                        throw $this->createErrorAtPosition($exception->getMessage(), $exception->getCode());
281
                    }
282
283
                    if ($closing && !$selfClosing) {
284
                        // Closing byte was more than two bytes back, meaning the tag is NOT self-closing, but is a
285
                        // closing tag for a previously opened+stacked node. Finalize the node now.
286
                        $closesNode = $this->state->popNodeFromStack();
287
                        if ($closesNode instanceof $expectedClass) {
288
                            $arguments = $closesNode->getParsedArguments();
289
                            $viewHelperNode = $closesNode;
290
                        } else {
291
                            throw $this->createErrorAtPosition(
292
                                sprintf(
293
                                    'Mismatched closing tag. Expecting: %s:%s (%s). Found: (%s).',
294
                                    $namespace,
295
                                    $method,
296
                                    $expectedClass,
297
                                    get_class($closesNode)
298
                                ),
299
                                1557700789
300
                            );
301
                        }
302
                    }
303
304
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES && $captured !== null) {
305
                        // We are still capturing arguments and the last yield contained a value. Null-coalesce key
306
                        // with captured string so object accessor becomes key name (ECMA shorthand literal)
307
                        $arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
308
                    }
309
310
                    $viewHelperNode = $viewHelperNode ?? $this->resolver->createViewHelperInstanceFromClassName($expectedClass);
311
                    #$this->escapingEnabled = $escapingEnabledBackup;
312
313
                    if (!$closing) {
314
                        $this->callInterceptor($viewHelperNode, $interceptionPoint);
315
                        $viewHelperNode->setParsedArguments($arguments);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TYPO3Fluid\Fluid\Core\Pa...yntaxTree\NodeInterface as the method setParsedArguments() does only exist in the following implementations of said interface: TYPO3Fluid\FluidExample\...elpers\CustomViewHelper, TYPO3Fluid\Fluid\Core\Vi...ractConditionViewHelper, TYPO3Fluid\Fluid\Core\Vi...tractTagBasedViewHelper, TYPO3Fluid\Fluid\Core\Vi...lper\AbstractViewHelper, TYPO3Fluid\Fluid\ViewHelpers\AliasViewHelper, TYPO3Fluid\Fluid\ViewHel...Cache\DisableViewHelper, TYPO3Fluid\Fluid\ViewHel...\Cache\StaticViewHelper, TYPO3Fluid\Fluid\ViewHel...\Cache\WarmupViewHelper, TYPO3Fluid\Fluid\ViewHelpers\CaseViewHelper, TYPO3Fluid\Fluid\ViewHelpers\CommentViewHelper, TYPO3Fluid\Fluid\ViewHelpers\CountViewHelper, TYPO3Fluid\Fluid\ViewHelpers\CycleViewHelper, TYPO3Fluid\Fluid\ViewHelpers\DebugViewHelper, TYPO3Fluid\Fluid\ViewHelpers\DefaultCaseViewHelper, TYPO3Fluid\Fluid\ViewHelpers\ElseViewHelper, TYPO3Fluid\Fluid\ViewHelpers\ForViewHelper, TYPO3Fluid\Fluid\ViewHel...\Format\CdataViewHelper, TYPO3Fluid\Fluid\ViewHel...lspecialcharsViewHelper, TYPO3Fluid\Fluid\ViewHel...Format\PrintfViewHelper, TYPO3Fluid\Fluid\ViewHelpers\Format\RawViewHelper, TYPO3Fluid\Fluid\ViewHelpers\GroupedForViewHelper, TYPO3Fluid\Fluid\ViewHelpers\IfViewHelper, TYPO3Fluid\Fluid\ViewHelpers\InlineViewHelper, TYPO3Fluid\Fluid\ViewHelpers\LayoutViewHelper, TYPO3Fluid\Fluid\ViewHelpers\OrViewHelper, TYPO3Fluid\Fluid\ViewHelpers\RenderViewHelper, TYPO3Fluid\Fluid\ViewHelpers\SectionViewHelper, TYPO3Fluid\Fluid\ViewHelpers\SpacelessViewHelper, TYPO3Fluid\Fluid\ViewHelpers\SwitchViewHelper, TYPO3Fluid\Fluid\ViewHelpers\ThenViewHelper, TYPO3Fluid\Fluid\ViewHelpers\VariableViewHelper.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
316
                        $this->state->pushNodeToStack($viewHelperNode);
317
                        return null;
318
                    }
319
320
                    $viewHelperNode = $viewHelperNode->postParse($arguments, $this->state, $this->renderingContext);
321
322
                    return $viewHelperNode;
323
324
                case Splitter::BYTE_WHITESPACE_TAB:
325
                case Splitter::BYTE_WHITESPACE_RETURN:
326
                case Splitter::BYTE_WHITESPACE_EOL:
327
                case Splitter::BYTE_WHITESPACE_SPACE:
328
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) {
329
                        if ($captured !== null) {
330
                            $arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
331
                            $key = null;
332
                        }
333
                    } else {
334
                        $text .= chr($symbol);
335
                        if (isset($namespace)) {
336
                            $method = $captured;
337
338
                            $this->escapingEnabled = false;
339
                            $viewHelperNode = $this->resolver->createViewHelperInstance($namespace, $method);
340
                            $definitions = $viewHelperNode->prepareArguments();
341
342
                            // A whitespace character, in tag context, means the beginning of an array sequence (which may
343
                            // or may not contain any items; the next symbol may be a tag end or tag close). We sequence the
344
                            // arguments array and create a ViewHelper node.
345
                            $this->splitter->switch($this->contexts->attributes);
346
                            break;
347
                        }
348
349
                        // A whitespace before a colon means the tag is not a namespaced tag. We will ignore everything
350
                        // inside this tag, except for inline syntax, until the tag ends. For this we use a special,
351
                        // limited variant of the root context where instead of scanning for "<" we scan for ">".
352
                        // We continue in this same loop because it still matches the potential symbols being yielded.
353
                        // Most importantly: this new reduced context will NOT match a colon which is the trigger symbol
354
                        // for a ViewHelper tag.
355
                        $this->splitter->switch($this->contexts->dead);
356
                    }
357
                    break;
358
            }
359
        }
360
361
        // This case on the surface of it, belongs as "default" case in the switch above. However, the only case that
362
        // would *actually* produce this error, is if the splitter reaches EOF (null byte) symbol before the tag was
363
        // closed. Literally every other possible error type will be thrown as more specific exceptions (e.g. invalid
364
        // argument, missing key, wrong quotes, bad inline and *everything* else with the exception of EOF). Even a
365
        // stray null byte would not be caught here as null byte is not part of the symbol collection for "tag" context.
366
        throw $this->createErrorAtPosition('Unexpected token in tag sequencing', 1557700786);
367
    }
368
369
    /**
370
     * @param \Iterator|string[]|null[] $sequence
371
     * @param bool $allowArray
372
     * @return NodeInterface
373
     */
374
    protected function sequenceInlineNodes(\Iterator $sequence, bool $allowArray = true): NodeInterface
375
    {
376
        $text = '{';
377
        $node = null;
378
        $key = null;
379
        $namespace = null;
380
        $method = null;
381
        $potentialAccessor = null;
382
        $callDetected = false;
383
        $hasPass = false;
384
        $hasColon = null;
385
        $hasWhitespace = false;
386
        $isArray = false;
387
        $array = [];
388
        $arguments = [];
389
        $ignoredEndingBraces = 0;
390
        $countedEscapes = 0;
391
392
        $this->splitter->switch($this->contexts->inline);
393
        $sequence->next();
394
        foreach ($sequence as $symbol => $captured) {
395
            $text .= $captured;
396
            switch ($symbol) {
397
                case Splitter::BYTE_BACKSLASH:
398
                    // Increase the number of counted escapes (is passed to sequenceNode() in the "QUOTE" cases and reset
399
                    // after the quoted string is extracted).
400
                    ++$countedEscapes;
401
                    break;
402
403
                case Splitter::BYTE_ARRAY_START:
404
405
                    $text .= chr($symbol);
406
                    $isArray = $allowArray;
407
408
                    #ArrayStart:
409
                    // Sequence the node. Pass the "use numeric keys?" boolean based on the current byte. Only array
410
                    // start creates numeric keys. Inline start with keyless values creates ECMA style {foo:foo, bar:bar}
411
                    // from {foo, bar}.
412
                    $array[$key ?? $captured ?? 0] = $node = $this->sequenceArrayNode($sequence, null, $symbol === Splitter::BYTE_ARRAY_START);
413
                    $this->splitter->switch($this->contexts->inline);
414
                    unset($key);
415
                    break;
416
417
                case Splitter::BYTE_INLINE:
418
                    // Encountering this case can mean different things: sub-syntax like {foo.{index}} or array, depending
419
                    // on presence of either a colon or comma before the inline. In protected mode it is simply added.
420
                    $text .= '{';
421
                    if (!$hasWhitespace && $text !== '{{') {
422
                        // Most likely, a nested object accessor syntax e.g. {foo.{bar}} - enter protected context since
423
                        // these accessors do not allow anything other than additional nested accessors.
424
                        $this->splitter->switch($this->contexts->accessor);
425
                        ++$ignoredEndingBraces;
426
                    } elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED) {
427
                        // Ignore one ending additional curly brace. Subtracted in the BYTE_INLINE_END case below.
428
                        // The expression in this case looks like {{inline}.....} and we capture the curlies.
429
                        $potentialAccessor .= $captured;
430
                        ++$ignoredEndingBraces;
431
                    } elseif ($allowArray || $isArray) {
432
                        $isArray = true;
433
                        $captured = $key ?? $captured ?? $potentialAccessor;
434
                        // This is a sub-syntax following a colon - meaning it is an array.
435
                        if ($captured !== null) {
436
                            #goto ArrayStart;
437
                            $array[$key ?? $captured ?? 0] = $node = $this->sequenceArrayNode($sequence, null, $symbol === Splitter::BYTE_ARRAY_START);
438
                            $this->splitter->switch($this->contexts->inline);
439
                        }
440
                    } else {
441
                        $childNodeToAdd = $this->sequenceInlineNodes($sequence, $allowArray);
442
                        $node = isset($node) ? $node->addChildNode($childNodeToAdd) : (new RootNode())->addChildNode($childNodeToAdd);
443
                    }
444
                    break;
445
446
                case Splitter::BYTE_MINUS:
447
                    $text .= '-';
448
                    break;
449
450
                // Backtick may be encountered in two different contexts: normal inline context, in which case it has
451
                // the same meaning as any quote and causes sequencing of a quoted string. Or protected context, in
452
                // which case it also sequences a quoted node but appends the result instead of assigning to array.
453
                // Note that backticks do not support escapes (they are a new feature that does not require escaping).
454
                case Splitter::BYTE_BACKTICK:
455
                    if ($this->splitter->context->context === Context::CONTEXT_PROTECTED) {
456
                        $node->addChildNode(new TextNode($text));
457
                        $node->addChildNode($this->sequenceQuotedNode($sequence)->flatten());
458
                        $text = '';
459
                        break;
460
                    }
461
                // Fallthrough is intentional: if not in protected context, consider the backtick a normal quote.
462
463
                // Case not normally countered in straight up "inline" context, but when encountered, means we have
464
                // explicitly found a quoted array key - and we extract it.
465
                case Splitter::BYTE_QUOTE_SINGLE:
466
                case Splitter::BYTE_QUOTE_DOUBLE:
467
                    if (!$allowArray) {
468
                        $text .= chr($symbol);
469
                        break;
470
                    }
471
                    if (isset($key)) {
472
                        $array[$key] = $this->sequenceQuotedNode($sequence, $countedEscapes)->flatten(true);
473
                        $key = null;
474
                    } else {
475
                        $key = $this->sequenceQuotedNode($sequence, $countedEscapes)->flatten(true);
476
                    }
477
                    $countedEscapes = 0;
478
                    $isArray = $allowArray;
479
                    break;
480
481
                case Splitter::BYTE_SEPARATOR_COMMA:
482
                    if (!$allowArray) {
483
                        $text .= ',';
484
                        break;
485
                    }
486
                    if (isset($captured)) {
487
                        $array[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
488
                    }
489
                    $key = null;
490
                    $isArray = $allowArray;
491
                    break;
492
493
                case Splitter::BYTE_SEPARATOR_EQUALS:
494
                    $text .= '=';
495
                    if (!$allowArray) {
496
                        $node = new RootNode();
497
                        $this->splitter->switch($this->contexts->protected);
498
                        break;
499
                    }
500
                    $key = $captured;
501
                    $isArray = $allowArray;
502
                    break;
503
504
                case Splitter::BYTE_SEPARATOR_COLON:
505
                    $text .= ':';
506
                    $hasColon = true;
507
                    $namespace = $captured;
508
                    $key = $key ?? $captured;
509
                    $isArray = $isArray || ($allowArray && is_numeric($key));
510
                    break;
511
512
                case Splitter::BYTE_WHITESPACE_SPACE:
513
                case Splitter::BYTE_WHITESPACE_EOL:
514
                case Splitter::BYTE_WHITESPACE_RETURN:
515
                case Splitter::BYTE_WHITESPACE_TAB:
516
                    // If we already collected some whitespace we must enter protected context.
517
                    $text .= $this->source->source[$this->splitter->index - 1];
518
                    if ($hasWhitespace && !$hasPass && !$allowArray) {
519
                        // Protection mode: this very limited context does not allow tags or inline syntax, and will
520
                        // protect things like CSS and JS - and will only enter a more reactive context if encountering
521
                        // the backtick character, meaning a quoted string will be sequenced. This backtick-quoted
522
                        // string can then contain inline syntax like variable accessors.
523
                        $node = $node ?? new RootNode();
524
                        $this->splitter->switch($this->contexts->protected);
525
                        break;
526
                    }
527
                    $key = $key ?? $captured;
528
                    $hasWhitespace = true;
529
                    $isArray = $allowArray && ($hasColon ?? $isArray ?? is_numeric($captured));
530
                    $potentialAccessor = ($potentialAccessor ?? $captured);
531
                    break;
532
533
                case Splitter::BYTE_TAG_END:
534
                case Splitter::BYTE_PIPE:
535
                    // If there is an accessor on the left side of the pipe and $node is not defined, we create $node
536
                    // as an object accessor. If $node already exists we do nothing (and expect the VH trigger, the
537
                    // parenthesis start case below, to add $node as childnode and create a new $node).
538
                    $hasPass = true;
539
                    $isArray = $allowArray;
540
                    $callDetected = false;
541
                    $potentialAccessor = $potentialAccessor ?? $captured;
542
                    $text .=  $this->source->source[$this->splitter->index - 1];
543
                    if (isset($potentialAccessor)) {
544
                        $childNodeToAdd = new ObjectAccessorNode($potentialAccessor);
545
                        $node = isset($node) ? $node->addChildNode($childNodeToAdd) : $childNodeToAdd; //$node ?? (is_numeric($potentialAccessor) ? $potentialAccessor + 0 : new ObjectAccessorNode($potentialAccessor));
546
                    }
547
                    //!isset($potentialAccessor) ?: ($node = ($node ?? $this->createObjectAccessorNodeOrRawValue($potentialAccessor)));
548
                    unset($namespace, $method, $potentialAccessor, $key);
549
                    break;
550
551
                case Splitter::BYTE_PARENTHESIS_START:
552
                    $isArray = false;
553
                    // Special case: if a parenthesis start was preceded by whitespace but had no pass operator we are
554
                    // not dealing with a ViewHelper call and will continue the sequencing, grabbing the parenthesis as
555
                    // part of the expression.
556
                    $text .= '(';
557
                    if (!$hasColon || ($hasWhitespace && !$hasPass)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hasColon of type null|boolean is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
558
                        $this->splitter->switch($this->contexts->protected);
559
                        unset($namespace, $method);
560
                        break;
561
                    }
562
563
                    $callDetected = true;
564
                    $method = $captured;
565
                    $childNodeToAdd = $node;
566
                    try {
567
                        $node = $this->resolver->createViewHelperInstance($namespace, $method);
568
                        $definitions = $node->prepareArguments();
569
                    } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
570
                        throw $this->createErrorAtPosition($exception->getMessage(), $exception->getCode());
571
                    }
572
                    $this->splitter->switch($this->contexts->array);
573
                    $arguments = $this->sequenceArrayNode($sequence, $definitions)->getInternalArray();
574
                    $this->splitter->switch($this->contexts->inline);
575
                    if ($childNodeToAdd) {
576
                        $escapingEnabledBackup = $this->escapingEnabled;
577
                        $this->escapingEnabled = (bool)$node->isChildrenEscapingEnabled();
578
                        if ($childNodeToAdd instanceof ObjectAccessorNode) {
579
                            $this->callInterceptor($childNodeToAdd, InterceptorInterface::INTERCEPT_OBJECTACCESSOR);
580
                        }
581
                        $this->escapingEnabled = $escapingEnabledBackup;
582
                        $node->addChildNode($childNodeToAdd);
583
                    }
584
                    $text .= ')';
585
                    unset($potentialAccessor);
586
                    break;
587
588
                case Splitter::BYTE_INLINE_END:
589
                    $text .= '}';
590
                    if (--$ignoredEndingBraces >= 0) {
591
                        break;
592
                    }
593
                    $isArray = $allowArray && ($isArray ?: ($hasColon && !$hasPass && !$callDetected));
594
                    $potentialAccessor = $potentialAccessor ?? $captured;
595
596
                    // Decision: if we did not detect a ViewHelper we match the *entire* expression, from the cached
597
                    // starting index, to see if it matches a known type of expression. If it does, we must return the
598
                    // appropriate type of ExpressionNode.
599
                    if ($isArray) {
600
                        if ($captured !== null) {
601
                            $array[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
602
                        }
603
                        return new ArrayNode($array);
604
                    } elseif ($callDetected) {
605
                        // The first-priority check is for a ViewHelper used right before the inline expression ends,
606
                        // in which case there is no further syntax to come.
607
                        $node = $node->postParse($arguments, $this->state, $this->renderingContext);
608
                        $interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER;
609
                    } elseif ($this->splitter->context->context === Context::CONTEXT_ACCESSOR) {
610
                        // If we are currently in "accessor" context we can now add the accessor by stripping the collected text.
611
                        $node = new ObjectAccessorNode(substr($text, 1, -1));
612
                        $interceptionPoint = InterceptorInterface::INTERCEPT_OBJECTACCESSOR;
613
                    } elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED || ($hasWhitespace && !$callDetected && !$hasPass)) {
614
                        // In order to qualify for potentially being an expression, the entire inline node must contain
615
                        // whitespace, must not contain parenthesis, must not contain a colon and must not contain an
616
                        // inline pass operand. This significantly limits the number of times this (expensive) routine
617
                        // has to be executed.
618
                        $interceptionPoint = InterceptorInterface::INTERCEPT_TEXT;
619
                        $childNodeToAdd = new TextNode($text);
620
                        foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) {
621
                            $matchedVariables = [];
622
                            // TODO: rewrite expression nodes to receive a sub-Splitter that lets the expression node
623
                            // consume a symbol+capture sequence and either match or ignore it; then use the already
624
                            // consumed (possibly halted mid-way through iterator!) sequence to achieve desired behavior.
625
                            preg_match_all($expressionNodeTypeClassName::$detectionExpression, $text, $matchedVariables, PREG_SET_ORDER);
626
                            foreach ($matchedVariables as $matchedVariableSet) {
0 ignored issues
show
Bug introduced by
The expression $matchedVariables of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
627
                                try {
628
                                    $childNodeToAdd = new $expressionNodeTypeClassName($matchedVariableSet[0], $matchedVariableSet, $this->state);
629
                                    $interceptionPoint = InterceptorInterface::INTERCEPT_EXPRESSION;
630
                                } catch (ExpressionException $error) {
631
                                    $childNodeToAdd = new TextNode($this->renderingContext->getErrorHandler()->handleExpressionError($error));
632
                                }
633
                                break;
634
                            }
635
                        }
636
                        $node = isset($node) ? $node->addChildNode($childNodeToAdd) : $childNodeToAdd;
637
                    } elseif (!$hasPass && !$callDetected) {
638
                        // Third priority check is if there was no pass syntax and no ViewHelper, in which case we
639
                        // create a standard ObjectAccessorNode; alternatively, if nothing was captured (expression
640
                        // was empty, e.g. {} was used) we create a TextNode with the captured text to output "{}".
641
                        if (isset($potentialAccessor)) {
642
                            // If the accessor is set we can trust it is not a numeric value, since this will have
643
                            // set $isArray to TRUE if nothing else already did so.
644
                            $node = is_numeric($potentialAccessor) ? $potentialAccessor + 0 : new ObjectAccessorNode($potentialAccessor);
645
                            $interceptionPoint = InterceptorInterface::INTERCEPT_OBJECTACCESSOR;
646
                        } else {
647
                            $node = new TextNode($text);
648
                            $interceptionPoint = InterceptorInterface::INTERCEPT_TEXT;
649
                        }
650
                    } elseif ($hasPass && $this->resolver->isAliasRegistered((string)$potentialAccessor)) {
651
                        // Fourth priority check is for a pass to a ViewHelper alias, e.g. "{value | raw}" in which case
652
                        // we look for the alias used and create a ViewHelperNode with no arguments.
653
                        $childNodeToAdd = $node;
654
                        $node = $this->resolver->createViewHelperInstance(null, $potentialAccessor);
655
                        $node->addChildNode($childNodeToAdd);
656
                        $node = $node->postParse($arguments, $this->state, $this->renderingContext);
657
                        $interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER;
658
                    } else {
659
                        # TODO: should this be an error case, or should it result in a TextNode?
660
                        throw $this->createErrorAtPosition(
661
                            'Invalid inline syntax - not accessor, not expression, not array, not ViewHelper, but ' .
662
                            'contains the tokens used by these in a sequence that is not valid Fluid syntax. You can ' .
663
                            'most likely avoid this by adding whitespace inside the curly braces before the first ' .
664
                            'Fluid-like symbol in the expression. Symbols recognized as Fluid are: "' .
665
                            addslashes(implode('","', array_map('chr', $this->contexts->inline->bytes))) . '"',
666
                            1558782228
667
                        );
668
                    }
669
670
                    $escapingEnabledBackup = $this->escapingEnabled;
671
                    $this->escapingEnabled = (bool)((isset($viewHelper) && $node->isOutputEscapingEnabled()) || $escapingEnabledBackup);
0 ignored issues
show
Bug introduced by
The variable $viewHelper seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
672
                    $this->callInterceptor($node, $interceptionPoint);
673
                    $this->escapingEnabled = $escapingEnabledBackup;
674
                    return $node;
675
            }
676
        }
677
678
        // See note in sequenceTagNode() end of method body. TL;DR: this is intentionally here instead of as "default"
679
        // case in the switch above for a very specific reason: the case is only encountered if seeing EOF before the
680
        // inline expression was closed.
681
        throw $this->createErrorAtPosition('Unterminated inline syntax', 1557838506);
682
    }
683
684
    /**
685
     * @param \Iterator|string[]|null[] $sequence
686
     * @param ArgumentDefinition[] $definitions
687
     * @param bool $numeric
688
     * @return ArrayNode
689
     */
690
    protected function sequenceArrayNode(\Iterator $sequence, array $definitions = null, bool $numeric = false): ArrayNode
691
    {
692
        $array = [];
693
694
        $keyOrValue = null;
695
        $key = null;
696
        $escapingEnabledBackup = $this->escapingEnabled;
697
        $this->escapingEnabled = false;
698
        $itemCount = -1;
699
        $countedEscapes = 0;
700
701
        $sequence->next();
702
        foreach ($sequence as $symbol => $captured) {
703
            switch ($symbol) {
704
                case Splitter::BYTE_SEPARATOR_COLON:
705
                case Splitter::BYTE_SEPARATOR_EQUALS:
706
                    // Colon or equals has same meaning (which allows tag syntax as argument syntax). Encountering this
707
                    // byte always means the preceding byte was a key. However, if nothing was captured before this,
708
                    // it means colon or equals was used without a key which is a syntax error.
709
                    $key = $key ?? $captured ?? (isset($keyOrValue) ? $keyOrValue->flatten(true) : null);
710
                    if (!isset($key)) {
711
                        throw $this->createErrorAtPosition('Unexpected colon or equals sign, no preceding key', 1559250839);
712
                    }
713
                    if ($definitions !== null && !$numeric && !isset($definitions[$key])) {
714
                        throw $this->createUnsupportedArgumentError((string)$key, $definitions);
715
                    }
716
                    break;
717
718
                case Splitter::BYTE_ARRAY_START:
719
                case Splitter::BYTE_INLINE:
720
                    // Minimal safeguards to improve error feedback. Theoretically such "garbage" could simply be ignored
721
                    // without causing problems to the parser, but it is probably best to report it as it could indicate
722
                    // the user expected X value but gets Y and doesn't notice why.
723
                    if ($captured !== null) {
724
                        throw $this->createErrorAtPosition('Unexpected content before array/inline start in associative array, ASCII: ' . ord($captured), 1559131849);
725
                    }
726
                    if (!isset($key) && !$numeric) {
727
                        throw $this->createErrorAtPosition('Unexpected array/inline start in associative array without preceding key', 1559131848);
728
                    }
729
730
                    // Encountering a curly brace or square bracket start byte will both cause a sub-array to be sequenced,
731
                    // the difference being that only the square bracket will cause third parameter ($numeric) passed to
732
                    // sequenceArrayNode() to be true, which in turn causes key-less items to be added with numeric indexes.
733
                    $key = $key ?? ++$itemCount;
734
                    $array[$key] = $this->sequenceArrayNode($sequence, null, $symbol === Splitter::BYTE_ARRAY_START);
735
                    $keyOrValue = null;
736
                    $key = null;
737
                    break;
738
739
                case Splitter::BYTE_QUOTE_SINGLE:
740
                case Splitter::BYTE_QUOTE_DOUBLE:
741
                    // Safeguard: if anything is captured before a quote this indicates garbage leading content. As with
742
                    // the garbage safeguards above, this one could theoretically be ignored in favor of silently making
743
                    // the odd syntax "just work".
744
                    if ($captured !== null) {
745
                        throw $this->createErrorAtPosition('Unexpected content before quote start in associative array, ASCII: ' . ord($captured), 1559145560);
746
                    }
747
748
                    // Quotes will always cause sequencing of the quoted string, but differs in behavior based on whether
749
                    // or not the $key is set. If $key is set, we know for sure we can assign a value. If it is not set
750
                    // we instead leave $keyOrValue defined so this will be processed by one of the next iterations.
751
                    $keyOrValue = $this->sequenceQuotedNode($sequence, $countedEscapes);
752
                    if (isset($key)) {
753
                        $array[$key] = $keyOrValue->flatten(true);
754
                        $keyOrValue = null;
755
                        $key = null;
756
                        $countedEscapes = 0;
757
                    }
758
                    break;
759
760
                case Splitter::BYTE_SEPARATOR_COMMA:
761
                    // Comma separator: if we've collected a key or value, use it. Otherwise, use captured string.
762
                    // If neither key nor value nor captured string exists, ignore the comma (likely a tailing comma).
763
                    if (isset($keyOrValue)) {
764
                        // Key or value came as quoted string and exists in $keyOrValue
765
                        $potentialValue = $keyOrValue->flatten(true);
766
                        $key = $numeric ? ++$itemCount : $potentialValue;
767
                        $array[$key] = $numeric ? $potentialValue : (is_numeric($key) ? $key + 0 : new ObjectAccessorNode($key));
0 ignored issues
show
Bug introduced by
It seems like $key defined by $numeric ? ++$itemCount : $potentialValue on line 766 can also be of type object<TYPO3Fluid\Fluid\...ntaxTree\NodeInterface>; however, TYPO3Fluid\Fluid\Core\Pa...ssorNode::__construct() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
768
                    } elseif (isset($captured)) {
769
                        $key = $key ?? ($numeric ? ++$itemCount : $captured);
770
                        if (!$numeric && isset($definitions) && !isset($definitions[$key])) {
771
                            throw $this->createUnsupportedArgumentError((string)$key, $definitions);
772
                        }
773
                        $array[$key] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
774
                    }
775
                    $keyOrValue = null;
776
                    $key = null;
777
                    break;
778
779
                case Splitter::BYTE_WHITESPACE_TAB:
780
                case Splitter::BYTE_WHITESPACE_RETURN:
781
                case Splitter::BYTE_WHITESPACE_EOL:
782
                case Splitter::BYTE_WHITESPACE_SPACE:
783
                    // Any whitespace attempts to set the key, if not already set. The captured string may be null as
784
                    // well, leaving the $key variable still null and able to be coalesced.
785
                    $key = $key ?? $captured;
786
                    break;
787
788
                case Splitter::BYTE_BACKSLASH:
789
                    // Escapes are simply counted and passed to the sequenceQuotedNode() method, causing that method
790
                    // to ignore exactly this number of backslashes before a matching quote is seen as closing quote.
791
                    ++$countedEscapes;
792
                    break;
793
794
                case Splitter::BYTE_INLINE_END:
795
                case Splitter::BYTE_ARRAY_END:
796
                case Splitter::BYTE_PARENTHESIS_END:
797
                    // Array end indication. Check if anything was collected previously or was captured currently,
798
                    // assign that to the array and return an ArrayNode with the full array inside.
799
                    $captured = $captured ?? (isset($keyOrValue) ? $keyOrValue->flatten(true) : null);
800
                    $key = $key ?? ($numeric ? ++$itemCount : $captured);
801
                    if (isset($captured, $key)) {
802
                        if (is_numeric($captured)) {
803
                            $array[$key] = $captured + 0;
804
                        } elseif (isset($keyOrValue)) {
805
                            $array[$key] = $keyOrValue->flatten();
806
                        } else {
807
                            $array[$key] = new ObjectAccessorNode($captured ?? $key);
808
                        }
809
                    }
810
                    if (!$numeric && isset($key, $definitions) && !isset($definitions[$key])) {
811
                        throw $this->createUnsupportedArgumentError((string)$key, $definitions);
812
                    }
813
                    $this->escapingEnabled = $escapingEnabledBackup;
814
                    return new ArrayNode($array);
815
            }
816
        }
817
818
        throw $this->createErrorAtPosition(
819
            'Unterminated array',
820
            1557748574
821
        );
822
    }
823
824
    /**
825
     * Sequence a quoted value
826
     *
827
     * The return can be either of:
828
     *
829
     * 1. A string value if source was for example "string"
830
     * 2. An integer if source was for example "1"
831
     * 3. A float if source was for example "1.25"
832
     * 4. A RootNode instance with multiple child nodes if source was for example "string {var}"
833
     *
834
     * The idea is to return the raw value if there is no reason for it to
835
     * be a node as such - which is only necessary if the quoted expression
836
     * contains other (dynamic) values like an inline syntax.
837
     *
838
     * @param \Iterator|string[]|null[] $sequence
839
     * @param int $leadingEscapes A backwards compatibility measure: when passed, this number of escapes must precede a closing quote for it to trigger node closing.
840
     * @param bool $allowArray
841
     * @return RootNode
842
     */
843
    protected function sequenceQuotedNode(\Iterator $sequence, int $leadingEscapes = 0, $allowArray = true): RootNode
844
    {
845
        $startingByte = $this->source->bytes[$this->splitter->index];
846
        $contextToRestore = $this->splitter->switch($this->contexts->quoted);
847
        $node = new RootNode();
848
        $sequence->next();
849
        $countedEscapes = 0;
850
851
        foreach ($sequence as $symbol => $captured) {
852
            switch ($symbol) {
853
854
                case Splitter::BYTE_ARRAY_START:
855
                    $countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored)
856
                    if ($captured === null) {
857
                        // Array start "[" only triggers array sequencing if it is the very first byte in the quoted
858
                        // string - otherwise, it is added as part of the text.
859
                        $this->splitter->switch($this->contexts->array);
860
                        $node->addChildNode($this->sequenceArrayNode($sequence, null, $allowArray));
861
                        $this->splitter->switch($this->contexts->quoted);
862
                    } else {
863
                        $node->addChildNode(new TextNode($captured . '['));
864
                    }
865
                    break;
866
867
                case Splitter::BYTE_INLINE:
868
                    $countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored)
869
                    // The quoted string contains a sub-expression. We extract the captured content so far and if it
870
                    // is not an empty string, add it as a child of the RootNode we're building, then we add the inline
871
                    // expression as next sibling and continue the loop.
872
                    if ($captured !== null) {
873
                        $childNode = new TextNode($captured);
874
                        $this->callInterceptor($childNode, InterceptorInterface::INTERCEPT_TEXT);
875
                        $node->addChildNode($childNode);
876
                    }
877
878
                    $node->addChildNode($this->sequenceInlineNodes($sequence));
879
                    $this->splitter->switch($this->contexts->quoted);
880
                    break;
881
882
                case Splitter::BYTE_BACKSLASH:
883
                    ++$countedEscapes;
884
                    if ($captured !== null) {
885
                        $node->addChildNode(new TextNode($captured));
886
                    }
887
                    break;
888
889
                // Note: although "case $startingByte:" could have been used here, it would not compile the switch
890
                // as a hash map and thus would not perform as well overall - when called frequently as it will be.
891
                // Backtick will only be encountered if the context is "protected" (insensitive inline sequencing)
892
                case Splitter::BYTE_QUOTE_SINGLE:
893
                case Splitter::BYTE_QUOTE_DOUBLE:
894
                case Splitter::BYTE_BACKTICK:
895
                    if ($symbol !== $startingByte || $countedEscapes !== $leadingEscapes) {
896
                        $node->addChildNode(new TextNode($captured . chr($symbol)));
897
                        $countedEscapes = 0; // If number of escapes do not match expected, reset the counter
898
                        break;
899
                    }
900
                    if ($captured !== null) {
901
                        $node->addChildNode(new TextNode($captured));
902
                    }
903
                    $this->splitter->switch($contextToRestore);
904
                    return $node;
905
            }
906
        }
907
908
        throw $this->createErrorAtPosition('Unterminated expression inside quotes', 1557700793);
909
    }
910
911
    /**
912
     * Call all interceptors registered for a given interception point.
913
     *
914
     * @param NodeInterface $node The syntax tree node which can be modified by the interceptors.
915
     * @param integer $interceptionPoint the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants.
916
     * @return void
917
     */
918
    protected function callInterceptor(NodeInterface &$node, $interceptionPoint)
919
    {
920
        if ($this->escapingEnabled) {
921
            /** @var $interceptor InterceptorInterface */
922
            foreach ($this->configuration->getEscapingInterceptors($interceptionPoint) as $interceptor) {
923
                $node = $interceptor->process($node, $interceptionPoint, $this->state);
924
            }
925
        }
926
927
        /** @var $interceptor InterceptorInterface */
928
        foreach ($this->configuration->getInterceptors($interceptionPoint) as $interceptor) {
929
            $node = $interceptor->process($node, $interceptionPoint, $this->state);
930
        }
931
    }
932
}