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

Sequencer::sequence()   C

Complexity

Conditions 12
Paths 87

Size

Total Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
nc 87
nop 0
dl 0
loc 68
rs 6.2714
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
        $this->nodeStack[] = (new EntryNode())->onOpen($this->renderingContext);
145
    }
146
147
    public function getComponent(): ComponentInterface
148
    {
149
        return reset($this->nodeStack) ?: $this->sequence();
150
    }
151
152
    public function sequence(): ComponentInterface
153
    {
154
        // Root context - the only symbols that cause any context switching are curly brace open and tag start, but
155
        // only if they are not preceded by a backslash character; in which case the symbol is ignored and merely
156
        // collected as part of the output string. NULL bytes are ignored in this context (the Splitter will yield
157
        // a single NULL byte when end of source is reached).
158
        $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...
159
        $countedEscapes = 0;
160
        foreach ($this->sequence as $symbol => $captured) {
161
            $node = end($this->nodeStack);
162
            $text = $captured . ($countedEscapes > 0 ? chr($symbol) : '');
163
            if ($text !== '') {
164
                $node->addChild(new TextNode($text));
165
            }
166
167
            if ($countedEscapes > 0) {
168
                $countedEscapes = 0;
169
                continue;
170
            }
171
172
            switch ($symbol) {
173
                case self::BYTE_BACKSLASH:
174
                    ++$countedEscapes;
175
                    break;
176
177
                case self::BYTE_INLINE:
178
                    $countedEscapes = 0;
179
                    $node->addChild($this->sequenceInlineNodes(false));
180
                    $this->splitter->switch($this->contexts->root);
181
                    break;
182
183
                case self::BYTE_TAG:
184
                    $countedEscapes = 0;
185
                    $childNode = $this->sequenceTagNode();
186
                    $this->splitter->switch($this->contexts->root);
187
188
                    if ($childNode) {
189
                        end($this->nodeStack)->addChild($childNode);
190
                    }
191
                    break;
192
193
                case self::BYTE_NULL:
194
                    break;
195
            }
196
        }
197
198
        // If there is more than a single node remaining in the stack this indicates an error. More precisely it
199
        // indicates that some function called in the above switch added a node to the stack but failed to remove it
200
        // before returning, which usually indicates that the template contains one or more incorrectly closed tags.
201
        // In order to report this as error we collect the classes of every remaining node in the stack. Unfortunately
202
        // we cannot report the position of where the closing tag was expected - this is simply not known to Fluid.
203
        if (count($this->nodeStack) !== 1) {
204
            $names = [];
205
            while (($unterminatedNode = array_pop($this->nodeStack))) {
206
                $names[] = get_class($unterminatedNode);
207
            }
208
            throw $this->createErrorAtPosition(
209
                'Unterminated node(s) detected: ' . implode(', ', array_reverse($names)),
210
                1562671632
211
            );
212
        }
213
214
        // Finishing sequencing means returning the single node that remains in the node stack, firing the onClose
215
        // method on it and assigning the rendering context to the ArgumentCollection carried by the root node.
216
        $node = array_pop($this->nodeStack)->onClose($this->renderingContext);
217
        $node->getArguments()->setRenderingContext($this->renderingContext);
218
        return $node;
219
    }
220
221
    public function sequenceUntilClosingTagAndIgnoreNested(ComponentInterface $parent, ?string $namespace, string $method): void
222
    {
223
        // Special method of sequencing which completely ignores any and all Fluid code inside a tag if said tag is
224
        // associated with a Component that implements SequencingComponentInterface and calls this method as a default
225
        // implementation of an "ignore everything until closed" type of behavior. Exists in Sequencer since this is
226
        // the most common expected use case which would otherwise 1) be likely to become duplicated, or 2) require the
227
        // use of a trait or base class for this single method alone. Since the Component which implements the signal
228
        // interface already receives the Sequencer instance it is readily available without composition concerns.
229
        $matchingTag = $namespace ? $namespace . ':' . $method : $method;
230
        $matchingTagLength = strlen($matchingTag);
231
        $ignoredNested = 0;
232
        $this->splitter->switch($this->contexts->inactive);
233
        $this->sequence->next();
234
        $text = '';
235
        foreach ($this->sequence as $symbol => $captured) {
236
            if ($symbol === self::BYTE_TAG_END && $captured !== null && strncmp($captured, $matchingTag, $matchingTagLength) === 0) {
237
                // An opening tag matching the parent tag - treat as text and add to ignored count.
238
                ++$ignoredNested;
239
            } elseif ($symbol === self::BYTE_TAG_END && $captured === '/' . $matchingTag) {
240
                // A closing version of the parent tag. Check counter; if zero, finish. If not, decrease ignored count.
241
                if ($ignoredNested === 0) {
242
                    $parent->addChild(new TextNode((string) substr($text, 0, -1)));
243
                    return;
244
                }
245
                --$ignoredNested;
246
            }
247
            $text .= (string) $captured . chr($symbol);
248
        }
249
250
        throw $this->createErrorAtPosition(
251
            'Unterminated inactive tag: ' . $matchingTag,
252
            1564665730
253
        );
254
    }
255
256
    protected function sequenceCharacterData(string $text): ComponentInterface
257
    {
258
        $this->splitter->switch($this->contexts->data);
259
        $this->sequence->next();
260
        foreach ($this->sequence as $symbol => $captured) {
261
            $text .= $captured;
262
            if ($symbol === self::BYTE_TAG_END && substr($this->source->source, $this->splitter->index - 3, 2) === ']]') {
263
                $text .= '>';
264
                break;
265
            }
266
        }
267
        return new TextNode($text);
268
    }
269
270
    /**
271
     * Sequence a Fluid feature toggle node. Does not return
272
     * any node, only toggles various features of the Fluid
273
     * parser configuration or assigns context parameters
274
     * like namespaces.
275
     *
276
     * For backwards compatibility we allow the toggle name
277
     * to be passed, which is used in an explicit check when
278
     * sequencing inline nodes to detect if a {namespace ...}
279
     * node was encountered, in which case, this is not known
280
     * until the "toggle" has already been captured.
281
     *
282
     * @param string|null $toggle
283
     */
284
    protected function sequenceToggleInstruction(?string $toggle = null): void
285
    {
286
        $this->splitter->switch($this->contexts->toggle);
287
        $this->sequence->next();
288
        $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...
289
        foreach ($this->sequence as $symbol => $captured) {
290
            switch ($symbol) {
291
                case self::BYTE_WHITESPACE_SPACE:
292
                    $toggle = $toggle ?? $captured;
293
                    break;
294
                case self::BYTE_INLINE_END:
295
                    if ($toggle === 'namespace') {
296
                        $parts = explode('=', (string) $captured);
297
                        $this->resolver->addNamespace($parts[0], $parts[1] ?? null);
298
                        return;
299
                    }
300
301
                    $this->configuration->setFeatureState($toggle, $captured ?? true);
302
                    // Re-read the parser configuration and react accordingly to any flags that may have changed.
303
                    $this->escapingEnabled = $this->configuration->isFeatureEnabled(Configuration::FEATURE_ESCAPING);
304
                    if (!$this->configuration->isFeatureEnabled(Configuration::FEATURE_PARSING)) {
305
                        throw (new PassthroughSourceException('Source must be represented as raw string', 1563379852))
306
                            ->setSource((string)$this->sequenceRemainderAsText());
307
                    }
308
                    return;
309
            }
310
        }
311
        throw $this->createErrorAtPosition('Unterminated feature toggle', 1563383038);
312
    }
