swc_ecma_lexer/common/parser/
jsx.rs

1use either::Either;
2use swc_common::{BytePos, Span, Spanned};
3use swc_ecma_ast::*;
4
5use super::{PResult, Parser};
6use crate::{
7    common::{
8        context::Context,
9        lexer::token::TokenFactory,
10        parser::{
11            buffer::Buffer,
12            eof_error,
13            expr::{parse_assignment_expr, parse_str_lit},
14            get_qualified_jsx_name,
15            ident::parse_ident_ref,
16            typescript::{parse_ts_type_args, try_parse_ts},
17        },
18    },
19    error::SyntaxError,
20};
21
22/// Parses JSX closing tag starting after "</".
23fn parse_jsx_closing_element_at<'a, P: Parser<'a>>(
24    p: &mut P,
25    start: BytePos,
26) -> PResult<Either<JSXClosingFragment, JSXClosingElement>> {
27    debug_assert!(p.input().syntax().jsx());
28
29    if p.input_mut().eat(&P::Token::JSX_TAG_END) {
30        return Ok(Either::Left(JSXClosingFragment {
31            span: p.span(start),
32        }));
33    }
34
35    let name = parse_jsx_element_name(p)?;
36    expect!(p, &P::Token::JSX_TAG_END);
37    Ok(Either::Right(JSXClosingElement {
38        span: p.span(start),
39        name,
40    }))
41}
42
43/// Parses JSX expression enclosed into curly brackets.
44pub fn parse_jsx_expr_container<'a, P: Parser<'a>>(p: &mut P) -> PResult<JSXExprContainer> {
45    debug_assert!(p.input().syntax().jsx());
46    debug_assert!(p.input().is(&P::Token::LBRACE));
47
48    let start = p.input().cur_pos();
49    p.bump(); // bump "{"
50    let expr = if p.input().is(&P::Token::RBRACE) {
51        JSXExpr::JSXEmptyExpr(parse_jsx_empty_expr(p))
52    } else {
53        p.parse_expr().map(JSXExpr::Expr)?
54    };
55    expect!(p, &P::Token::RBRACE);
56    Ok(JSXExprContainer {
57        span: p.span(start),
58        expr,
59    })
60}
61
62/// Parse next token as JSX identifier
63fn parse_jsx_ident<'a, P: Parser<'a>>(p: &mut P) -> PResult<Ident> {
64    debug_assert!(p.input().syntax().jsx());
65    trace_cur!(p, parse_jsx_ident);
66    let cur = p.input().cur();
67    if cur.is_jsx_name() {
68        let name = p.input_mut().expect_jsx_name_token_and_bump();
69        let span = p.input().prev_span();
70        Ok(Ident::new_no_ctxt(name, span))
71    } else if p.ctx().contains(Context::InForcedJsxContext) {
72        parse_ident_ref(p)
73    } else {
74        unexpected!(p, "jsx identifier")
75    }
76}
77
78/// Parse namespaced identifier.
79fn parse_jsx_namespaced_name<'a, P: Parser<'a>>(p: &mut P) -> PResult<JSXAttrName> {
80    debug_assert!(p.input().syntax().jsx());
81    trace_cur!(p, parse_jsx_namespaced_name);
82    let start = p.input().cur_pos();
83    let ns = parse_jsx_ident(p)?.into();
84    if !p.input_mut().eat(&P::Token::COLON) {
85        return Ok(JSXAttrName::Ident(ns));
86    }
87    let name = parse_jsx_ident(p).map(IdentName::from)?;
88    Ok(JSXAttrName::JSXNamespacedName(JSXNamespacedName {
89        span: Span::new_with_checked(start, name.span.hi),
90        ns,
91        name,
92    }))
93}
94
95/// Parses element name in any form - namespaced, member or single
96/// identifier.
97fn parse_jsx_element_name<'a, P: Parser<'a>>(p: &mut P) -> PResult<JSXElementName> {
98    debug_assert!(p.input().syntax().jsx());
99    trace_cur!(p, parse_jsx_element_name);
100    let start = p.input().cur_pos();
101    let mut node = match parse_jsx_namespaced_name(p)? {
102        JSXAttrName::Ident(i) => JSXElementName::Ident(i.into()),
103        JSXAttrName::JSXNamespacedName(i) => JSXElementName::JSXNamespacedName(i),
104    };
105    while p.input_mut().eat(&P::Token::DOT) {
106        let prop = parse_jsx_ident(p).map(IdentName::from)?;
107        let new_node = JSXElementName::JSXMemberExpr(JSXMemberExpr {
108            span: p.span(start),
109            obj: match node {
110                JSXElementName::Ident(i) => JSXObject::Ident(i),
111                JSXElementName::JSXMemberExpr(i) => JSXObject::JSXMemberExpr(Box::new(i)),
112                _ => unimplemented!("JSXNamespacedName -> JSXObject"),
113            },
114            prop,
115        });
116        node = new_node;
117    }
118    Ok(node)
119}
120
121/// JSXEmptyExpression is unique type since it doesn't actually parse
122/// anything, and so it should start at the end of last read token (left
123/// brace) and finish at the beginning of the next one (right brace).
124pub fn parse_jsx_empty_expr<'a>(p: &mut impl Parser<'a>) -> JSXEmptyExpr {
125    debug_assert!(p.input().syntax().jsx());
126    let start = p.input().cur_pos();
127    JSXEmptyExpr {
128        span: Span::new_with_checked(start, start),
129    }
130}
131
132pub fn parse_jsx_text<'a>(p: &mut impl Parser<'a>) -> JSXText {
133    debug_assert!(p.input().syntax().jsx());
134
135    let cur = p.input().cur();
136    debug_assert!(cur.is_jsx_text());
137    let (value, raw) = p.input_mut().expect_jsx_text_token_and_bump();
138    let span = p.input().prev_span();
139    JSXText { span, value, raw }
140}
141
142pub fn jsx_expr_container_to_jsx_attr_value<'a, P: Parser<'a>>(
143    p: &mut P,
144    start: BytePos,
145    node: JSXExprContainer,
146) -> PResult<JSXAttrValue> {
147    match node.expr {
148        JSXExpr::JSXEmptyExpr(..) => {
149            syntax_error!(p, p.span(start), SyntaxError::EmptyJSXAttr)
150        }
151        JSXExpr::Expr(..) => Ok(node.into()),
152    }
153}
154
155/// Parses any type of JSX attribute value.
156///
157/// TODO(kdy1): Change return type to JSXAttrValue
158fn parse_jsx_attr_value<'a, P: Parser<'a>>(p: &mut P) -> PResult<JSXAttrValue> {
159    debug_assert!(p.input().syntax().jsx());
160    trace_cur!(p, parse_jsx_attr_value);
161
162    let start = p.cur_pos();
163
164    let cur = p.input().cur();
165    if cur.is_lbrace() {
166        let node = parse_jsx_expr_container(p)?;
167        jsx_expr_container_to_jsx_attr_value(p, start, node)
168    } else if cur.is_str() {
169        Ok(JSXAttrValue::Lit(Lit::Str(parse_str_lit(p))))
170    } else if cur.is_jsx_tag_start() {
171        let expr = parse_jsx_element(p)?;
172        match expr {
173            Either::Left(n) => Ok(JSXAttrValue::JSXFragment(n)),
174            Either::Right(n) => Ok(JSXAttrValue::JSXElement(Box::new(n))),
175        }
176    } else {
177        let span = p.input().cur_span();
178        syntax_error!(p, span, SyntaxError::InvalidJSXValue)
179    }
180}
181
182/// Parse JSX spread child
183fn parse_jsx_spread_child<'a, P: Parser<'a>>(p: &mut P) -> PResult<JSXSpreadChild> {
184    debug_assert!(p.input().syntax().jsx());
185    debug_assert!(p.input().cur().is_lbrace());
186    debug_assert!(peek!(p).is_some_and(|peek| peek.is_dotdotdot()));
187
188    let start = p.cur_pos();
189    p.bump(); // bump "{"
190    p.bump(); // bump "..."
191    let expr = p.parse_expr()?;
192    expect!(p, &P::Token::RBRACE);
193
194    Ok(JSXSpreadChild {
195        span: p.span(start),
196        expr,
197    })
198}
199
200/// Parses following JSX attribute name-value pair.
201fn parse_jsx_attr<'a, P: Parser<'a>>(p: &mut P) -> PResult<JSXAttrOrSpread> {
202    debug_assert!(p.input().syntax().jsx());
203    let start = p.cur_pos();
204
205    debug_tracing!(p, "parse_jsx_attr");
206
207    if p.input_mut().eat(&P::Token::LBRACE) {
208        let dot3_start = p.cur_pos();
209        expect!(p, &P::Token::DOTDOTDOT);
210        let dot3_token = p.span(dot3_start);
211        let expr = parse_assignment_expr(p)?;
212        expect!(p, &P::Token::RBRACE);
213        return Ok(SpreadElement { dot3_token, expr }.into());
214    }
215
216    let name = parse_jsx_namespaced_name(p)?;
217    let value = if p.input_mut().eat(&P::Token::EQUAL) {
218        p.do_outside_of_context(
219            Context::InCondExpr.union(Context::WillExpectColonForCond),
220            parse_jsx_attr_value,
221        )
222        .map(Some)?
223    } else {
224        None
225    };
226
227    Ok(JSXAttr {
228        span: p.span(start),
229        name,
230        value,
231    }
232    .into())
233}
234
235/// Parses JSX opening tag starting after "<".
236fn parse_jsx_opening_element_at<'a, P: Parser<'a>>(
237    p: &mut P,
238    start: BytePos,
239) -> PResult<Either<JSXOpeningFragment, JSXOpeningElement>> {
240    debug_assert!(p.input().syntax().jsx());
241
242    if p.input_mut().eat(&P::Token::JSX_TAG_END) {
243        return Ok(Either::Left(JSXOpeningFragment {
244            span: p.span(start),
245        }));
246    }
247
248    let name =
249        p.do_outside_of_context(Context::ShouldNotLexLtOrGtAsType, parse_jsx_element_name)?;
250    parse_jsx_opening_element_after_name(p, start, name).map(Either::Right)
251}
252
253#[inline(always)]
254fn parse_jsx_attrs<'a, P: Parser<'a>>(p: &mut P) -> PResult<Vec<JSXAttrOrSpread>> {
255    let mut attrs = Vec::with_capacity(8);
256
257    while !p.input().cur().is_eof() {
258        trace_cur!(p, parse_jsx_opening__attrs_loop);
259
260        let cur = p.input().cur();
261        if cur.is_slash() || cur.is_jsx_tag_end() {
262            break;
263        }
264
265        let attr = parse_jsx_attr(p)?;
266        attrs.push(attr);
267    }
268
269    Ok(attrs)
270}
271
272/// `jsxParseOpeningElementAfterName`
273fn parse_jsx_opening_element_after_name<'a, P: Parser<'a>>(
274    p: &mut P,
275    start: BytePos,
276    name: JSXElementName,
277) -> PResult<JSXOpeningElement> {
278    debug_assert!(p.input().syntax().jsx());
279
280    let type_args = if p.input().syntax().typescript() && p.input().is(&P::Token::LESS) {
281        try_parse_ts(p, |p| {
282            let ret = parse_ts_type_args(p)?;
283            p.assert_and_bump(&P::Token::GREATER);
284            Ok(Some(ret))
285        })
286    } else {
287        None
288    };
289
290    let attrs = parse_jsx_attrs(p)?;
291
292    let self_closing = p.input_mut().eat(&P::Token::DIV);
293    if !p.input_mut().eat(&P::Token::JSX_TAG_END)
294        & !(p.ctx().contains(Context::InForcedJsxContext) && p.input_mut().eat(&P::Token::GREATER))
295    {
296        unexpected!(p, "> (jsx closing tag)");
297    }
298    Ok(JSXOpeningElement {
299        span: p.span(start),
300        name,
301        attrs,
302        self_closing,
303        type_args,
304    })
305}
306
307/// Parses entire JSX element, including it"s opening tag
308/// (starting after "<"), attributes, contents and closing tag.
309///
310/// babel: `jsxParseElementAt`
311fn parse_jsx_element_at<'a, P: Parser<'a>>(
312    p: &mut P,
313    start_pos: BytePos,
314) -> PResult<Either<JSXFragment, JSXElement>> {
315    debug_assert!(p.input().syntax().jsx());
316
317    let cur = p.input().cur();
318    if cur.is_error() {
319        let error = p.input_mut().expect_error_token_and_bump();
320        return Err(error);
321    } else if cur.is_eof() {
322        return Err(eof_error(p));
323    }
324    let forced_jsx_context = if cur.is_less() {
325        true
326    } else {
327        debug_assert!(cur.is_jsx_tag_start());
328        false
329    };
330    let start = p.cur_pos();
331    p.bump();
332
333    p.do_outside_of_context(Context::ShouldNotLexLtOrGtAsType, |p| {
334        let f = |p: &mut P| {
335            debug_tracing!(p, "parse_jsx_element");
336
337            let opening_element = parse_jsx_opening_element_at(p, start_pos)?;
338
339            trace_cur!(p, parse_jsx_element__after_opening_element);
340
341            let mut children = Vec::new();
342            let mut closing_element = None;
343
344            let self_closing = match opening_element {
345                Either::Right(ref el) => el.self_closing,
346                _ => false,
347            };
348
349            if !self_closing {
350                'contents: loop {
351                    let cur = p.input().cur();
352                    if cur.is_jsx_tag_start() {
353                        let start = p.cur_pos();
354                        if peek!(p).is_some_and(|peek| peek.is_slash()) {
355                            p.bump(); // JSXTagStart
356                            if p.input().cur().is_eof() {
357                                return Err(eof_error(p));
358                            }
359                            p.assert_and_bump(&P::Token::DIV);
360                            closing_element = parse_jsx_closing_element_at(p, start).map(Some)?;
361                            break 'contents;
362                        }
363                        children.push(parse_jsx_element_at(p, start).map(|e| match e {
364                            Either::Left(e) => JSXElementChild::from(e),
365                            Either::Right(e) => JSXElementChild::from(Box::new(e)),
366                        })?);
367                    } else if cur.is_jsx_text() {
368                        children.push(JSXElementChild::from(parse_jsx_text(p)))
369                    } else if cur.is_lbrace() {
370                        if peek!(p).is_some_and(|peek| peek.is_dotdotdot()) {
371                            children.push(parse_jsx_spread_child(p).map(JSXElementChild::from)?);
372                        } else {
373                            children.push(parse_jsx_expr_container(p).map(JSXElementChild::from)?);
374                        }
375                    } else {
376                        unexpected!(p, "< (jsx tag start), jsx text or {")
377                    }
378                }
379            }
380            let span = p.span(start);
381
382            Ok(match (opening_element, closing_element) {
383                (Either::Left(..), Some(Either::Right(closing))) => {
384                    syntax_error!(p, closing.span(), SyntaxError::JSXExpectedClosingTagForLtGt);
385                }
386                (Either::Right(opening), Some(Either::Left(closing))) => {
387                    syntax_error!(
388                        p,
389                        closing.span(),
390                        SyntaxError::JSXExpectedClosingTag {
391                            tag: get_qualified_jsx_name(&opening.name)
392                        }
393                    );
394                }
395                (Either::Left(opening), Some(Either::Left(closing))) => Either::Left(JSXFragment {
396                    span,
397                    opening,
398                    children,
399                    closing,
400                }),
401                (Either::Right(opening), None) => Either::Right(JSXElement {
402                    span,
403                    opening,
404                    children,
405                    closing: None,
406                }),
407                (Either::Right(opening), Some(Either::Right(closing))) => {
408                    if get_qualified_jsx_name(&closing.name)
409                        != get_qualified_jsx_name(&opening.name)
410                    {
411                        syntax_error!(
412                            p,
413                            closing.span(),
414                            SyntaxError::JSXExpectedClosingTag {
415                                tag: get_qualified_jsx_name(&opening.name)
416                            }
417                        );
418                    }
419                    Either::Right(JSXElement {
420                        span,
421                        opening,
422                        children,
423                        closing: Some(closing),
424                    })
425                }
426                _ => unreachable!(),
427            })
428        };
429        if forced_jsx_context {
430            p.do_inside_of_context(Context::InForcedJsxContext, f)
431        } else {
432            p.do_outside_of_context(Context::InForcedJsxContext, f)
433        }
434    })
435}
436
437/// Parses entire JSX element from current position.
438///
439/// babel: `jsxParseElement`
440pub(crate) fn parse_jsx_element<'a, P: Parser<'a>>(
441    p: &mut P,
442) -> PResult<Either<JSXFragment, JSXElement>> {
443    trace_cur!(p, parse_jsx_element);
444
445    debug_assert!(p.input().syntax().jsx());
446    debug_assert!({
447        let cur = p.input().cur();
448        cur.is_jsx_tag_start() || cur.is_less()
449    });
450
451    let start_pos = p.cur_pos();
452
453    p.do_outside_of_context(
454        Context::InCondExpr.union(Context::WillExpectColonForCond),
455        |p| parse_jsx_element_at(p, start_pos),
456    )
457}