Completed
Pull Request — master (#470)
by Claus
06:03
created

Sequencer   F

Complexity

Total Complexity 289

Size/Duplication

Total Lines 1325
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 22

Importance

Changes 0
Metric Value
dl 0
loc 1325
rs 0.8
c 0
b 0
f 0
wmc 289
lcom 1
cbo 22

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 1
C sequence() 0 69 12
B sequenceUntilClosingTagAndIgnoreNested() 0 34 9
A sequenceCharacterData() 0 19 5
B sequenceToggleInstruction() 0 29 6
F sequenceTagNode() 0 237 55
F sequenceInlineNodes() 0 365 83
D sequenceBooleanNode() 0 59 18
F sequenceArrayNode() 0 155 55
C sequenceQuotedNode() 0 73 16
A sequenceRemainderAsText() 0 10 2
C callInterceptor() 0 35 12
A extractSourceDumpOfLineAtPosition() 0 17 1
A createErrorAtPosition() 0 7 1
A createUnsupportedArgumentError() 0 11 1
A countCharactersMatchingMask() 0 11 4
A findBytePositionBeforeOffset() 0 10 4
A findBytePositionAfterOffset() 0 10 4

How to fix   Complexity   

Complex Class

Complex classes like Sequencer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Sequencer, and based on these observations, apply Extract Interface, too.

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

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

Loading history...
1337
    {
1338
        $bytes = &$this->source->bytes;
1339
        $counted = 0;
1340
        for ($index = $offset; $index < $this->source->length; $index++) {
1341
            if (($primaryMask & (1 << $bytes[$index])) && $bytes[$index] < 64) {
1342
                $counted++;
1343
            }
1344
        }
1345
        return $counted;
1346
    }
1347
1348
    protected function findBytePositionBeforeOffset(int $primaryMask, int $offset): int
1349
    {
1350
        $bytes = &$this->source->bytes;
1351
        for ($index = min($offset, $this->source->length); $index > 0; $index--) {
1352
            if (($primaryMask & (1 << $bytes[$index])) && $bytes[$index] < 64) {
1353
                return $index;
1354
            }
1355
        }
1356
        return 0;
1357
    }
1358
1359
    protected function findBytePositionAfterOffset(int $primaryMask, int $offset): int
1360
    {
1361
        $bytes = &$this->source->bytes;
1362
        for ($index = $offset; $index < $this->source->length; $index++) {
1363
            if (($primaryMask & (1 << $bytes[$index])) && $bytes[$index] < 64) {
1364
                return $index;
1365
            }
1366
        }
1367
        return max($this->source->length, $offset);
1368
    }
1369
}