Completed
Pull Request — master (#470)
by Claus
01:35
created

Sequencer::sequenceInlineNodes()   F

Complexity

Conditions 75
Paths 195

Size

Total Lines 353

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 75
nc 195
nop 1
dl 0
loc 353
rs 2.6999
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
namespace TYPO3Fluid\Fluid\Core\Parser;
4
5
/*
6
 * This file belongs to the package "TYPO3 Fluid".
7
 * See LICENSE.txt that was shipped with this package.
8
 */
9
10
use TYPO3Fluid\Fluid\Component\Argument\ArgumentCollection;
11
use TYPO3Fluid\Fluid\Component\ComponentInterface;
12
use TYPO3Fluid\Fluid\Component\SequencingComponentInterface;
13
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode;
14
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\BooleanNode;
15
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\EntryNode;
16
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\EscapingNode;
17
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode;
18
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
19
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
20
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
21
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperResolver;
22
23
/**
24
 * Sequencer for Fluid syntax
25
 *
26
 * Uses a NoRewindIterator around a sequence of byte values to
27
 * iterate over each syntax-relevant character and determine
28
 * which nodes to create.
29
 *
30
 * Passes the outer iterator between functions that perform the
31
 * iterations. Since the iterator is a NoRewindIterator it will
32
 * not be reset before the start of each loop - meaning that
33
 * when it is passed to a new function, that function continues
34
 * from the last touched index in the byte sequence.
35
 *
36
 * The circumstances around "break or return" in the switches is
37
 * very, very important to understand in context of how iterators
38
 * work. Returning does not advance the iterator like breaking
39
 * would and this causes a different position in the byte sequence
40
 * to be experienced in the method that uses the return value (it
41
 * sees the index of the symbol which terminated the expression,
42
 * not the next symbol after that).
43
 */
44
class Sequencer
45
{
46
    public const BYTE_NULL = Splitter::BYTE_NULL; // Zero-byte for terminating documents
47
    public const BYTE_INLINE = 123; // The "{" character indicating an inline expression started
48
    public const BYTE_INLINE_END = 125; // The "}" character indicating an inline expression ended
49
    public const BYTE_PIPE = 124; // The "|" character indicating an inline expression pass operation
50
    public const BYTE_MINUS = 45; // The "-" character (for legacy pass operations)
51
    public const BYTE_TAG = 60; // The "<" character indicating a tag has started
52
    public const BYTE_TAG_END = 62; // The ">" character indicating a tag has ended
53
    public const BYTE_TAG_CLOSE = 47; // The "/" character indicating a tag is a closing tag
54
    public const BYTE_QUOTE_DOUBLE = 34; // The " (standard double-quote) character
55
    public const BYTE_QUOTE_SINGLE = 39; // The ' (standard single-quote) character
56
    public const BYTE_WHITESPACE_SPACE = 32; // A standard space character
57
    public const BYTE_WHITESPACE_TAB = 9; // A standard carriage-return character
58
    public const BYTE_WHITESPACE_RETURN = 13; // A standard tab character
59
    public const BYTE_WHITESPACE_EOL = 10; // A standard (UNIX) line-break character
60
    public const BYTE_SEPARATOR_EQUALS = 61; // The "=" character
61
    public const BYTE_SEPARATOR_COLON = 58; // The ":" character
62
    public const BYTE_SEPARATOR_COMMA = 44; // The "," character
63
    public const BYTE_PARENTHESIS_START = 40; // The "(" character
64
    public const BYTE_PARENTHESIS_END = 41; // The ")" character
65
    public const BYTE_ARRAY_START = 91; // The "[" character
66
    public const BYTE_ARRAY_END = 93; // The "]" character
67
    public const BYTE_BACKSLASH = 92; // The "\" character
68
    public const BYTE_BACKTICK = 96; // The "`" character
69
    public const BYTE_AT = 64; // The "@" character
70
    public const MASK_LINEBREAKS = 0 | (1 << self::BYTE_WHITESPACE_EOL) | (1 << self::BYTE_WHITESPACE_RETURN);
71
    
72
    private const INTERCEPT_OPENING_VIEWHELPER = 1;
73
    private const INTERCEPT_CLOSING_VIEWHELPER = 2;
74
    private const INTERCEPT_OBJECTACCESSOR = 4;
75
    private const INTERCEPT_EXPRESSION = 5;
76
    private const INTERCEPT_SELFCLOSING_VIEWHELPER = 6;
77
    
78
    /**
79
     * A counter of nodes which currently disable the interceptor.
80
     * Needed to enable the interceptor again.
81
     *
82
     * @var int
83
     */
84
    protected $viewHelperNodesWhichDisableTheInterceptor = 0;
85
86
    /**
87
     * @var RenderingContextInterface
88
     */
89
    public $renderingContext;
90
91
    /**
92
     * @var Contexts
93
     */
94
    public $contexts;
95
96
    /**
97
     * @var Source
98
     */
99
    public $source;
100
101
    /**
102
     * @var Splitter
103
     */
104
    public $splitter;
105
106
    /** @var \NoRewindIterator */
107
    public $sequence;
108
109
    /**
110
     * @var Configuration
111
     */
112
    public $configuration;
113
114
    /**
115
     * @var ViewHelperResolver
116
     */
117
    protected $resolver;
118
119
    /**
120
     * Whether or not the escaping interceptors are active
121
     *
122
     * @var boolean
123
     */
124
    protected $escapingEnabled = true;
125
126
    /**
127
     * @var ComponentInterface[]
128
     */
129
    protected $nodeStack = [];
130
131
    public function __construct(
132
        RenderingContextInterface $renderingContext,
133
        Contexts $contexts,
134
        Source $source,
135
        ?Configuration $configuration = null
136
    ) {
137
        $this->source = $source;
138
        $this->contexts = $contexts;
139
        $this->renderingContext = $renderingContext;
140
        $this->resolver = $renderingContext->getViewHelperResolver();
141
        $this->configuration = $configuration ?? $renderingContext->getParserConfiguration();
142
        $this->escapingEnabled = $this->configuration->isFeatureEnabled(Configuration::FEATURE_ESCAPING);
143
        $this->splitter = new Splitter($this->source, $this->contexts);
144
    }
145
146
    public function sequence(): ComponentInterface
147
    {
148
        // Root context - the only symbols that cause any context switching are curly brace open and tag start, but
149
        // only if they are not preceded by a backslash character; in which case the symbol is ignored and merely
150
        // collected as part of the output string. NULL bytes are ignored in this context (the Splitter will yield
151
        // a single NULL byte when end of source is reached).
152
        $this->nodeStack[] = (new EntryNode())->onOpen($this->renderingContext);
153
        $this->sequence = $this->splitter->parse();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->splitter->parse() can also be of type array<integer,string> or array<integer,null>. However, the property $sequence is declared as type object<NoRewindIterator>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
154
        $countedEscapes = 0;
155
        foreach ($this->sequence as $symbol => $captured) {
156
            $node = end($this->nodeStack);
157
            $text = $captured . ($countedEscapes > 0 ? chr($symbol) : '');
158
            if ($text !== '') {
159
                $node->addChild(new TextNode($text));
160
            }
161
162
            if ($countedEscapes > 0) {
163
                $countedEscapes = 0;
164
                continue;
165
            }
166
167
            switch ($symbol) {
168
                case self::BYTE_BACKSLASH:
169
                    ++$countedEscapes;
170
                    break;
171
172
                case self::BYTE_INLINE:
173
                    $countedEscapes = 0;
174
                    $node->addChild($this->sequenceInlineNodes(false));
175
                    $this->splitter->switch($this->contexts->root);
176
                    break;
177
178
                case self::BYTE_TAG:
179
                    $countedEscapes = 0;
180
                    $childNode = $this->sequenceTagNode();
181
                    $this->splitter->switch($this->contexts->root);
182
183
                    if ($childNode) {
184
                        end($this->nodeStack)->addChild($childNode);
185
                    }
186
                    break;
187
188
                case self::BYTE_NULL:
189
                    break;
190
            }
191
        }
192
193
        // If there is more than a single node remaining in the stack this indicates an error. More precisely it
194
        // indicates that some function called in the above switch added a node to the stack but failed to remove it
195
        // before returning, which usually indicates that the template contains one or more incorrectly closed tags.
196
        // In order to report this as error we collect the classes of every remaining node in the stack. Unfortunately
197
        // we cannot report the position of where the closing tag was expected - this is simply not known to Fluid.
198
        if (count($this->nodeStack) !== 1) {
199
            $names = [];
200
            while (($unterminatedNode = array_pop($this->nodeStack))) {
201
                $names[] = get_class($unterminatedNode);
202
            }
203
            throw $this->createErrorAtPosition(
204
                'Unterminated node(s) detected: ' . implode(', ', array_reverse($names)),
205
                1562671632
206
            );
207
        }
208
209
        // Finishing sequencing means returning the single node that remains in the node stack, firing the onClose
210
        // method on it and assigning the rendering context to the ArgumentCollection carried by the root node.
211
        $node = array_pop($this->nodeStack)->onClose($this->renderingContext);
212
        $node->getArguments()->setRenderingContext($this->renderingContext);
213
        return $node;
214
    }
215
216
    public function sequenceUntilClosingTagAndIgnoreNested(ComponentInterface $parent, ?string $namespace, string $method): void
217
    {
218
        // Special method of sequencing which completely ignores any and all Fluid code inside a tag if said tag is
219
        // associated with a Component that implements SequencingComponentInterface and calls this method as a default
220
        // implementation of an "ignore everything until closed" type of behavior. Exists in Sequencer since this is
221
        // the most common expected use case which would otherwise 1) be likely to become duplicated, or 2) require the
222
        // use of a trait or base class for this single method alone. Since the Component which implements the signal
223
        // interface already receives the Sequencer instance it is readily available without composition concerns.
224
        $matchingTag = $namespace ? $namespace . ':' . $method : $method;
225
        $matchingTagLength = strlen($matchingTag);
226
        $ignoredNested = 0;
227
        $this->splitter->switch($this->contexts->inactive);
228
        $this->sequence->next();
229
        $text = '';
230
        foreach ($this->sequence as $symbol => $captured) {
231
            if ($symbol === self::BYTE_TAG_END && $captured !== null && strncmp($captured, $matchingTag, $matchingTagLength) === 0) {
232
                // An opening tag matching the parent tag - treat as text and add to ignored count.
233
                ++$ignoredNested;
234
            } elseif ($symbol === self::BYTE_TAG_END && $captured === '/' . $matchingTag) {
235
                // A closing version of the parent tag. Check counter; if zero, finish. If not, decrease ignored count.
236
                if ($ignoredNested === 0) {
237
                    $parent->addChild(new TextNode((string) substr($text, 0, -1)));
238
                    return;
239
                }
240
                --$ignoredNested;
241
            }
242
            $text .= (string) $captured . chr($symbol);
243
        }
244
245
        throw $this->createErrorAtPosition(
246
            'Unterminated inactive tag: ' . $matchingTag,
247
            1564665730
248
        );
249
    }
250
251
    protected function sequenceCharacterData(string $text): ComponentInterface
252
    {
253
        $this->splitter->switch($this->contexts->data);
254
        $this->sequence->next();
255
        foreach ($this->sequence as $symbol => $captured) {
256
            $text .= $captured;
257
            if ($symbol === self::BYTE_TAG_END && substr($this->source->source, $this->splitter->index - 3, 2) === ']]') {
258
                $text .= '>';
259
                break;
260
            }
261
        }
262
        return new TextNode($text);
263
    }
264
265
    /**
266
     * Sequence a Fluid feature toggle node. Does not return
267
     * any node, only toggles various features of the Fluid
268
     * parser configuration or assigns context parameters
269
     * like namespaces.
270
     *
271
     * For backwards compatibility we allow the toggle name
272
     * to be passed, which is used in an explicit check when
273
     * sequencing inline nodes to detect if a {namespace ...}
274
     * node was encountered, in which case, this is not known
275
     * until the "toggle" has already been captured.
276
     *
277
     * @param string|null $toggle
278
     */
279
    protected function sequenceToggleInstruction(?string $toggle = null): void
280
    {
281
        $this->splitter->switch($this->contexts->toggle);
282
        $this->sequence->next();
283
        $flag = null;
0 ignored issues
show
Unused Code introduced by
$flag is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
284
        foreach ($this->sequence as $symbol => $captured) {
285
            switch ($symbol) {
286
                case self::BYTE_WHITESPACE_SPACE:
287
                    $toggle = $toggle ?? $captured;
288
                    break;
289
                case self::BYTE_INLINE_END:
290
                    if ($toggle === 'namespace') {
291
                        $parts = explode('=', (string) $captured);
292
                        $this->resolver->addNamespace($parts[0], $parts[1] ?? null);
293
                        return;
294
                    }
295
296
                    $this->configuration->setFeatureState($toggle, $captured ?? true);
297
                    // Re-read the parser configuration and react accordingly to any flags that may have changed.
298
                    $this->escapingEnabled = $this->configuration->isFeatureEnabled(Configuration::FEATURE_ESCAPING);
299
                    if (!$this->configuration->isFeatureEnabled(Configuration::FEATURE_PARSING)) {
300
                        throw (new PassthroughSourceException('Source must be represented as raw string', 1563379852))
301
                            ->setSource((string)$this->sequenceRemainderAsText());
302
                    }
303
                    return;
304
            }
305
        }
306
        throw $this->createErrorAtPosition('Unterminated feature toggle', 1563383038);
307
    }
308
309
    protected function sequenceTagNode(): ?ComponentInterface
310
    {
311
        $arguments = null;
312
        $definitions = null;
313
        $text = '<';
314
        $key = null;
315
        $namespace = null;
316
        $method = null;
317
        $bytes = &$this->source->bytes;
318
        $node = new RootNode();
319
        $closesNode = null;
0 ignored issues
show
Unused Code introduced by
$closesNode is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
320
        $selfClosing = false;
321
        $closing = false;
322
        $escapingEnabledBackup = $this->escapingEnabled;
323
        $viewHelperNode = null;
324
325
        $this->splitter->switch($this->contexts->tag);
326
        $this->sequence->next();
327
        foreach ($this->sequence as $symbol => $captured) {
328
            $text .= $captured;
329
            switch ($symbol) {
330
                case self::BYTE_ARRAY_START:
331
                    // Possible P/CDATA section. Check text explicitly for match, if matched, begin parsing-insensitive
332
                    // pass through sequenceCharacterDataNode()
333
                    $text .= '[';
334
                    if ($text === '<![CDATA[' || $text === '<![PCDATA[') {
335
                        return $this->sequenceCharacterData($text);
336
                    }
337
                    break;
338
339
                case self::BYTE_INLINE:
340
                    $contextBefore = $this->splitter->context;
341
                    $collected = $this->sequenceInlineNodes(isset($namespace, $method));
342
                    $node->addChild(new TextNode($text));
343
                    $node->addChild($collected);
344
                    $text = '';
345
                    $this->splitter->switch($contextBefore);
346
                    break;
347
348
                case self::BYTE_SEPARATOR_EQUALS:
349
                    $key = $key . $captured;
350
                    $text .= '=';
351
                    if ($key === '') {
352
                        throw $this->createErrorAtPosition('Unexpected equals sign without preceding attribute/key name', 1561039838);
353
                    } elseif ($definitions !== null && !isset($definitions[$key]) && !$viewHelperNode->allowUndeclaredArgument($key)) {
354
                        $error = $this->createUnsupportedArgumentError($key, $definitions);
355
                        return new TextNode($this->renderingContext->getErrorHandler()->handleParserError($error));
356
                    }
357
                    break;
358
359
                case self::BYTE_QUOTE_DOUBLE:
360
                case self::BYTE_QUOTE_SINGLE:
361
                    $text .= chr($symbol);
362
                    if ($key === null) {
363
                        throw $this->createErrorAtPosition('Quoted value without a key is not allowed in tags', 1558952412);
364
                    }
365
                    if ($arguments->isArgumentBoolean($key)) {
366
                        $arguments[$key] = $this->sequenceBooleanNode()->flatten(true);
367
                    } else {
368
                        $arguments[$key] = $this->sequenceQuotedNode()->flatten(true);
369
                    }
370
                    $key = null;
371
                    break;
372
373
                case self::BYTE_TAG_CLOSE:
374
                    $method = $method ?? $captured;
375
                    $text .= '/';
376
                    $closing = true;
377
                    $selfClosing = $bytes[$this->splitter->index - 1] !== self::BYTE_TAG;
378
379
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) {
380
                        // Arguments may be pending: if $key is set we must create an ECMA literal style shorthand
381
                        // (attribute value is variable of same name as attribute). Two arguments may be created in
382
                        // this case, if both $key and $captured are non-null. The former contains a potentially
383
                        // pending argument and the latter contains a captured value-less attribute right before the
384
                        // tag closing character.
385
                        if ($key !== null) {
386
                            $arguments[$key] = new ObjectAccessorNode((string) $key);
387
                            $key = null;
388
                        }
389
                        // (see comment above) Hence, the two conditions must not be compunded to else-if.
390
                        if ($captured !== null) {
391
                            $arguments[$captured] = new ObjectAccessorNode($captured);
392
                        }
393
                    }
394
                    break;
395
396
                case self::BYTE_SEPARATOR_COLON:
397
                    $text .= ':';
398
                    if (!$method) {
399
                        // If we haven't encountered a method yet, then $method won't be set, and we can assign NS now
400
                        $namespace = $namespace ?? $captured;
401
                    } else {
402
                        // If we do have a method this means we encountered a colon as part of an attribute name
403
                        $key = $key ?? ($captured . ':');
404
                    }
405
                    break;
406
407
                case self::BYTE_TAG_END:
408
                    $text .= '>';
409
                    $method = $method ?? $captured;
410
411
                    $this->escapingEnabled = $escapingEnabledBackup;
412
413
                    if (($namespace === null && ($this->splitter->context->context === Context::CONTEXT_DEAD || !$this->resolver->isAliasRegistered((string) $method))) || $this->resolver->isNamespaceIgnored((string) $namespace)) {
414
                        return $node->addChild(new TextNode($text))->flatten();
415
                    }
416
417
                    try {
418
                        if (!$closing || $selfClosing) {
419
                            $viewHelperNode = $viewHelperNode ?? $this->resolver->createViewHelperInstance($namespace, (string) $method);
420
                            $viewHelperNode->onOpen($this->renderingContext)->getArguments()->validate();
421
                        } else {
422
                            // $closing will be true and $selfClosing false; add to stack, continue with children.
423
                            $viewHelperNode = array_pop($this->nodeStack);
424
                            $expectedClass = $this->resolver->resolveViewHelperClassName($namespace, (string) $method);
425
                            if (!$viewHelperNode instanceof $expectedClass) {
426
                                throw $this->createErrorAtPosition(
427
                                    sprintf(
428
                                        'Mismatched closing tag. Expecting: %s:%s (%s). Found: (%s).',
429
                                        $namespace,
430
                                        $method,
431
                                        $expectedClass,
432
                                        get_class($viewHelperNode)
433
                                    ),
434
                                    1557700789
435
                                );
436
                            }
437
                        }
438
                    } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
439
                        $error = $this->createErrorAtPosition($exception->getMessage(), $exception->getCode());
440
                        return new TextNode($this->renderingContext->getErrorHandler()->handleParserError($error));
441
                    }
442
443
                    // Possibly pending argument still needs to be processed since $key is not null. Create an ECMA
444
                    // literal style associative array entry. Do the same for $captured.
445
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) {
446
                        if ($key !== null) {
447
                            $value = new ObjectAccessorNode((string) $key);
448
                            $arguments[$key] = $value;
449
                        }
450
451
                        if ($captured !== null) {
452
                            $value = new ObjectAccessorNode((string) $captured);
453
                            $arguments[$captured] = $value;
454
                        }
455
                    }
456
457
                    if (!$closing) {
458
                        // The node is neither a closing or self-closing node (= an opening node expecting tag content).
459
                        // Add it to the stack and return null to return the Sequencer to "root" context and continue
460
                        // sequencing the tag's body - parsed nodes then get attached to this node as children.
461
                        $viewHelperNode = $this->callInterceptor($viewHelperNode, self::INTERCEPT_OPENING_VIEWHELPER);
462
                        if ($viewHelperNode instanceof SequencingComponentInterface) {
463
                            // The Component will take over sequencing. It will return if encountering the right closing
464
                            // tag - so when it returns, we reached the end of the Component and must pop the stack.
465
                            $viewHelperNode->sequence($this, $namespace, (string) $method);
466
                            return $viewHelperNode;
467
                        }
468
                        $this->nodeStack[] = $viewHelperNode;
469
                        return null;
470
                    }
471
472
                    $viewHelperNode = $viewHelperNode->onClose($this->renderingContext);
473
474
                    $viewHelperNode = $this->callInterceptor(
475
                        $viewHelperNode,
476
                        $selfClosing ? self::INTERCEPT_SELFCLOSING_VIEWHELPER : self::INTERCEPT_CLOSING_VIEWHELPER
477
                    );
478
479
                    return $viewHelperNode;
480
481
                case self::BYTE_WHITESPACE_TAB:
482
                case self::BYTE_WHITESPACE_RETURN:
483
                case self::BYTE_WHITESPACE_EOL:
484
                case self::BYTE_WHITESPACE_SPACE:
485
                    $text .= chr($symbol);
486
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) {
487
                        if ($captured !== null) {
488
                            // Encountering this case means we've collected a previous key and now collected a non-empty
489
                            // string value before encountering an equals sign. This is treated as ECMA literal short
490
                            // hand equivalent of having written `attr="{attr}"` in the Fluid template.
491
                            $key = $captured;
492
                        }
493
                    } elseif ($namespace !== null || (!isset($namespace, $method) && $this->resolver->isAliasRegistered((string)$captured))) {
494
                        $method = $captured;
495
496
                        try {
497
                            $viewHelperNode = $this->resolver->createViewHelperInstance($namespace, $method);
498
                            $arguments = $viewHelperNode->getArguments();
499
                            $definitions = $arguments->getDefinitions();
500
                        } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
501
                            $error = $this->createErrorAtPosition($exception->getMessage(), $exception->getCode());
502
                            return new TextNode($this->renderingContext->getErrorHandler()->handleParserError($error));
503
                        }
504
505
                        // Forcibly disable escaping OFF as default decision for whether or not to escape an argument.
506
                        $this->escapingEnabled = false;
507
                        $this->splitter->switch($this->contexts->attributes);
508
                        break;
509
                    } else {
510
                        // A whitespace before a colon means the tag is not a namespaced tag. We will ignore everything
511
                        // inside this tag, except for inline syntax, until the tag ends. For this we use a special,
512
                        // limited variant of the root context where instead of scanning for "<" we scan for ">".
513
                        // We continue in this same loop because it still matches the potential symbols being yielded.
514
                        // Most importantly: this new reduced context will NOT match a colon which is the trigger symbol
515
                        // for a ViewHelper tag.
516
                        $this->splitter->switch($this->contexts->dead);
517
                    }