313
314
    protected function sequenceTagNode(): ?ComponentInterface
315
    {
316
        $arguments = null;
317
        $definitions = null;
318
        $text = '<';
319
        $key = null;
320
        $namespace = null;
321
        $method = null;
322
        $bytes = &$this->source->bytes;
323
        $node = new RootNode();
324
        $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...
325
        $selfClosing = false;
326
        $closing = false;
327
        $escapingEnabledBackup = $this->escapingEnabled;
328
        $viewHelperNode = null;
329
330
        $this->splitter->switch($this->contexts->tag);
331
        $this->sequence->next();
332
        foreach ($this->sequence as $symbol => $captured) {
333
            $text .= $captured;
334
            switch ($symbol) {
335
                case self::BYTE_ARRAY_START:
336
                    // Possible P/CDATA section. Check text explicitly for match, if matched, begin parsing-insensitive
337
                    // pass through sequenceCharacterDataNode()
338
                    $text .= '[';
339
                    if ($text === '<![CDATA[' || $text === '<![PCDATA[') {
340
                        return $this->sequenceCharacterData($text);
341
                    }
342
                    break;
343
344
                case self::BYTE_INLINE:
345
                    $contextBefore = $this->splitter->context;
346
                    $collected = $this->sequenceInlineNodes(isset($namespace, $method));
347
                    $node->addChild(new TextNode($text));
348
                    $node->addChild($collected);
349
                    $text = '';
350
                    $this->splitter->switch($contextBefore);
351
                    break;
352
353
                case self::BYTE_SEPARATOR_EQUALS:
354
                    $key = $key . $captured;
355
                    $text .= '=';
356
                    if ($key === '') {
357
                        throw $this->createErrorAtPosition('Unexpected equals sign without preceding attribute/key name', 1561039838);
358
                    } elseif ($definitions !== null && !isset($definitions[$key]) && !$viewHelperNode->allowUndeclaredArgument($key)) {
359
                        $error = $this->createUnsupportedArgumentError($key, $definitions);
360
                        return new TextNode($this->renderingContext->getErrorHandler()->handleParserError($error));
361
                    }
362
                    break;
363
364
                case self::BYTE_QUOTE_DOUBLE:
365
                case self::BYTE_QUOTE_SINGLE:
366
                    $text .= chr($symbol);
367
                    if ($key === null) {
368
                        throw $this->createErrorAtPosition('Quoted value without a key is not allowed in tags', 1558952412);
369
                    }
370
                    if ($arguments->isArgumentBoolean($key)) {
371
                        $arguments[$key] = $this->sequenceBooleanNode()->flatten(true);
372
                    } else {
373
                        $arguments[$key] = $this->sequenceQuotedNode()->flatten(true);
374
                    }
375
                    $key = null;
376
                    break;
377
378
                case self::BYTE_TAG_CLOSE:
379
                    $method = $method ?? $captured;
380
                    $text .= '/';
381
                    $closing = true;
382
                    $selfClosing = $bytes[$this->splitter->index - 1] !== self::BYTE_TAG;
383
384
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) {
385
                        // Arguments may be pending: if $key is set we must create an ECMA literal style shorthand
386
                        // (attribute value is variable of same name as attribute). Two arguments may be created in
387
                        // this case, if both $key and $captured are non-null. The former contains a potentially
388
                        // pending argument and the latter contains a captured value-less attribute right before the
389
                        // tag closing character.
390
                        if ($key !== null) {
391
                            $arguments[$key] = new ObjectAccessorNode((string) $key);
392
                            $key = null;
393
                        }
394
                        // (see comment above) Hence, the two conditions must not be compunded to else-if.
395
                        if ($captured !== null) {
396
                            $arguments[$captured] = new ObjectAccessorNode($captured);
397
                        }
398
                    }
399
                    break;
400
401
                case self::BYTE_SEPARATOR_COLON:
402
                    $text .= ':';
403
                    if (!$method) {
404
                        // If we haven't encountered a method yet, then $method won't be set, and we can assign NS now
405
                        $namespace = $namespace ?? $captured;
406
                    } else {
407
                        // If we do have a method this means we encountered a colon as part of an attribute name
408
                        $key = $key ?? ($captured . ':');
409
                    }
410
                    break;
411
412
                case self::BYTE_TAG_END:
413
                    $text .= '>';
414
                    $method = $method ?? $captured;
415
416
                    $this->escapingEnabled = $escapingEnabledBackup;
417
418
                    if (($namespace === null && ($this->splitter->context->context === Context::CONTEXT_DEAD || !$this->resolver->isAliasRegistered((string) $method))) || $this->resolver->isNamespaceIgnored((string) $namespace)) {
419
                        return $node->addChild(new TextNode($text))->flatten();
420
                    }
421
422
                    try {
423
                        if (!$closing || $selfClosing) {
424
                            $viewHelperNode = $viewHelperNode ?? $this->resolver->createViewHelperInstance($namespace, (string) $method);
425
                            $viewHelperNode->onOpen($this->renderingContext)->getArguments()->validate();
426
                        } else {
427
                            // $closing will be true and $selfClosing false; add to stack, continue with children.
428
                            $viewHelperNode = array_pop($this->nodeStack);
429
                            $expectedClass = $this->resolver->resolveViewHelperClassName($namespace, (string) $method);
430
                            if (!$viewHelperNode instanceof $expectedClass) {
431
                                throw $this->createErrorAtPosition(
432
                                    sprintf(
433
                                        'Mismatched closing tag. Expecting: %s:%s (%s). Found: (%s).',
434
                                        $namespace,
435
                                        $method,
436
                                        $expectedClass,
437
                                        get_class($viewHelperNode)
438
                                    ),
439
                                    1557700789
440
                                );
441
                            }
442
                        }
443
                    } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
444
                        $error = $this->createErrorAtPosition($exception->getMessage(), $exception->getCode());
445
                        return new TextNode($this->renderingContext->getErrorHandler()->handleParserError($error));
446
                    }
447
448
                    // Possibly pending argument still needs to be processed since $key is not null. Create an ECMA
449
                    // literal style associative array entry. Do the same for $captured.
450
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) {
451
                        if ($key !== null) {
452
                            $value = new ObjectAccessorNode((string) $key);
453
                            $arguments[$key] = $value;
454
                        }
455
456
                        if ($captured !== null) {
457
                            $value = new ObjectAccessorNode((string) $captured);
458
                            $arguments[$captured] = $value;
459
                        }
460
                    }
