Completed
Pull Request — master (#457)
by Claus
01:57
created

Sequencer   F

Complexity

Total Complexity 190

Size/Duplication

Total Lines 896
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 0
Metric Value
dl 0
loc 896
rs 1.704
c 0
b 0
f 0
wmc 190
lcom 1
cbo 17

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 1
A extractSourceDumpOfLineAtPosition() 0 17 1
A createErrorAtPosition() 0 8 1
A createUnsupportedArgumentError() 0 10 1
A createIterator() 0 4 1
B sequence() 0 48 11
F sequenceTagNode() 0 165 37
F sequenceInlineNodes() 0 309 74
F sequenceArrayNode() 0 133 45
C sequenceQuotedNode() 0 67 14
A callInterceptor() 0 14 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
4
namespace TYPO3Fluid\Fluid\Core\Parser;
5
6
use TYPO3Fluid\Fluid\Core\Parser\Splitter;
7
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode;
8
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionException;
9
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionNodeInterface;
10
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
11
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NumericNode;
12
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode;
13
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
14
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
15
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
16
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition;
17
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperResolver;
18
19
/**
20
 * Sequencer for Fluid syntax
21
 *
22
 * Uses a NoRewindIterator around a sequence of byte values to
23
 * iterate over each syntax-relevant character and determine
24
 * which nodes to create.
25
 *
26
 * Passes the outer iterator between functions that perform the
27
 * iterations. Since the iterator is a NoRewindIterator it will
28
 * not be reset before the start of each loop - meaning that
29
 * when it is passed to a new function, that function continues
30
 * from the last touched index in the byte sequence.
31
 *
32
 * The circumstances around "break or return" in the switches is
33
 * very, very important to understand in context of how iterators
34
 * work. Returning does not advance the iterator like breaking
35
 * would and this causes a different position in the byte sequence
36
 * to be experienced in the method that uses the return value (it
37
 * sees the index of the symbol which terminated the expression,
38
 * not the next symbol after that).
39
 */
40
class Sequencer
41
{
42
    /**
43
     * @var RenderingContextInterface
44
     */
45
    protected $renderingContext;
46
47
    /**
48
     * @var ParsingState
49
     */
50
    protected $state;
51
52
    /**
53
     * @var Contexts
54
     */
55
    protected $contexts;
56
57
    /**
58
     * @var Source
59
     */
60
    protected $source;
61
62
    /**
63
     * @var Splitter
64
     */
65
    protected $splitter;
66
67
    /**
68
     * @var Configuration
69
     */
70
    protected $configuration;
71
72
    /**
73
     * @var ViewHelperResolver
74
     */
75
    protected $resolver;
76
77
    /**
78
     * Whether or not the escaping interceptors are active
79
     *
80
     * @var boolean
81
     */
82
    protected $escapingEnabled = true;
83
84
    public function __construct(
85
        RenderingContextInterface $renderingContext,
86
        ParsingState $state,
87
        Contexts $contexts,
88
        Source $source
89
    ) {
90
        $this->renderingContext = $renderingContext;
91
        $this->resolver = $renderingContext->getViewHelperResolver();
92
        $this->configuration = $renderingContext->buildParserConfiguration();
93
        $this->state = clone $state;
94
        $this->contexts = $contexts;
95
        $this->source = $source;
96
        $this->splitter = new Splitter($this->source, $this->contexts);
97
    }
98
99
    /**
100
     * Creates a dump, starting from the first line break before $position,
101
     * to the next line break from $position, counting the lines and characters
102
     * and inserting a marker pointing to the exact offending character.
103
     *
104
     * Is not very efficient - but adds bug tracing information. Should only
105
     * be called when exceptions are raised during sequencing.
106
     *
107
     * @param Position $position
108
     * @return string
109
     */
110
    public function extractSourceDumpOfLineAtPosition(Position $position): string
111
    {
112
        $lines = $this->splitter->countCharactersMatchingMask(Splitter::MASK_LINEBREAKS, 1, $position->index) + 1;
113
        $offset = $this->splitter->findBytePositionBeforeOffset(Splitter::MASK_LINEBREAKS, $position->index);
114
        $line = substr(
115
            $this->source->source,
116
            $offset,
117
            $this->splitter->findBytePositionAfterOffset(Splitter::MASK_LINEBREAKS, $position->index)
118
        );
119
        $character = $position->index - $offset - 1;
120
        $string = 'Line ' . $lines . ' character ' . $character . PHP_EOL;
121
        $string .= PHP_EOL;
122
        $string .= str_repeat(' ', max($character, 0)) . 'v' . PHP_EOL;
123
        $string .= trim($line) . PHP_EOL;
124
        $string .= str_repeat(' ', max($character, 0)) . '^' . PHP_EOL;
125
        return $string;
126
    }
127
128
    protected function createErrorAtPosition(string $message, int $code): SequencingException
129
    {
130
        $position = new Position($this->splitter->context, $this->splitter->index);
131
        $ascii = (string) $this->source->bytes[$this->splitter->index];
132
        $message .=  ' ASCII: ' . $ascii . ': ' . $this->extractSourceDumpOfLineAtPosition($position);
133
        $error = new SequencingException($message, $code);
134
        return $error;
135
    }
136
137
    protected function createUnsupportedArgumentError(string $argument, array $definitions): SequencingException
138
    {
139
        return $this->createErrorAtPosition(
140
            sprintf(
141
                'Unsupported argument "%s". Supported: ' . implode(', ', array_keys($definitions)),
142
                $argument
143
            ),
144
            1558298976
145
        );
146
    }
147
148
    protected function createIterator(\Generator $generator): \NoRewindIterator
149
    {
150
        return new \NoRewindIterator($generator);
151
    }
152
153
    public function sequence(): ParsingState
154
    {
155
        $split = $this->splitter->parse();
156
        $sequence = $this->createIterator($split);
157
158
        // Please note: repeated calls to $this->getTopmostNodeFromStack() are indeed intentional. That method may
159
        // return different nodes at different times depending on what has occured in other methods! Only the places
160
        // where $node is actually extracted is it (by design) safe to do so. DO NOT REFACTOR!
161
        // It is *also* intentional that this switch has no default case. The root context is very specific and will
162
        // only apply when the splitter is actually in root, which means there is no chance of it yielding an unexpected
163
        // character (because that implies a method called by this method already threw a SequencingException).
164
        foreach ($sequence as $symbol => $captured) {
165
            switch ($symbol) {
166
                case Splitter::BYTE_INLINE:
167
                    $node = $this->state->getNodeFromStack();
168
                    if ($this->splitter->index > 1 && $this->source->bytes[$this->splitter->index - 1] === Splitter::BYTE_BACKSLASH) {
169
                        $node->addChildNode(new TextNode(substr($captured, 0, -1) . '{'));
170
                        break;
171
                    }
172
                    if ($captured !== null) {
173
                        $node->addChildNode(new TextNode($captured));
174
                    }
175
                    $node->addChildNode($this->sequenceInlineNodes($sequence, false));
176
                    $this->splitter->switch($this->contexts->root);
177
                    break;
178
179
                case Splitter::BYTE_TAG:
180
                    if ($captured !== null) {
181
                        $this->state->getNodeFromStack()->addChildNode(new TextNode($captured));
182
                    }
183
184
                    $childNode = $this->sequenceTagNode($sequence);
185
                    $this->splitter->switch($this->contexts->root);
186
                    if ($childNode) {
187
                        $this->state->getNodeFromStack()->addChildNode($childNode);
188
                    }
189
                    break;
190
191
                case Splitter::BYTE_NULL:
192
                    if ($captured !== null) {
193
                        $this->state->getNodeFromStack()->addChildNode(new TextNode($captured));
194
                    }
195
                    break;
196
            }
197
        }
198
199
        return $this->state;
200
    }
201
202
    /**
203
     * @param \Iterator|?string[] $sequence
0 ignored issues
show
Documentation introduced by
The doc-type \Iterator|?string[] could not be parsed: Unknown type name "?string" at position 10. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
204
     * @return NodeInterface|null
205
     */
206
    protected function sequenceTagNode(\Iterator $sequence): ?NodeInterface
207
    {
208
        $arguments = [];
209
        $definitions = null;
210
        $text = '<';
211
        $namespace = null;
212
        $method = null;
213
        $bytes = &$this->source->bytes;
214
        $node = new RootNode();
215
        $selfClosing = false;
216
        $closing = false;
217
        #$escapingEnabledBackup = $this->escapingEnabled;
218
219
        $interceptionPoint = InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER;
220
221
        $this->splitter->switch($this->contexts->tag);
222
        $sequence->next();
223
        foreach ($sequence as $symbol => $captured) {
224
            $text .= $captured;
225
            switch ($symbol) {
226
                case Splitter::BYTE_INLINE:
227
                    $contextBefore = $this->splitter->context;
228
                    $collected = $this->sequenceInlineNodes($sequence, isset($namespace) && isset($method));
229
                    $node->addChildNode(new TextNode($text));
230
                    $node->addChildNode($collected);
231
                    $text = '';
232
                    $this->splitter->switch($contextBefore);
233
                    break;
234
235
                case Splitter::BYTE_SEPARATOR_EQUALS:
236
                    $key = $captured;
237
                    if ($definitions !== null && !isset($definitions[$key])) {
238
                        throw $this->createUnsupportedArgumentError($key, $definitions);
239
                    }
240
                    break;
241
242
                case Splitter::BYTE_QUOTE_DOUBLE:
243
                case Splitter::BYTE_QUOTE_SINGLE:
244
                    $text .= chr($symbol);
245
                    if (!isset($key)) {
246
                        throw $this->createErrorAtPosition('Quoted value without a key is not allowed in tags', 1558952412);
247
                    } else {
248
                        $arguments[$key] = $this->sequenceQuotedNode($sequence, 0, isset($namespace) && isset($method))->flatten(true);
249
                        $key = null;
250
                    }
251
                    break;
252
253
                case Splitter::BYTE_TAG_CLOSE:
254
                    $method = $method ?? $captured;
255
                    $text .= '/';
256
                    $closing = true;
257
                    $selfClosing = $bytes[$this->splitter->index - 1] !== Splitter::BYTE_TAG;
258
                    $interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER;
259
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES && $captured !== null) {
260
                        // We are still capturing arguments and the last yield contained a value. Null-coalesce key
261
                        // with captured string so object accessor becomes key name (ECMA shorthand literal)
262
                        $arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
263
                        $key = null;
264
                    }
265
                    break;
266
267
                case Splitter::BYTE_SEPARATOR_COLON:
268
                    $text .= ':';
269
                    $namespace = $namespace ?? $captured;
270
                    break;
271
272
                case Splitter::BYTE_TAG_END:
273
                    $text .= '>';
274
                    $method = $method ?? $captured;
275
276
                    if (!isset($namespace) || !isset($method) || $this->splitter->context->context === Context::CONTEXT_DEAD || $this->resolver->isNamespaceIgnored($namespace)) {
277
                        return $node->addChildNode(new TextNode($text))->flatten();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $node->addChildNo...ode($text))->flatten(); (null|integer|double|stri...yntaxTree\NodeInterface) is incompatible with the return type documented by TYPO3Fluid\Fluid\Core\Pa...uencer::sequenceTagNode of type TYPO3Fluid\Fluid\Core\Pa...Tree\NodeInterface|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
278
                    }
279
280
                    try {
281
                        $expectedClass = $this->resolver->resolveViewHelperClassName($namespace, $method);
282
                    } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
283
                        throw $this->createErrorAtPosition($exception->getMessage(), $exception->getCode());
284
                    }
285
286
                    if ($closing && !$selfClosing) {
287
                        // Closing byte was more than two bytes back, meaning the tag is NOT self-closing, but is a
288
                        // closing tag for a previously opened+stacked node. Finalize the node now.
289
                        $closesNode = $this->state->popNodeFromStack();
290
                        if ($closesNode instanceof $expectedClass) {
291
                            $arguments = $closesNode->getParsedArguments();
292
                            $viewHelperNode = $closesNode;
293
                        } else {
294
                            throw $this->createErrorAtPosition(
295
                                sprintf(
296
                                    'Mismatched closing tag. Expecting: %s:%s (%s). Found: (%s).',
297
                                    $namespace,
298
                                    $method,
299
                                    $expectedClass,
300
                                    get_class($closesNode)
301
                                ),
302
                                1557700789
303
                            );
304
                        }
305
                    }
306
307
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES && $captured !== null) {
308
                        // We are still capturing arguments and the last yield contained a value. Null-coalesce key
309
                        // with captured string so object accessor becomes key name (ECMA shorthand literal)
310
                        $arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
311
                    }
312
313
                    $viewHelperNode = $viewHelperNode ?? $this->resolver->createViewHelperInstanceFromClassName($expectedClass);
314
                    #$this->escapingEnabled = $escapingEnabledBackup;
315
316
                    if (!$closing) {
317
                        $this->callInterceptor($viewHelperNode, $interceptionPoint);
0 ignored issues
show
Bug introduced by
It seems like $viewHelperNode defined by $viewHelperNode ?? $this...assName($expectedClass) on line 313 can also be of type object<TYPO3Fluid\Fluid\...er\ViewHelperInterface>; however, TYPO3Fluid\Fluid\Core\Pa...ncer::callInterceptor() does only seem to accept object<TYPO3Fluid\Fluid\...ntaxTree\NodeInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
318
                        $viewHelperNode->setParsedArguments($arguments);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TYPO3Fluid\Fluid\Core\Pa...yntaxTree\NodeInterface as the method setParsedArguments() does only exist in the following implementations of said interface: TYPO3Fluid\FluidExample\...elpers\CustomViewHelper, TYPO3Fluid\Fluid\Core\Vi...ractConditionViewHelper, TYPO3Fluid\Fluid\Core\Vi...tractTagBasedViewHelper, TYPO3Fluid\Fluid\Core\Vi...lper\AbstractViewHelper, TYPO3Fluid\Fluid\ViewHelpers\AliasViewHelper, TYPO3Fluid\Fluid\ViewHelpers\CViewHelper, TYPO3Fluid\Fluid\ViewHel...Cache\DisableViewHelper, TYPO3Fluid\Fluid\ViewHel...\Cache\StaticViewHelper, TYPO3Fluid\Fluid\ViewHel...\Cache\WarmupViewHelper, TYPO3Fluid\Fluid\ViewHelpers\CaseViewHelper, TYPO3Fluid\Fluid\ViewHelpers\CommentViewHelper, TYPO3Fluid\Fluid\ViewHelpers\CountViewHelper, TYPO3Fluid\Fluid\ViewHelpers\CycleViewHelper, TYPO3Fluid\Fluid\ViewHelpers\DebugViewHelper, TYPO3Fluid\Fluid\ViewHelpers\DefaultCaseViewHelper, TYPO3Fluid\Fluid\ViewHelpers\ElseViewHelper, TYPO3Fluid\Fluid\ViewHelpers\ForViewHelper, TYPO3Fluid\Fluid\ViewHel...\Format\CdataViewHelper, TYPO3Fluid\Fluid\ViewHel...lspecialcharsViewHelper, TYPO3Fluid\Fluid\ViewHel...Format\PrintfViewHelper, TYPO3Fluid\Fluid\ViewHelpers\Format\RawViewHelper, TYPO3Fluid\Fluid\ViewHelpers\GroupedForViewHelper, TYPO3Fluid\Fluid\ViewHelpers\IfViewHelper, TYPO3Fluid\Fluid\ViewHelpers\InlineViewHelper, TYPO3Fluid\Fluid\ViewHelpers\LayoutViewHelper, TYPO3Fluid\Fluid\ViewHelpers\OrViewHelper, TYPO3Fluid\Fluid\ViewHelpers\RenderViewHelper, TYPO3Fluid\Fluid\ViewHelpers\SectionViewHelper, TYPO3Fluid\Fluid\ViewHelpers\SpacelessViewHelper, TYPO3Fluid\Fluid\ViewHelpers\SwitchViewHelper, TYPO3Fluid\Fluid\ViewHelpers\ThenViewHelper, TYPO3Fluid\Fluid\ViewHelpers\VariableViewHelper.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
319
                        $this->state->pushNodeToStack($viewHelperNode);
320
                        return null;
321
                    }
322
323
                    $viewHelperNode = $viewHelperNode->postParse($arguments, $this->state, $this->renderingContext);
0 ignored issues
show
Bug introduced by
The method postParse does only exist in TYPO3Fluid\Fluid\Core\Vi...per\ViewHelperInterface, but not in TYPO3Fluid\Fluid\Core\Pa...yntaxTree\NodeInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
324
325
                    return $viewHelperNode;
326
327
                case Splitter::BYTE_WHITESPACE_TAB:
328
                case Splitter::BYTE_WHITESPACE_RETURN:
329
                case Splitter::BYTE_WHITESPACE_EOL:
330
                case Splitter::BYTE_WHITESPACE_SPACE:
331
                    if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) {
332
                        if ($captured !== null) {
333
                            $arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
334
                            $key = null;
335
                        }
336
                    } else {
337
                        $text .= chr($symbol);
338
                        if (isset($namespace)) {
339
                            $method = $captured;
340
341
                            $this->escapingEnabled = false;
342
                            $viewHelperNode = $this->resolver->createViewHelperInstance($namespace, $method);
343
                            $definitions = $viewHelperNode->prepareArguments();
344
345
                            // A whitespace character, in tag context, means the beginning of an array sequence (which may
346
                            // or may not contain any items; the next symbol may be a tag end or tag close). We sequence the
347
                            // arguments array and create a ViewHelper node.
348
                            $this->splitter->switch($this->contexts->attributes);
349
                            break;
350
                        }
351
352
                        // A whitespace before a colon means the tag is not a namespaced tag. We will ignore everything
353
                        // inside this tag, except for inline syntax, until the tag ends. For this we use a special,
354
                        // limited variant of the root context where instead of scanning for "<" we scan for ">".
355
                        // We continue in this same loop because it still matches the potential symbols being yielded.
356
                        // Most importantly: this new reduced context will NOT match a colon which is the trigger symbol
357
                        // for a ViewHelper tag.
358
                        $this->splitter->switch($this->contexts->dead);
359
                    }
