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