461
462
                    if (!$closing) {
463
                        // The node is neither a closing or self-closing node (= an opening node expecting tag content).
464
                        // Add it to the stack and return null to return the Sequencer to "root" context and continue
465
                        // sequencing the tag's body - parsed nodes then get attached to this node as children.
466
                        $viewHelperNode = $this->callInterceptor($viewHelperNode, self::INTERCEPT_OPENING_VIEWHELPER);
467
                        if ($viewHelperNode instanceof SequencingComponentInterface) {
468
                            // The Component will take over sequencing. It will return if encountering the right closing
469
                            // tag - so when it returns, we reached the end of the Component and must pop the stack.
470
                            $viewHelperNode->sequence($this, $namespace, (string) $method);
471
                            return $viewHelperNode;
472
                        }
473
                        $this->nodeStack[] = $viewHelperNode;
474
                        return null;
475
                    }
476
477
                    $viewHelperNode = $viewHelperNode->onClose($this->renderingContext);
478
479
                    $viewHelperNode = $this->callInterceptor(
480
                        $viewHelperNode,
481
                        $selfClosing ? self::INTERCEPT_SELFCLOSING_VIEWHELPER : self::INTERCEPT_CLOSING_VIEWHELPER
482
                    );
483
484
                    return $viewHelperNode;
485
486
                case self::BYTE_WHITESPACE_TAB:
487
                case self::BYTE_WHITESPACE_RETURN:
488
                case self::BYTE_WHITESPACE_EOL:
489
                case self::BYTE_WHITESPACE_SPACE:
490
                    $text .= chr($symbol);
491
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) {
492
                        if ($captured !== null) {
493
                            // Encountering this case means we've collected a previous key and now collected a non-empty
494
                            // string value before encountering an equals sign. This is treated as ECMA literal short
495
                            // hand equivalent of having written `attr="{attr}"` in the Fluid template.
496
                            $key = $captured;
497
                        }
498
                    } elseif ($namespace !== null || (!isset($namespace, $method) && $this->resolver->isAliasRegistered((string)$captured))) {
499
                        $method = $captured;
500
501
                        try {
502
                            $viewHelperNode = $this->resolver->createViewHelperInstance($namespace, $method);
503
                            $arguments = $viewHelperNode->getArguments();
504
                            $definitions = $arguments->getDefinitions();
505
                        } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
506
                            $error = $this->createErrorAtPosition($exception->getMessage(), $exception->getCode());
507
                            return new TextNode($this->renderingContext->getErrorHandler()->handleParserError($error));
508
                        }
509
510
                        // Forcibly disable escaping OFF as default decision for whether or not to escape an argument.
511
                        $this->escapingEnabled = false;
512
                        $this->splitter->switch($this->contexts->attributes);
513
                        break;
514
                    } else {
515
                        // A whitespace before a colon means the tag is not a namespaced tag. We will ignore everything
516
                        // inside this tag, except for inline syntax, until the tag ends. For this we use a special,
517
                        // limited variant of the root context where instead of scanning for "<" we scan for ">".
518
                        // We continue in this same loop because it still matches the potential symbols being yielded.
519
                        // Most importantly: this new reduced context will NOT match a colon which is the trigger symbol
520
                        // for a ViewHelper tag.
521
                        $this->splitter->switch($this->contexts->dead);
522
                    }
523
                    break;
524
            }
525
        }
526
527
        // This case on the surface of it, belongs as "default" case in the switch above. However, the only case that
528
        // would *actually* produce this error, is if the splitter reaches EOF (null byte) symbol before the tag was
529
        // closed. Literally every other possible error type will be thrown as more specific exceptions (e.g. invalid
530
        // argument, missing key, wrong quotes, bad inline and *everything* else with the exception of EOF). Even a
531
        // stray null byte would not be caught here as null byte is not part of the symbol collection for "tag" context.
532
        throw $this->createErrorAtPosition('Unexpected token in tag sequencing', 1557700786);
533
    }
534
535
    protected function sequenceInlineNodes(bool $allowArray = true): ComponentInterface
