Skip to main content

oxc_css_parser/parser/
stmt.rs

1use super::{
2    Parser,
3    state::{ParserState, QualifiedRuleContext},
4};
5use crate::{
6    Parse, Syntax,
7    ast::*,
8    bump, eat,
9    error::{Error, ErrorKind, PResult},
10    expect, peek,
11    pos::{Span, Spanned},
12    tokenizer::{Token, TokenWithSpan},
13    util::PairedToken,
14};
15
16impl<'cmt, 's: 'cmt> Parse<'cmt, 's> for Declaration<'s> {
17    fn parse(input: &mut Parser<'cmt, 's>) -> PResult<Self> {
18        // A css-in-js `${}` placeholder may stand in for the property name
19        // (`${foo}: ${bar}`); it is not a real ident, so accept it directly.
20        let name = if let Token::Placeholder(..) = peek!(input).token {
21            let (placeholder, span) = expect!(input, Placeholder);
22            InterpolableIdent::Placeholder((placeholder, span).into())
23        } else {
24            input
25                .with_state(ParserState {
26                    qualified_rule_ctx: Some(QualifiedRuleContext::DeclarationName),
27                    ..input.state
28                })
29                .parse::<InterpolableIdent>()?
30        };
31
32        // https://tailwindcss.com/docs/theme#overriding-the-default-theme
33        let name_suffix = if let TokenWithSpan {
34            token: Token::Asterisk(..),
35            span,
36        } = peek!(input)
37            && name.span().end == span.start
38        {
39            bump!(input);
40            Some('*')
41        } else {
42            None
43        };
44
45        let less_property_merge = if input.syntax == Syntax::Less {
46            input.parse()?
47        } else {
48            None
49        };
50
51        let (_, colon_span) = expect!(input, Colon);
52        let value = {
53            let mut parser = input.with_state(ParserState {
54                qualified_rule_ctx: Some(QualifiedRuleContext::DeclarationValue),
55                ..input.state
56            });
57            match &name {
58                InterpolableIdent::Literal(ident)
59                    if ident.name.starts_with("--")
60                        || matches!(
61                            &peek!(parser).token,
62                            // for IE-compatibility, regardless of the property
63                            // name (`filter`, `-ms-filter`, vendor variants...):
64                            // filter: progid:DXImageTransform.Microsoft...
65                            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("progid")
66                        ) =>
67                'value: {
68                    if parser.options.try_parsing_value_in_custom_property
69                        && let Ok(values) = parser.try_parse(Parser::parse_declaration_value)
70                    {
71                        break 'value values;
72                    }
73
74                    let mut values = Vec::with_capacity(3);
75                    let mut pairs = Vec::with_capacity(1);
76                    loop {
77                        match &peek!(parser).token {
78                            Token::Dedent(..) | Token::Linebreak(..) | Token::Eof(..) => break,
79                            Token::Semicolon(..) if pairs.is_empty() => {
80                                break;
81                            }
82                            Token::LParen(..) => {
83                                pairs.push(PairedToken::Paren);
84                            }
85                            Token::RParen(..) => {
86                                if let Some(PairedToken::Paren) = pairs.pop() {
87                                } else {
88                                    break;
89                                }
90                            }
91                            Token::LBracket(..) => {
92                                pairs.push(PairedToken::Bracket);
93                            }
94                            Token::RBracket(..) => {
95                                if let Some(PairedToken::Bracket) = pairs.pop() {
96                                } else {
97                                    break;
98                                }
99                            }
100                            Token::LBrace(..) | Token::HashLBrace(..) => {
101                                pairs.push(PairedToken::Brace);
102                            }
103                            Token::RBrace(..) => {
104                                if let Some(PairedToken::Brace) = pairs.pop() {
105                                } else {
106                                    break;
107                                }
108                            }
109                            // An interpolated string (e.g. `'#{$expr}'` inside
110                            // `filter: progid:...`) must be parsed structurally:
111                            // the tokenizer needs `scan_string_template` to resume
112                            // the string after each `#{...}`, so consuming its
113                            // tokens as a plain stream would mis-lex the rest.
114                            Token::StrTemplate(..) => {
115                                values.push(ComponentValue::InterpolableStr(parser.parse()?));
116                                continue;
117                            }
118                            _ => {}
119                        }
120                        values.push(ComponentValue::TokenWithSpan(bump!(parser)));
121                    }
122                    values
123                }
124                _ => parser.parse_declaration_value()?,
125            }
126        };
127
128        let important = if let Token::Exclamation(..) = &peek!(input).token {
129            input.parse::<ImportantAnnotation>().map(Some)?
130        } else {
131            None
132        };
133
134        let span = Span {
135            start: name.span().start,
136            end: if let Some(important) = &important {
137                important.span.end
138            } else if let Some(last) = value.last() {
139                last.span().end
140            } else {
141                colon_span.end
142            },
143        };
144        Ok(Declaration {
145            name,
146            name_suffix,
147            colon_span,
148            value,
149            important,
150            less_property_merge,
151            span,
152        })
153    }
154}
155
156impl<'cmt, 's: 'cmt> Parse<'cmt, 's> for ImportantAnnotation<'s> {
157    fn parse(input: &mut Parser<'cmt, 's>) -> PResult<Self> {
158        let (_, span) = expect!(input, Exclamation);
159        let ident: Ident = input.parse::<Ident>()?;
160        let span = Span {
161            start: span.start,
162            end: ident.span.end,
163        };
164        if ident.name.eq_ignore_ascii_case("important") {
165            Ok(ImportantAnnotation { ident, span })
166        } else {
167            Err(Error {
168                kind: ErrorKind::ExpectImportantAnnotation,
169                span,
170            })
171        }
172    }
173}
174
175impl<'cmt, 's: 'cmt> Parse<'cmt, 's> for QualifiedRule<'s> {
176    fn parse(input: &mut Parser<'cmt, 's>) -> PResult<Self> {
177        let selector_list = input
178            .with_state(ParserState {
179                qualified_rule_ctx: Some(QualifiedRuleContext::Selector),
180                ..input.state
181            })
182            .parse::<SelectorList>()?;
183        let block = input.parse::<SimpleBlock>()?;
184        let span = Span {
185            start: selector_list.span.start,
186            end: block.span.end,
187        };
188        Ok(QualifiedRule {
189            selector: selector_list,
190            block,
191            span,
192        })
193    }
194}
195
196impl<'cmt, 's: 'cmt> Parse<'cmt, 's> for SimpleBlock<'s> {
197    fn parse(input: &mut Parser<'cmt, 's>) -> PResult<Self> {
198        let is_sass = input.syntax == Syntax::Sass;
199        let start = if is_sass {
200            if let Some((_, span)) = eat!(input, Indent) {
201                span.end
202            } else {
203                let offset = peek!(input).span.start;
204                return Ok(SimpleBlock {
205                    statements: vec![],
206                    span: Span {
207                        start: offset,
208                        end: offset,
209                    },
210                });
211            }
212        } else {
213            expect!(input, LBrace).1.start
214        };
215
216        let statements = input.parse_statements(/* is_top_level */ false)?;
217
218        if is_sass {
219            match bump!(input) {
220                TokenWithSpan {
221                    token: Token::Dedent(..) | Token::Eof(..),
222                    span,
223                } => {
224                    let end = statements.last().map_or(span.start, |last| last.span().end);
225                    Ok(SimpleBlock {
226                        statements,
227                        span: Span { start, end },
228                    })
229                }
230                TokenWithSpan { span, .. } => Err(Error {
231                    kind: ErrorKind::ExpectDedentOrEof,
232                    span,
233                }),
234            }
235        } else {
236            let end = expect!(input, RBrace).1.end;
237            Ok(SimpleBlock {
238                statements,
239                span: Span { start, end },
240            })
241        }
242    }
243}
244
245impl<'cmt, 's: 'cmt> Parse<'cmt, 's> for Stylesheet<'s> {
246    fn parse(input: &mut Parser<'cmt, 's>) -> PResult<Self> {
247        let statements = input.parse_statements(/* is_top_level */ true)?;
248        expect!(input, Eof);
249        Ok(Stylesheet {
250            statements,
251            span: Span {
252                start: 0,
253                end: input.source.len(),
254            },
255        })
256    }
257}
258
259impl<'cmt, 's: 'cmt> Parser<'cmt, 's> {
260    fn parse_declaration_value(&mut self) -> PResult<Vec<ComponentValue<'s>>> {
261        let mut values = Vec::with_capacity(3);
262        loop {
263            match &peek!(self).token {
264                Token::RBrace(..)
265                | Token::RParen(..)
266                | Token::Semicolon(..)
267                | Token::Dedent(..)
268                | Token::Linebreak(..)
269                | Token::Exclamation(..)
270                | Token::Eof(..) => break,
271                _ => {
272                    let value = self.parse::<ComponentValue>()?;
273                    match &value {
274                        ComponentValue::SassNestingDeclaration(..)
275                            if matches!(self.syntax, Syntax::Scss | Syntax::Sass) =>
276                        {
277                            values.push(value);
278                            break;
279                        }
280                        _ => values.push(value),
281                    }
282                }
283            }
284        }
285        Ok(values)
286    }
287
288    fn parse_statements(&mut self, is_top_level: bool) -> PResult<Vec<Statement<'s>>> {
289        let mut statements = Vec::with_capacity(1);
290        loop {
291            // Set true for braced blocks AND `${}` placeholder statements: both
292            // make the trailing terminator optional. A placeholder substitutes a
293            // whole statement/declaration and, like postcss, needs no `;`, so the
294            // next statement may follow directly (`${mixin}\n@media {...}`,
295            // `${a} ${b}`, `${foo}: ${bar}`).
296            let mut is_block_element = false;
297            let TokenWithSpan { token, span } = peek!(self);
298            match token {
299                Token::Ident(..) | Token::HashLBrace(..) | Token::AtLBraceVar(..) => {
300                    match self.syntax {
301                        Syntax::Css => {
302                            if self.state.in_keyframes_at_rule {
303                                statements.push(Statement::KeyframeBlock(self.parse()?));
304                                is_block_element = true;
305                            } else {
306                                match self.try_parse(QualifiedRule::parse) {
307                                    Ok(rule) => {
308                                        statements.push(Statement::QualifiedRule(rule));
309                                        is_block_element = true;
310                                    }
311                                    Err(error_rule) => match self.parse::<Declaration>() {
312                                        Ok(decl) => {
313                                            if is_top_level {
314                                                self.recoverable_errors.push(Error {
315                                                    kind: ErrorKind::TopLevelDeclaration,
316                                                    span: decl.span.clone(),
317                                                });
318                                            }
319                                            statements.push(Statement::Declaration(decl));
320                                        }
321                                        Err(error_decl) => {
322                                            if is_top_level {
323                                                return Err(error_rule);
324                                            } else {
325                                                return Err(error_decl);
326                                            }
327                                        }
328                                    },
329                                }
330                            }
331                        }
332                        Syntax::Scss | Syntax::Sass => {
333                            if let Ok(sass_var_decl) =
334                                self.try_parse(SassVariableDeclaration::parse)
335                            {
336                                statements.push(Statement::SassVariableDeclaration(sass_var_decl));
337                            } else if self.state.in_keyframes_at_rule {
338                                statements.push(Statement::KeyframeBlock(self.parse()?));
339                                is_block_element = true;
340                            } else {
341                                match self.try_parse(QualifiedRule::parse) {
342                                    Ok(rule) => {
343                                        statements.push(Statement::QualifiedRule(rule));
344                                        is_block_element = true;
345                                    }
346                                    Err(error_rule) => match self.parse::<Declaration>() {
347                                        Ok(decl) => {
348                                            is_block_element = matches!(
349                                                decl.value.last(),
350                                                Some(ComponentValue::SassNestingDeclaration(..))
351                                            );
352                                            if is_top_level {
353                                                self.recoverable_errors.push(Error {
354                                                    kind: ErrorKind::TopLevelDeclaration,
355                                                    span: decl.span.clone(),
356                                                });
357                                            }
358                                            statements.push(Statement::Declaration(decl));
359                                        }
360                                        Err(error_decl) => {
361                                            if is_top_level {
362                                                return Err(error_rule);
363                                            } else {
364                                                return Err(error_decl);
365                                            }
366                                        }
367                                    },
368                                }
369                            }
370                        }
371                        Syntax::Less => {
372                            if let Ok(stmt) = self.try_parse(Parser::parse_less_qualified_rule) {
373                                statements.push(stmt);
374                                is_block_element = true;
375                            } else if let Ok(decl) = self.try_parse(|parser| {
376                                if is_top_level {
377                                    Err(Error {
378                                        kind: ErrorKind::TryParseError,
379                                        span: bump!(parser).span,
380                                    })
381                                } else {
382                                    parser.parse()
383                                }
384                            }) {
385                                statements.push(Statement::Declaration(decl));
386                            } else if self.state.in_keyframes_at_rule {
387                                statements.push(Statement::KeyframeBlock(self.parse()?));
388                                is_block_element = true;
389                            } else {
390                                let fn_call = self.parse::<Function>()?;
391                                is_block_element = matches!(
392                                    fn_call.args.last(),
393                                    Some(ComponentValue::LessDetachedRuleset(..))
394                                );
395                                statements.push(Statement::LessFunctionCall(fn_call));
396                            }
397                        }
398                    }
399                }
400                Token::Dot(..) | Token::Hash(..) if self.syntax == Syntax::Less => {
401                    let stmt = if let Ok(stmt) = self.try_parse(Parser::parse_less_qualified_rule) {
402                        is_block_element = true;
403                        stmt
404                    } else if let Ok(mixin_def) = self.try_parse(LessMixinDefinition::parse) {
405                        is_block_element = true;
406                        Statement::LessMixinDefinition(mixin_def)
407                    } else {
408                        self.parse().map(Statement::LessMixinCall)?
409                    };
410                    statements.push(stmt);
411                }
412                Token::Dot(..) | Token::Hash(..) if !self.state.in_keyframes_at_rule => {
413                    statements.push(Statement::QualifiedRule(self.parse()?));
414                    is_block_element = true;
415                }
416                Token::Ampersand(..)
417                | Token::LBracket(..)
418                | Token::Colon(..)
419                | Token::ColonColon(..)
420                | Token::Asterisk(..)
421                | Token::Bar(..)
422                | Token::NumberSign(..)
423                    if !self.state.in_keyframes_at_rule =>
424                {
425                    if self.syntax == Syntax::Less {
426                        if let Ok(extend_rule) = self.try_parse(LessExtendRule::parse) {
427                            statements.push(Statement::LessExtendRule(extend_rule));
428                        } else {
429                            statements.push(self.parse_less_qualified_rule()?);
430                            is_block_element = true;
431                        }
432                    } else {
433                        statements.push(Statement::QualifiedRule(self.parse()?));
434                        is_block_element = true;
435                    }
436                }
437                Token::AtKeyword(at_keyword) => match self.syntax {
438                    Syntax::Css => {
439                        let at_rule = self.parse::<AtRule>()?;
440                        is_block_element = at_rule.block.is_some();
441                        statements.push(Statement::AtRule(at_rule));
442                    }
443                    Syntax::Scss | Syntax::Sass => {
444                        let at_keyword_name = at_keyword.ident.name();
445                        match &*at_keyword_name {
446                            "if" => {
447                                statements.push(Statement::SassIfAtRule(self.parse()?));
448                                is_block_element = true;
449                            }
450                            "else" => {
451                                return Err(Error {
452                                    kind: ErrorKind::UnexpectedSassElseAtRule,
453                                    span: bump!(self).span,
454                                });
455                            }
456                            _ => {
457                                let at_rule = self.parse::<AtRule>()?;
458                                is_block_element = at_rule.block.is_some();
459                                statements.push(Statement::AtRule(at_rule));
460                            }
461                        }
462                    }
463                    Syntax::Less => {
464                        if let Ok(less_variable_declaration) =
465                            self.try_parse(LessVariableDeclaration::parse)
466                        {
467                            is_block_element = matches!(
468                                less_variable_declaration.value,
469                                ComponentValue::LessDetachedRuleset(..)
470                            );
471                            statements.push(Statement::LessVariableDeclaration(
472                                less_variable_declaration,
473                            ));
474                        } else if let Ok(variable_call) = self.try_parse(LessVariableCall::parse) {
475                            statements.push(Statement::LessVariableCall(variable_call));
476                        } else {
477                            let at_rule = self.parse::<AtRule>()?;
478                            is_block_element = at_rule.block.is_some();
479                            statements.push(Statement::AtRule(at_rule));
480                        }
481                    }
482                },
483                Token::Placeholder(..) => {
484                    // A placeholder may start a qualified rule (a substituted
485                    // selector, e.g. CSS-in-JS `${Component} { ... }`) or stand
486                    // alone as a statement (e.g. `` `PLACEHOLDER-0`; ``).
487                    //
488                    // A placeholder-led selector must not absorb across a newline:
489                    // prettier keeps `${mixin}` on its own line and the following
490                    // selector as a separate rule (`${mixin}\n& > .x {}` is two
491                    // statements, not one). So only attempt the rule when the block
492                    // `{` is reachable without an intervening newline-then-selector.
493                    //
494                    // A placeholder may also be a declaration property name
495                    // (`${foo}: ${bar}`), so try a declaration before falling back
496                    // to a bare placeholder statement.
497                    let ph_end = peek!(self).span.end;
498                    if self.placeholder_starts_qualified_rule(ph_end)
499                        && let Ok(rule) = self.try_parse(QualifiedRule::parse)
500                    {
501                        statements.push(Statement::QualifiedRule(rule));
502                        is_block_element = true;
503                    } else if let Ok(declaration) = self.try_parse(Declaration::parse) {
504                        // Reached only via the placeholder token above, so this
505                        // is the `${foo}: ${bar}` form (placeholder property name).
506                        statements.push(Statement::Declaration(declaration));
507                        is_block_element = true;
508                    } else {
509                        let (placeholder, span) = expect!(self, Placeholder);
510                        statements.push(Statement::Placeholder((placeholder, span).into()));
511                        is_block_element = true;
512                    }
513                }
514                Token::Percent(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
515                    statements.push(Statement::QualifiedRule(self.parse()?));
516                    is_block_element = true;
517                }
518                Token::DollarVar(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
519                    statements.push(Statement::SassVariableDeclaration(self.parse()?));
520                }
521                Token::GreaterThan(..) | Token::Plus(..) | Token::Tilde(..) | Token::BarBar(..) => {
522                    if self.syntax == Syntax::Less {
523                        statements.push(self.parse_less_qualified_rule()?);
524                    } else {
525                        statements.push(Statement::QualifiedRule(self.parse()?));
526                    }
527                    is_block_element = true;
528                }
529                Token::DollarLBraceVar(..) if self.syntax == Syntax::Less => {
530                    statements.push(self.parse().map(Statement::Declaration)?);
531                }
532                Token::Cdo(..) | Token::Cdc(..) => {
533                    bump!(self);
534                    continue;
535                }
536                Token::At(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
537                    let unknown_sass_at_rule = self.parse::<UnknownSassAtRule>()?;
538                    is_block_element = unknown_sass_at_rule.block.is_some();
539                    statements.push(Statement::UnknownSassAtRule(Box::new(unknown_sass_at_rule)));
540                }
541                Token::Percentage(..)
542                    if self.state.in_keyframes_at_rule
543                        || self.state.sass_ctx & super::state::SASS_CTX_ALLOW_KEYFRAME_BLOCK
544                            != 0
545                        || self.state.less_ctx & super::state::LESS_CTX_ALLOW_KEYFRAME_BLOCK
546                            != 0 =>
547                {
548                    statements.push(Statement::KeyframeBlock(self.parse()?));
549                    is_block_element = true;
550                }
551                Token::RBrace(..) | Token::Eof(..) | Token::Dedent(..) => break,
552                Token::Semicolon(..) | Token::Linebreak(..) => {
553                    bump!(self);
554                    continue;
555                }
556                _ => {
557                    return Err(Error {
558                        kind: if self.state.in_keyframes_at_rule {
559                            ErrorKind::ExpectKeyframeBlock
560                        } else {
561                            ErrorKind::ExpectRule
562                        },
563                        span: span.clone(),
564                    });
565                }
566            };
567            match &peek!(self).token {
568                Token::RBrace(..) | Token::Eof(..) | Token::Dedent(..) => break,
569                _ => {
570                    if self.syntax == Syntax::Sass {
571                        if is_block_element {
572                            eat!(self, Linebreak);
573                        } else if self.options.tolerate_semicolon_in_sass {
574                            if let Some((_, span)) = eat!(self, Semicolon) {
575                                self.recoverable_errors.push(Error {
576                                    kind: ErrorKind::UnexpectedSemicolonInSass,
577                                    span,
578                                });
579                            }
580                        } else {
581                            expect!(self, Linebreak);
582                        }
583                    } else if is_block_element {
584                        eat!(self, Semicolon);
585                    } else {
586                        expect!(self, Semicolon);
587                    }
588                }
589            }
590        }
591        Ok(statements)
592    }
593
594    /// Whether a statement-position `${}` placeholder (ending at byte `from`)
595    /// should be offered to `QualifiedRule::parse`. The css-in-js rule the parser
596    /// can't see on its own, matching prettier:
597    /// - a bare `{` after the placeholder IS absorbed — the placeholder is the
598    ///   selector for that block (`${mixin}\n{ color: red }` is one rule; a bare
599    ///   `{...}` is meaningless without a selector, so this is the only valid read)
600    /// - but selector CONTENT appearing after a newline splits into a separate
601    ///   rule (`${mixin}\n& > .x {}` is two statements, not one)
602    ///
603    /// So this only answers "is a `{` reachable before any non-whitespace content
604    /// appears past a newline?" — the real grammar (strings, comments, `#{...}`
605    /// interpolations, validity) is left to `QualifiedRule::parse`, which runs
606    /// next and rolls back if this guess was wrong. Deliberately NOT a tokenizer:
607    /// it never early-exits on `;`/`}` (those may sit inside an attribute string
608    /// or comment), so it can't misclassify a same-line selector containing them.
609    fn placeholder_starts_qualified_rule(&self, from: usize) -> bool {
610        let mut newline_seen = false;
611        for &b in &self.source.as_bytes()[from..] {
612            match b {
613                // First `{` (block opener or `#{` interpolation), even after a
614                // newline with no content yet: the placeholder is its selector.
615                b'{' => return true,
616                // `\r`, `\r\n`, and `\n` all count as a newline (the tokenizer
617                // treats a bare `\r` as a line break too).
618                b'\n' | b'\r' => newline_seen = true,
619                _ if b.is_ascii_whitespace() => {}
620                // Non-whitespace content after a newline = a separate rule.
621                _ if newline_seen => return false,
622                _ => {}
623            }
624        }
625        // No block at all -> a declaration or a bare placeholder, not a rule.
626        false
627    }
628}