Completed
Pull Request — master (#457)
by Claus
07:49
created

Sequencer::callInterceptor()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 5
nop 2
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
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\ViewHelperInterface;
15
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperResolver;
16
17
/**
18
 * Sequencer for Fluid syntax
19
 *
20
 * Uses a NoRewindIterator around a sequence of byte values to
21
 * iterate over each syntax-relevant character and determine
22
 * which nodes to create.
23
 *
24
 * Passes the outer iterator between functions that perform the
25
 * iterations. Since the iterator is a NoRewindIterator it will
26
 * not be reset before the start of each loop - meaning that
27
 * when it is passed to a new function, that function continues
28
 * from the last touched index in the byte sequence.
29
 *
30
 * The circumstances around "break or return" in the switches is
31
 * very, very important to understand in context of how iterators
32
 * work. Returning does not advance the iterator like breaking
33
 * would and this causes a different position in the byte sequence
34
 * to be experienced in the method that uses the return value (it
35
 * sees the index of the symbol which terminated the expression,
36
 * not the next symbol after that).
37
 */
38
class Sequencer
39
{
40
    /**
41
     * @var RenderingContextInterface
42
     */
43
    protected $renderingContext;
44
45
    /**
46
     * @var ParsingState
47
     */
48
    protected $state;
49
50
    /**
51
     * @var Contexts
52
     */
53
    protected $contexts;
54
55
    /**
56
     * @var Source
57
     */
58
    protected $source;
59
60
    /**
61
     * @var Splitter
62
     */
63
    protected $splitter;
64
65
    /**
66
     * @var Configuration
67
     */
68
    protected $configuration;
69
70
    /**
71
     * @var ViewHelperResolver
72
     */
73
    protected $resolver;
74
75
    /**
76
     * Whether or not the escaping interceptors are active
77
     *
78
     * @var boolean
79
     */
80
    protected $escapingEnabled = true;
81
82
    public function __construct(
83
        RenderingContextInterface $renderingContext,
84
        ParsingState $state,
85
        Contexts $contexts,
86
        Source $source
87
    ) {
88
        $this->renderingContext = $renderingContext;
89
        $this->resolver = $renderingContext->getViewHelperResolver();
90
        $this->configuration = $renderingContext->buildParserConfiguration();
91
        $this->state = clone $state;
92
        $this->contexts = $contexts;
93
        $this->source = $source;
94
        $this->splitter = new Splitter($this->source, $this->contexts);
95
    }
96
97
    public function sequence(): ParsingState
98
    {
99
        #$sequence = $this->splitter->parse();
100
101
        // Please note: repeated calls to $this->state->getTopmostNodeFromStack() are indeed intentional. That method may
102
        // return different nodes at different times depending on what has occurred in other methods! Only the places
103
        // where $node is actually extracted is it (by design) safe to do so. DO NOT REFACTOR!
104
        // It is *also* intentional that this switch has no default case. The root context is very specific and will
105
        // only apply when the splitter is actually in root, which means there is no chance of it yielding an unexpected
106
        // character (because that implies a method called by this method already threw a SequencingException).
107
        foreach ($this->splitter->sequence as $symbol => $captured) {
108
            switch ($symbol) {
109
                case Splitter::BYTE_INLINE:
110
                    $node = $this->state->getNodeFromStack();
111
                    if ($this->splitter->index > 1 && $this->source->bytes[$this->splitter->index - 1] === Splitter::BYTE_BACKSLASH) {
112
                        $node->addChildNode(new TextNode(substr($captured, 0, -1) . '{'));
113
                        break;
114
                    }
115
                    if ($captured !== null) {
116
                        $node->addChildNode(new TextNode($captured));
117
                    }
118
                    $node->addChildNode($this->sequenceInlineNodes(false));
119
                    $this->splitter->switch($this->contexts->root);
120
                    break;
121
122
                case Splitter::BYTE_TAG:
123
                    if ($captured !== null) {
124
                        $this->state->getNodeFromStack()->addChildNode(new TextNode($captured));
125
                    }
126
127
                    $childNode = $this->sequenceTagNode();
128
                    $this->splitter->switch($this->contexts->root);
129
                    if ($childNode) {
130
                        $this->state->getNodeFromStack()->addChildNode($childNode);
131
                    }
132
                    break;
133
134
                case Splitter::BYTE_NULL:
135
                    if ($captured !== null) {
136
                        $this->state->getNodeFromStack()->addChildNode(new TextNode($captured));
137
                    }
138
                    break;
139
            }
140
        }
141
142
        return $this->state;
143
    }
144
145
    /**
146
     * @return NodeInterface|null
147
     */
148
    protected function sequenceTagNode(): ?NodeInterface
149
    {
150
        $arguments = [];
151
        $definitions = null;
152
        $text = '<';
153
        $namespace = null;
154
        $method = null;
155
        $bytes = &$this->source->bytes;
156
        $node = new RootNode();
157
        $selfClosing = false;
158
        $closing = false;
159
        #$escapingEnabledBackup = $this->escapingEnabled;
160
161
        $interceptionPoint = InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER;
162
163
        $this->splitter->switch($this->contexts->tag);
164
        $this->splitter->sequence->next();
165
        foreach ($this->splitter->sequence as $symbol => $captured) {
166
            $text .= $captured;
167
            switch ($symbol) {
168
                case Splitter::BYTE_INLINE:
169
                    $contextBefore = $this->splitter->context;
170
                    $collected = $this->sequenceInlineNodes(isset($namespace) && isset($method));
171
                    $node->addChildNode(new TextNode($text));
172
                    $node->addChildNode($collected);
173
                    $text = '';
174
                    $this->splitter->switch($contextBefore);
175
                    break;
176
177
                case Splitter::BYTE_SEPARATOR_EQUALS:
178
                    $key = $captured;
179
                    if ($key === null) {
180
                        throw $this->splitter->createErrorAtPosition('Unexpected equals sign without preceding attribute/key name', 1561039838);
181
                    } elseif ($definitions !== null && !isset($definitions[$key])) {
182
                        throw $this->splitter->createUnsupportedArgumentError($key, $definitions);
183
                    }
184
                    break;
185
186
                case Splitter::BYTE_QUOTE_DOUBLE:
187
                case Splitter::BYTE_QUOTE_SINGLE:
188
                    $text .= chr($symbol);
189
                    if (!isset($key)) {
190
                        throw $this->splitter->createErrorAtPosition('Quoted value without a key is not allowed in tags', 1558952412);
191
                    } else {
192
                        $arguments[$key] = $this->sequenceQuotedNode(0, isset($namespace) && isset($method))->flatten(true);
193
                        $key = null;
194
                    }
195
                    break;
196
197
                case Splitter::BYTE_TAG_CLOSE:
198
                    $method = $method ?? $captured;
199
                    $text .= '/';
200
                    $closing = true;
201
                    $selfClosing = $bytes[$this->splitter->index - 1] !== Splitter::BYTE_TAG;
202
                    $interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER;
203
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES && $captured !== null) {
204
                        // We are still capturing arguments and the last yield contained a value. Null-coalesce key
205
                        // with captured string so object accessor becomes key name (ECMA shorthand literal)
206
                        $arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
207
                        $key = null;
208
                    }
209
                    break;
210
211
                case Splitter::BYTE_SEPARATOR_COLON:
212
                    $text .= ':';
213
                    $namespace = $namespace ?? $captured;
214
                    break;
215
216
                case Splitter::BYTE_TAG_END:
217
                    $text .= '>';
218
                    $method = $method ?? $captured;
219
220
                    if (!isset($namespace) || !isset($method) || $this->splitter->context->context === Context::CONTEXT_DEAD || $this->resolver->isNamespaceIgnored($namespace)) {
221
                        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(); (TYPO3Fluid\Fluid\Core\Pa...ing|integer|double|null) 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...
222
                    }
223
224
                    try {
225
                        $expectedClass = $this->resolver->resolveViewHelperClassName($namespace, $method);
226
                    } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
227
                        throw $this->splitter->createErrorAtPosition($exception->getMessage(), $exception->getCode());
228
                    }
229
230
                    if ($closing && !$selfClosing) {
231
                        // Closing byte was more than two bytes back, meaning the tag is NOT self-closing, but is a
232
                        // closing tag for a previously opened+stacked node. Finalize the node now.
233
                        $closesNode = $this->state->popNodeFromStack();
234
                        if ($closesNode instanceof $expectedClass) {
235
                            $arguments = $closesNode->getParsedArguments();
236
                            $viewHelperNode = $closesNode;
237
                        } else {
238
                            throw $this->splitter->createErrorAtPosition(
239
                                sprintf(
240
                                    'Mismatched closing tag. Expecting: %s:%s (%s). Found: (%s).',
241
                                    $namespace,
242
                                    $method,
243
                                    $expectedClass,
244
                                    get_class($closesNode)
245
                                ),
246
                                1557700789
247
                            );
248
                        }
249
                    }
250
251
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES && $captured !== null) {
252
                        // We are still capturing arguments and the last yield contained a value. Null-coalesce key
253
                        // with captured string so object accessor becomes key name (ECMA shorthand literal)
254
                        $arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
255
                    }
256
257
                    $viewHelperNode = $viewHelperNode ?? $this->resolver->createViewHelperInstanceFromClassName($expectedClass);
258
                    #$this->escapingEnabled = $escapingEnabledBackup;
259
260
                    if (!$closing) {
261
                        $this->callInterceptor($viewHelperNode, $interceptionPoint);
262
                        $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...
263
                        $this->state->pushNodeToStack($viewHelperNode);
264
                        return null;
265
                    }
266
267
                    $viewHelperNode = $viewHelperNode->postParse($arguments, $this->state, $this->renderingContext);
268
269
                    return $viewHelperNode;
270
271
                case Splitter::BYTE_WHITESPACE_TAB:
272
                case Splitter::BYTE_WHITESPACE_RETURN:
273
                case Splitter::BYTE_WHITESPACE_EOL:
274
                case Splitter::BYTE_WHITESPACE_SPACE:
275
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) {
276
                        if ($captured !== null) {
277
                            $arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
278
                            $key = null;
279
                        }
280
                    } else {
281
                        $text .= chr($symbol);
282
                        if (isset($namespace)) {
283
                            $method = $captured;
284
285
                            $this->escapingEnabled = false;
286
                            $viewHelperNode = $this->resolver->createViewHelperInstance($namespace, $method);
287
                            $definitions = $viewHelperNode->prepareArguments();
288
289
                            // A whitespace character, in tag context, means the beginning of an array sequence (which may
290
                            // or may not contain any items; the next symbol may be a tag end or tag close). We sequence the
291
                            // arguments array and create a ViewHelper node.
292
                            $this->splitter->switch($this->contexts->attributes);
293
                            break;
294
                        }
295
296
                        // A whitespace before a colon means the tag is not a namespaced tag. We will ignore everything
297
                        // inside this tag, except for inline syntax, until the tag ends. For this we use a special,
298
                        // limited variant of the root context where instead of scanning for "<" we scan for ">".
299
                        // We continue in this same loop because it still matches the potential symbols being yielded.
300
                        // Most importantly: this new reduced context will NOT match a colon which is the trigger symbol
301
                        // for a ViewHelper tag.
302
                        $this->splitter->switch($this->contexts->dead);
303
                    }
304
                    break;
305
            }