536
    {
537
        $text = '{';
538
        /** @var ComponentInterface|null $node */
539
        $node = null;
540
        $key = null;
541
        $namespace = null;
542
        $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...
543
        $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...
544
        $potentialAccessor = null;
545
        $callDetected = false;
546
        $hasPass = false;
547
        $hasColon = null;
548
        $hasWhitespace = false;
549
        $isArray = false;
550
        $arguments = new ArgumentCollection();
551
        $parts = [];
552
        $ignoredEndingBraces = 0;
553
        $countedEscapes = 0;
554
        $restore = $this->splitter->switch($this->contexts->inline);
555
        $this->sequence->next();
556
        foreach ($this->sequence as $symbol => $captured) {
557
            $text .= $captured;
558
            switch ($symbol) {
559
                case self::BYTE_AT:
560
                    $this->sequenceToggleInstruction();
561
                    $this->splitter->switch($restore);
562
                    return new TextNode('');
563
                    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...
564
565
                case self::BYTE_BACKSLASH:
566
                    // Increase the number of counted escapes (is passed to sequenceNode() in the "QUOTE" cases and reset
567
                    // after the quoted string is extracted).
568
                    ++$countedEscapes;
569
                    if ($hasWhitespace) {
570
                        $node = $node ?? new RootNode();
571
                    } else {
572
                        $node = $node ?? new ObjectAccessorNode();
573
                    }
574
                    if ($captured !== null) {
575
                        $node->addChild(new TextNode((string) $captured));
576
                    }
577
                    break;
578
579
                case self::BYTE_ARRAY_START:
580
                    $text .= chr($symbol);
581
                    $isArray = $allowArray;
582
583
                    // Sequence the node. Pass the "use numeric keys?" boolean based on the current byte. Only array
584
                    // start creates numeric keys. Inline start with keyless values creates ECMA style {foo:foo, bar:bar}
585
                    // from {foo, bar}.
586
                    $arguments[$key ?? $captured ?? 0] = $node = new ArrayNode();
587
                    $this->sequenceArrayNode($node, true);
588
                    $key = null;
589
                    break;
590
591
                case self::BYTE_INLINE:
592
                    // Encountering this case can mean different things: sub-syntax like {foo.{index}} or array, depending
593
                    // on presence of either a colon or comma before the inline. In protected mode it is simply added.
594
                    $text .= '{';
595
                    $node = $node ?? new ObjectAccessorNode();
596
                    if ($countedEscapes > 0) {
597
                        ++$ignoredEndingBraces;
598
                        $countedEscapes = 0;
599
                        if ($captured !== null) {
600
                            $node->addChild(new TextNode((string)$captured));
601
                        }
602
                    } elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED) {
603
                        // Ignore one ending additional curly brace. Subtracted in the BYTE_INLINE_END case below.
604
                        // The expression in this case looks like {{inline}.....} and we capture the curlies.
605
                        $potentialAccessor .= $captured;
606
                    } elseif ($allowArray || $isArray) {
607
                        $isArray = true;
608
                        $captured = $key ?? $captured ?? $potentialAccessor;
609
                        // This is a sub-syntax following a colon - meaning it is an array.
610
                        if ($captured !== null) {
611
                            $arguments[$key ?? $captured ?? 0] = $node = new ArrayNode();
612
                            $this->sequenceArrayNode($node);
613
                        }
614
                    } else {
615
                        if ($captured !== null) {
616
                            $node->addChild(new TextNode((string) $captured));
617
                        }
618
                        $childNodeToAdd = $this->sequenceInlineNodes($allowArray);
619
                        $node->addChild($childNodeToAdd);
620
                    }
621
                    break;
622
623
                case self::BYTE_MINUS:
624
                    $text .= '-';
625
                    $potentialAccessor = $potentialAccessor ?? $captured;
626
                    break;
627
628
                // Backtick may be encountered in two different contexts: normal inline context, in which case it has
629
                // the same meaning as any quote and causes sequencing of a quoted string. Or protected context, in
630
                // which case it also sequences a quoted node but appends the result instead of assigning to array.
631
                // Note that backticks do not support escapes (they are a new feature that does not require escaping).
632
                case self::BYTE_BACKTICK:
633
                    if ($this->splitter->context->context === Context::CONTEXT_PROTECTED) {
634
                        $node->addChild(new TextNode($text));
635
                        $node->addChild($this->sequenceQuotedNode()->flatten());
636
                        $text = '';
637
                        break;
638
                    }
639
                // Fallthrough is intentional: if not in protected context, consider the backtick a normal quote.
640
641
                // Case not normally countered in straight up "inline" context, but when encountered, means we have
642
                // explicitly found a quoted array key - and we extract it.
643
                case self::BYTE_QUOTE_SINGLE:
644
                case self::BYTE_QUOTE_DOUBLE:
645
                    if (!$allowArray) {
646
                        $text .= chr($symbol);
647
                        break;
648
                    }
649
                    if ($key !== null) {
650
                        $arguments[$key] = $this->sequenceQuotedNode($countedEscapes)->flatten(true);
651
                        $key = null;
652
                    } else {
653
                        $key = $this->sequenceQuotedNode($countedEscapes)->flatten(true);
654
                    }
655
                    $countedEscapes = 0;
656
                    $isArray = $allowArray;
657
                    break;
658
659
                case self::BYTE_SEPARATOR_COMMA:
660
                    if (!$allowArray) {
661
                        $text .= ',';
662
                        break;
663
                    }
664
                    if ($captured !== null) {
665
                        $arguments[$key ?? $captured] = new ObjectAccessorNode($captured);
666
                    }
667
                    $key = null;
668
                    $isArray = $allowArray;
669
                    break;
670
671
                case self::BYTE_SEPARATOR_EQUALS:
672
                    $text .= '=';
673
                    if (!$allowArray) {
674
                        $node = new RootNode();
675
                        $this->splitter->switch($this->contexts->protected);
676
                        break;
677
                    }
678
                    $key = $captured;
679
                    $isArray = $allowArray;
680
                    break;
681
682
                case self::BYTE_SEPARATOR_COLON:
683
                    $text .= ':';
684
                    $hasColon = true;
685
                    $namespace = $captured;
686
                    $key = $key ?? $captured;
687
                    $isArray = $isArray || ($allowArray && is_numeric($key));
688
                    if ($captured !== null) {
689
                        $parts[] = $captured;
690
                    }
691
                    $parts[] = ':';
692
                    break;
693
694
                case self::BYTE_WHITESPACE_SPACE:
695
                case self::BYTE_WHITESPACE_EOL:
696
                case self::BYTE_WHITESPACE_RETURN:
697
                case self::BYTE_WHITESPACE_TAB:
698
                    // If we already collected some whitespace we must enter protected context.
699
                    $text .= $this->source->source[$this->splitter->index - 1];
700
701
                    if ($captured !== null) {
702
                        // Store a captured part: a whitespace inside inline syntax will engage the expression matching
703
                        // that occurs when the node is closed. Allows matching the various parts to create the appropriate
704
                        // node type.
705
                        $parts[] = $captured;
706
                    }
707
708
                    if ($hasWhitespace && !$hasPass && !$allowArray) {
709
                        // Protection mode: this very limited context does not allow tags or inline syntax, and will
710
                        // protect things like CSS and JS - and will only enter a more reactive context if encountering
711
                        // the backtick character, meaning a quoted string will be sequenced. This backtick-quoted
712
                        // string can then contain inline syntax like variable accessors.
713
                        $node = $node ?? new RootNode();
714
                        $this->splitter->switch($this->contexts->protected);
715
                        break;
716
                    }
717
718
                    if ($captured === 'namespace') {
719
                        // Special case: we catch namespace definitions with {namespace xyz=foo} syntax here, although
720
                        // the proper way with current code is to use {@namespace xyz=foo}. We have this case here since
721
                        // it is relatively cheap (only happens when we see a space inside inline and a straight-up
722
                        // string comparison with strict types enabled). We then return an empty TextNode which is
723
                        // ignored by the parent node when attached so we don't create any output.
724
                        $this->sequenceToggleInstruction('namespace');
725
                        $this->splitter->switch($restore);
726
                        return new TextNode('');
727
                    }
728
                    $key = $key ?? $captured;
729
                    $hasWhitespace = true;
730
                    $isArray = $allowArray && ($hasColon ?? $isArray ?? is_numeric($captured));
731
                    $potentialAccessor = ($potentialAccessor ?? $captured);
732
                    break;
733
734
                case self::BYTE_TAG_END:
735
                case self::BYTE_PIPE:
736
                    // If there is an accessor on the left side of the pipe and $node is not defined, we create $node
737
                    // as an object accessor. If $node already exists we do nothing (and expect the VH trigger, the
738
                    // parenthesis start case below, to add $node as childnode and create a new $node).
739
                    $hasPass = true;
740
                    $isArray = $allowArray;
741
                    $callDetected = false;
742
743
                    $text .=  $this->source->source[$this->splitter->index - 1];
744
                    $node = $node ?? new ObjectAccessorNode();
745
                    if ($potentialAccessor ?? $captured) {
746
                        $node->addChild(new TextNode($potentialAccessor . $captured));
747
                    }
748
749
                    $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...
750
                    break;
751
752
                case self::BYTE_PARENTHESIS_START:
753
                    $isArray = false;
754
                    // Special case: if a parenthesis start was preceded by whitespace but had no pass operator we are
755
                    // not dealing with a ViewHelper call and will continue the sequencing, grabbing the parenthesis as
756
                    // part of the expression.
757
                    $text .= '(';
758
                    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...
759
                        $this->splitter->switch($this->contexts->protected);
760
                        $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...
761
                        break;
762
                    }
763
764
                    $callDetected = true;
765
                    $method = $captured;
766
                    $childNodeToAdd = $node;
767
                    try {
768
                        $node = $this->resolver->createViewHelperInstance($namespace, $method);
769
                        $arguments = $node->getArguments();
770
                        $node = $this->callInterceptor($node, self::INTERCEPT_OPENING_VIEWHELPER);
771
                    } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
772
                        throw $this->createErrorAtPosition($exception->getMessage(), $exception->getCode());
773
                    }
