Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
229 / 229
100.00% covered (success)
100.00%
26 / 26
CRAP
100.00% covered (success)
100.00%
1 / 1
Parser
100.00% covered (success)
100.00%
229 / 229
100.00% covered (success)
100.00%
26 / 26
108
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 newFromDataSource
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 newFromTokens
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 consumeToken
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 consumeTokenAndWhitespace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getParseErrors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearParseErrors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseError
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parseStylesheet
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 parseRuleList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parseRule
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 parseDeclaration
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 parseDeclarationList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parseDeclarationOrAtRuleList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parseComponentValue
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 parseComponentValueList
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 parseCommaSeparatedComponentValueList
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 consumeRuleList
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
10
 consumeDeclarationOrAtRuleList
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
13
 consumeDeclaration
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
16
 consumeAtRule
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 consumeQualifiedRule
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 consumeComponentValue
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 consumeSimpleBlock
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 consumeFunction
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2/**
3 * @file
4 * @license https://opensource.org/licenses/Apache-2.0 Apache-2.0
5 */
6
7namespace Wikimedia\CSS\Parser;
8
9use Wikimedia\CSS\Objects\AtRule;
10use Wikimedia\CSS\Objects\ComponentValue;
11use Wikimedia\CSS\Objects\ComponentValueList;
12use Wikimedia\CSS\Objects\CSSFunction;
13use Wikimedia\CSS\Objects\Declaration;
14use Wikimedia\CSS\Objects\DeclarationList;
15use Wikimedia\CSS\Objects\DeclarationOrAtRuleList;
16use Wikimedia\CSS\Objects\QualifiedRule;
17use Wikimedia\CSS\Objects\Rule;
18use Wikimedia\CSS\Objects\RuleList;
19use Wikimedia\CSS\Objects\SimpleBlock;
20use Wikimedia\CSS\Objects\Stylesheet;
21use Wikimedia\CSS\Objects\Token;
22
23// Note: While reading the code below, you might find that my calls to
24// consumeToken() don't match what the spec says, and I don't ever "reconsume" a
25// token. It turns out that the spec is overcomplicated and confused with
26// respect to the "current input token" and the "next input token". It turns
27// out things are pretty simple: every "consume an X" is called with the
28// current input token being the first token of X, and returns with the current
29// input token being the last token of X (or EOF if X ends at EOF).
30
31// Also, of note is that, since our Tokenizer can only return a stream of tokens
32// rather than a stream of component values, the consume functions here only
33// consider tokens. ComponentValueList::toTokenArray() may be used to convert a
34// list of component values to a list of tokens if necessary.
35
36/**
37 * Parse CSS into a structure for further processing.
38 *
39 * This implements the CSS Syntax Module Level 3 candidate recommendation.
40 * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/
41 *
42 * The usual entry points are:
43 *  - Parser::parseStylesheet() to parse a stylesheet or the contents of a <style> tag.
44 *  - Parser::parseDeclarationList() to parse an inline style attribute
45 */
46class Parser {
47    /**
48     * Maximum depth of nested ComponentValues
49     *
50     * Arbitrary number that seems like it should be enough
51     */
52    private const CV_DEPTH_LIMIT = 100;
53
54    /** @var Tokenizer */
55    protected $tokenizer;
56
57    /** @var Token|null The most recently consumed token */
58    protected $currentToken = null;
59
60    /** @var array Parse errors. Each error is [ string $tag, int $line, int $pos ] */
61    protected $parseErrors = [];
62
63    /** @var int Recursion depth, incremented in self::consumeComponentValue() */
64    protected $cvDepth = 0;
65
66    /**
67     * @param Tokenizer $tokenizer CSS Tokenizer
68     */
69    public function __construct( Tokenizer $tokenizer ) {
70        $this->tokenizer = $tokenizer;
71    }
72
73    /**
74     * Create a Parser for a CSS string
75     * @param string $source CSS to parse.
76     * @param array $options Configuration options, see DataSourceTokenizer::__construct(). Also,
77     *  - convert: (array) If specified, detect the encoding as defined in the
78     *    CSS spec. The value is passed as the $encodings argument to
79     *    Encoder::convert().
80     * @return static
81     */
82    public static function newFromString( $source, array $options = [] ) {
83        if ( isset( $options['convert'] ) ) {
84            $source = Encoder::convert( $source, $options['convert'] );
85        }
86        return static::newFromDataSource( new StringDataSource( $source ), $options );
87    }
88
89    /**
90     * Create a Parser for a CSS DataSource
91     * @param DataSource $source CSS to parse.
92     * @param array $options Configuration options, see DataSourceTokenizer::__construct().
93     * @return static
94     */
95    public static function newFromDataSource( DataSource $source, array $options = [] ) {
96        $tokenizer = new DataSourceTokenizer( $source, $options );
97        return new static( $tokenizer );
98    }
99
100    /**
101     * Create a Parser for a list of Tokens
102     * @param Token[] $tokens Token-stream to parse
103     * @param Token|null $eof EOF-token
104     * @return static
105     */
106    public static function newFromTokens( array $tokens, Token $eof = null ) {
107        $tokenizer = new TokenListTokenizer( $tokens, $eof );
108        return new static( $tokenizer );
109    }
110
111    /**
112     * Consume a token
113     */
114    protected function consumeToken() {
115        if ( !$this->currentToken || $this->currentToken->type() !== Token::T_EOF ) {
116            $this->currentToken = $this->tokenizer->consumeToken();
117
118            // Copy any parse errors encountered
119            foreach ( $this->tokenizer->getParseErrors() as $error ) {
120                $this->parseErrors[] = $error;
121            }
122            $this->tokenizer->clearParseErrors();
123        }
124    }
125
126    /**
127     * Consume a token, also consuming any following whitespace (and comments)
128     */
129    protected function consumeTokenAndWhitespace() {
130        do {
131            $this->consumeToken();
132        } while ( $this->currentToken->type() === Token::T_WHITESPACE );
133    }
134
135    /**
136     * Return all parse errors seen so far
137     * @return array Array of [ string $tag, int $line, int $pos, ... ]
138     */
139    public function getParseErrors() {
140        return $this->parseErrors;
141    }
142
143    /**
144     * Clear parse errors
145     */
146    public function clearParseErrors() {
147        $this->parseErrors = [];
148    }
149
150    /**
151     * Record a parse error
152     * @param string $tag Error tag
153     * @param Token $token Report the error as starting at this token.
154     * @param array $data Extra data about the error.
155     */
156    protected function parseError( $tag, Token $token, array $data = [] ) {
157        [ $line, $pos ] = $token->getPosition();
158        $this->parseErrors[] = array_merge( [ $tag, $line, $pos ], $data );
159    }
160
161    /**
162     * Parse a stylesheet
163     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-stylesheet
164     * @return Stylesheet
165     */
166    public function parseStylesheet() {
167        // Move to the first token
168        $this->consumeToken();
169        $list = $this->consumeRuleList( true );
170
171        return new Stylesheet( $list );
172    }
173
174    /**
175     * Parse a list of rules
176     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-list-of-rules
177     * @return RuleList
178     */
179    public function parseRuleList() {
180        // Move to the first token
181        $this->consumeToken();
182        return $this->consumeRuleList( false );
183    }
184
185    /**
186     * Parse a rule
187     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-rule
188     * @return Rule|null
189     */
190    public function parseRule() {
191        // 1.
192        $this->consumeTokenAndWhitespace();
193
194        // 2.
195        if ( $this->currentToken->type() === Token::T_EOF ) {
196            // "return a syntax error"?
197            $this->parseError( 'unexpected-eof', $this->currentToken );
198            return null;
199        }
200
201        if ( $this->currentToken->type() === Token::T_AT_KEYWORD ) {
202            $rule = $this->consumeAtRule();
203        } else {
204            $rule = $this->consumeQualifiedRule();
205            if ( !$rule ) {
206                return null;
207            }
208        }
209
210        // 3.
211        $this->consumeTokenAndWhitespace();
212
213        // 4.
214        if ( $this->currentToken->type() === Token::T_EOF ) {
215            return $rule;
216        }
217
218        // "return a syntax error"?
219        $this->parseError( 'expected-eof', $this->currentToken );
220
221        return null;
222    }
223
224    /**
225     * Parse a declaration
226     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-declaration
227     * @return Declaration|null
228     */
229    public function parseDeclaration() {
230        // 1.
231        $this->consumeTokenAndWhitespace();
232
233        // 2.
234        if ( $this->currentToken->type() !== Token::T_IDENT ) {
235            // "return a syntax error"?
236            $this->parseError( 'expected-ident', $this->currentToken );
237            return null;
238        }
239
240        // 3.
241        // Declarations always run to EOF, no need to check.
242        return $this->consumeDeclaration();
243    }
244
245    /**
246     * Parse a list of declarations
247     * @note This is not the entry point the standard calls "parse a list of declarations",
248     *  see self::parseDeclarationOrAtRuleList()
249     * @return DeclarationList
250     */
251    public function parseDeclarationList() {
252        // Move to the first token
253        $this->consumeToken();
254        return $this->consumeDeclarationOrAtRuleList( false );
255    }
256
257    /**
258     * Parse a list of declarations and at-rules
259     * @note This is the entry point the standard calls "parse a list of declarations"
260     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-list-of-declarations
261     * @return DeclarationOrAtRuleList
262     */
263    public function parseDeclarationOrAtRuleList() {
264        // Move to the first token
265        $this->consumeToken();
266        return $this->consumeDeclarationOrAtRuleList();
267    }
268
269    /**
270     * Parse a (non-whitespace) component value
271     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-component-value
272     * @return ComponentValue|null
273     */
274    public function parseComponentValue() {
275        // 1.
276        $this->consumeTokenAndWhitespace();
277
278        // 2.
279        if ( $this->currentToken->type() === Token::T_EOF ) {
280            // "return a syntax error"?
281            $this->parseError( 'unexpected-eof', $this->currentToken );
282            return null;
283        }
284
285        // 3.
286        $value = $this->consumeComponentValue();
287
288        // 4.
289        $this->consumeTokenAndWhitespace();
290
291        // 5.
292        if ( $this->currentToken->type() === Token::T_EOF ) {
293            return $value;
294        }
295
296        // "return a syntax error"?
297        $this->parseError( 'expected-eof', $this->currentToken );
298
299        return null;
300    }
301
302    /**
303     * Parse a list of component values
304     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-list-of-component-values
305     * @return ComponentValueList
306     */
307    public function parseComponentValueList() {
308        $list = new ComponentValueList();
309        while ( true ) {
310            // Move to the first/next token
311            $this->consumeToken();
312            $value = $this->consumeComponentValue();
313            if ( $value instanceof Token && $value->type() === Token::T_EOF ) {
314                break;
315            }
316            $list->add( $value );
317        }
318
319        return $list;
320    }
321
322    /**
323     * Parse a comma-separated list of component values
324     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#parse-comma-separated-list-of-component-values
325     * @return ComponentValueList[]
326     */
327    public function parseCommaSeparatedComponentValueList() {
328        $lists = [];
329        do {
330            $list = new ComponentValueList();
331            while ( true ) {
332                // Move to the first/next token
333                $this->consumeToken();
334                $value = $this->consumeComponentValue();
335                if ( $value instanceof Token &&
336                    ( $value->type() === Token::T_EOF || $value->type() === Token::T_COMMA )
337                ) {
338                    break;
339                }
340                $list->add( $value );
341            }
342            $lists[] = $list;
343        } while ( $value->type() === Token::T_COMMA );
344
345        return $lists;
346    }
347
348    /**
349     * Consume a list of rules
350     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-list-of-rules
351     * @param bool $topLevel Determines the behavior when CDO and CDC tokens are encountered
352     * @return RuleList
353     */
354    protected function consumeRuleList( $topLevel ) {
355        // @phan-suppress-previous-line PhanPluginNeverReturnMethod
356        $list = new RuleList();
357        // @phan-suppress-next-line PhanInfiniteLoop
358        while ( true ) {
359            $rule = false;
360            switch ( $this->currentToken->type() ) {
361                case Token::T_WHITESPACE:
362                    break;
363
364                case Token::T_EOF:
365                    break 2;
366
367                case Token::T_CDO:
368                case Token::T_CDC:
369                    if ( !$topLevel ) {
370                        $rule = $this->consumeQualifiedRule();
371                    }
372                    // Else, do nothing
373                    break;
374
375                case Token::T_AT_KEYWORD:
376                    $rule = $this->consumeAtRule();
377                    break;
378
379                default:
380                    $rule = $this->consumeQualifiedRule();
381                    break;
382            }
383
384            if ( $rule ) {
385                $list->add( $rule );
386            }
387            $this->consumeToken();
388        }
389
390        // @phan-suppress-next-line PhanPluginUnreachableCode Reached by break 2
391        return $list;
392    }
393
394    /**
395     * Consume a list of declarations and at-rules
396     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-list-of-declarations
397     * @param bool $allowAtRules Whether to allow at-rules. This flag is not in
398     *  the spec and is used to implement the non-spec self::parseDeclarationList().
399     * @return DeclarationOrAtRuleList|DeclarationList
400     */
401    protected function consumeDeclarationOrAtRuleList( $allowAtRules = true ) {
402        // @phan-suppress-previous-line PhanPluginNeverReturnMethod
403        $list = $allowAtRules ? new DeclarationOrAtRuleList() : new DeclarationList();
404        // @phan-suppress-next-line PhanInfiniteLoop
405        while ( true ) {
406            $declaration = false;
407            switch ( $this->currentToken->type() ) {
408                case Token::T_WHITESPACE:
409                    break;
410
411                case Token::T_SEMICOLON:
412                    $declaration = null;
413                    break;
414
415                case Token::T_EOF:
416                    break 2;
417
418                case Token::T_AT_KEYWORD:
419                    if ( $allowAtRules ) {
420                        $declaration = $this->consumeAtRule();
421                    } else {
422                        $this->parseError( 'unexpected-token-in-declaration-list', $this->currentToken );
423                        $this->consumeAtRule();
424                        $declaration = null;
425                    }
426                    break;
427
428                case Token::T_IDENT:
429                    $cvs = [];
430                    do {
431                        $cvs[] = $this->consumeComponentValue();
432                        $this->consumeToken();
433                    } while (
434                        $this->currentToken->type() !== Token::T_SEMICOLON &&
435                        $this->currentToken->type() !== Token::T_EOF
436                    );
437                    $tokens = ( new ComponentValueList( $cvs ) )->toTokenArray();
438                    $parser = static::newFromTokens( $tokens, $this->currentToken );
439                    // Load that first token
440                    $parser->consumeToken();
441                    $declaration = $parser->consumeDeclaration();
442                    // Propagate any errors
443                    $this->parseErrors = array_merge( $this->parseErrors, $parser->parseErrors );
444                    break;
445
446                default:
447                    $this->parseError( 'unexpected-token-in-declaration-list', $this->currentToken );
448                    do {
449                        $this->consumeComponentValue();
450                        $this->consumeToken();
451                    } while (
452                        $this->currentToken->type() !== Token::T_SEMICOLON &&
453                        $this->currentToken->type() !== Token::T_EOF
454                    );
455                    $declaration = null;
456                    break;
457            }
458
459            if ( $declaration ) {
460                $list->add( $declaration );
461            }
462            $this->consumeToken();
463        }
464
465        // @phan-suppress-next-line PhanPluginUnreachableCode Reached by break 2
466        return $list;
467    }
468
469    /**
470     * Consume a declaration
471     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-declaration
472     * @return Declaration|null
473     */
474    protected function consumeDeclaration() {
475        $declaration = new Declaration( $this->currentToken );
476
477        // 1.
478        $this->consumeTokenAndWhitespace();
479
480        // 2. and 3.
481        if ( $this->currentToken->type() !== Token::T_COLON ) {
482            $this->parseError( 'expected-colon', $this->currentToken );
483            return null;
484        }
485        $this->consumeTokenAndWhitespace();
486
487        // 4.
488        $value = $declaration->getValue();
489        $l1 = $l2 = -1;
490        while ( $this->currentToken->type() !== Token::T_EOF ) {
491            $value->add( $this->consumeComponentValue() );
492            if ( $this->currentToken->type() !== Token::T_WHITESPACE ) {
493                $l1 = $l2;
494                $l2 = $value->count() - 1;
495            }
496            $this->consumeToken();
497        }
498
499        // 5. and part of 6.
500        // @phan-suppress-next-line PhanSuspiciousValueComparison False positive about $l1 is -1
501        $v1 = $l1 >= 0 ? $value[$l1] : null;
502        $v2 = $l2 >= 0 ? $value[$l2] : null;
503        if ( $v1 instanceof Token &&
504            $v1->type() === Token::T_DELIM &&
505            $v1->value() === '!' &&
506            $v2 instanceof Token &&
507            $v2->type() === Token::T_IDENT &&
508            !strcasecmp( $v2->value(), 'important' )
509        ) {
510            // This removes the "!" and "important" (5), and also any whitespace between/after (6)
511            while ( isset( $value[$l1] ) ) {
512                $value->remove( $l1 );
513            }
514            $declaration->setImportant( true );
515        }
516
517        // Rest of 6.
518        $i = $value->count();
519        // @phan-suppress-next-line PhanNonClassMethodCall False positive
520        while ( --$i >= 0 && $value[$i] instanceof Token && $value[$i]->type() === Token::T_WHITESPACE ) {
521            $value->remove( $i );
522        }
523
524        // 7.
525        return $declaration;
526    }
527
528    /**
529     * Consume an at-rule
530     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-at-rule
531     * @return AtRule
532     * @suppress PhanPluginNeverReturnMethod due to break 2;
533     */
534    protected function consumeAtRule() {
535        $rule = new AtRule( $this->currentToken );
536        $this->consumeToken();
537        // @phan-suppress-next-line PhanInfiniteLoop
538        while ( true ) {
539            switch ( $this->currentToken->type() ) {
540                case Token::T_SEMICOLON:
541                    break 2;
542
543                case Token::T_EOF:
544                    if ( $this->currentToken->typeFlag() !== 'recursion-depth-exceeded' ) {
545                        $this->parseError( 'unexpected-eof-in-rule', $this->currentToken );
546                    }
547                    break 2;
548
549                case Token::T_LEFT_BRACE:
550                    $rule->setBlock( $this->consumeSimpleBlock() );
551                    break 2;
552
553                // Spec has "simple block with an associated token of <{-token>" here, but that isn't possible
554                // because it's not a Token.
555
556                default:
557                    $rule->getPrelude()->add( $this->consumeComponentValue() );
558                    break;
559            }
560            $this->consumeToken();
561        }
562
563        // @phan-suppress-next-line PhanPluginUnreachableCode False positive due to break 2;
564        return $rule;
565    }
566
567    /**
568     * Consume a qualified rule
569     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-qualified-rule
570     * @return QualifiedRule|null
571     */
572    protected function consumeQualifiedRule() {
573        $rule = new QualifiedRule( $this->currentToken );
574        while ( true ) {
575            switch ( $this->currentToken->type() ) {
576                case Token::T_EOF:
577                    if ( $this->currentToken->typeFlag() !== 'recursion-depth-exceeded' ) {
578                        $this->parseError( 'unexpected-eof-in-rule', $this->currentToken );
579                    }
580                    return null;
581
582                case Token::T_LEFT_BRACE:
583                    $rule->setBlock( $this->consumeSimpleBlock() );
584                    break 2;
585
586                // Spec has "simple block with an associated token of <{-token>" here, but that isn't possible
587                // because it's not a Token.
588
589                default:
590                    $rule->getPrelude()->add( $this->consumeComponentValue() );
591                    break;
592            }
593            $this->consumeToken();
594        }
595
596        // @phan-suppress-next-line PhanPluginUnreachableCode False positive due to break 2;
597        return $rule;
598    }
599
600    /**
601     * Consume a component value
602     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-component-value
603     * @return ComponentValue
604     */
605    protected function consumeComponentValue() {
606        if ( ++$this->cvDepth > static::CV_DEPTH_LIMIT ) {
607            $this->parseError( 'recursion-depth-exceeded', $this->currentToken );
608            // There's no way to safely recover from this without more recursion.
609            // So just eat the rest of the input, then return a
610            // specially-flagged EOF, so we can avoid 100 "unexpected EOF"
611            // errors.
612            $position = $this->currentToken->getPosition();
613            while ( $this->currentToken->type() !== Token::T_EOF ) {
614                $this->consumeToken();
615            }
616            $this->currentToken = new Token( Token::T_EOF, [
617                'position' => $position,
618                'typeFlag' => 'recursion-depth-exceeded'
619            ] );
620        }
621
622        switch ( $this->currentToken->type() ) {
623            case Token::T_LEFT_BRACE:
624            case Token::T_LEFT_BRACKET:
625            case Token::T_LEFT_PAREN:
626                $ret = $this->consumeSimpleBlock();
627                break;
628
629            case Token::T_FUNCTION:
630                $ret = $this->consumeFunction();
631                break;
632
633            default:
634                $ret = $this->currentToken;
635                break;
636        }
637
638        $this->cvDepth--;
639        // @phan-suppress-next-line PhanTypeMismatchReturnNullable $ret always set
640        return $ret;
641    }
642
643    /**
644     * Consume a simple block
645     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-simple-block
646     * @return SimpleBlock
647     * @suppress PhanPluginNeverReturnMethod due to break 2;
648     */
649    protected function consumeSimpleBlock() {
650        $block = new SimpleBlock( $this->currentToken );
651        $endTokenType = $block->getEndTokenType();
652        $this->consumeToken();
653        // @phan-suppress-next-line PhanInfiniteLoop
654        while ( true ) {
655            switch ( $this->currentToken->type() ) {
656                case Token::T_EOF:
657                    if ( $this->currentToken->typeFlag() !== 'recursion-depth-exceeded' ) {
658                        $this->parseError( 'unexpected-eof-in-block', $this->currentToken );
659                    }
660                    break 2;
661
662                case $endTokenType:
663                    break 2;
664
665                default:
666                    $block->getValue()->add( $this->consumeComponentValue() );
667                    break;
668            }
669            $this->consumeToken();
670        }
671
672        // @phan-suppress-next-line PhanPluginUnreachableCode False positive due to break 2;
673        return $block;
674    }
675
676    /**
677     * Consume a function
678     * @see https://www.w3.org/TR/2019/CR-css-syntax-3-20190716/#consume-function
679     * @return CSSFunction
680     * @suppress PhanPluginNeverReturnMethod due to break 2;
681     */
682    protected function consumeFunction() {
683        $function = new CSSFunction( $this->currentToken );
684        $this->consumeToken();
685
686        // @phan-suppress-next-line PhanInfiniteLoop
687        while ( true ) {
688            switch ( $this->currentToken->type() ) {
689                case Token::T_EOF:
690                    if ( $this->currentToken->typeFlag() !== 'recursion-depth-exceeded' ) {
691                        $this->parseError( 'unexpected-eof-in-function', $this->currentToken );
692                    }
693                    break 2;
694
695                case Token::T_RIGHT_PAREN:
696                    break 2;
697
698                default:
699                    $function->getValue()->add( $this->consumeComponentValue() );
700                    break;
701            }
702            $this->consumeToken();
703        }
704
705        // @phan-suppress-next-line PhanPluginUnreachableCode False positive due to break 2;
706        return $function;
707    }
708
709    // @codeCoverageIgnoreEnd
710}