518
                    break;
519
            }
520
        }
521
522
        // This case on the surface of it, belongs as "default" case in the switch above. However, the only case that
523
        // would *actually* produce this error, is if the splitter reaches EOF (null byte) symbol before the tag was
524
        // closed. Literally every other possible error type will be thrown as more specific exceptions (e.g. invalid
525
        // argument, missing key, wrong quotes, bad inline and *everything* else with the exception of EOF). Even a
526
        // stray null byte would not be caught here as null byte is not part of the symbol collection for "tag" context.
527
        throw $this->createErrorAtPosition('Unexpected token in tag sequencing', 1557700786);
528
    }
529
530
    protected function sequenceInlineNodes(bool $allowArray = true): ComponentInterface
531
    {
532
        $text = '{';
533
        /** @var ComponentInterface|null $node */
534
        $node = null;
535
        $key = null;
536
        $namespace = null;
537
        $method = null;
0 ignored issues
show
Unused Code introduced by
$method is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
538
        $definitions = null;
0 ignored issues
show
Unused Code introduced by
$definitions is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
539
        $potentialAccessor = null;
540
        $callDetected = false;
541
        $hasPass = false;
542
        $hasColon = null;
543
        $hasWhitespace = false;
544
        $isArray = false;
545
        $arguments = new ArgumentCollection();
546
        $parts = [];
547
        $ignoredEndingBraces = 0;
548
        $countedEscapes = 0;
549
        $restore = $this->splitter->switch($this->contexts->inline);
550
        $this->sequence->next();
551
        foreach ($this->sequence as $symbol => $captured) {
552
            $text .= $captured;
553
            switch ($symbol) {
554
                case self::BYTE_AT:
555
                    $this->sequenceToggleInstruction();
556
                    $this->splitter->switch($restore);
557
                    return new TextNode('');
558
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
559
560
                case self::BYTE_BACKSLASH:
561
                    // Increase the number of counted escapes (is passed to sequenceNode() in the "QUOTE" cases and reset
562
                    // after the quoted string is extracted).
563
                    ++$countedEscapes;
564
                    if ($hasWhitespace) {
565
                        $node = $node ?? new RootNode();
566
                    } else {
567
                        $node = $node ?? new ObjectAccessorNode();
568
                    }
569
                    if ($captured !== null) {
570
                        $node->addChild(new TextNode((string) $captured));
571
                    }
572
                    break;
573
574
                case self::BYTE_ARRAY_START:
575
                    $text .= chr($symbol);
576
                    $isArray = $allowArray;
577
578
                    // Sequence the node. Pass the "use numeric keys?" boolean based on the current byte. Only array
579
                    // start creates numeric keys. Inline start with keyless values creates ECMA style {foo:foo, bar:bar}
580
                    // from {foo, bar}.
581
                    $arguments[$key ?? $captured ?? 0] = $node = new ArrayNode();
582
                    $this->sequenceArrayNode($node, true);
583
                    $key = null;
584
                    break;
585
586
                case self::BYTE_INLINE:
587
                    // Encountering this case can mean different things: sub-syntax like {foo.{index}} or array, depending
588
                    // on presence of either a colon or comma before the inline. In protected mode it is simply added.
589
                    $text .= '{';
590
                    $node = $node ?? new ObjectAccessorNode();
591
                    if ($countedEscapes > 0) {
592
                        ++$ignoredEndingBraces;
593
                        $countedEscapes = 0;
594
                        if ($captured !== null) {
595
                            $node->addChild(new TextNode((string)$captured));
596
                        }
597
                    } elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED) {
598
                        // Ignore one ending additional curly brace. Subtracted in the BYTE_INLINE_END case below.
599
                        // The expression in this case looks like {{inline}.....} and we capture the curlies.
600
                        $potentialAccessor .= $captured;
601
                    } elseif ($allowArray || $isArray) {
602
                        $isArray = true;
603
                        $captured = $key ?? $captured ?? $potentialAccessor;
604
                        // This is a sub-syntax following a colon - meaning it is an array.
605
                        if ($captured !== null) {
606
                            $arguments[$key ?? $captured ?? 0] = $node = new ArrayNode();
607
                            $this->sequenceArrayNode($node);
608
                        }
609
                    } else {
610
                        if ($captured !== null) {
611
                            $node->addChild(new TextNode((string) $captured));
612
                        }
613
                        $childNodeToAdd = $this->sequenceInlineNodes($allowArray);
614
                        $node->addChild($childNodeToAdd);
615
                    }
616
                    break;
617
618
                case self::BYTE_MINUS:
619
                    $text .= '-';
620
                    $potentialAccessor = $potentialAccessor ?? $captured;
621
                    break;
622
623
                // Backtick may be encountered in two different contexts: normal inline context, in which case it has
624
                // the same meaning as any quote and causes sequencing of a quoted string. Or protected context, in
625
                // which case it also sequences a quoted node but appends the result instead of assigning to array.
626
                // Note that backticks do not support escapes (they are a new feature that does not require escaping).
627
                case self::BYTE_BACKTICK:
628
                    if ($this->splitter->context->context === Context::CONTEXT_PROTECTED) {
629
                        $node->addChild(new TextNode($text));
630
                        $node->addChild($this->sequenceQuotedNode()->flatten());
631
                        $text = '';
632
                        break;
633
                    }
634
                // Fallthrough is intentional: if not in protected context, consider the backtick a normal quote.
635
636
                // Case not normally countered in straight up "inline" context, but when encountered, means we have
637
                // explicitly found a quoted array key - and we extract it.
638
                case self::BYTE_QUOTE_SINGLE:
639
                case self::BYTE_QUOTE_DOUBLE:
640
                    if (!$allowArray) {
641
                        $text .= chr($symbol);
642
                        break;
643
                    }
644
                    if ($key !== null) {
645
                        $arguments[$key] = $this->sequenceQuotedNode($countedEscapes)->flatten(true);
646
                        $key = null;
647
                    } else {
648
                        $key = $this->sequenceQuotedNode($countedEscapes)->flatten(true);
649
                    }
650
                    $countedEscapes = 0;
651
                    $isArray = $allowArray;
652
                    break;
653
654
                case self::BYTE_SEPARATOR_COMMA:
655
                    if (!$allowArray) {
656
                        $text .= ',';
657
                        break;
658
                    }
659
                    if ($captured !== null) {
660
                        $arguments[$key ?? $captured] = new ObjectAccessorNode($captured);
661
                    }
662
                    $key = null;
663
                    $isArray = $allowArray;
664
                    break;
665
666
                case self::BYTE_SEPARATOR_EQUALS:
667
                    $text .= '=';
668
                    if (!$allowArray) {
669
                        $node = new RootNode();
670
                        $this->splitter->switch($this->contexts->protected);
671
                        break;
672
                    }
673
                    $key = $captured;
674
                    $isArray = $allowArray;
675
                    break;
676
677
                case self::BYTE_SEPARATOR_COLON:
678
                    $text .= ':';
679
                    $hasColon = true;
680
                    $namespace = $captured;
681
                    $key = $key ?? $captured;
682
                    $isArray = $isArray || ($allowArray && is_numeric($key));
683
                    if ($captured !== null) {
684
                        $parts[] = $captured;
685
                    }
686
                    $parts[] = ':';
687
                    break;
688
689
                case self::BYTE_WHITESPACE_SPACE:
690
                case self::BYTE_WHITESPACE_EOL:
691
                case self::BYTE_WHITESPACE_RETURN:
692
                case self::BYTE_WHITESPACE_TAB:
693
                    // If we already collected some whitespace we must enter protected context.
694
                    $text .= $this->source->source[$this->splitter->index - 1];
695
696
                    if ($captured !== null) {
697
                        // Store a captured part: a whitespace inside inline syntax will engage the expression matching
698
                        // that occurs when the node is closed. Allows matching the various parts to create the appropriate
699
                        // node type.
700
                        $parts[] = $captured;
701
                    }
702
703
                    if ($hasWhitespace && !$hasPass && !$allowArray) {
704
                        // Protection mode: this very limited context does not allow tags or inline syntax, and will
705
                        // protect things like CSS and JS - and will only enter a more reactive context if encountering
706
                        // the backtick character, meaning a quoted string will be sequenced. This backtick-quoted
707
                        // string can then contain inline syntax like variable accessors.
708
                        $node = $node ?? new RootNode();
709
                        $this->splitter->switch($this->contexts->protected);
710
                        break;
711
                    }
712
713
                    if ($captured === 'namespace') {
714
                        // Special case: we catch namespace definitions with {namespace xyz=foo} syntax here, although
715
                        // the proper way with current code is to use {@namespace xyz=foo}. We have this case here since
716
                        // it is relatively cheap (only happens when we see a space inside inline and a straight-up
717
                        // string comparison with strict types enabled). We then return an empty TextNode which is
718
                        // ignored by the parent node when attached so we don't create any output.
719
                        $this->sequenceToggleInstruction('namespace');
720
                        $this->splitter->switch($restore);
721
                        return new TextNode('');
722
                    }
723
                    $key = $key ?? $captured;
724
                    $hasWhitespace = true;
725
                    $isArray = $allowArray && ($hasColon ?? $isArray ?? is_numeric($captured));
726
                    $potentialAccessor = ($potentialAccessor ?? $captured);
727
                    break;
728
729
                case self::BYTE_TAG_END:
730
                case self::BYTE_PIPE:
731
                    // If there is an accessor on the left side of the pipe and $node is not defined, we create $node
732
                    // as an object accessor. If $node already exists we do nothing (and expect the VH trigger, the
733
                    // parenthesis start case below, to add $node as childnode and create a new $node).
734
                    $hasPass = true;
735
                    $isArray = $allowArray;
736
                    $callDetected = false;
737
738
                    $text .=  $this->source->source[$this->splitter->index - 1];
739
                    $node = $node ?? new ObjectAccessorNode();
740
                    if ($potentialAccessor ?? $captured) {
741
                        $node->addChild(new TextNode($potentialAccessor . $captured));
742
                    }
743
744
                    $potentialAccessor = $namespace = $method = $key = null;
0 ignored issues
show
Unused Code introduced by
$method is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
745
                    break;
746
747
                case self::BYTE_PARENTHESIS_START:
748
                    $isArray = false;
749
                    // Special case: if a parenthesis start was preceded by whitespace but had no pass operator we are
750
                    // not dealing with a ViewHelper call and will continue the sequencing, grabbing the parenthesis as
751
                    // part of the expression.
752
                    $text .= '(';
753
                    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...
754
                        $this->splitter->switch($this->contexts->protected);
755
                        $namespace = $method = null;
0 ignored issues
show
Unused Code introduced by
$method is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
756
                        break;
757
                    }
758
759
                    $callDetected = true;
760
                    $method = $captured;
761
                    $childNodeToAdd = $node;
762
                    try {
763
                        $node = $this->resolver->createViewHelperInstance($namespace, $method);
764
                        $arguments = $node->getArguments();
765
                        $node = $this->callInterceptor($node, self::INTERCEPT_OPENING_VIEWHELPER);
766
                    } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
767
                        throw $this->createErrorAtPosition($exception->getMessage(), $exception->getCode());
768
                    }
