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

Sequencer::sequence()   C

Complexity

Conditions 14
Paths 123

Size

Total Lines 91

Duplication

Lines 0
Ratio 0 %

Importance

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