774
775
                    $this->sequenceArrayNode($arguments);
776
                    $arguments->setRenderingContext($this->renderingContext)->validate();
777
                    $node = $node->onOpen($this->renderingContext);
778
779
                    if ($childNodeToAdd) {
780
                        if ($childNodeToAdd instanceof ObjectAccessorNode) {
781
                            $childNodeToAdd = $this->callInterceptor($childNodeToAdd, self::INTERCEPT_OBJECTACCESSOR);
782
                        }
783
                        $node->addChild($childNodeToAdd);
784
                    }
785
                    $node = $node->onClose($this->renderingContext);
786
                    $text .= ')';
787
                    $potentialAccessor = null;
788
                    break;
789
790
                case self::BYTE_INLINE_END:
791
                    $text .= '}';
792
793
                    if (--$ignoredEndingBraces >= 0) {
794
                        if ($captured !== null) {
795
                            $node->addChild(new TextNode('{' . $captured . '}'));
796
                        }
797
                        break;
798
                    }
799
800
                    if ($text === '{}') {
801
                        // Edge case handling of empty JS objects
802
                        return new TextNode('{}');
803
                    }
804
805
                    $isArray = $allowArray && ($isArray ?: ($hasColon && !$hasPass && !$callDetected));
806
                    $potentialAccessor .= $captured;
807
                    $interceptionPoint = self::INTERCEPT_OBJECTACCESSOR;
808
809
                    // Decision: if we did not detect a ViewHelper we match the *entire* expression, from the cached
810
                    // starting index, to see if it matches a known type of expression. If it does, we must return the
811
                    // appropriate type of ExpressionNode.
812
                    if ($isArray) {
813
                        if ($captured !== null) {
814
                            $arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
815
                        }
816
                        $this->splitter->switch($restore);
817
                        return $arguments->toArrayNode();
818
                    } elseif ($callDetected) {
819
                        // The first-priority check is for a ViewHelper used right before the inline expression ends,
820
                        // in which case there is no further syntax to come.
821
                        $arguments->validate();
822
                        $node = $node->onOpen($this->renderingContext)->onClose($this->renderingContext);
823
                        $interceptionPoint = self::INTERCEPT_SELFCLOSING_VIEWHELPER;
824
                    } elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED || ($hasWhitespace && !$callDetected && !$hasPass)) {
825
                        // In order to qualify for potentially being an expression, the entire inline node must contain
826
                        // whitespace, must not contain parenthesis, must not contain a colon and must not contain an
827
                        // inline pass operand. This significantly limits the number of times this (expensive) routine
828
                        // has to be executed.
829
                        $parts[] = $captured;
830
                        try {
831
                            foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) {
832
                                if ($expressionNodeTypeClassName::matches($parts)) {
833
                                    $childNodeToAdd = new $expressionNodeTypeClassName($parts);
834
                                    $childNodeToAdd = $this->callInterceptor($childNodeToAdd, self::INTERCEPT_EXPRESSION);
835
                                    break;
836
                                }
837
                            }
838
                        } catch (ExpressionException $exception) {
839
                            // ErrorHandler will either return a string or throw the exception anew, depending on the
840
                            // exact implementation of ErrorHandlerInterface. When it returns a string we use that as
841
                            // text content of a new TextNode so the message is output as part of the rendered result.
842
                            $childNodeToAdd = new TextNode(
843
                                $this->renderingContext->getErrorHandler()->handleExpressionError($exception)
844
                            );
845
                        }
846
                        $node = $childNodeToAdd ?? ($node ?? new RootNode())->addChild(new TextNode($text));
847
                        return $node;
848
                    } elseif (!$hasPass && !$callDetected) {
849
                        $node = $node ?? new ObjectAccessorNode();
850
                        if ($potentialAccessor !== '') {
851
                            $node->addChild(new TextNode((string) $potentialAccessor));
852
                        }
853
                    } elseif ($hasPass && $this->resolver->isAliasRegistered((string) $potentialAccessor)) {
854
                        // Fourth priority check is for a pass to a ViewHelper alias, e.g. "{value | raw}" in which case
855
                        // we look for the alias used and create a ViewHelper with no arguments.
856
                        $childNodeToAdd = $node;
857
                        $node = $this->resolver->createViewHelperInstance(null, (string) $potentialAccessor);
858
                        $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...
859
                        $node = $node->onOpen($this->renderingContext);
860
                        $node->addChild($childNodeToAdd);
861
                        $node->onClose(
862
                            $this->renderingContext
863
                        );
864
                        $interceptionPoint = self::INTERCEPT_SELFCLOSING_VIEWHELPER;
865
                    } else {
866
                        # TODO: should this be an error case, or should it result in a TextNode?
867
                        throw $this->createErrorAtPosition(
868
                            'Invalid inline syntax - not accessor, not expression, not array, not ViewHelper, but ' .
869
                            'contains the tokens used by these in a sequence that is not valid Fluid syntax. You can ' .
870
                            'most likely avoid this by adding whitespace inside the curly braces before the first ' .
871
                            'Fluid-like symbol in the expression. Symbols recognized as Fluid are: "' .
872
                            addslashes(implode('","', array_map('chr', $this->contexts->inline->bytes))) . '"',
873
                            1558782228
874
                        );
875
                    }
876
877
                    $node = $this->callInterceptor($node, $interceptionPoint);
878
                    $this->splitter->switch($restore);
879
                    return $node;
880
            }
881
        }
882
883
        // See note in sequenceTagNode() end of method body. TL;DR: this is intentionally here instead of as "default"
884
        // case in the switch above for a very specific reason: the case is only encountered if seeing EOF before the
885
        // inline expression was closed.
886
        throw $this->createErrorAtPosition('Unterminated inline syntax', 1557838506);
887
    }
888
889
    protected function sequenceBooleanNode(int $leadingEscapes = 0): BooleanNode