769
770
                    $this->sequenceArrayNode($arguments);
771
                    $arguments->setRenderingContext($this->renderingContext)->validate();
772
                    $node = $node->onOpen($this->renderingContext);
773
774
                    if ($childNodeToAdd) {
775
                        if ($childNodeToAdd instanceof ObjectAccessorNode) {
776
                            $childNodeToAdd = $this->callInterceptor($childNodeToAdd, self::INTERCEPT_OBJECTACCESSOR);
777
                        }
778
                        $node->addChild($childNodeToAdd);
779
                    }
780
                    $node = $node->onClose($this->renderingContext);
781
                    $text .= ')';
782
                    $potentialAccessor = null;
783
                    break;
784
785
                case self::BYTE_INLINE_END:
786
                    $text .= '}';
787
788
                    if (--$ignoredEndingBraces >= 0) {
789
                        if ($captured !== null) {
790
                            $node->addChild(new TextNode('{' . $captured . '}'));
791
                        }
792
                        break;
793
                    }
794
795
                    if ($text === '{}') {
796
                        // Edge case handling of empty JS objects
797
                        return new TextNode('{}');
798
                    }
799
800
                    $isArray = $allowArray && ($isArray ?: ($hasColon && !$hasPass && !$callDetected));
801
                    $potentialAccessor .= $captured;