306
        }
307
308
        // This case on the surface of it, belongs as "default" case in the switch above. However, the only case that
309
        // would *actually* produce this error, is if the splitter reaches EOF (null byte) symbol before the tag was
310
        // closed. Literally every other possible error type will be thrown as more specific exceptions (e.g. invalid
311
        // argument, missing key, wrong quotes, bad inline and *everything* else with the exception of EOF). Even a
312
        // stray null byte would not be caught here as null byte is not part of the symbol collection for "tag" context.
313
        throw $this->splitter->createErrorAtPosition('Unexpected token in tag sequencing', 1557700786);
314
    }
315
316
    /**
317
     * @param bool $allowArray
318
     * @return NodeInterface
319
     */
320
    protected function sequenceInlineNodes(bool $allowArray = true): NodeInterface
321
    {
322
        $text = '{';
323
        $node = null;
324
        $key = null;
325
        $namespace = null;
326
        $method = null;
327
        $potentialAccessor = null;
328
        $callDetected = false;
329
        $hasPass = false;
330
        $hasColon = null;
331
        $hasWhitespace = false;
332
        $isArray = false;
333
        $array = [];
334
        $arguments = [];
335
        $ignoredEndingBraces = 0;
336
        $countedEscapes = 0;
337
338
        $this->splitter->switch($this->contexts->inline);
339
        $this->splitter->sequence->next();
340
        foreach ($this->splitter->sequence as $symbol => $captured) {
341
            $text .= $captured;
342
            switch ($symbol) {
343
                case Splitter::BYTE_BACKSLASH:
344
                    // Increase the number of counted escapes (is passed to sequenceNode() in the "QUOTE" cases and reset
345
                    // after the quoted string is extracted).
346
                    ++$countedEscapes;
347
                    break;
348
349
                case Splitter::BYTE_ARRAY_START:
350
351
                    $text .= chr($symbol);
352
                    $isArray = $allowArray;
353
354
                    #ArrayStart:
355
                    // Sequence the node. Pass the "use numeric keys?" boolean based on the current byte. Only array
356
                    // start creates numeric keys. Inline start with keyless values creates ECMA style {foo:foo, bar:bar}
357
                    // from {foo, bar}.
358
                    $array[$key ?? $captured ?? 0] = $node = $this->sequenceArrayNode(null, $symbol === Splitter::BYTE_ARRAY_START);
359
                    $this->splitter->switch($this->contexts->inline);
360
                    unset($key);
361
                    break;
362
363
                case Splitter::BYTE_INLINE:
364
                    // Encountering this case can mean different things: sub-syntax like {foo.{index}} or array, depending
365
                    // on presence of either a colon or comma before the inline. In protected mode it is simply added.
366
                    $text .= '{';
367
                    if (!$hasWhitespace && $text !== '{{') {
368
                        // Most likely, a nested object accessor syntax e.g. {foo.{bar}} - enter protected context since
369
                        // these accessors do not allow anything other than additional nested accessors.
370
                        $this->splitter->switch($this->contexts->accessor);
371
                        ++$ignoredEndingBraces;
372
                    } elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED) {
373
                        // Ignore one ending additional curly brace. Subtracted in the BYTE_INLINE_END case below.
374
                        // The expression in this case looks like {{inline}.....} and we capture the curlies.
375
                        $potentialAccessor .= $captured;
376
                        ++$ignoredEndingBraces;
377
                    } elseif ($allowArray || $isArray) {
378
                        $isArray = true;
379
                        $captured = $key ?? $captured ?? $potentialAccessor;
380
                        // This is a sub-syntax following a colon - meaning it is an array.
381
                        if ($captured !== null) {
382
                            #goto ArrayStart;
383
                            $array[$key ?? $captured ?? 0] = $node = $this->sequenceArrayNode(null, $symbol === Splitter::BYTE_ARRAY_START);
384
                            $this->splitter->switch($this->contexts->inline);
385
                        }
386
                    } else {
387
                        $childNodeToAdd = $this->sequenceInlineNodes($allowArray);
388
                        $node = isset($node) ? $node->addChildNode($childNodeToAdd) : (new RootNode())->addChildNode($childNodeToAdd);
389
                    }
390
                    break;
391
392
                case Splitter::BYTE_MINUS:
393
                    $text .= '-';
394
                    $potentialAccessor = $potentialAccessor ?? $captured;
395
                    break;
396
397
                // Backtick may be encountered in two different contexts: normal inline context, in which case it has
398
                // the same meaning as any quote and causes sequencing of a quoted string. Or protected context, in
399
                // which case it also sequences a quoted node but appends the result instead of assigning to array.
400
                // Note that backticks do not support escapes (they are a new feature that does not require escaping).
401
                case Splitter::BYTE_BACKTICK:
402
                    if ($this->splitter->context->context === Context::CONTEXT_PROTECTED) {
403
                        $node->addChildNode(new TextNode($text));
404
                        $node->addChildNode($this->sequenceQuotedNode()->flatten());
405
                        $text = '';
406
                        break;
407
                    }
408
                // Fallthrough is intentional: if not in protected context, consider the backtick a normal quote.
409
410
                // Case not normally countered in straight up "inline" context, but when encountered, means we have
411
                // explicitly found a quoted array key - and we extract it.
412
                case Splitter::BYTE_QUOTE_SINGLE:
413
                case Splitter::BYTE_QUOTE_DOUBLE:
414
                    if (!$allowArray) {
415
                        $text .= chr($symbol);
416
                        break;
417
                    }
418
                    if (isset($key)) {
419
                        $array[$key] = $this->sequenceQuotedNode($countedEscapes)->flatten(true);
420
                        $key = null;
421
                    } else {
422
                        $key = $this->sequenceQuotedNode($countedEscapes)->flatten(true);
423
                    }
424
                    $countedEscapes = 0;
425
                    $isArray = $allowArray;
426
                    break;
427
428
                case Splitter::BYTE_SEPARATOR_COMMA:
429
                    if (!$allowArray) {
430
                        $text .= ',';
431
                        break;
432
                    }
433
                    if (isset($captured)) {
434
                        $array[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
435
                    }
436
                    $key = null;
437
                    $isArray = $allowArray;
438
                    break;
439
440
                case Splitter::BYTE_SEPARATOR_EQUALS:
441
                    $text .= '=';
442
                    if (!$allowArray) {
443
                        $node = new RootNode();
444
                        $this->splitter->switch($this->contexts->protected);
445
                        break;
446
                    }
447
                    $key = $captured;
448
                    $isArray = $allowArray;
449
                    break;
450
451
                case Splitter::BYTE_SEPARATOR_COLON:
452
                    $text .= ':';
453
                    $hasColon = true;
454
                    $namespace = $captured;
455
                    $key = $key ?? $captured;
456
                    $isArray = $isArray || ($allowArray && is_numeric($key));
457
                    break;
458
459
                case Splitter::BYTE_WHITESPACE_SPACE:
460
                case Splitter::BYTE_WHITESPACE_EOL:
461
                case Splitter::BYTE_WHITESPACE_RETURN:
462
                case Splitter::BYTE_WHITESPACE_TAB:
463
                    // If we already collected some whitespace we must enter protected context.
464
                    $text .= $this->source->source[$this->splitter->index - 1];
465
                    if ($hasWhitespace && !$hasPass && !$allowArray) {
466
                        // Protection mode: this very limited context does not allow tags or inline syntax, and will
467
                        // protect things like CSS and JS - and will only enter a more reactive context if encountering
468
                        // the backtick character, meaning a quoted string will be sequenced. This backtick-quoted
469
                        // string can then contain inline syntax like variable accessors.
470
                        $node = $node ?? new RootNode();
471
                        $this->splitter->switch($this->contexts->protected);
472
                        break;
473
                    }
474
                    $key = $key ?? $captured;
475
                    $hasWhitespace = true;
476
                    $isArray = $allowArray && ($hasColon ?? $isArray ?? is_numeric($captured));
477
                    $potentialAccessor = ($potentialAccessor ?? $captured);
478
                    break;
479
480
                case Splitter::BYTE_TAG_END:
481
                case Splitter::BYTE_PIPE:
482
                    // If there is an accessor on the left side of the pipe and $node is not defined, we create $node
483
                    // as an object accessor. If $node already exists we do nothing (and expect the VH trigger, the
484
                    // parenthesis start case below, to add $node as childnode and create a new $node).
485
                    $hasPass = true;
486
                    $isArray = $allowArray;
487
                    $callDetected = false;
488
                    $potentialAccessor = $potentialAccessor ?? $captured;
489
                    $text .=  $this->source->source[$this->splitter->index - 1];
490
                    if ($node instanceof ViewHelperInterface) {
491
                        $node->postParse($arguments, $this->state, $this->renderingContext);
492
                    }
493
                    if (isset($potentialAccessor)) {
494
                        $childNodeToAdd = new ObjectAccessorNode($potentialAccessor);
495
                        $node = isset($node) ? $node->addChildNode($childNodeToAdd) : $childNodeToAdd; //$node ?? (is_numeric($potentialAccessor) ? $potentialAccessor + 0 : new ObjectAccessorNode($potentialAccessor));
496
                    }
497
                    unset($namespace, $method, $potentialAccessor, $key);
498
                    break;
499
500
                case Splitter::BYTE_PARENTHESIS_START:
501
                    $isArray = false;
502
                    // Special case: if a parenthesis start was preceded by whitespace but had no pass operator we are
503
                    // not dealing with a ViewHelper call and will continue the sequencing, grabbing the parenthesis as
504
                    // part of the expression.
505
                    $text .= '(';
506
                    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...
507
                        $this->splitter->switch($this->contexts->protected);
508
                        unset($namespace, $method);
509
                        break;
510
                    }
511
512
                    $callDetected = true;
513
                    $method = $captured;
514
                    $childNodeToAdd = $node;
515
                    try {
516
                        $node = $this->resolver->createViewHelperInstance($namespace, $method);
517
                        $definitions = $node->prepareArguments();
518
                    } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
519
                        throw $this->splitter->createErrorAtPosition($exception->getMessage(), $exception->getCode());
520
                    }
521
                    $this->splitter->switch($this->contexts->array);
522
                    $arguments = $this->sequenceArrayNode($definitions)->getInternalArray();
523
                    $this->splitter->switch($this->contexts->inline);
524
                    if ($childNodeToAdd) {
525
                        $escapingEnabledBackup = $this->escapingEnabled;
526
                        $this->escapingEnabled = (bool)$node->isChildrenEscapingEnabled();
527
                        if ($childNodeToAdd instanceof ObjectAccessorNode) {
528
                            $this->callInterceptor($childNodeToAdd, InterceptorInterface::INTERCEPT_OBJECTACCESSOR);
529
                        }
530
                        $this->escapingEnabled = $escapingEnabledBackup;
531
                        $node->addChildNode($childNodeToAdd);
532
                    }
533
                    $text .= ')';
534
                    unset($potentialAccessor);
535
                    break;
536
537
                case Splitter::BYTE_INLINE_END:
538
                    $text .= '}';
539
                    if (--$ignoredEndingBraces >= 0) {
540
                        break;
541
                    }
542
                    $isArray = $allowArray && ($isArray ?: ($hasColon && !$hasPass && !$callDetected));
543
                    $potentialAccessor = $potentialAccessor ?? $captured;
544
545
                    // Decision: if we did not detect a ViewHelper we match the *entire* expression, from the cached
546
                    // starting index, to see if it matches a known type of expression. If it does, we must return the
547
                    // appropriate type of ExpressionNode.
548
                    if ($isArray) {
549
                        if ($captured !== null) {
550
                            $array[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
551
                        }
552
                        return new ArrayNode($array);
553
                    } elseif ($callDetected) {
554
                        // The first-priority check is for a ViewHelper used right before the inline expression ends,
555
                        // in which case there is no further syntax to come.
556
                        $node = $node->postParse($arguments, $this->state, $this->renderingContext);
557
                        $interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER;
558
                    } elseif ($this->splitter->context->context === Context::CONTEXT_ACCESSOR) {
559
                        // If we are currently in "accessor" context we can now add the accessor by stripping the collected text.
560
                        $node = new ObjectAccessorNode(substr($text, 1, -1));
561
                        $interceptionPoint = InterceptorInterface::INTERCEPT_OBJECTACCESSOR;
562
                    } elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED || ($hasWhitespace && !$callDetected && !$hasPass)) {
563
                        // In order to qualify for potentially being an expression, the entire inline node must contain
564
                        // whitespace, must not contain parenthesis, must not contain a colon and must not contain an
565
                        // inline pass operand. This significantly limits the number of times this (expensive) routine
566
                        // has to be executed.
567
                        $interceptionPoint = InterceptorInterface::INTERCEPT_TEXT;
568
                        $childNodeToAdd = new TextNode($text);
569
                        foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) {
570
                            $matchedVariables = [];
571
                            // TODO: rewrite expression nodes to receive a sub-Splitter that lets the expression node
572
                            // consume a symbol+capture sequence and either match or ignore it; then use the already
573
                            // consumed (possibly halted mid-way through iterator!) sequence to achieve desired behavior.
574
                            preg_match_all($expressionNodeTypeClassName::$detectionExpression, $text, $matchedVariables, PREG_SET_ORDER);
575
                            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...
576
                                try {
577
                                    $childNodeToAdd = new $expressionNodeTypeClassName($matchedVariableSet[0], $matchedVariableSet, $this->state);
578
                                    $interceptionPoint = InterceptorInterface::INTERCEPT_EXPRESSION;
579
                                } catch (ExpressionException $error) {
580
                                    $childNodeToAdd = new TextNode($this->renderingContext->getErrorHandler()->handleExpressionError($error));
581
                                }
582
                                break;
583
                            }
584
                        }