890
    {
891
        $startingByte = $this->source->bytes[$this->splitter->index];
892
        $closingByte = $startingByte === self::BYTE_PARENTHESIS_START ? self::BYTE_PARENTHESIS_END : $startingByte;
893
        $countedEscapes = 0;
894
        $node = new BooleanNode();
895
        $restore = $this->splitter->switch($this->contexts->boolean);
896
        $this->sequence->next();
897
        foreach ($this->sequence as $symbol => $captured) {
898
            if ($captured !== null) {
899
                $node->addChild(new TextNode($captured));
900
            }
901
            switch ($symbol) {
902
                case self::BYTE_INLINE:
903
                    $node->addChild($this->sequenceInlineNodes(true));
904
                    break;
905
906
                case self::BYTE_QUOTE_DOUBLE:
907
                case self::BYTE_QUOTE_SINGLE:
908
                    if ($symbol === $closingByte && $countedEscapes === $leadingEscapes) {
909
                        $this->splitter->switch($restore);
910
                        return $node;
911
                    }
912
                    // Sequence a quoted node and set the "quoted" flag on the resulting root node (which is not
913
                    // flattened even if it contains a single child). This allows the BooleanNode to enforce a string
914
                    // value whenever parts of the expression are quoted, indicating user explicitly wants string type.
915
                    $node->addChild($this->sequenceQuotedNode($countedEscapes)->setQuoted(true));
916
                    break;
917
918
                case self::BYTE_PARENTHESIS_START:
919
                    $node->addChild($this->sequenceBooleanNode());
920
                    break;
921
922
                case self::BYTE_PARENTHESIS_END:
923
                    $this->splitter->switch($restore);
924
                    return $node;
925
926
                case self::BYTE_WHITESPACE_SPACE:
927
                case self::BYTE_WHITESPACE_TAB:
928
                case self::BYTE_WHITESPACE_RETURN:
929
                case self::BYTE_WHITESPACE_EOL:
930
                    break;
931
932
                case self::BYTE_BACKSLASH:
933
                    ++$countedEscapes;
934
                    break;
935
            }
936
            if ($symbol !== self::BYTE_BACKSLASH) {
937
                $countedEscapes = 0;
938
            }
939
        }
940
        throw $this->createErrorAtPosition('Unterminated boolean expression', 1564159986);
941
    }
942
943
    protected function sequenceArrayNode(\ArrayAccess &$array, bool $numeric = false): void
944
    {
945
        $definitions = null;
946
        if ($array instanceof ArgumentCollection) {
947
            $definitions = $array->getDefinitions();
948
        }
949
950
        $keyOrValue = null;
951
        $key = null;
952
        $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...
953
        $itemCount = -1;
954
        $countedEscapes = 0;
955
        $escapingEnabledBackup = $this->escapingEnabled;
956
957
        $restore = $this->splitter->switch($this->contexts->array);
958
        $this->sequence->next();
959
        foreach ($this->sequence as $symbol => $captured) {
960
            switch ($symbol) {
961
                case self::BYTE_SEPARATOR_COLON:
962
                case self::BYTE_SEPARATOR_EQUALS:
963
                    // Colon or equals has same meaning (which allows tag syntax as argument syntax). Encountering this
964
                    // byte always means the preceding byte was a key. However, if nothing was captured before this,
965
                    // it means colon or equals was used without a key which is a syntax error.
966
                    $key = $key ?? $captured ?? ($keyOrValue !== null ? $keyOrValue->flatten(true) : null);
967
                    if ($key === null) {
968
                        throw $this->createErrorAtPosition('Unexpected colon or equals sign, no preceding key', 1559250839);
969
                    }
970
                    if ($definitions !== null && !$numeric && !isset($definitions[$key])) {
971
                        throw $this->createUnsupportedArgumentError((string)$key, $definitions);
972
                    }
973
                    break;
974
975
                case self::BYTE_ARRAY_START:
976
                case self::BYTE_INLINE:
977
                    // Minimal safeguards to improve error feedback. Theoretically such "garbage" could simply be ignored
978
                    // without causing problems to the parser, but it is probably best to report it as it could indicate
979
                    // the user expected X value but gets Y and doesn't notice why.
980
                    if ($captured !== null) {
981
                        throw $this->createErrorAtPosition('Unexpected content before array/inline start in associative array, ASCII: ' . ord($captured), 1559131849);
982
                    }
983
                    if ($key === null && !$numeric) {
984
                        throw $this->createErrorAtPosition('Unexpected array/inline start in associative array without preceding key', 1559131848);
985
                    }
986
987
                    // Encountering a curly brace or square bracket start byte will both cause a sub-array to be sequenced,
988
                    // the difference being that only the square bracket will cause third parameter ($numeric) passed to
989
                    // sequenceArrayNode() to be true, which in turn causes key-less items to be added with numeric indexes.
990
                    $key = $key ?? ++$itemCount;
991
                    $arrayNode = new ArrayNode();
992
                    $this->sequenceArrayNode($arrayNode, $symbol === self::BYTE_ARRAY_START);
993
                    $array[$key] = $arrayNode;
994
                    $keyOrValue = null;
995
                    $key = null;
996
                    break;
997
998
                case self::BYTE_QUOTE_SINGLE:
999
                case self::BYTE_QUOTE_DOUBLE:
1000
                    // Safeguard: if anything is captured before a quote this indicates garbage leading content. As with
1001
                    // the garbage safeguards above, this one could theoretically be ignored in favor of silently making
1002
                    // the odd syntax "just work".
1003
                    if ($captured !== null) {
1004
                        throw $this->createErrorAtPosition('Unexpected content before quote start in associative array, ASCII: ' . ord($captured), 1559145560);
1005
                    }
1006
1007
                    // Quotes will always cause sequencing of the quoted string, but differs in behavior based on whether
1008
                    // or not the $key is set. If $key is set, we know for sure we can assign a value. If it is not set
1009
                    // we instead leave $keyOrValue defined so this will be processed by one of the next iterations.
1010
                    if (isset($key, $definitions[$key]) && $definitions[$key]->getType() === 'boolean') {
1011
                        $keyOrValue = $this->sequenceBooleanNode($countedEscapes);
1012
                    } else {
1013
                        $keyOrValue = $this->sequenceQuotedNode($countedEscapes);
1014
                    }
1015
                    if ($key !== null) {
1016
                        $array[$key] = $keyOrValue->flatten(true);
1017
                        $keyOrValue = null;
1018
                        $key = null;
1019
                        $countedEscapes = 0;
1020
                    }
1021
                    break;
1022
1023
                case self::BYTE_SEPARATOR_COMMA:
1024
                    // Comma separator: if we've collected a key or value, use it. Otherwise, use captured string.
1025
                    // If neither key nor value nor captured string exists, ignore the comma (likely a tailing comma).
1026
                    $value = null;
1027
                    if ($keyOrValue !== null) {
1028
                        // Key or value came as quoted string and exists in $keyOrValue
1029
                        $potentialValue = $keyOrValue->flatten(true);
1030
                        $key = $numeric ? ++$itemCount : $potentialValue;
1031
                        $value = $numeric ? $potentialValue : (is_numeric($key) ? $key + 0 : new ObjectAccessorNode((string) $key));
1032
                    } elseif ($captured !== null) {
1033
                        $key = $key ?? ($numeric ? ++$itemCount : $captured);
1034
                        if (!$numeric && $definitions !== null && !isset($definitions[$key])) {
1035
                            throw $this->createUnsupportedArgumentError((string)$key, $definitions);
1036
                        }
1037
                        $value = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
1038
                    }
1039
                    if ($value !== null) {
1040
                        $array[$key] = $value;
1041
                    }
1042
                    $keyOrValue = null;
1043
                    $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...
1044
                    $key = null;
1045
                    break;
1046
1047
                case self::BYTE_WHITESPACE_TAB:
1048
                case self::BYTE_WHITESPACE_RETURN:
1049
                case self::BYTE_WHITESPACE_EOL:
1050
                case self::BYTE_WHITESPACE_SPACE:
1051
                    // Any whitespace attempts to set the key, if not already set. The captured string may be null as
1052
                    // well, leaving the $key variable still null and able to be coalesced.
1053
                    $key = $key ?? $captured;
1054
                    break;
1055
1056
                case self::BYTE_BACKSLASH:
1057
                    // Escapes are simply counted and passed to the sequenceQuotedNode() method, causing that method
1058
                    // to ignore exactly this number of backslashes before a matching quote is seen as closing quote.
1059
                    ++$countedEscapes;
1060
                    break;
1061
1062
                case self::BYTE_INLINE_END:
1063
                case self::BYTE_ARRAY_END:
1064
                case self::BYTE_PARENTHESIS_END:
1065
                    // Array end indication. Check if anything was collected previously or was captured currently,
1066
                    // assign that to the array and return an ArrayNode with the full array inside.
1067
                    $captured = $captured ?? ($keyOrValue !== null ? $keyOrValue->flatten(true) : null);
1068
                    $key = $key ?? ($numeric ? ++$itemCount : $captured);
1069
                    if (isset($captured, $key)) {
1070
                        if (is_numeric($captured)) {
1071
                            $array[$key] = $captured + 0;
1072
                        } elseif ($keyOrValue !== null) {
1073
                            $array[$key] = $keyOrValue->flatten();
1074
                        } else {
1075
                            $array[$key] = new ObjectAccessorNode((string) ($captured ?? $key));
1076
                        }
1077
                    }
1078
                    if (!$numeric && isset($key, $definitions) && !isset($definitions[$key])) {
1079
                        throw $this->createUnsupportedArgumentError((string)$key, $definitions);
1080
                    }
1081
                    $this->escapingEnabled = $escapingEnabledBackup;
1082
                    $this->splitter->switch($restore);
1083
                    return;
1084
            }
1085
        }
1086
1087
        throw $this->createErrorAtPosition(
1088
            'Unterminated array',
1089
            1557748574
1090
        );
1091
    }