802
                    $interceptionPoint = self::INTERCEPT_OBJECTACCESSOR;
803
804
                    // Decision: if we did not detect a ViewHelper we match the *entire* expression, from the cached
805
                    // starting index, to see if it matches a known type of expression. If it does, we must return the
806
                    // appropriate type of ExpressionNode.
807
                    if ($isArray) {
808
                        if ($captured !== null) {
809
                            $arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
810
                        }
811
                        $this->splitter->switch($restore);
812
                        return $arguments->toArrayNode();
813
                    } elseif ($callDetected) {
814
                        // The first-priority check is for a ViewHelper used right before the inline expression ends,
815
                        // in which case there is no further syntax to come.
816
                        $arguments->validate();
817
                        $node = $node->onOpen($this->renderingContext)->onClose($this->renderingContext);
818
                        $interceptionPoint = self::INTERCEPT_SELFCLOSING_VIEWHELPER;
819
                    } elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED || ($hasWhitespace && !$callDetected && !$hasPass)) {
820
                        // In order to qualify for potentially being an expression, the entire inline node must contain
821
                        // whitespace, must not contain parenthesis, must not contain a colon and must not contain an
822
                        // inline pass operand. This significantly limits the number of times this (expensive) routine
823
                        // has to be executed.
824
                        $parts[] = $captured;
825
                        try {
826
                            foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) {
827
                                if ($expressionNodeTypeClassName::matches($parts)) {
828
                                    $childNodeToAdd = new $expressionNodeTypeClassName($parts);
829
                                    $childNodeToAdd = $this->callInterceptor($childNodeToAdd, self::INTERCEPT_EXPRESSION);
830
                                    break;
831
                                }
832
                            }
833
                        } catch (ExpressionException $exception) {
834
                            // ErrorHandler will either return a string or throw the exception anew, depending on the
835
                            // exact implementation of ErrorHandlerInterface. When it returns a string we use that as
836
                            // text content of a new TextNode so the message is output as part of the rendered result.
837
                            $childNodeToAdd = new TextNode(
838
                                $this->renderingContext->getErrorHandler()->handleExpressionError($exception)
839
                            );
840
                        }