360
                    break;
361
            }
362
        }
363
364
        // This case on the surface of it, belongs as "default" case in the switch above. However, the only case that
365
        // would *actually* produce this error, is if the splitter reaches EOF (null byte) symbol before the tag was
366
        // closed. Literally every other possible error type will be thrown as more specific exceptions (e.g. invalid
367
        // argument, missing key, wrong quotes, bad inline and *everything* else with the exception of EOF). Even a
368
        // stray null byte would not be caught here as null byte is not part of the symbol collection for "tag" context.
369
        throw $this->createErrorAtPosition('Unexpected token in tag sequencing', 1557700786);
370
    }
371
372
    /**
373
     * @param \Iterator|?string[] $sequence
0 ignored issues
show
Documentation introduced by
The doc-type \Iterator|?string[] could not be parsed: Unknown type name "?string" at position 10. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
374
     * @param bool $allowArray
375
     * @return NodeInterface
376
     */
377
    protected function sequenceInlineNodes(\Iterator $sequence, bool $allowArray = true): NodeInterface
378
    {
379
        $text = '{';
380
        $node = null;
381
        $key = null;
382
        $namespace = null;
383
        $method = null;
384
        $potentialAccessor = null;
385
        $callDetected = false;
386
        $hasPass = false;
387
        $hasColon = null;
388
        $hasWhitespace = false;
389
        $isArray = false;
390
        $array = [];
391
        $arguments = [];
392
        $ignoredEndingBraces = 0;
393
        $countedEscapes = 0;
394
395
        $this->splitter->switch($this->contexts->inline);
396
        $sequence->next();
397
        foreach ($sequence as $symbol => $captured) {
398
            $text .= $captured;
399
            switch ($symbol) {
400
                case Splitter::BYTE_BACKSLASH:
401
                    // Increase the number of counted escapes (is passed to sequenceNode() in the "QUOTE" cases and reset
402
                    // after the quoted string is extracted).
403
                    ++$countedEscapes;
404
                    break;
405
406
                case Splitter::BYTE_ARRAY_START:
407
408
                    $text .= chr($symbol);
409
                    $isArray = $allowArray;
410
411
                    #ArrayStart:
412
                    // Sequence the node. Pass the "use numeric keys?" boolean based on the current byte. Only array
413
                    // start creates numeric keys. Inline start with keyless values creates ECMA style {foo:foo, bar:bar}
414
                    // from {foo, bar}.
415
                    $array[$key ?? $captured ?? 0] = $node = $this->sequenceArrayNode($sequence, null, $symbol === Splitter::BYTE_ARRAY_START);
416
                    $this->splitter->switch($this->contexts->inline);
417
                    unset($key);
418
                    break;
419
420
                case Splitter::BYTE_INLINE:
421
                    // Encountering this case can mean different things: sub-syntax like {foo.{index}} or array, depending
422
                    // on presence of either a colon or comma before the inline. In protected mode it is simply added.
423
                    $text .= '{';
424
                    if (!$hasWhitespace && $text !== '{{') {
425
                        // Most likely, a nested object accessor syntax e.g. {foo.{bar}} - enter protected context since
426
                        // these accessors do not allow anything other than additional nested accessors.
427
                        $this->splitter->switch($this->contexts->accessor);
428
                        ++$ignoredEndingBraces;
429
                    } elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED) {
430
                        // Ignore one ending additional curly brace. Subtracted in the BYTE_INLINE_END case below.
431
                        // The expression in this case looks like {{inline}.....} and we capture the curlies.
432
                        $potentialAccessor .= $captured;
433
                        ++$ignoredEndingBraces;
434
                    } elseif ($allowArray || $isArray) {
435
                        $isArray = true;
436
                        $captured = $key ?? $captured ?? $potentialAccessor;
437
                        // This is a sub-syntax following a colon - meaning it is an array.
438
                        if ($captured !== null) {
439
                            #goto ArrayStart;
440
                            $array[$key ?? $captured ?? 0] = $node = $this->sequenceArrayNode($sequence, null, $symbol === Splitter::BYTE_ARRAY_START);
441
                            $this->splitter->switch($this->contexts->inline);
442
                        }
443
                    } else {
444
                        $childNodeToAdd = $this->sequenceInlineNodes($sequence, $allowArray);
445
                        $node = isset($node) ? $node->addChildNode($childNodeToAdd) : (new RootNode())->addChildNode($childNodeToAdd);
446
                    }
447
                    break;
448
449
                case Splitter::BYTE_MINUS:
450
                    $text .= '-';
451
                    break;
452
453
                // Backtick may be encountered in two different contexts: normal inline context, in which case it has
454
                // the same meaning as any quote and causes sequencing of a quoted string. Or protected context, in
455
                // which case it also sequences a quoted node but appends the result instead of assigning to array.
456
                // Note that backticks do not support escapes (they are a new feature that does not require escaping).
457
                case Splitter::BYTE_BACKTICK:
458
                    if ($this->splitter->context->context === Context::CONTEXT_PROTECTED) {
459
                        $node->addChildNode(new TextNode($text));
460
                        $node->addChildNode($this->sequenceQuotedNode($sequence)->flatten());
461
                        $text = '';
462
                        break;
463
                    }
464
                // Fallthrough is intentional: if not in protected context, consider the backtick a normal quote.
465
466
                // Case not normally countered in straight up "inline" context, but when encountered, means we have
467
                // explicitly found a quoted array key - and we extract it.
468
                case Splitter::BYTE_QUOTE_SINGLE:
469
                case Splitter::BYTE_QUOTE_DOUBLE:
470
                    if (!$allowArray) {
471
                        $text .= chr($symbol);
472
                        break;
473
                    }
474
                    if (isset($key)) {
475
                        $array[$key] = $this->sequenceQuotedNode($sequence, $countedEscapes)->flatten(true);
476
                        $key = null;
477
                    } else {
478
                        $key = $this->sequenceQuotedNode($sequence, $countedEscapes)->flatten(true);
479
                    }
480
                    $countedEscapes = 0;
481
                    $isArray = $allowArray;
482
                    break;
483
484
                case Splitter::BYTE_SEPARATOR_COMMA:
485
                    if (!$allowArray) {
486
                        $text .= ',';
487
                        break;
488
                    }
489
                    if (isset($captured)) {
490
                        $array[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
491
                    }
492
                    $key = null;
493
                    $isArray = $allowArray;
494
                    break;
495
496
                case Splitter::BYTE_SEPARATOR_EQUALS:
497
                    $text .= '=';
498
                    if (!$allowArray) {
499
                        $node = new RootNode();
500
                        $this->splitter->switch($this->contexts->protected);
501
                        break;
502
                    }
503
                    $key = $captured;
504
                    $isArray = $allowArray;
505
                    break;
506
507
                case Splitter::BYTE_SEPARATOR_COLON:
508
                    $text .= ':';
509
                    $hasColon = true;
510
                    $namespace = $captured;
511
                    $key = $key ?? $captured;
512
                    $isArray = $isArray || ($allowArray && is_numeric($key));
513
                    break;
514
515
                case Splitter::BYTE_WHITESPACE_SPACE:
516
                case Splitter::BYTE_WHITESPACE_EOL:
517
                case Splitter::BYTE_WHITESPACE_RETURN:
518
                case Splitter::BYTE_WHITESPACE_TAB:
519
                    // If we already collected some whitespace we must enter protected context.
520
                    $text .= $this->source->source[$this->splitter->index - 1];
521
                    if ($hasWhitespace && !$hasPass && !$allowArray) {
522
                        // Protection mode: this very limited context does not allow tags or inline syntax, and will
523
                        // protect things like CSS and JS - and will only enter a more reactive context if encountering
524
                        // the backtick character, meaning a quoted string will be sequenced. This backtick-quoted
525
                        // string can then contain inline syntax like variable accessors.
526
                        $node = $node ?? new RootNode();
527
                        $this->splitter->switch($this->contexts->protected);
528
                        break;
529
                    }
530
                    $key = $key ?? $captured;
531
                    $hasWhitespace = true;
532
                    $isArray = $allowArray && ($hasColon ?? $isArray ?? is_numeric($captured));
533
                    $potentialAccessor = ($potentialAccessor ?? $captured);
534
                    break;
535
536
                case Splitter::BYTE_TAG_END:
537
                case Splitter::BYTE_PIPE:
538
                    // If there is an accessor on the left side of the pipe and $node is not defined, we create $node
539
                    // as an object accessor. If $node already exists we do nothing (and expect the VH trigger, the
540
                    // parenthesis start case below, to add $node as childnode and create a new $node).
541
                    $hasPass = true;
542
                    $isArray = $allowArray;
543
                    $callDetected = false;
544
                    $potentialAccessor = $potentialAccessor ?? $captured;
545
                    $text .=  $this->source->source[$this->splitter->index - 1];
546
                    if (isset($potentialAccessor)) {
547
                        $childNodeToAdd = new ObjectAccessorNode($potentialAccessor);
548
                        $node = isset($node) ? $node->addChildNode($childNodeToAdd) : $childNodeToAdd; //$node ?? (is_numeric($potentialAccessor) ? $potentialAccessor + 0 : new ObjectAccessorNode($potentialAccessor));
549
                    }
550
                    //!isset($potentialAccessor) ?: ($node = ($node ?? $this->createObjectAccessorNodeOrRawValue($potentialAccessor)));
551
                    unset($namespace, $method, $potentialAccessor, $key);
552
                    break;
553
554
                case Splitter::BYTE_PARENTHESIS_START:
555
                    $isArray = false;
556
                    // Special case: if a parenthesis start was preceded by whitespace but had no pass operator we are
557
                    // not dealing with a ViewHelper call and will continue the sequencing, grabbing the parenthesis as
558
                    // part of the expression.
559
                    $text .= '(';
560
                    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...
561
                        $this->splitter->switch($this->contexts->protected);
562
                        unset($namespace, $method);
563
                        break;
564
                    }
565
566
                    $callDetected = true;
567
                    $method = $captured;
568
                    $childNodeToAdd = $node;
569
                    try {
570
                        $node = $this->resolver->createViewHelperInstance($namespace, $method);
571
                        $definitions = $node->prepareArguments();
572
                    } catch (\TYPO3Fluid\Fluid\Core\Exception $exception) {
573
                        throw $this->createErrorAtPosition($exception->getMessage(), $exception->getCode());
574
                    }
575
                    $this->splitter->switch($this->contexts->array);
576
                    $arguments = $this->sequenceArrayNode($sequence, $definitions)->getInternalArray();
577
                    $this->splitter->switch($this->contexts->inline);
578
                    if ($childNodeToAdd) {
579
                        $escapingEnabledBackup = $this->escapingEnabled;
580
                        $this->escapingEnabled = (bool)$node->isChildrenEscapingEnabled();
581
                        if ($childNodeToAdd instanceof ObjectAccessorNode) {
582
                            $this->callInterceptor($childNodeToAdd, InterceptorInterface::INTERCEPT_OBJECTACCESSOR);
583
                        }
584
                        $this->escapingEnabled = $escapingEnabledBackup;
585
                        $node->addChildNode($childNodeToAdd);
586
                    }
587
                    $text .= ')';
588
                    unset($potentialAccessor);
589
                    break;
590
591
                case Splitter::BYTE_INLINE_END:
592
                    $text .= '}';
593
                    if (--$ignoredEndingBraces >= 0) {
594
                        break;
595
                    }
596
                    $isArray = $allowArray && ($isArray ?: ($hasColon && !$hasPass && !$callDetected));
597
                    $potentialAccessor = $potentialAccessor ?? $captured;
598
599
                    // Decision: if we did not detect a ViewHelper we match the *entire* expression, from the cached
600
                    // starting index, to see if it matches a known type of expression. If it does, we must return the
601
                    // appropriate type of ExpressionNode.
602
                    if ($isArray) {
603
                        if ($captured !== null) {
604
                            $array[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
605
                        }
606
                        return new ArrayNode($array);
607
                    } elseif ($callDetected) {
608
                        // The first-priority check is for a ViewHelper used right before the inline expression ends,
609
                        // in which case there is no further syntax to come.
610
                        $node = $node->postParse($arguments, $this->state, $this->renderingContext);
611
                        $interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER;
612
                    } elseif ($this->splitter->context->context === Context::CONTEXT_ACCESSOR) {
613
                        // If we are currently in "accessor" context we can now add the accessor by stripping the collected text.
614
                        $node = new ObjectAccessorNode(substr($text, 1, -1));
615
                        $interceptionPoint = InterceptorInterface::INTERCEPT_OBJECTACCESSOR;
616
                    } elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED || ($hasWhitespace && !$callDetected && !$hasPass)) {
617
                        // In order to qualify for potentially being an expression, the entire inline node must contain
618
                        // whitespace, must not contain parenthesis, must not contain a colon and must not contain an
619
                        // inline pass operand. This significantly limits the number of times this (expensive) routine
620
                        // has to be executed.
621
                        $interceptionPoint = InterceptorInterface::INTERCEPT_TEXT;
622
                        $childNodeToAdd = new TextNode($text);
623
                        foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) {
624
                            $matchedVariables = [];
625
                            // TODO: rewrite expression nodes to receive a sub-Splitter that lets the expression node
626
                            // consume a symbol+capture sequence and either match or ignore it; then use the already
627
                            // consumed (possibly halted mid-way through iterator!) sequence to achieve desired behavior.
628
                            preg_match_all($expressionNodeTypeClassName::$detectionExpression, $text, $matchedVariables, PREG_SET_ORDER);
629
                            foreach ($matchedVariables as $matchedVariableSet) {
0 ignored issues
show
Bug introduced by
The expression $matchedVariables of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
630
                                try {
631
                                    $childNodeToAdd = new $expressionNodeTypeClassName($matchedVariableSet[0], $matchedVariableSet, $this->state);
632
                                    $interceptionPoint = InterceptorInterface::INTERCEPT_EXPRESSION;
633
                                } catch (ExpressionException $error) {
634
                                    $childNodeToAdd = new TextNode($this->renderingContext->getErrorHandler()->handleExpressionError($error));
635
                                }
636
                                break;
637
                            }
638
                        }
639
                        $node = isset($node) ? $node->addChildNode($childNodeToAdd) : $childNodeToAdd;
640
                    } elseif (!$hasPass && !$callDetected) {
641
                        // Third priority check is if there was no pass syntax and no ViewHelper, in which case we
642
                        // create a standard ObjectAccessorNode; alternatively, if nothing was captured (expression
643
                        // was empty, e.g. {} was used) we create a TextNode with the captured text to output "{}".
644
                        if (isset($potentialAccessor)) {
645
                            // If the accessor is set we can trust it is not a numeric value, since this will have
646
                            // set $isArray to TRUE if nothing else already did so.
647
                            $node = is_numeric($potentialAccessor) ? $potentialAccessor + 0 : new ObjectAccessorNode($potentialAccessor);
648
                            $interceptionPoint = InterceptorInterface::INTERCEPT_OBJECTACCESSOR;
649
                        } else {
650
                            $node = new TextNode($text);
651
                            $interceptionPoint = InterceptorInterface::INTERCEPT_TEXT;
652
                        }
653
                    } elseif ($hasPass && $this->resolver->isAliasRegistered((string)$potentialAccessor)) {
654
                        // Fourth priority check is for a pass to a ViewHelper alias, e.g. "{value | raw}" in which case
655
                        // we look for the alias used and create a ViewHelperNode with no arguments.
656
                        $childNodeToAdd = $node;
657
                        $node = $this->resolver->createViewHelperInstance(null, $potentialAccessor);
658
                        $node->addChildNode($childNodeToAdd);
659
                        $node = $node->postParse($arguments, $this->state, $this->renderingContext);
660
                        $interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER;
661
                    } else {
662
                        # TODO: should this be an error case, or should it result in a TextNode?
663
                        throw $this->createErrorAtPosition(
664
                            'Invalid inline syntax - not accessor, not expression, not array, not ViewHelper, but ' .
665
                            'contains the tokens used by these in a sequence that is not valid Fluid syntax. You can ' .
666
                            'most likely avoid this by adding whitespace inside the curly braces before the first ' .
667
                            'Fluid-like symbol in the expression. Symbols recognized as Fluid are: "' .
668
                            addslashes(implode('","', array_map('chr', $this->contexts->inline->bytes))) . '"',
669
                            1558782228
670
                        );
671
                    }
672
673
                    $escapingEnabledBackup = $this->escapingEnabled;
674
                    $this->escapingEnabled = (bool)((isset($viewHelper) && $node->isOutputEscapingEnabled()) || $escapingEnabledBackup);
0 ignored issues
show
Bug introduced by
The variable $viewHelper seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
675
                    $this->callInterceptor($node, $interceptionPoint, $this->state);
0 ignored issues
show
Unused Code introduced by
The call to Sequencer::callInterceptor() has too many arguments starting with $this->state.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
676
                    $this->escapingEnabled = $escapingEnabledBackup;
677
                    return $node;
678
            }