1092
1093
    /**
1094
     * Sequence a quoted value
1095
     *
1096
     * The return can be either of:
1097
     *
1098
     * 1. A string value if source was for example "string"
1099
     * 2. An integer if source was for example "1"
1100
     * 3. A float if source was for example "1.25"
1101
     * 4. A RootNode instance with multiple child nodes if source was for example "string {var}"
1102
     *
1103
     * The idea is to return the raw value if there is no reason for it to
1104
     * be a node as such - which is only necessary if the quoted expression
1105
     * contains other (dynamic) values like an inline syntax.
1106
     *
1107
     * @param int $leadingEscapes A backwards compatibility measure: when passed, this number of escapes must precede a closing quote for it to trigger node closing.
1108
     * @return RootNode
1109
     */
1110
    protected function sequenceQuotedNode(int $leadingEscapes = 0): RootNode
1111
    {
1112
        $startingByte = $this->source->bytes[$this->splitter->index];
1113
        $node = new RootNode();
1114
        $countedEscapes = 0;
1115
1116
        $contextToRestore = $this->splitter->switch($this->contexts->quoted);
1117
        $this->sequence->next();
1118
        foreach ($this->sequence as $symbol => $captured) {
1119
            switch ($symbol) {
1120
1121
                case self::BYTE_ARRAY_START:
1122
                    $countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored)
1123
                    if ($captured === null) {
1124
                        // Array start "[" only triggers array sequencing if it is the very first byte in the quoted
1125
                        // string - otherwise, it is added as part of the text.
1126
                        $child = new ArrayNode();
1127
                        $this->sequenceArrayNode($child, true);
1128
                        $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...
1129
                    } else {
1130
                        $node->addChild(new TextNode($captured . '['));
1131
                    }
1132
                    break;
1133
1134
                case self::BYTE_INLINE:
1135
                    $countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored)
1136
                    // The quoted string contains a sub-expression. We extract the captured content so far and if it
1137
                    // is not an empty string, add it as a child of the RootNode we're building, then we add the inline
1138
                    // expression as next sibling and continue the loop.
1139
                    if ($captured !== null) {
1140
                        $childNode = new TextNode($captured);
1141
                        $node->addChild($childNode);
1142
                    }
1143
1144
                    $node->addChild($this->sequenceInlineNodes());
1145
                    break;
1146
1147
                case self::BYTE_BACKSLASH:
1148
                    $next = $this->source->bytes[$this->splitter->index + 1] ?? null;
1149
                    ++$countedEscapes;
1150
                    if ($next === $startingByte || $next === self::BYTE_BACKSLASH) {
1151
                        if ($captured !== null) {
1152
                            $node->addChild(new TextNode($captured));
1153
                        }
1154
                    } else {
1155
                        $node->addChild(new TextNode($captured . str_repeat('\\', $countedEscapes)));
1156
                        $countedEscapes = 0;
1157
                    }
1158
                    break;
1159
1160
                // Note: although "case $startingByte:" could have been used here, it would not compile the switch
1161
                // as a hash map and thus would not perform as well overall - when called frequently as it will be.
1162
                // Backtick will only be encountered if the context is "protected" (insensitive inline sequencing)
1163
                case self::BYTE_QUOTE_SINGLE:
1164
                case self::BYTE_QUOTE_DOUBLE:
1165
                case self::BYTE_BACKTICK:
1166
                    if ($symbol !== $startingByte || $countedEscapes !== $leadingEscapes) {
1167
                        $childNode = new TextNode($captured . chr($symbol));
1168
                        $node->addChild($childNode);
1169
                        $countedEscapes = 0; // If number of escapes do not match expected, reset the counter
1170
                        break;
1171
                    }
1172
                    if ($captured !== null) {
1173
                        $childNode = new TextNode($captured);
1174
                        $node->addChild($childNode);
1175
                    }
1176
                    $this->splitter->switch($contextToRestore);
1177
                    return $node;
1178
            }
1179
        }
1180
1181
        throw $this->createErrorAtPosition('Unterminated expression inside quotes', 1557700793);
1182
    }
1183
1184
    /**
1185
     * Dead-end sequencing; if parsing is switched off it cannot be switched on again,
1186
     * and the remainder of the template source must be sequenced as dead text.
1187
     *
1188
     * @return string|null
1189
     */