841
                        $node = $childNodeToAdd ?? ($node ?? new RootNode())->addChild(new TextNode($text));
842
                        return $node;
843
                    } elseif (!$hasPass && !$callDetected) {
844
                        $node = $node ?? new ObjectAccessorNode();
845
                        if ($potentialAccessor !== '') {
846
                            $node->addChild(new TextNode((string) $potentialAccessor));
847
                        }
848
                    } elseif ($hasPass && $this->resolver->isAliasRegistered((string) $potentialAccessor)) {
849
                        // Fourth priority check is for a pass to a ViewHelper alias, e.g. "{value | raw}" in which case
850
                        // we look for the alias used and create a ViewHelper with no arguments.
851
                        $childNodeToAdd = $node;
852
                        $node = $this->resolver->createViewHelperInstance(null, (string) $potentialAccessor);
853
                        $arguments = $node->getArguments()->validate()->setRenderingContext($this->renderingContext);
0 ignored issues
show
Unused Code introduced by
$arguments is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
854
                        $node = $node->onOpen($this->renderingContext);
855
                        $node->addChild($childNodeToAdd);
856
                        $node->onClose(
857
                            $this->renderingContext
858
                        );
859
                        $interceptionPoint = self::INTERCEPT_SELFCLOSING_VIEWHELPER;
860
                    } else {
861
                        # TODO: should this be an error case, or should it result in a TextNode?
862
                        throw $this->createErrorAtPosition(
863
                            'Invalid inline syntax - not accessor, not expression, not array, not ViewHelper, but ' .
864
                            'contains the tokens used by these in a sequence that is not valid Fluid syntax. You can ' .
865
                            'most likely avoid this by adding whitespace inside the curly braces before the first ' .
866
                            'Fluid-like symbol in the expression. Symbols recognized as Fluid are: "' .
867
                            addslashes(implode('","', array_map('chr', $this->contexts->inline->bytes))) . '"',
868
                            1558782228
869
                        );
870
                    }