585
                        $node = isset($node) ? $node->addChildNode($childNodeToAdd) : $childNodeToAdd;
586
                    } elseif (!$hasPass && !$callDetected) {
587
                        // Third priority check is if there was no pass syntax and no ViewHelper, in which case we
588
                        // create a standard ObjectAccessorNode; alternatively, if nothing was captured (expression
589
                        // was empty, e.g. {} was used) we create a TextNode with the captured text to output "{}".
590
                        if (isset($potentialAccessor)) {
591
                            // If the accessor is set we can trust it is not a numeric value, since this will have
592
                            // set $isArray to TRUE if nothing else already did so.
593
                            $node = is_numeric($potentialAccessor) ? $potentialAccessor + 0 : new ObjectAccessorNode($potentialAccessor);
594
                            $interceptionPoint = InterceptorInterface::INTERCEPT_OBJECTACCESSOR;
595
                        } else {
596
                            $node = new TextNode($text);
597
                            $interceptionPoint = InterceptorInterface::INTERCEPT_TEXT;
598
                        }
599
                    } elseif ($hasPass && $this->resolver->isAliasRegistered((string)$potentialAccessor)) {
600
                        // Fourth priority check is for a pass to a ViewHelper alias, e.g. "{value | raw}" in which case
601
                        // we look for the alias used and create a ViewHelperNode with no arguments.
602
                        $childNodeToAdd = $node;
603
                        $node = $this->resolver->createViewHelperInstance(null, $potentialAccessor);
604
                        $node->addChildNode($childNodeToAdd);
605
                        $node = $node->postParse($arguments, $this->state, $this->renderingContext);
606
                        $interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER;
607
                    } else {
608
                        # TODO: should this be an error case, or should it result in a TextNode?
609
                        throw $this->splitter->createErrorAtPosition(
610
                            'Invalid inline syntax - not accessor, not expression, not array, not ViewHelper, but ' .
611
                            'contains the tokens used by these in a sequence that is not valid Fluid syntax. You can ' .
612
                            'most likely avoid this by adding whitespace inside the curly braces before the first ' .
613
                            'Fluid-like symbol in the expression. Symbols recognized as Fluid are: "' .
614
                            addslashes(implode('","', array_map('chr', $this->contexts->inline->bytes))) . '"',
615
                            1558782228
616
                        );
617
                    }