679
        }
680
681
        // See note in sequenceTagNode() end of method body. TL;DR: this is intentionally here instead of as "default"
682
        // case in the switch above for a very specific reason: the case is only encountered if seeing EOF before the
683
        // inline expression was closed.
684
        throw $this->createErrorAtPosition('Unterminated inline syntax', 1557838506);
685
    }
686
687
    /**
688
     * @param \Iterator|Position[] $sequence
689
     * @param ArgumentDefinition[] $definitions
690
     * @param bool $numeric
691
     * @return ArrayNode
692
     */
693
    protected function sequenceArrayNode(\Iterator $sequence, array $definitions = null, bool $numeric = false): ArrayNode
694
    {
695
        $array = [];
696
697
        $keyOrValue = null;
698
        $key = null;
699
        $escapingEnabledBackup = $this->escapingEnabled;
700
        $this->escapingEnabled = false;
701
        $itemCount = -1;
702
        $countedEscapes = 0;
703
704
        $sequence->next();
705
        foreach ($sequence as $symbol => $captured) {
706
            switch ($symbol) {
707
                case Splitter::BYTE_SEPARATOR_COLON:
708
                case Splitter::BYTE_SEPARATOR_EQUALS:
709
                    // Colon or equals has same meaning (which allows tag syntax as argument syntax). Encountering this
710
                    // byte always means the preceding byte was a key. However, if nothing was captured before this,
711
                    // it means colon or equals was used without a key which is a syntax error.
712
                    $key = $key ?? $captured ?? (isset($keyOrValue) ? $keyOrValue->flatten(true) : null);
713
                    if (!isset($key)) {
714
                        throw $this->createErrorAtPosition('Unexpected colon or equals sign, no preceding key', 1559250839);
715
                    }
716
                    if ($definitions !== null && !$numeric && !isset($definitions[$key])) {
717
                        throw $this->createUnsupportedArgumentError((string)$key, $definitions);
718
                    }
719
                    break;
720
721
                case Splitter::BYTE_ARRAY_START:
722
                case Splitter::BYTE_INLINE:
723
                    // Minimal safeguards to improve error feedback. Theoretically such "garbage" could simply be ignored
724
                    // without causing problems to the parser, but it is probably best to report it as it could indicate
725
                    // the user expected X value but gets Y and doesn't notice why.
726
                    if ($captured !== null) {
727
                        throw $this->createErrorAtPosition('Unexpected content before array/inline start in associative array, ASCII: ' . ord($captured), 1559131849);
728
                    }
729
                    if (!isset($key) && !$numeric) {
730
                        throw $this->createErrorAtPosition('Unexpected array/inline start in associative array without preceding key', 1559131848);
731
                    }
732
733
                    // Encountering a curly brace or square bracket start byte will both cause a sub-array to be sequenced,
734
                    // the difference being that only the square bracket will cause third parameter ($numeric) passed to
735
                    // sequenceArrayNode() to be true, which in turn causes key-less items to be added with numeric indexes.
736
                    $key = $key ?? ++$itemCount;
737
                    $array[$key] = $this->sequenceArrayNode($sequence, null, $symbol === Splitter::BYTE_ARRAY_START);
738
                    $keyOrValue = null;
739
                    $key = null;
740
                    break;
741
742
                case Splitter::BYTE_QUOTE_SINGLE:
743
                case Splitter::BYTE_QUOTE_DOUBLE:
744
                    // Safeguard: if anything is captured before a quote this indicates garbage leading content. As with
745
                    // the garbage safeguards above, this one could theoretically be ignored in favor of silently making
746
                    // the odd syntax "just work".
747
                    if ($captured !== null) {
748
                        throw $this->createErrorAtPosition('Unexpected content before quote start in associative array, ASCII: ' . ord($captured), 1559145560);
749
                    }
750
751
                    // Quotes will always cause sequencing of the quoted string, but differs in behavior based on whether
752
                    // or not the $key is set. If $key is set, we know for sure we can assign a value. If it is not set
753
                    // we instead leave $keyOrValue defined so this will be processed by one of the next iterations.
754
                    $keyOrValue = $this->sequenceQuotedNode($sequence, $countedEscapes);
755
                    if (isset($key)) {
756
                        $array[$key] = $keyOrValue->flatten(true);
757
                        $keyOrValue = null;
758
                        $key = null;
759
                        $countedEscapes = 0;
760
                    }
761
                    break;
762
763
                case Splitter::BYTE_SEPARATOR_COMMA:
764
                    // Comma separator: if we've collected a key or value, use it. Otherwise, use captured string.
765
                    // If neither key nor value nor captured string exists, ignore the comma (likely a tailing comma).
766
                    if (isset($keyOrValue)) {
767
                        // Key or value came as quoted string and exists in $keyOrValue
768
                        $potentialValue = $keyOrValue->flatten(true);
769
                        $key = $numeric ? ++$itemCount : $potentialValue;
770
                        $array[$key] = $numeric ? $potentialValue : (is_numeric($key) ? $key + 0 : new ObjectAccessorNode($key));
0 ignored issues
show
Bug introduced by
It seems like $key defined by $numeric ? ++$itemCount : $potentialValue on line 769 can also be of type object<TYPO3Fluid\Fluid\...ntaxTree\NodeInterface>; however, TYPO3Fluid\Fluid\Core\Pa...ssorNode::__construct() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
771
                    } elseif (isset($captured)) {
772
                        $key = $key ?? ($numeric ? ++$itemCount : $captured);
773
                        if (!$numeric && isset($definitions) && !isset($definitions[$key])) {
774
                            throw $this->createUnsupportedArgumentError((string)$key, $definitions);
775
                        }
776
                        $array[$key] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured);
777
                    }
778
                    $keyOrValue = null;
779
                    $key = null;
780
                    break;
781
782
                case Splitter::BYTE_WHITESPACE_TAB:
783
                case Splitter::BYTE_WHITESPACE_RETURN:
784
                case Splitter::BYTE_WHITESPACE_EOL:
785
                case Splitter::BYTE_WHITESPACE_SPACE:
786
                    // Any whitespace attempts to set the key, if not already set. The captured string may be null as
787
                    // well, leaving the $key variable still null and able to be coalesced.
788
                    $key = $key ?? $captured;
789
                    break;
790
791
                case Splitter::BYTE_BACKSLASH:
792
                    // Escapes are simply counted and passed to the sequenceQuotedNode() method, causing that method
793
                    // to ignore exactly this number of backslashes before a matching quote is seen as closing quote.
794
                    ++$countedEscapes;
795
                    break;
796
797
                case Splitter::BYTE_INLINE_END:
798
                case Splitter::BYTE_ARRAY_END:
799
                case Splitter::BYTE_PARENTHESIS_END:
800
                    // Array end indication. Check if anything was collected previously or was captured currently,
801
                    // assign that to the array and return an ArrayNode with the full array inside.
802
                    $captured = $captured ?? (isset($keyOrValue) ? $keyOrValue->flatten(true) : null);
803
                    $key = $key ?? ($numeric ? ++$itemCount : $captured);
804
                    if (isset($captured, $key)) {
805
                        if (is_numeric($captured)) {
806
                            $array[$key] = $captured + 0;
807
                        } elseif (isset($keyOrValue)) {
808
                            $array[$key] = $keyOrValue->flatten();
809
                        } else {
810
                            $array[$key] = new ObjectAccessorNode($captured ?? $key);
811
                        }
812
                    }
813
                    if (!$numeric && isset($key, $definitions) && !isset($definitions[$key])) {
814
                        throw $this->createUnsupportedArgumentError((string)$key, $definitions);
815
                    }
816
                    $this->escapingEnabled = $escapingEnabledBackup;
817
                    return new ArrayNode($array);
818
            }
819
        }