871
872
                    $node = $this->callInterceptor($node, $interceptionPoint);
873
                    $this->splitter->switch($restore);
874
                    return $node;
875
            }
876
        }
877
878
        // See note in sequenceTagNode() end of method body. TL;DR: this is intentionally here instead of as "default"
879
        // case in the switch above for a very specific reason: the case is only encountered if seeing EOF before the
880
        // inline expression was closed.
881
        throw $this->createErrorAtPosition('Unterminated inline syntax', 1557838506);
882
    }
883
884
    protected function sequenceBooleanNode(int $leadingEscapes = 0): BooleanNode
885
    {
886
        $startingByte = $this->source->bytes[$this->splitter->index];
887
        $closingByte = $startingByte === self::BYTE_PARENTHESIS_START ? self::BYTE_PARENTHESIS_END : $startingByte;
888
        $countedEscapes = 0;
889
        $node = new BooleanNode();
890
        $restore = $this->splitter->switch($this->contexts->boolean);
891
        $this->sequence->next();
892
        foreach ($this->sequence as $symbol => $captured) {
893
            if ($captured !== null) {
894
                $node->addChild(new TextNode($captured));
895
            }
896
            switch ($symbol) {
897
                case self::BYTE_INLINE:
898
                    $node->addChild($this->sequenceInlineNodes(true));
899
                    break;
900
901
                case self::BYTE_QUOTE_DOUBLE:
902
                case self::BYTE_QUOTE_SINGLE:
903
                    if ($symbol === $closingByte && $countedEscapes === $leadingEscapes) {
904
                        $this->splitter->switch($restore);
905
                        return $node;
906
                    }
907
                    // Sequence a quoted node and set the "quoted" flag on the resulting root node (which is not
908
                    // flattened even if it contains a single child). This allows the BooleanNode to enforce a string
909
                    // value whenever parts of the expression are quoted, indicating user explicitly wants string type.
910
                    $node->addChild($this->sequenceQuotedNode($countedEscapes)->setQuoted(true));
911
                    break;
912
913
                case self::BYTE_PARENTHESIS_START:
914
                    $node->addChild($this->sequenceBooleanNode());
915
                    break;
916
917
                case self::BYTE_PARENTHESIS_END:
918
                    $this->splitter->switch($restore);
919
                    return $node;
920
921
                case self::BYTE_WHITESPACE_SPACE:
922
                case self::BYTE_WHITESPACE_TAB:
923
                case self::BYTE_WHITESPACE_RETURN:
924
                case self::BYTE_WHITESPACE_EOL:
925
                    break;
926
927
                case self::BYTE_BACKSLASH:
928
                    ++$countedEscapes;
929
                    break;
930
            }
931
            if ($symbol !== self::BYTE_BACKSLASH) {
932
                $countedEscapes = 0;
933
            }
934
        }
935
        throw $this->createErrorAtPosition('Unterminated boolean expression', 1564159986);
936
    }
937
938
    protected function sequenceArrayNode(\ArrayAccess &$array, bool $numeric = false): void