618
619
                    $escapingEnabledBackup = $this->escapingEnabled;
620
                    $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...
621
                    $this->callInterceptor($node, $interceptionPoint);
622
                    $this->escapingEnabled = $escapingEnabledBackup;
623
                    return $node;
624
            }
625
        }
626
627
        // See note in sequenceTagNode() end of method body. TL;DR: this is intentionally here instead of as "default"
628
        // case in the switch above for a very specific reason: the case is only encountered if seeing EOF before the
629
        // inline expression was closed.
630
        throw $this->splitter->createErrorAtPosition('Unterminated inline syntax', 1557838506);
631
    }
632
633
    /**
634
     * @param ArgumentDefinition[] $definitions
635
     * @param bool $numeric
636
     * @return ArrayNode
637
     */
638
    protected function sequenceArrayNode(array $definitions = null, bool $numeric = false): ArrayNode
639
    {
640
        $array = [];
641
642
        $keyOrValue = null;
643
        $key = null;
644
        $escapingEnabledBackup = $this->escapingEnabled;
645
        $this->escapingEnabled = false;
646
        $itemCount = -1;
647
        $countedEscapes = 0;
648
649
        $this->splitter->sequence->next();
650
        foreach ($this->splitter->sequence as $symbol => $captured) {
651
            switch ($symbol) {
652
                case Splitter::BYTE_SEPARATOR_COLON:
653
                case Splitter::BYTE_SEPARATOR_EQUALS:
654
                    // Colon or equals has same meaning (which allows tag syntax as argument syntax). Encountering this
655
                    // byte always means the preceding byte was a key. However, if nothing was captured before this,
656
                    // it means colon or equals was used without a key which is a syntax error.
657
                    $key = $key ?? $captured ?? (isset($keyOrValue) ? $keyOrValue->flatten(true) : null);
658
                    if (!isset($key)) {
659
                        throw $this->splitter->createErrorAtPosition('Unexpected colon or equals sign, no preceding key', 1559250839);
660
                    }
661
                    if ($definitions !== null && !$numeric && !isset($definitions[$key])) {
662
                        throw $this->splitter->createUnsupportedArgumentError((string)$key, $definitions);
663
                    }
664
                    break;
665
666
                case Splitter::BYTE_ARRAY_START:
667
                case Splitter::BYTE_INLINE:
668
                    // Minimal safeguards to improve error feedback. Theoretically such "garbage" could simply be ignored
669
                    // without causing problems to the parser, but it is probably best to report it as it could indicate
670
                    // the user expected X value but gets Y and doesn't notice why.
671
                    if ($captured !== null) {
672
                        throw $this->splitter->createErrorAtPosition('Unexpected content before array/inline start in associative array, ASCII: ' . ord($captured), 1559131849);
673
                    }
674
                    if (!isset($key) && !$numeric) {
675
                        throw $this->splitter->createErrorAtPosition('Unexpected array/inline start in associative array without preceding key', 1559131848);
676
                    }
677
678
                    // Encountering a curly brace or square bracket start byte will both cause a sub-array to be sequenced,
679
                    // the difference being that only the square bracket will cause third parameter ($numeric) passed to
680
                    // sequenceArrayNode() to be true, which in turn causes key-less items to be added with numeric indexes.
681
                    $key = $key ?? ++$itemCount;
682
                    $array[$key] = $this->sequenceArrayNode(null, $symbol === Splitter::BYTE_ARRAY_START);
683
                    $keyOrValue = null;
684
                    $key = null;
685
                    break;
686
687
                case Splitter::BYTE_QUOTE_SINGLE:
688
                case Splitter::BYTE_QUOTE_DOUBLE:
689
                    // Safeguard: if anything is captured before a quote this indicates garbage leading content. As with
690
                    // the garbage safeguards above, this one could theoretically be ignored in favor of silently making
691
                    // the odd syntax "just work".
692
                    if ($captured !== null) {
693
                        throw $this->splitter->createErrorAtPosition('Unexpected content before quote start in associative array, ASCII: ' . ord($captured), 1559145560);
694
                    }
695
696
                    // Quotes will always cause sequencing of the quoted string, but differs in behavior based on whether
697
                    // or not the $key is set. If $key is set, we know for sure we can assign a value. If it is not set
698
                    // we instead leave $keyOrValue defined so this will be processed by one of the next iterations.
699
                    $keyOrValue = $this->sequenceQuotedNode($countedEscapes);
700
                    if (isset($key)) {
701
                        $array[$key] = $keyOrValue->flatten(true);
702
                        $keyOrValue = null;
703
                        $key = null;
704
                        $countedEscapes = 0;
705
                    }
706
                    break;
707
708
                case Splitter::BYTE_SEPARATOR_COMMA:
709
                    // Comma separator: if we've collected a key or value, use it. Otherwise, use captured string.
710
                    // If neither key nor value nor captured string exists, ignore the comma (likely a tailing comma).
711
                    if (isset($keyOrValue)) {
712
                        // Key or value came as quoted string and exists in $keyOrValue
713
                        $potentialValue = $keyOrValue->flatten(true);
714
                        $key = $numeric ? ++$itemCount : $potentialValue;
715
                        $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 714 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...
716
                    } elseif (isset($captured)) {
717
                        $key = $key ?? ($numeric ? ++$itemCount : $captured);
718
                        if (!$numeric && isset($definitions) && !isset($definitions[$key])) {
719
                            throw $this->splitter->createUnsupportedArgumentError((string)$key, $definitions);
720
                        }
721
                        $array[$key] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
722
                    }
723
                    $keyOrValue = null;
724
                    $key = null;
725
                    break;
726
727
                case Splitter::BYTE_WHITESPACE_TAB:
728
                case Splitter::BYTE_WHITESPACE_RETURN:
729
                case Splitter::BYTE_WHITESPACE_EOL:
730
                case Splitter::BYTE_WHITESPACE_SPACE:
731
                    // Any whitespace attempts to set the key, if not already set. The captured string may be null as
732
                    // well, leaving the $key variable still null and able to be coalesced.
733
                    $key = $key ?? $captured;
734
                    break;
735
736
                case Splitter::BYTE_BACKSLASH:
737
                    // Escapes are simply counted and passed to the sequenceQuotedNode() method, causing that method
738
                    // to ignore exactly this number of backslashes before a matching quote is seen as closing quote.
739
                    ++$countedEscapes;
740
                    break;
741
742
                case Splitter::BYTE_INLINE_END:
743
                case Splitter::BYTE_ARRAY_END:
744
                case Splitter::BYTE_PARENTHESIS_END:
745
                    // Array end indication. Check if anything was collected previously or was captured currently,
746
                    // assign that to the array and return an ArrayNode with the full array inside.
747
                    $captured = $captured ?? (isset($keyOrValue) ? $keyOrValue->flatten(true) : null);
748
                    $key = $key ?? ($numeric ? ++$itemCount : $captured);
749
                    if (isset($captured, $key)) {
750
                        if (is_numeric($captured)) {
751
                            $array[$key] = $captured + 0;
752
                        } elseif (isset($keyOrValue)) {
753
                            $array[$key] = $keyOrValue->flatten();
754
                        } else {
755
                            $array[$key] = new ObjectAccessorNode($captured ?? $key);
756
                        }
757
                    }
758
                    if (!$numeric && isset($key, $definitions) && !isset($definitions[$key])) {
759
                        throw $this->splitter->createUnsupportedArgumentError((string)$key, $definitions);
760
                    }
761
                    $this->escapingEnabled = $escapingEnabledBackup;
762
                    return new ArrayNode($array);
763
            }
764
        }
765
766
        throw $this->splitter->createErrorAtPosition(
767
            'Unterminated array',
768
            1557748574
769
        );
770
    }