820
821
        throw $this->createErrorAtPosition(
822
            'Unterminated array',
823
            1557748574
824
        );
825
    }
826
827
    /**
828
     * Sequence a quoted value
829
     *
830
     * The return can be either of:
831
     *
832
     * 1. A string value if source was for example "string"
833
     * 2. An integer if source was for example "1"
834
     * 3. A float if source was for example "1.25"
835
     * 4. A RootNode instance with multiple child nodes if source was for example "string {var}"
836
     *
837
     * The idea is to return the raw value if there is no reason for it to
838
     * be a node as such - which is only necessary if the quoted expression
839
     * contains other (dynamic) values like an inline syntax.
840
     *
841
     * @param \Iterator|Position[] $sequence
842
     * @param int $leadingEscapes A backwards compatibility measure: when passed, this number of escapes must precede a closing quote for it to trigger node closing.
843
     * @param bool $allowArray
844
     * @return RootNode
845
     */
846
    protected function sequenceQuotedNode(\Iterator $sequence, int $leadingEscapes = 0, $allowArray = true): RootNode
847
    {
848
        $startingByte = $this->source->bytes[$this->splitter->index];
849
        $contextToRestore = $this->splitter->switch($this->contexts->quoted);
850
        $node = new RootNode();
851
        $sequence->next();
852
        $countedEscapes = 0;
853
854
        foreach ($sequence as $symbol => $captured) {
855
            switch ($symbol) {
856
857
                case Splitter::BYTE_ARRAY_START:
858
                    $countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored)
859
                    if ($captured === null) {
860
                        // Array start "[" only triggers array sequencing if it is the very first byte in the quoted
861
                        // string - otherwise, it is added as part of the text.
862
                        $this->splitter->switch($this->contexts->array);
863
                        $node->addChildNode($this->sequenceArrayNode($sequence, null, $allowArray));
864
                        $this->splitter->switch($this->contexts->quoted);
865
                    } else {
866
                        $node->addChildNode(new TextNode($captured . '['));
867
                    }
868
                    break;
869
870
                case Splitter::BYTE_INLINE:
871
                    $countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored)
872
                    // The quoted string contains a sub-expression. We extract the captured content so far and if it
873
                    // is not an empty string, add it as a child of the RootNode we're building, then we add the inline
874
                    // expression as next sibling and continue the loop.
875
                    if ($captured !== null) {
876
                        $childNode = new TextNode($captured);
877
                        $this->callInterceptor($childNode, InterceptorInterface::INTERCEPT_TEXT, $this->state);
0 ignored issues
show
Unused Code introduced by
The call to Sequencer::callInterceptor() has too many arguments starting with $this->state.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
878
                        $node->addChildNode($childNode);
879
                    }
880
881
                    $node->addChildNode($this->sequenceInlineNodes($sequence));
882
                    $this->splitter->switch($this->contexts->quoted);
883
                    break;
884
885
                case Splitter::BYTE_BACKSLASH:
886
                    ++$countedEscapes;
887
                    if ($captured !== null) {
888
                        $node->addChildNode(new TextNode($captured));
889
                    }
890
                    break;
891
892
                // Note: although "case $startingByte:" could have been used here, it would not compile the switch
893
                // as a hash map and thus would not perform as well overall - when called frequently as it will be.
894
                // Backtick will only be encountered if the context is "protected" (insensitive inline sequencing)
895
                case Splitter::BYTE_QUOTE_SINGLE:
896
                case Splitter::BYTE_QUOTE_DOUBLE:
897
                case Splitter::BYTE_BACKTICK:
898
                    if ($symbol !== $startingByte || $countedEscapes !== $leadingEscapes) {
899
                        $node->addChildNode(new TextNode($captured . chr($symbol)));
900
                        $countedEscapes = 0; // If number of escapes do not match expected, reset the counter
901
                        break;
902
                    }
903
                    if ($captured !== null) {
904
                        $node->addChildNode(new TextNode($captured));
905
                    }
906
                    $this->splitter->switch($contextToRestore);
907
                    return $node;
908
            }
909
        }
910
911
        throw $this->createErrorAtPosition('Unterminated expression inside quotes', 1557700793);
912
    }
913
914
    /**
915
     * Call all interceptors registered for a given interception point.
916
     *
917
     * @param NodeInterface $node The syntax tree node which can be modified by the interceptors.
918
     * @param integer $interceptionPoint the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants.
919
     * @return void
920
     */
921
    protected function callInterceptor(NodeInterface &$node, $interceptionPoint)
922
    {
923
        if ($this->escapingEnabled) {
924
            /** @var $interceptor InterceptorInterface */
925
            foreach ($this->configuration->getEscapingInterceptors($interceptionPoint) as $interceptor) {
926
                $node = $interceptor->process($node, $interceptionPoint, $this->state);
927
            }
928
        }
929
930
        /** @var $interceptor InterceptorInterface */
931
        foreach ($this->configuration->getInterceptors($interceptionPoint) as $interceptor) {
932
            $node = $interceptor->process($node, $interceptionPoint, $this->state);
933
        }
934
    }
935
}