939
    {
940
        $definitions = null;
941
        if ($array instanceof ArgumentCollection) {
942
            $definitions = $array->getDefinitions();
943
        }
944
945
        $keyOrValue = null;
946
        $key = null;
947
        $value = null;
0 ignored issues
show
Unused Code introduced by
$value is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
948
        $itemCount = -1;
949
        $countedEscapes = 0;
950
        $escapingEnabledBackup = $this->escapingEnabled;
951
952
        $restore = $this->splitter->switch($this->contexts->array);
953
        $this->sequence->next();
954
        foreach ($this->sequence as $symbol => $captured) {
955
            switch ($symbol) {
956
                case self::BYTE_SEPARATOR_COLON:
957
                case self::BYTE_SEPARATOR_EQUALS:
958
                    // Colon or equals has same meaning (which allows tag syntax as argument syntax). Encountering this
959
                    // byte always means the preceding byte was a key. However, if nothing was captured before this,
960
                    // it means colon or equals was used without a key which is a syntax error.
961
                    $key = $key ?? $captured ?? ($keyOrValue !== null ? $keyOrValue->flatten(true) : null);
962
                    if ($key === null) {
963
                        throw $this->createErrorAtPosition('Unexpected colon or equals sign, no preceding key', 1559250839);
964
                    }
965
                    if ($definitions !== null && !$numeric && !isset($definitions[$key])) {
966
                        throw $this->createUnsupportedArgumentError((string)$key, $definitions);
967
                    }
968
                    break;
969
970
                case self::BYTE_ARRAY_START:
971
                case self::BYTE_INLINE:
972
                    // Minimal safeguards to improve error feedback. Theoretically such "garbage" could simply be ignored
973
                    // without causing problems to the parser, but it is probably best to report it as it could indicate
974
                    // the user expected X value but gets Y and doesn't notice why.
975
                    if ($captured !== null) {
976
                        throw $this->createErrorAtPosition('Unexpected content before array/inline start in associative array, ASCII: ' . ord($captured), 1559131849);
977
                    }
978
                    if ($key === null && !$numeric) {
979
                        throw $this->createErrorAtPosition('Unexpected array/inline start in associative array without preceding key', 1559131848);
980
                    }
981
982
                    // Encountering a curly brace or square bracket start byte will both cause a sub-array to be sequenced,
983
                    // the difference being that only the square bracket will cause third parameter ($numeric) passed to
984
                    // sequenceArrayNode() to be true, which in turn causes key-less items to be added with numeric indexes.
985
                    $key = $key ?? ++$itemCount;
986
                    $arrayNode = new ArrayNode();
987
                    $this->sequenceArrayNode($arrayNode, $symbol === self::BYTE_ARRAY_START);
988
                    $array[$key] = $arrayNode;
989
                    $keyOrValue = null;
990
                    $key = null;
991
                    break;
992
993
                case self::BYTE_QUOTE_SINGLE:
994
                case self::BYTE_QUOTE_DOUBLE:
995
                    // Safeguard: if anything is captured before a quote this indicates garbage leading content. As with
996
                    // the garbage safeguards above, this one could theoretically be ignored in favor of silently making
997
                    // the odd syntax "just work".
998
                    if ($captured !== null) {
999
                        throw $this->createErrorAtPosition('Unexpected content before quote start in associative array, ASCII: ' . ord($captured), 1559145560);
1000
                    }
1001
1002
                    // Quotes will always cause sequencing of the quoted string, but differs in behavior based on whether
1003
                    // or not the $key is set. If $key is set, we know for sure we can assign a value. If it is not set
1004
                    // we instead leave $keyOrValue defined so this will be processed by one of the next iterations.
1005
                    if (isset($key, $definitions[$key]) && $definitions[$key]->getType() === 'boolean') {
1006
                        $keyOrValue = $this->sequenceBooleanNode($countedEscapes);
1007
                    } else {
1008
                        $keyOrValue = $this->sequenceQuotedNode($countedEscapes);
1009
                    }
1010
                    if ($key !== null) {
1011
                        $array[$key] = $keyOrValue->flatten(true);
1012
                        $keyOrValue = null;
1013
                        $key = null;
1014
                        $countedEscapes = 0;
1015
                    }
1016
                    break;
1017
1018
                case self::BYTE_SEPARATOR_COMMA:
1019
                    // Comma separator: if we've collected a key or value, use it. Otherwise, use captured string.
1020
                    // If neither key nor value nor captured string exists, ignore the comma (likely a tailing comma).
1021
                    $value = null;
1022
                    if ($keyOrValue !== null) {
1023
                        // Key or value came as quoted string and exists in $keyOrValue
1024
                        $potentialValue = $keyOrValue->flatten(true);
1025
                        $key = $numeric ? ++$itemCount : $potentialValue;
1026
                        $value = $numeric ? $potentialValue : (is_numeric($key) ? $key + 0 : new ObjectAccessorNode((string) $key));
1027
                    } elseif ($captured !== null) {
1028
                        $key = $key ?? ($numeric ? ++$itemCount : $captured);
1029
                        if (!$numeric && $definitions !== null && !isset($definitions[$key])) {
1030
                            throw $this->createUnsupportedArgumentError((string)$key, $definitions);
1031
                        }
1032
                        $value = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
1033
                    }
1034
                    if ($value !== null) {
1035
                        $array[$key] = $value;
1036
                    }
1037
                    $keyOrValue = null;
1038
                    $value = null;
0 ignored issues
show
Unused Code introduced by
$value is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1039
                    $key = null;
1040
                    break;
1041
1042
                case self::BYTE_WHITESPACE_TAB:
1043
                case self::BYTE_WHITESPACE_RETURN:
1044
                case self::BYTE_WHITESPACE_EOL:
1045
                case self::BYTE_WHITESPACE_SPACE:
1046
                    // Any whitespace attempts to set the key, if not already set. The captured string may be null as
1047
                    // well, leaving the $key variable still null and able to be coalesced.
1048
                    $key = $key ?? $captured;
1049
                    break;
1050
1051
                case self::BYTE_BACKSLASH:
1052
                    // Escapes are simply counted and passed to the sequenceQuotedNode() method, causing that method
1053
                    // to ignore exactly this number of backslashes before a matching quote is seen as closing quote.
1054
                    ++$countedEscapes;
1055
                    break;
1056
1057
                case self::BYTE_INLINE_END:
1058
                case self::BYTE_ARRAY_END:
1059
                case self::BYTE_PARENTHESIS_END:
1060
                    // Array end indication. Check if anything was collected previously or was captured currently,
1061
                    // assign that to the array and return an ArrayNode with the full array inside.
1062
                    $captured = $captured ?? ($keyOrValue !== null ? $keyOrValue->flatten(true) : null);
1063
                    $key = $key ?? ($numeric ? ++$itemCount : $captured);
1064
                    if (isset($captured, $key)) {
1065
                        if (is_numeric($captured)) {
1066
                            $array[$key] = $captured + 0;
1067
                        } elseif ($keyOrValue !== null) {
1068
                            $array[$key] = $keyOrValue->flatten();
1069
                        } else {
1070
                            $array[$key] = new ObjectAccessorNode((string) ($captured ?? $key));
1071
                        }
1072
                    }
1073
                    if (!$numeric && isset($key, $definitions) && !isset($definitions[$key])) {
1074
                        throw $this->createUnsupportedArgumentError((string)$key, $definitions);
1075
                    }
1076
                    $this->escapingEnabled = $escapingEnabledBackup;
1077
                    $this->splitter->switch($restore);
1078
                    return;
1079
            }
1080
        }
1081
1082
        throw $this->createErrorAtPosition(
1083
            'Unterminated array',
1084
            1557748574
1085
        );
1086
    }
1087
1088
    /**
1089
     * Sequence a quoted value
1090
     *
1091
     * The return can be either of:
1092
     *
1093
     * 1. A string value if source was for example "string"
1094
     * 2. An integer if source was for example "1"
1095
     * 3. A float if source was for example "1.25"
1096
     * 4. A RootNode instance with multiple child nodes if source was for example "string {var}"
1097
     *
1098
     * The idea is to return the raw value if there is no reason for it to
1099
     * be a node as such - which is only necessary if the quoted expression
1100
     * contains other (dynamic) values like an inline syntax.
1101
     *
1102
     * @param int $leadingEscapes A backwards compatibility measure: when passed, this number of escapes must precede a closing quote for it to trigger node closing.
1103
     * @return RootNode
1104
     */
1105
    protected function sequenceQuotedNode(int $leadingEscapes = 0): RootNode
1106
    {
1107
        $startingByte = $this->source->bytes[$this->splitter->index];
1108
        $node = new RootNode();
1109
        $countedEscapes = 0;
1110
1111
        $contextToRestore = $this->splitter->switch($this->contexts->quoted);
1112
        $this->sequence->next();
1113
        foreach ($this->sequence as $symbol => $captured) {
1114
            switch ($symbol) {
1115
1116
                case self::BYTE_ARRAY_START:
1117
                    $countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored)
1118
                    if ($captured === null) {
1119
                        // Array start "[" only triggers array sequencing if it is the very first byte in the quoted
1120
                        // string - otherwise, it is added as part of the text.
1121
                        $child = new ArrayNode();
1122
                        $this->sequenceArrayNode($child, true);
1123
                        $node->addChild($child);
0 ignored issues
show
Documentation introduced by
$child is of type object<ArrayAccess>, but the function expects a object<TYPO3Fluid\Fluid\...ent\ComponentInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1124
                    } else {
1125
                        $node->addChild(new TextNode($captured . '['));
1126
                    }
1127
                    break;
1128
1129
                case self::BYTE_INLINE:
1130
                    $countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored)
1131
                    // The quoted string contains a sub-expression. We extract the captured content so far and if it
1132
                    // is not an empty string, add it as a child of the RootNode we're building, then we add the inline
1133
                    // expression as next sibling and continue the loop.
1134
                    if ($captured !== null) {
1135
                        $childNode = new TextNode($captured);
1136
                        $node->addChild($childNode);
1137
                    }
1138
1139
                    $node->addChild($this->sequenceInlineNodes());
1140
                    break;
1141
1142
                case self::BYTE_BACKSLASH:
1143
                    $next = $this->source->bytes[$this->splitter->index + 1] ?? null;
1144
                    ++$countedEscapes;
1145
                    if ($next === $startingByte || $next === self::BYTE_BACKSLASH) {
1146
                        if ($captured !== null) {
1147
                            $node->addChild(new TextNode($captured));
1148
                        }
1149
                    } else {
1150
                        $node->addChild(new TextNode($captured . str_repeat('\\', $countedEscapes)));
1151
                        $countedEscapes = 0;
1152
                    }
1153
                    break;
1154
1155
                // Note: although "case $startingByte:" could have been used here, it would not compile the switch
1156
                // as a hash map and thus would not perform as well overall - when called frequently as it will be.
1157
                // Backtick will only be encountered if the context is "protected" (insensitive inline sequencing)
1158
                case self::BYTE_QUOTE_SINGLE:
1159
                case self::BYTE_QUOTE_DOUBLE:
1160
                case self::BYTE_BACKTICK:
1161
                    if ($symbol !== $startingByte || $countedEscapes !== $leadingEscapes) {
1162
                        $childNode = new TextNode($captured . chr($symbol));
1163
                        $node->addChild($childNode);
1164
                        $countedEscapes = 0; // If number of escapes do not match expected, reset the counter
1165
                        break;
1166
                    }
1167
                    if ($captured !== null) {
1168
                        $childNode = new TextNode($captured);
1169
                        $node->addChild($childNode);
1170
                    }
1171
                    $this->splitter->switch($contextToRestore);
1172
                    return $node;
1173
            }
