Skip to main content

oxc_css_parser/parser/
stmt.rs

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