swc_ecma_parser/parser/
jsx.rs

1use swc_atoms::Atom;
2use swc_common::{BytePos, Span, Spanned};
3use swc_ecma_ast::*;
4
5use super::{input::Tokens, Parser};
6use crate::{
7    error::SyntaxError,
8    lexer::{Token, TokenFlags},
9    Context, PResult,
10};
11
12impl<I: Tokens> Parser<I> {
13    /// Parses JSX expression enclosed into curly brackets.
14    fn parse_jsx_expr_container(&mut self) -> PResult<JSXExprContainer> {
15        debug_assert!(self.input().syntax().jsx());
16        debug_assert!(self.input().is(Token::LBrace));
17
18        let start = self.input().cur_pos();
19        self.bump(); // bump "{"
20        let expr = if self.input().is(Token::RBrace) {
21            JSXExpr::JSXEmptyExpr(self.parse_jsx_empty_expr())
22        } else {
23            self.parse_expr().map(JSXExpr::Expr)?
24        };
25        expect!(self, Token::RBrace);
26        Ok(JSXExprContainer {
27            span: self.span(start),
28            expr,
29        })
30    }
31
32    /// JSXEmptyExpression is unique type since it doesn't actually parse
33    /// anything, and so it should start at the end of last read token (left
34    /// brace) and finish at the beginning of the next one (right brace).
35    fn parse_jsx_empty_expr(&mut self) -> JSXEmptyExpr {
36        debug_assert!(self.input().syntax().jsx());
37        let start = self.input().cur_pos();
38        JSXEmptyExpr {
39            span: Span::new_with_checked(start, start),
40        }
41    }
42
43    fn jsx_expr_container_to_jsx_attr_value(
44        &mut self,
45        start: BytePos,
46        node: JSXExprContainer,
47    ) -> PResult<JSXAttrValue> {
48        match node.expr {
49            JSXExpr::JSXEmptyExpr(..) => {
50                syntax_error!(self, self.span(start), SyntaxError::EmptyJSXAttr)
51            }
52            JSXExpr::Expr(..) => Ok(node.into()),
53            #[cfg(swc_ast_unknown)]
54            _ => unreachable!(),
55        }
56    }
57
58    fn parse_jsx_text(&mut self) -> JSXText {
59        debug_assert!(self.input().syntax().jsx());
60        let cur = self.input_mut().cur();
61        debug_assert!(cur == Token::JSXText);
62        let (value, raw) = cur.take_jsx_text(self.input_mut());
63        self.input_mut().scan_jsx_token(true);
64        let span = self.input().prev_span();
65        JSXText { span, value, raw }
66    }
67
68    fn parse_jsx_ident(&mut self) -> PResult<Ident> {
69        debug_assert!(self.input().syntax().jsx());
70        trace_cur!(self, parse_jsx_ident);
71        let cur = self.input().cur();
72        if cur == Token::JSXName || cur == Token::Ident {
73            if self.input().token_flags().contains(TokenFlags::UNICODE) {
74                syntax_error!(
75                    self,
76                    self.input().cur_span(),
77                    SyntaxError::InvalidUnicodeEscape
78                );
79            }
80            let name = cur.take_jsx_name(self.input_mut());
81            self.bump();
82            let span = self.input().prev_span();
83            Ok(Ident::new_no_ctxt(name, span))
84        } else {
85            unexpected!(self, "jsx identifier")
86        }
87    }
88
89    fn parse_jsx_tag_name(&mut self) -> PResult<JSXAttrName> {
90        debug_assert!(self.input().syntax().jsx());
91        trace_cur!(self, parse_jsx_tag_name);
92        let start = self.input().cur_pos();
93        self.input_mut().scan_jsx_identifier();
94
95        let ns = self.parse_jsx_ident()?.into();
96        Ok(if self.input_mut().eat(Token::Colon) {
97            self.input_mut().scan_jsx_identifier();
98            let name: IdentName = self.parse_jsx_ident()?.into();
99            JSXAttrName::JSXNamespacedName(JSXNamespacedName {
100                span: Span::new_with_checked(start, name.span.hi),
101                ns,
102                name,
103            })
104        } else {
105            JSXAttrName::Ident(ns)
106        })
107    }
108
109    fn parse_jsx_element_name(&mut self) -> PResult<JSXElementName> {
110        debug_assert!(self.input().syntax().jsx());
111        trace_cur!(self, parse_jsx_element_name);
112        let start = self.input().cur_pos();
113        let mut node = match self.parse_jsx_tag_name()? {
114            JSXAttrName::Ident(i) => JSXElementName::Ident(i.into()),
115            JSXAttrName::JSXNamespacedName(i) => JSXElementName::JSXNamespacedName(i),
116            #[cfg(swc_ast_unknown)]
117            _ => unreachable!(),
118        };
119        while self.input_mut().eat(Token::Dot) {
120            self.input_mut().scan_jsx_identifier();
121            let prop: IdentName = self.parse_jsx_ident()?.into();
122            let new_node = JSXElementName::JSXMemberExpr(JSXMemberExpr {
123                span: self.span(start),
124                obj: match node {
125                    JSXElementName::Ident(i) => JSXObject::Ident(i),
126                    JSXElementName::JSXMemberExpr(i) => JSXObject::JSXMemberExpr(Box::new(i)),
127                    _ => unreachable!("JSXNamespacedName -> JSXObject"),
128                },
129                prop,
130            });
131            node = new_node;
132        }
133        Ok(node)
134    }
135
136    fn parse_jsx_closing_element(
137        &mut self,
138        in_expr_context: bool,
139        open_name: &JSXElementName,
140    ) -> PResult<JSXClosingElement> {
141        let start = self.cur_pos();
142        self.expect(Token::LessSlash)?;
143        let tagname = self.parse_jsx_element_name()?;
144
145        // Handle JSX closing tag followed by '=': '</tag>='
146        // When lexer sees '>=' it combines into GtEq, but JSX only needs '>'
147        // Use rescan_jsx_open_el_terminal_token to split >= back into >
148        self.input_mut().rescan_jsx_open_el_terminal_token();
149        self.expect_without_advance(Token::Gt)?;
150
151        if in_expr_context {
152            self.bump();
153        } else {
154            self.input_mut().scan_jsx_token(true);
155        }
156
157        if get_qualified_jsx_name(open_name) != get_qualified_jsx_name(&tagname) {
158            syntax_error!(
159                self,
160                tagname.span(),
161                SyntaxError::JSXExpectedClosingTag {
162                    tag: get_qualified_jsx_name(open_name),
163                }
164            )
165        }
166
167        let span = self.span(start);
168        Ok(JSXClosingElement {
169            span,
170            name: tagname,
171        })
172    }
173
174    fn parse_jsx_closing_fragment(&mut self, in_expr_context: bool) -> PResult<JSXClosingFragment> {
175        let start = self.cur_pos();
176        self.expect(Token::LessSlash)?;
177
178        // Handle JSX closing fragment followed by '=': '</>=
179        // When lexer sees '>=' it combines into GtEq, but JSX only needs '>'
180        // Use rescan_jsx_open_el_terminal_token to split >= back into >
181        self.input_mut().rescan_jsx_open_el_terminal_token();
182        self.expect_without_advance(Token::Gt)?;
183
184        if in_expr_context {
185            self.bump();
186        } else {
187            self.input_mut().scan_jsx_token(true);
188        }
189        let span = self.span(start);
190        Ok(JSXClosingFragment { span })
191    }
192
193    fn parse_jsx_children(&mut self) -> Vec<JSXElementChild> {
194        let mut list = Vec::with_capacity(8);
195        loop {
196            self.input_mut().rescan_jsx_token(true);
197            let Ok(Some(child)) = self.parse_jsx_child(self.input().get_cur().token) else {
198                break;
199            };
200            list.push(child);
201        }
202        list
203    }
204
205    fn parse_jsx_child(&mut self, t: Token) -> PResult<Option<JSXElementChild>> {
206        debug_assert!(self.input().syntax().jsx());
207
208        match t {
209            Token::LessSlash => Ok(None),
210            Token::LBrace => Ok(Some({
211                self.do_outside_of_context(
212                    Context::InCondExpr.union(Context::WillExpectColonForCond),
213                    |p| {
214                        let start = p.cur_pos();
215                        p.bump(); // bump "{"
216                        let ret = if p.input().cur() == Token::DotDotDot {
217                            p.bump(); // bump "..."
218                            let expr = p.parse_expr()?;
219                            p.expect_without_advance(Token::RBrace)?;
220                            p.input_mut().scan_jsx_token(true);
221                            JSXElementChild::JSXSpreadChild(JSXSpreadChild {
222                                span: p.span(start),
223                                expr,
224                            })
225                        } else {
226                            let expr = if p.input().cur() == Token::RBrace {
227                                JSXExpr::JSXEmptyExpr(p.parse_jsx_empty_expr())
228                            } else {
229                                p.parse_expr().map(JSXExpr::Expr)?
230                            };
231                            p.expect_without_advance(Token::RBrace)?;
232                            p.input_mut().scan_jsx_token(true);
233                            JSXElementChild::JSXExprContainer(JSXExprContainer {
234                                span: p.span(start),
235                                expr,
236                            })
237                        };
238                        Ok(ret)
239                    },
240                )?
241            })),
242            Token::Lt => {
243                let ele = self.parse_jsx_element(false)?;
244                match ele {
245                    either::Either::Left(frag) => Ok(Some(JSXElementChild::JSXFragment(frag))),
246                    either::Either::Right(ele) => {
247                        Ok(Some(JSXElementChild::JSXElement(Box::new(ele))))
248                    }
249                }
250            }
251            Token::JSXText => Ok(Some(JSXElementChild::JSXText(self.parse_jsx_text()))),
252            Token::Eof => {
253                unexpected!(self, "< (jsx tag start), jsx text or {")
254            }
255            _ => unreachable!(),
256        }
257    }
258
259    fn parse_jsx_attr_name(&mut self) -> PResult<JSXAttrName> {
260        debug_assert!(self.input().syntax().jsx());
261        trace_cur!(self, parse_jsx_attr_name);
262        let start = self.input().cur_pos();
263        self.input_mut().scan_jsx_identifier();
264
265        let attr_name = self.parse_jsx_ident()?;
266        if self.input_mut().eat(Token::Colon) {
267            self.input_mut().scan_jsx_identifier();
268            let name = self.parse_jsx_ident()?;
269            Ok(JSXAttrName::JSXNamespacedName(JSXNamespacedName {
270                span: Span::new_with_checked(start, name.span.hi),
271                ns: attr_name.into(),
272                name: name.into(),
273            }))
274        } else {
275            Ok(JSXAttrName::Ident(attr_name.into()))
276        }
277    }
278
279    fn parse_jsx_attr_value(&mut self) -> PResult<Option<JSXAttrValue>> {
280        debug_assert!(self.input().syntax().jsx());
281        trace_cur!(self, parse_jsx_attr_value);
282        if self.input().is(Token::Eq) {
283            self.input_mut().scan_jsx_attribute_value();
284            let cur = self.input().get_cur();
285            match cur.token {
286                Token::Str => {
287                    let value = self.parse_str_lit();
288                    Ok(Some(JSXAttrValue::Str(value)))
289                }
290                Token::LBrace => {
291                    let start = self.cur_pos();
292                    let node = self.parse_jsx_expr_container()?;
293                    self.jsx_expr_container_to_jsx_attr_value(start, node)
294                        .map(Some)
295                }
296                Token::Lt => match self.parse_jsx_element(true)? {
297                    either::Either::Left(frag) => Ok(Some(JSXAttrValue::JSXFragment(frag))),
298                    either::Either::Right(ele) => Ok(Some(JSXAttrValue::JSXElement(Box::new(ele)))),
299                },
300                _ => {
301                    let span = self.input().cur_span();
302                    syntax_error!(self, span, SyntaxError::InvalidJSXValue)
303                }
304            }
305        } else {
306            Ok(None)
307        }
308    }
309
310    fn parse_jsx_attr(&mut self) -> PResult<JSXAttrOrSpread> {
311        debug_assert!(self.input().syntax().jsx());
312        trace_cur!(self, parse_jsx_attr);
313        if self.input_mut().eat(Token::LBrace) {
314            let dot3_start = self.input().cur_pos();
315            self.expect(Token::DotDotDot)?;
316            let dot3_token = self.span(dot3_start);
317            let expr = self.parse_assignment_expr()?;
318            self.expect(Token::RBrace)?;
319            Ok(JSXAttrOrSpread::SpreadElement(SpreadElement {
320                dot3_token,
321                expr,
322            }))
323        } else {
324            let start = self.input().cur_pos();
325            let name = self.parse_jsx_attr_name()?;
326            let value = self.do_outside_of_context(
327                Context::InCondExpr.union(Context::WillExpectColonForCond),
328                |p| p.parse_jsx_attr_value(),
329            )?;
330            Ok(JSXAttrOrSpread::JSXAttr(JSXAttr {
331                span: self.span(start),
332                name,
333                value,
334            }))
335        }
336    }
337
338    fn parse_jsx_attrs(&mut self) -> PResult<Vec<JSXAttrOrSpread>> {
339        let mut attrs = Vec::with_capacity(8);
340
341        loop {
342            trace_cur!(self, parse_jsx_opening__attrs_loop);
343            self.input_mut().rescan_jsx_open_el_terminal_token();
344            let cur = self.input().get_cur();
345            if matches!(cur.token, Token::Gt | Token::Slash) {
346                break;
347            }
348            let attr = self.parse_jsx_attr()?;
349            attrs.push(attr);
350        }
351
352        Ok(attrs)
353    }
354
355    pub(crate) fn parse_jsx_element(
356        &mut self,
357        in_expr_context: bool,
358    ) -> PResult<either::Either<JSXFragment, JSXElement>> {
359        debug_assert!(self.input().syntax().jsx());
360        trace_cur!(self, parse_jsx_element);
361
362        let start = self.cur_pos();
363
364        self.do_outside_of_context(Context::ShouldNotLexLtOrGtAsType, |p| {
365            p.expect(Token::Lt)?;
366
367            // Handle JSX fragment opening followed by '=': '<>='
368            // When lexer sees '>=' it combines into GtEq, but JSX fragment only needs '>'
369            // Use rescan_jsx_open_el_terminal_token to split >= back into >
370            p.input_mut().rescan_jsx_open_el_terminal_token();
371
372            if p.input().cur() == Token::Gt {
373                // <>xxxxxx</>
374                p.input_mut().scan_jsx_token(true);
375                let opening = JSXOpeningFragment {
376                    span: p.span(start),
377                };
378                let children = p.parse_jsx_children();
379                let closing = p.parse_jsx_closing_fragment(in_expr_context)?;
380                let span = p.span(start);
381                Ok(either::Either::Left(JSXFragment {
382                    span,
383                    opening,
384                    children,
385                    closing,
386                }))
387            } else {
388                let name = p.do_outside_of_context(Context::ShouldNotLexLtOrGtAsType, |p| {
389                    p.parse_jsx_element_name()
390                })?;
391                let type_args = if p.input().syntax().typescript() && p.input().is(Token::Lt) {
392                    p.try_parse_ts(|p| {
393                        let ret = p.parse_ts_type_args()?;
394                        p.assert_and_bump(Token::Gt);
395                        Ok(Some(ret))
396                    })
397                } else {
398                    None
399                };
400                let attrs = p.parse_jsx_attrs()?;
401                if p.input().cur() == Token::Gt {
402                    // <xxxxx>xxxxx</xxxxx>
403                    p.input_mut().scan_jsx_token(true);
404                    let span = Span::new_with_checked(start, p.input.get_cur().span.lo);
405                    let opening = JSXOpeningElement {
406                        span,
407                        name,
408                        type_args,
409                        attrs,
410                        self_closing: false,
411                    };
412                    let children = p.parse_jsx_children();
413                    let closing = p.parse_jsx_closing_element(in_expr_context, &opening.name)?;
414                    let span = if in_expr_context {
415                        Span::new_with_checked(start, p.last_pos())
416                    } else {
417                        Span::new_with_checked(start, p.cur_pos())
418                    };
419                    Ok(either::Either::Right(JSXElement {
420                        span,
421                        opening,
422                        children,
423                        closing: Some(closing),
424                    }))
425                } else {
426                    // <xxxxx/>
427                    p.expect(Token::Slash)?;
428
429                    // Handle JSX self-closing tag followed by '=': '<tag/>='
430                    // When lexer sees '>=' it combines into GtEq, but JSX only needs '>'
431                    // Use rescan_jsx_open_el_terminal_token to split >= back into >
432                    p.input_mut().rescan_jsx_open_el_terminal_token();
433                    p.expect_without_advance(Token::Gt)?;
434
435                    if in_expr_context {
436                        p.bump();
437                    } else {
438                        p.input_mut().scan_jsx_token(true);
439                    }
440                    let span = if in_expr_context {
441                        p.span(start)
442                    } else {
443                        Span::new_with_checked(start, p.cur_pos())
444                    };
445                    Ok(either::Either::Right(JSXElement {
446                        span,
447                        opening: JSXOpeningElement {
448                            span,
449                            name,
450                            type_args,
451                            attrs,
452                            self_closing: true,
453                        },
454                        children: Vec::new(),
455                        closing: None,
456                    }))
457                }
458            }
459        })
460    }
461}
462
463fn get_qualified_jsx_name(name: &JSXElementName) -> Atom {
464    fn get_qualified_obj_name(obj: &JSXObject) -> Atom {
465        match *obj {
466            JSXObject::Ident(ref i) => i.sym.clone(),
467            JSXObject::JSXMemberExpr(ref member) => format!(
468                "{}.{}",
469                get_qualified_obj_name(&member.obj),
470                member.prop.sym
471            )
472            .into(),
473            #[cfg(swc_ast_unknown)]
474            _ => unreachable!(),
475        }
476    }
477    match *name {
478        JSXElementName::Ident(ref i) => i.sym.clone(),
479        JSXElementName::JSXNamespacedName(JSXNamespacedName {
480            ref ns, ref name, ..
481        }) => format!("{}:{}", ns.sym, name.sym).into(),
482        JSXElementName::JSXMemberExpr(JSXMemberExpr {
483            ref obj, ref prop, ..
484        }) => format!("{}.{}", get_qualified_obj_name(obj), prop.sym).into(),
485        #[cfg(swc_ast_unknown)]
486        _ => unreachable!(),
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use swc_atoms::atom;
493    use swc_common::DUMMY_SP as span;
494    use swc_ecma_visit::assert_eq_ignore_span;
495
496    use super::super::*;
497
498    fn jsx(src: &'static str) -> Box<Expr> {
499        test_parser(
500            src,
501            crate::Syntax::Es(crate::EsSyntax {
502                jsx: true,
503                ..Default::default()
504            }),
505            |p| p.parse_expr(),
506        )
507    }
508
509    #[test]
510    fn self_closing_01() {
511        assert_eq_ignore_span!(
512            jsx("<a />"),
513            Box::new(Expr::JSXElement(Box::new(JSXElement {
514                span,
515                opening: JSXOpeningElement {
516                    span,
517                    name: JSXElementName::Ident(Ident::new_no_ctxt(atom!("a"), span)),
518                    self_closing: true,
519                    attrs: Vec::new(),
520                    type_args: None,
521                },
522                children: Vec::new(),
523                closing: None,
524            })))
525        );
526    }
527
528    #[test]
529    fn normal_01() {
530        assert_eq_ignore_span!(
531            jsx("<a>foo</a>"),
532            Box::new(Expr::JSXElement(Box::new(JSXElement {
533                span,
534                opening: JSXOpeningElement {
535                    span,
536                    name: JSXElementName::Ident(Ident::new_no_ctxt(atom!("a"), span)),
537                    self_closing: false,
538                    attrs: Vec::new(),
539                    type_args: None,
540                },
541                children: vec![JSXElementChild::JSXText(JSXText {
542                    span,
543                    raw: atom!("foo"),
544                    value: atom!("foo"),
545                })],
546                closing: Some(JSXClosingElement {
547                    span,
548                    name: JSXElementName::Ident(Ident::new_no_ctxt(atom!("a"), span)),
549                })
550            })))
551        );
552    }
553
554    #[test]
555    fn escape_in_attr() {
556        assert_eq_ignore_span!(
557            jsx(r#"<div id="w &lt; w" />;"#),
558            Box::new(Expr::JSXElement(Box::new(JSXElement {
559                span,
560                opening: JSXOpeningElement {
561                    span,
562                    attrs: vec![JSXAttrOrSpread::JSXAttr(JSXAttr {
563                        span,
564                        name: JSXAttrName::Ident(IdentName::new(atom!("id"), span)),
565                        value: Some(JSXAttrValue::Str(Str {
566                            span,
567                            value: atom!("w < w").into(),
568                            raw: Some(atom!("\"w &lt; w\"")),
569                        })),
570                    })],
571                    name: JSXElementName::Ident(Ident::new_no_ctxt(atom!("div"), span)),
572                    self_closing: true,
573                    type_args: None,
574                },
575                children: Vec::new(),
576                closing: None
577            })))
578        );
579    }
580
581    #[test]
582    fn issue_584() {
583        assert_eq_ignore_span!(
584            jsx(r#"<test other={4} />;"#),
585            Box::new(Expr::JSXElement(Box::new(JSXElement {
586                span,
587                opening: JSXOpeningElement {
588                    span,
589                    name: JSXElementName::Ident(Ident::new_no_ctxt(atom!("test"), span)),
590                    attrs: vec![JSXAttrOrSpread::JSXAttr(JSXAttr {
591                        span,
592                        name: JSXAttrName::Ident(IdentName::new(atom!("other"), span)),
593                        value: Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
594                            span,
595                            expr: JSXExpr::Expr(Box::new(Expr::Lit(Lit::Num(Number {
596                                span,
597                                value: 4.0,
598                                raw: Some(atom!("4"))
599                            }))))
600                        })),
601                    })],
602                    self_closing: true,
603                    type_args: None,
604                },
605                children: Vec::new(),
606                closing: None
607            })))
608        );
609    }
610}