1174
        }
1175
1176
        throw $this->createErrorAtPosition('Unterminated expression inside quotes', 1557700793);
1177
    }
1178
1179
    /**
1180
     * Dead-end sequencing; if parsing is switched off it cannot be switched on again,
1181
     * and the remainder of the template source must be sequenced as dead text.
1182
     *
1183
     * @return string|null
1184
     */
1185
    protected function sequenceRemainderAsText(): ?string
1186
    {
1187
        $this->sequence->next();
1188
        $this->splitter->switch($this->contexts->empty);
1189
        $source = null;
1190
        foreach ($this->sequence as $symbol => $captured) {
1191
            $source .= $captured;
1192
        }
1193
        return $source;
1194
    }
1195
1196
    /**
1197
     * Call all interceptors registered for a given interception point.
1198
     *
1199
     * @param ComponentInterface $node The syntax tree node which can be modified by the interceptors.
1200
     * @param integer $interceptorPosition the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\self::INTERCEPT_* constants.
1201
     * @return ComponentInterface
1202
     */
1203
    protected function callInterceptor(ComponentInterface $node, int $interceptorPosition): ComponentInterface
1204
    {
1205
        if (!$this->escapingEnabled) {
1206
            // Escaping is either explicitly disabled (for example, we may be parsing arguments) or there are
1207
            // at least 2 preceding ViewHelpers which have disabled child escaping. The reason for checking >1
1208
            // instead of >0 is because escaping must happen if the value is decremented to zero, which is not
1209
            // possible unless the value is >1 to begin with. This early return avoids entering the switch and
1210
            // conditions below when they are simply not necessary to consult.
1211
            return $node;
1212
        }
1213
1214
        switch ($interceptorPosition) {
1215
            case self::INTERCEPT_OPENING_VIEWHELPER:
1216
                if (!$node->isChildrenEscapingEnabled()) {
1217
                    ++$this->viewHelperNodesWhichDisableTheInterceptor;
1218
                }
1219
                break;
1220
1221
            case self::INTERCEPT_CLOSING_VIEWHELPER:
1222
                if (!$node->isChildrenEscapingEnabled()) {
1223
                    --$this->viewHelperNodesWhichDisableTheInterceptor;
1224
                }
1225
                if ($this->viewHelperNodesWhichDisableTheInterceptor === 0 && $node->isOutputEscapingEnabled()) {
1226
                    $node = new EscapingNode($node);
1227
                }
1228
                break;
1229
1230
            case self::INTERCEPT_SELFCLOSING_VIEWHELPER:
1231
                if ($this->viewHelperNodesWhichDisableTheInterceptor === 0 && $node->isOutputEscapingEnabled()) {
1232
                    $node = new EscapingNode($node);
1233
                }
1234
                break;
1235
1236
            case self::INTERCEPT_OBJECTACCESSOR:
1237
            case self::INTERCEPT_EXPRESSION:
1238
                if ($this->viewHelperNodesWhichDisableTheInterceptor === 0) {
1239
                    $node = new EscapingNode($node);
1240
                }
1241
                break;
1242
        }
1243
        return $node;
1244
    }
1245
1246
    /**
1247
     * Creates a dump, starting from the first line break before $position,
1248
     * to the next line break from $position, counting the lines and characters
1249
     * and inserting a marker pointing to the exact offending character.
1250
     *
1251
     * Is not very efficient - but adds bug tracing information. Should only
1252
     * be called when exceptions are raised during sequencing.
1253
     *
1254
     * @param int $index
1255
     * @return string
1256
     */
1257
    protected function extractSourceDumpOfLineAtPosition(int $index): string
1258
    {
1259
        $lines = $this->countCharactersMatchingMask(self::MASK_LINEBREAKS, 1, $index) + 1;
1260
        $offset = $this->findBytePositionBeforeOffset(self::MASK_LINEBREAKS, $index);
1261
        $line = substr(
1262
            $this->source->source,
1263
            $offset,
1264
            $this->findBytePositionAfterOffset(self::MASK_LINEBREAKS, $index)
1265
        );
1266
        $character = $index - $offset - 1;
1267
        $string = 'Line ' . $lines . ' character ' . $character . PHP_EOL;
1268
        $string .= PHP_EOL;
1269
        $string .= str_repeat(' ', max($character, 0)) . 'v' . PHP_EOL;
1270
        $string .= trim($line) . PHP_EOL;
1271
        $string .= str_repeat(' ', max($character, 0)) . '^' . PHP_EOL;
1272
        return $string;
1273
    }
1274
1275
    protected function createErrorAtPosition(string $message, int $code): SequencingException
1276
    {
1277
        $error = new SequencingException($message, $code);
1278
        $error->setExcerpt($this->extractSourceDumpOfLineAtPosition($this->splitter->index));
1279
        $error->setByte($this->source->bytes[$this->splitter->index] ?? 0);
1280
        return $error;
1281
    }
1282
1283
    protected function createUnsupportedArgumentError(string $argument, array $definitions): SequencingException
1284
    {
1285
        return $this->createErrorAtPosition(
1286
            sprintf(
1287
                'Undeclared argument: %s. Valid arguments are: %s',
1288
                $argument,
1289
                implode(', ', array_keys($definitions))
1290
            ),
1291
            1558298976
1292
        );
1293
    }
1294
1295
    protected function countCharactersMatchingMask(int $primaryMask, int $offset, int $length): int
0 ignored issues
show
Unused Code introduced by
The parameter $length is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1296
    {
1297
        $bytes = &$this->source->bytes;
1298
        $counted = 0;
1299
        for ($index = $offset; $index < $this->source->length; $index++) {
1300
            if (($primaryMask & (1 << $bytes[$index])) && $bytes[$index] < 64) {
1301
                $counted++;
1302
            }
1303
        }
1304
        return $counted;
1305
    }
1306
1307
    protected function findBytePositionBeforeOffset(int $primaryMask, int $offset): int
1308
    {
1309
        $bytes = &$this->source->bytes;
1310
        for ($index = min($offset, $this->source->length); $index > 0; $index--) {
1311
            if (($primaryMask & (1 << $bytes[$index])) && $bytes[$index] < 64) {
1312
                return $index;
1313
            }
1314
        }
1315
        return 0;
1316
    }
1317
1318
    protected function findBytePositionAfterOffset(int $primaryMask, int $offset): int
1319
    {
1320
        $bytes = &$this->source->bytes;
1321
        for ($index = $offset; $index < $this->source->length; $index++) {
1322
            if (($primaryMask & (1 << $bytes[$index])) && $bytes[$index] < 64) {
1323
                return $index;
1324
            }
1325
        }
1326
        return max($this->source->length, $offset);
1327
    }
1328
}