771
772
    /**
773
     * Sequence a quoted value
774
     *
775
     * The return can be either of:
776
     *
777
     * 1. A string value if source was for example "string"
778
     * 2. An integer if source was for example "1"
779
     * 3. A float if source was for example "1.25"
780
     * 4. A RootNode instance with multiple child nodes if source was for example "string {var}"
781
     *
782
     * The idea is to return the raw value if there is no reason for it to
783
     * be a node as such - which is only necessary if the quoted expression
784
     * contains other (dynamic) values like an inline syntax.
785
     *
786
     * @param int $leadingEscapes A backwards compatibility measure: when passed, this number of escapes must precede a closing quote for it to trigger node closing.
787
     * @param bool $allowArray
788
     * @return RootNode
789
     */
790
    protected function sequenceQuotedNode(int $leadingEscapes = 0, $allowArray = true): RootNode
791
    {
792
        $startingByte = $this->source->bytes[$this->splitter->index];
793
        $contextToRestore = $this->splitter->switch($this->contexts->quoted);
794
        $node = new RootNode();
795
        $this->splitter->sequence->next();
796
        $countedEscapes = 0;
797
798
        foreach ($this->splitter->sequence as $symbol => $captured) {
799
            switch ($symbol) {
800
801
                case Splitter::BYTE_ARRAY_START:
802
                    $countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored)
803
                    if ($captured === null) {
804
                        // Array start "[" only triggers array sequencing if it is the very first byte in the quoted
805
                        // string - otherwise, it is added as part of the text.
806
                        $this->splitter->switch($this->contexts->array);
807
                        $node->addChildNode($this->sequenceArrayNode(null, $allowArray));
808
                        $this->splitter->switch($this->contexts->quoted);
809
                    } else {
810
                        $node->addChildNode(new TextNode($captured . '['));
811
                    }
812
                    break;
813
814
                case Splitter::BYTE_INLINE:
815
                    $countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored)
816
                    // The quoted string contains a sub-expression. We extract the captured content so far and if it
817
                    // is not an empty string, add it as a child of the RootNode we're building, then we add the inline
818
                    // expression as next sibling and continue the loop.
819
                    if ($captured !== null) {
820
                        $childNode = new TextNode($captured);
821
                        $this->callInterceptor($childNode, InterceptorInterface::INTERCEPT_TEXT);
822
                        $node->addChildNode($childNode);
823
                    }
824
825
                    $node->addChildNode($this->sequenceInlineNodes());
826
                    $this->splitter->switch($this->contexts->quoted);
827
                    break;
828
829
                case Splitter::BYTE_BACKSLASH:
830
                    $next = $this->source->bytes[$this->splitter->index + 1] ?? null;
831
                    ++$countedEscapes;
832
                    if ($next === $startingByte || $next === Splitter::BYTE_BACKSLASH) {
833
                        if ($captured !== null) {
834
                            $node->addChildNode(new TextNode($captured));
835
                        }
836
                    } else {
837
                        $node->addChildNode(new TextNode($captured . str_repeat('\\', $countedEscapes)));
838
                        $countedEscapes = 0;
839
                    }
840
                    break;
841
842
                // Note: although "case $startingByte:" could have been used here, it would not compile the switch
843
                // as a hash map and thus would not perform as well overall - when called frequently as it will be.
844
                // Backtick will only be encountered if the context is "protected" (insensitive inline sequencing)
845
                case Splitter::BYTE_QUOTE_SINGLE:
846
                case Splitter::BYTE_QUOTE_DOUBLE:
847
                case Splitter::BYTE_BACKTICK:
848
                    if ($symbol !== $startingByte || $countedEscapes !== $leadingEscapes) {
849
                        $node->addChildNode(new TextNode($captured . chr($symbol)));
850
                        $countedEscapes = 0; // If number of escapes do not match expected, reset the counter
851
                        break;
852
                    }
853
                    if ($captured !== null) {
854
                        $node->addChildNode(new TextNode($captured));
855
                    }
856
                    $this->splitter->switch($contextToRestore);
857
                    return $node;
858
            }
859
        }
860
861
        throw $this->splitter->createErrorAtPosition('Unterminated expression inside quotes', 1557700793);
862
    }
863
864
    /**
865
     * Call all interceptors registered for a given interception point.
866
     *
867
     * @param NodeInterface $node The syntax tree node which can be modified by the interceptors.
868
     * @param integer $interceptionPoint the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants.
869
     * @return void
870
     */
871
    protected function callInterceptor(NodeInterface &$node, $interceptionPoint)
872
    {
873
        if ($this->escapingEnabled) {
874
            /** @var $interceptor InterceptorInterface */
875
            foreach ($this->configuration->getEscapingInterceptors($interceptionPoint) as $interceptor) {
876
                $node = $interceptor->process($node, $interceptionPoint, $this->state);
877
            }
878
        }
879
880
        /** @var $interceptor InterceptorInterface */
881
        foreach ($this->configuration->getInterceptors($interceptionPoint) as $interceptor) {
882
            $node = $interceptor->process($node, $interceptionPoint, $this->state);
883
        }
884
    }
885
}