1190
    protected function sequenceRemainderAsText(): ?string
1191
    {
1192
        $this->sequence->next();
1193
        $this->splitter->switch($this->contexts->empty);
1194
        $source = null;
1195
        foreach ($this->sequence as $symbol => $captured) {
1196
            $source .= $captured;
1197
        }
1198
        return $source;
1199
    }
1200
1201
    /**
1202
     * Call all interceptors registered for a given interception point.
1203
     *
1204
     * @param ComponentInterface $node The syntax tree node which can be modified by the interceptors.
1205
     * @param integer $interceptorPosition the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\self::INTERCEPT_* constants.
1206
     * @return ComponentInterface
1207
     */
1208
    protected function callInterceptor(ComponentInterface $node, int $interceptorPosition): ComponentInterface
1209
    {
1210
        if (!$this->escapingEnabled) {
1211
            // Escaping is either explicitly disabled (for example, we may be parsing arguments) or there are
1212
            // at least 2 preceding ViewHelpers which have disabled child escaping. The reason for checking >1
1213
            // instead of >0 is because escaping must happen if the value is decremented to zero, which is not
1214
            // possible unless the value is >1 to begin with. This early return avoids entering the switch and
1215
            // conditions below when they are simply not necessary to consult.
1216
            return $node;
1217
        }
1218
1219
        switch ($interceptorPosition) {
1220
            case self::INTERCEPT_OPENING_VIEWHELPER:
1221
                if (!$node->isChildrenEscapingEnabled()) {
1222
                    ++$this->viewHelperNodesWhichDisableTheInterceptor;
1223
                }
1224
                break;
1225
1226
            case self::INTERCEPT_CLOSING_VIEWHELPER:
1227
                if (!$node->isChildrenEscapingEnabled()) {
1228
                    --$this->viewHelperNodesWhichDisableTheInterceptor;
1229
                }
1230
                if ($this->viewHelperNodesWhichDisableTheInterceptor === 0 && $node->isOutputEscapingEnabled()) {
1231
                    $node = new EscapingNode($node);
1232
                }
1233
                break;
1234
1235
            case self::INTERCEPT_SELFCLOSING_VIEWHELPER:
1236
                if ($this->viewHelperNodesWhichDisableTheInterceptor === 0 && $node->isOutputEscapingEnabled()) {
1237
                    $node = new EscapingNode($node);
1238
                }
1239
                break;
1240
1241
            case self::INTERCEPT_OBJECTACCESSOR:
1242
            case self::INTERCEPT_EXPRESSION:
1243
                if ($this->viewHelperNodesWhichDisableTheInterceptor === 0) {
1244
                    $node = new EscapingNode($node);
1245
                }
1246
                break;
1247
        }
1248
        return $node;
1249
    }
1250
1251
    /**
1252
     * Creates a dump, starting from the first line break before $position,
1253
     * to the next line break from $position, counting the lines and characters
1254
     * and inserting a marker pointing to the exact offending character.
1255
     *
1256
     * Is not very efficient - but adds bug tracing information. Should only
1257
     * be called when exceptions are raised during sequencing.
1258
     *
1259
     * @param int $index
1260
     * @return string
1261
     */
1262
    protected function extractSourceDumpOfLineAtPosition(int $index): string
1263
    {
1264
        $lines = $this->countCharactersMatchingMask(self::MASK_LINEBREAKS, 1, $index) + 1;
1265
        $offset = $this->findBytePositionBeforeOffset(self::MASK_LINEBREAKS, $index);
1266
        $line = substr(
1267
            $this->source->source,
1268
            $offset,
1269
            $this->findBytePositionAfterOffset(self::MASK_LINEBREAKS, $index)
1270
        );
1271
        $character = $index - $offset - 1;
1272
        $string = 'Line ' . $lines . ' character ' . $character . PHP_EOL;
1273
        $string .= PHP_EOL;
1274
        $string .= str_repeat(' ', max($character, 0)) . 'v' . PHP_EOL;
1275
        $string .= trim($line) . PHP_EOL;
1276
        $string .= str_repeat(' ', max($character, 0)) . '^' . PHP_EOL;
1277
        return $string;
1278
    }
1279
1280
    protected function createErrorAtPosition(string $message, int $code): SequencingException
1281
    {
1282
        $error = new SequencingException($message, $code);
1283
        $error->setExcerpt($this->extractSourceDumpOfLineAtPosition($this->splitter->index));
1284
        $error->setByte($this->source->bytes[$this->splitter->index] ?? 0);
1285
        $error->setLine($this->countCharactersMatchingMask(1 << self::BYTE_WHITESPACE_EOL, 0, $this->splitter->index) + 1);
1286
        if ($this->source instanceof FileSource) {
1287
            $error->setFile($this->source->filePathAndFilename);
1288
        } else {
1289
            $error->setFile('Source hash: ' . sha1($this->source->source));
1290
        }
1291
        return $error;
1292
    }
1293
1294
    protected function createUnsupportedArgumentError(string $argument, array $definitions): SequencingException
1295
    {
1296
        return $this->createErrorAtPosition(
1297
            sprintf(
1298
                'Undeclared argument: %s. Valid arguments are: %s',
1299
                $argument,
1300
                implode(', ', array_keys($definitions))
1301
            ),
1302
            1558298976
1303
        );
1304
    }
1305
1306
    protected function countCharactersMatchingMask(int $primaryMask, int $offset, int $length): int
1307
    {
1308
        $bytes = &$this->source->bytes;
1309
        $counted = 0;
1310
        ++$offset; // We must start one byte after offset since source byte array index starts with 1. See unpack().
1311
        for ($index = $offset; $index < $this->source->length && $index <= $length && isset($bytes[$index]); $index++) {
1312
            $byte = $bytes[$index];
1313
            if (($primaryMask & (1 << $byte)) && $byte < 64) {
1314
                $counted++;
1315
            }
1316
        }
1317
        return $counted;
1318
    }
1319
1320
    protected function findBytePositionBeforeOffset(int $primaryMask, int $offset): int
1321
    {
1322
        $bytes = &$this->source->bytes;
1323
        for ($index = min($offset, $this->source->length); $index > 0; $index--) {
1324
            if (($primaryMask & (1 << $bytes[$index])) && $bytes[$index] < 64) {
1325
                return $index;
1326
            }
1327
        }
1328
        return 0;
1329
    }
1330
1331
    protected function findBytePositionAfterOffset(int $primaryMask, int $offset): int
1332
    {
1333
        $bytes = &$this->source->bytes;
1334
        for ($index = $offset; $index < $this->source->length; $index++) {
1335
            if (($primaryMask & (1 << $bytes[$index])) && $bytes[$index] < 64) {
1336
                return $index;
1337
            }
1338
        }
1339
        return max($this->source->length, $offset);
1340
    }
1341
}