aboutsummaryrefslogblamecommitdiffstats
path: root/tests/test_utils/jsx_rewrite.rs
blob: 9dd2605b6324d55cf1742a3f5f10c711604890c0 (plain) (tree)
1
2
3
4
5
6
7
8
9

                          





                                                                                                  
  
                                                                                       
                                                                  















                                                                                




                                

                           
                 
                                                           

                                         


















                                                                          



                                                                      










































                                                                           
                                                      












                                                               



                                   

                               

                                                










                                                                            
                    

























































































































































































































































































































                                                                                            
                                                          
                                            






































                                                                                      











                                                                                            
                             













































































                                                                                                 
 













































































                                                                                    
                                 







                                                                              
                                                                           























                                                                                      










                                                                                           


                                                                                     















































































                                                                                           
                                                                                              





































































































































































































                                                                                                                                                             

                                                                                             

















                                                                      












                                                                      







































                                                                                               




































                                                                                          



































                                                                                     



















                                                                                     
                                 






                         
extern crate swc_common;
extern crate swc_ecma_ast;
use crate::test_utils::{
    micromark_swc_utils::{position_to_string, span_to_position},
    swc_utils::{
        create_binary_expression, create_ident, create_ident_expression, create_member_expression,
    },
    to_swc::Program,
};
use micromark::{id_cont_ as id_cont, id_start_ as id_start, unist::Position, Location};
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};

/// Configuration.
#[derive(Debug, Default, Clone)]
pub struct Options {
    /// Place to import a provider from.
    ///
    /// See [MDX provider](https://mdxjs.com/docs/using-mdx/#mdx-provider)
    /// on the MDX website for more info.
    pub provider_import_source: Option<String>,
    /// Whether to add extra information to error messages in generated code.
    /// This is not yet supported.
    pub development: bool,
}

/// Rewrite JSX in an MDX file so that components can be passed in and provided.
#[allow(dead_code)]
pub fn jsx_rewrite(
    mut program: Program,
    options: &Options,
    location: Option<&Location>,
) -> Program {
    let mut state = State {
        scopes: vec![],
        location,
        provider: options.provider_import_source.is_some(),
        path: program.path.clone(),
        development: options.development,
        create_provider_import: false,
        create_error_helper: false,
    };
    state.enter(Some(Info::default()));
    program.module.visit_mut_with(&mut state);

    // If a provider is used (and can be used), import it.
    if let Some(source) = &options.provider_import_source {
        if state.create_provider_import {
            program
                .module
                .body
                .insert(0, create_import_provider(source))
        }
    }

    // If potentially missing components are used, add the helper used for
    // errors.
    if state.create_error_helper {
        program
            .module
            .body
            .push(create_error_helper(state.development, state.path));
    }

    program
}

/// Collection of different SWC functions.
#[derive(Debug)]
enum Func<'a> {
    /// Function declaration.
    Decl(&'a mut swc_ecma_ast::FnDecl),
    /// Function expression.
    Expr(&'a mut swc_ecma_ast::FnExpr),
    /// Arrow function.
    Arrow(&'a mut swc_ecma_ast::ArrowExpr),
}

/// Info for a function scope.
#[derive(Debug, Default, Clone)]
struct Info {
    /// Function name.
    name: Option<String>,
    /// Used objects (`a` in `<a.b />`).
    objects: Vec<String>,
    /// Used components (`<A />`).
    components: Vec<String>,
    /// Used literals (`<a />`).
    tags: Vec<String>,
    /// List of JSX identifiers of literal tags that are not valid JS
    /// identifiers in the shape of `Vec<(invalid, valid)>`.
    ///
    /// Example:
    ///
    /// ```
    /// vec![("a-b".into(), "_component0".into())]
    /// ```
    aliases: Vec<(String, String)>,
    /// Non-literal references in the shape of `Vec<(name, is_component)>`.
    ///
    /// Example:
    ///
    /// ```
    /// vec![("a".into(), false), ("a.b".into(), true)]
    /// ```
    references: Vec<(String, bool, Option<Position>)>,
}

/// Scope (block or function/global).
#[derive(Debug, Clone)]
struct Scope {
    /// If this is a function (or global) scope, we track info.
    info: Option<Info>,
    /// Things that are defined in this scope.
    defined: Vec<String>,
}

/// Context.
#[derive(Debug, Default, Clone)]
struct State<'a> {
    location: Option<&'a Location>,
    /// Path to file.
    path: Option<String>,
    /// List of current scopes.
    scopes: Vec<Scope>,
    /// Whether the user is in development mode.
    development: bool,
    /// Whether the user uses a provider.
    provider: bool,
    /// Whether a provider is referenced.
    create_provider_import: bool,
    /// Whether a missing component helper is referenced.
    ///
    /// When things are referenced that might not be defined, we reference a
    /// helper function to throw when they are missing.
    create_error_helper: bool,
}

impl<'a> State<'a> {
    /// Open a new scope.
    fn enter(&mut self, info: Option<Info>) {
        self.scopes.push(Scope {
            info,
            defined: vec![],
        });
    }

    /// Close the current scope.
    fn exit(&mut self) -> Scope {
        self.scopes.pop().expect("expected scope")
    }

    /// Close a function.
    fn exit_func(&mut self, func: Func) {
        let mut scope = self.exit();
        let mut defaults = vec![];
        let mut info = scope.info.take().unwrap();
        let mut index = 0;

        // Create defaults for tags.
        //
        // ```jsx
        // {h1: 'h1'}
        // ```
        while index < info.tags.len() {
            let name = &info.tags[index];

            defaults.push(swc_ecma_ast::PropOrSpread::Prop(Box::new(
                swc_ecma_ast::Prop::KeyValue(swc_ecma_ast::KeyValueProp {
                    key: if is_identifier_name(name) {
                        swc_ecma_ast::PropName::Ident(create_ident(name))
                    } else {
                        swc_ecma_ast::PropName::Str(swc_ecma_ast::Str {
                            value: name.clone().into(),
                            span: swc_common::DUMMY_SP,
                            raw: None,
                        })
                    },
                    value: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(
                        swc_ecma_ast::Str {
                            value: name.clone().into(),
                            span: swc_common::DUMMY_SP,
                            raw: None,
                        },
                    ))),
                }),
            )));

            index += 1;
        }

        let mut actual = info.components.split_off(0);
        let mut index = 0;

        // In some cases, a component is used directly (`<X>`) but it’s also
        // used as an object (`<X.Y>`).
        while index < info.objects.len() {
            if !actual.contains(&info.objects[index]) {
                actual.push(info.objects[index].clone());
            }
            index += 1;
        }

        let mut statements = vec![];

        if !defaults.is_empty() || !actual.is_empty() || !info.aliases.is_empty() {
            let mut parameters = vec![];

            // Use a provider, if configured.
            //
            // ```jsx
            // _provideComponents()
            // ```
            if self.provider {
                self.create_provider_import = true;
                parameters.push(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr {
                    callee: swc_ecma_ast::Callee::Expr(Box::new(create_ident_expression(
                        "_provideComponents",
                    ))),
                    args: vec![],
                    type_args: None,
                    span: swc_common::DUMMY_SP,
                }));
            }

            // Accept `components` as a prop if this is the `MDXContent` or
            // `_createMdxContent` function.
            //
            // ```jsx
            // props.components
            // ```
            if is_props_receiving_fn(&info.name) {
                parameters.push(swc_ecma_ast::Expr::Member(swc_ecma_ast::MemberExpr {
                    obj: Box::new(create_ident_expression("props")),
                    prop: swc_ecma_ast::MemberProp::Ident(create_ident("components")),
                    span: swc_common::DUMMY_SP,
                }));
            }

            // Inject an object at the start, when:
            // - there are defaults,
            // - there are two sources
            //
            // ```jsx
            // (_provideComponents(), props.components)
            // ()
            // ```
            //
            // To:
            //
            // ```jsx
            // ({}, _provideComponents(), props.components)
            // ({h1: 'h1'})
            // ```
            if !defaults.is_empty() || parameters.len() > 1 {
                parameters.insert(
                    0,
                    swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit {
                        props: defaults,
                        span: swc_common::DUMMY_SP,
                    }),
                );
            }

            // Merge things and prevent errors.
            //
            // ```jsx
            // {}, _provideComponents(), props.components
            // props.components
            // _provideComponents()
            // ```
            //
            // To:
            //
            // ```jsx
            // Object.assign({}, _provideComponents(), props.components)
            // props.components || {}
            // _provideComponents()
            // ```
            let mut components_init = if parameters.len() > 1 {
                let mut args = vec![];
                parameters.reverse();
                while let Some(param) = parameters.pop() {
                    args.push(swc_ecma_ast::ExprOrSpread {
                        spread: None,
                        expr: Box::new(param),
                    });
                }
                swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr {
                    callee: swc_ecma_ast::Callee::Expr(Box::new(swc_ecma_ast::Expr::Member(
                        swc_ecma_ast::MemberExpr {
                            obj: Box::new(create_ident_expression("Object")),
                            prop: swc_ecma_ast::MemberProp::Ident(create_ident("assign")),
                            span: swc_common::DUMMY_SP,
                        },
                    ))),
                    args,
                    type_args: None,
                    span: swc_common::DUMMY_SP,
                })
            } else {
                // Always one.
                let param = parameters.pop().unwrap();

                if let swc_ecma_ast::Expr::Member(_) = param {
                    create_binary_expression(
                        vec![
                            param,
                            swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit {
                                props: vec![],
                                span: swc_common::DUMMY_SP,
                            }),
                        ],
                        swc_ecma_ast::BinaryOp::LogicalOr,
                    )
                } else {
                    param
                }
            };

            // Add components to scope.
            //
            // For `['MyComponent', 'MDXLayout']` this generates:
            //
            // ```js
            // const {MyComponent, wrapper: MDXLayout} = _components
            // ```
            //
            // Note that MDXLayout is special as it’s taken from
            // `_components.wrapper`.
            let components_pattern = if actual.is_empty() {
                None
            } else {
                let mut props = vec![];
                actual.reverse();
                while let Some(key) = actual.pop() {
                    // `wrapper: MDXLayout`
                    if key == "MDXLayout" {
                        props.push(swc_ecma_ast::ObjectPatProp::KeyValue(
                            swc_ecma_ast::KeyValuePatProp {
                                key: swc_ecma_ast::PropName::Ident(create_ident("wrapper")),
                                value: Box::new(swc_ecma_ast::Pat::Ident(
                                    swc_ecma_ast::BindingIdent {
                                        id: create_ident(&key),
                                        type_ann: None,
                                    },
                                )),
                            },
                        ))
                    }
                    // `MyComponent`
                    else {
                        props.push(swc_ecma_ast::ObjectPatProp::Assign(
                            swc_ecma_ast::AssignPatProp {
                                key: create_ident(&key),
                                value: None,
                                span: swc_common::DUMMY_SP,
                            },
                        ))
                    }
                }

                Some(swc_ecma_ast::ObjectPat {
                    props,
                    optional: false,
                    span: swc_common::DUMMY_SP,
                    type_ann: None,
                })
            };

            let mut declarators = vec![];

            // If there are tags, they take them from `_components`, so we need
            // to make it defined.
            if !info.tags.is_empty() {
                declarators.push(swc_ecma_ast::VarDeclarator {
                    span: swc_common::DUMMY_SP,
                    name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent {
                        id: create_ident("_components"),
                        type_ann: None,
                    }),
                    init: Some(Box::new(components_init)),
                    definite: false,
                });
                components_init = create_ident_expression("_components");
            }

            // For JSX IDs that can’t be represented as JavaScript IDs (as in,
            // those with dashes, such as `custom-element`), we generated a
            // separate variable that is a valid JS ID (such as `_component0`),
            // and here we take it from components:
            // ```js
            // const _component0 = _components['custom-element']
            // ```
            if !info.aliases.is_empty() {
                info.aliases.reverse();

                while let Some((id, name)) = info.aliases.pop() {
                    declarators.push(swc_ecma_ast::VarDeclarator {
                        span: swc_common::DUMMY_SP,
                        name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent {
                            id: create_ident(&name),
                            type_ann: None,
                        }),
                        init: Some(Box::new(swc_ecma_ast::Expr::Member(
                            swc_ecma_ast::MemberExpr {
                                obj: Box::new(create_ident_expression("_components")),
                                prop: swc_ecma_ast::MemberProp::Computed(
                                    swc_ecma_ast::ComputedPropName {
                                        expr: Box::new(swc_ecma_ast::Expr::Lit(
                                            swc_ecma_ast::Lit::Str(swc_ecma_ast::Str {
                                                value: id.into(),
                                                span: swc_common::DUMMY_SP,
                                                raw: None,
                                            }),
                                        )),
                                        span: swc_common::DUMMY_SP,
                                    },
                                ),
                                span: swc_common::DUMMY_SP,
                            },
                        ))),
                        definite: false,
                    });
                }
            }

            if let Some(pat) = components_pattern {
                declarators.push(swc_ecma_ast::VarDeclarator {
                    name: swc_ecma_ast::Pat::Object(pat),
                    init: Some(Box::new(components_init)),
                    span: swc_common::DUMMY_SP,
                    definite: false,
                });
            }

            // Add the variable declaration.
            statements.push(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Var(Box::new(
                swc_ecma_ast::VarDecl {
                    kind: swc_ecma_ast::VarDeclKind::Const,
                    decls: declarators,
                    span: swc_common::DUMMY_SP,
                    declare: false,
                },
            ))));
        }

        // Add checks at runtime to verify that object/components are passed.
        //
        // ```js
        // if (!a) _missingMdxReference("a", false);
        // if (!a.b) _missingMdxReference("a.b", true);
        // ```
        for (id, component, position) in info.references {
            self.create_error_helper = true;

            let mut args = vec![
                swc_ecma_ast::ExprOrSpread {
                    spread: None,
                    expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(
                        swc_ecma_ast::Str {
                            value: id.clone().into(),
                            span: swc_common::DUMMY_SP,
                            raw: None,
                        },
                    ))),
                },
                swc_ecma_ast::ExprOrSpread {
                    spread: None,
                    expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Bool(
                        swc_ecma_ast::Bool {
                            value: component,
                            span: swc_common::DUMMY_SP,
                        },
                    ))),
                },
            ];

            // Add the source location if it exists and if `development` is on.
            if let Some(position) = position.as_ref() {
                if self.development {
                    args.push(swc_ecma_ast::ExprOrSpread {
                        spread: None,
                        expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(
                            swc_ecma_ast::Str {
                                value: position_to_string(position).into(),
                                span: swc_common::DUMMY_SP,
                                raw: None,
                            },
                        ))),
                    })
                }
            }

            statements.push(swc_ecma_ast::Stmt::If(swc_ecma_ast::IfStmt {
                test: Box::new(swc_ecma_ast::Expr::Unary(swc_ecma_ast::UnaryExpr {
                    op: swc_ecma_ast::UnaryOp::Bang,
                    arg: Box::new(create_member_expression(&id)),
                    span: swc_common::DUMMY_SP,
                })),
                cons: Box::new(swc_ecma_ast::Stmt::Expr(swc_ecma_ast::ExprStmt {
                    span: swc_common::DUMMY_SP,
                    expr: Box::new(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr {
                        callee: swc_ecma_ast::Callee::Expr(Box::new(create_ident_expression(
                            "_missingMdxReference",
                        ))),
                        args,
                        type_args: None,
                        span: swc_common::DUMMY_SP,
                    })),
                })),
                alt: None,
                span: swc_common::DUMMY_SP,
            }));
        }

        // Add statements to functions.
        if !statements.is_empty() {
            let mut body: &mut swc_ecma_ast::BlockStmt = match func {
                Func::Expr(expr) => {
                    if expr.function.body.is_none() {
                        expr.function.body = Some(swc_ecma_ast::BlockStmt {
                            stmts: vec![],
                            span: swc_common::DUMMY_SP,
                        });
                    }
                    expr.function.body.as_mut().unwrap()
                }
                Func::Decl(decl) => {
                    if decl.function.body.is_none() {
                        decl.function.body = Some(swc_ecma_ast::BlockStmt {
                            stmts: vec![],
                            span: swc_common::DUMMY_SP,
                        });
                    }
                    decl.function.body.as_mut().unwrap()
                }
                Func::Arrow(arr) => {
                    if let swc_ecma_ast::BlockStmtOrExpr::Expr(expr) = &mut arr.body {
                        arr.body =
                            swc_ecma_ast::BlockStmtOrExpr::BlockStmt(swc_ecma_ast::BlockStmt {
                                stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt {
                                    // To do: figure out non-clone.
                                    arg: Some(expr.clone()),
                                    span: swc_common::DUMMY_SP,
                                })],
                                span: swc_common::DUMMY_SP,
                            });
                    }
                    arr.body.as_mut_block_stmt().unwrap()
                }
            };

            statements.append(&mut body.stmts.split_off(0));
            body.stmts = statements;
        }
    }

    /// Get the current function scope.
    fn current_fn_scope_mut(&mut self) -> &mut Scope {
        let mut index = self.scopes.len();

        while index > 0 {
            index -= 1;
            if self.scopes[index].info.is_some() {
                return &mut self.scopes[index];
            }
        }

        unreachable!("expected scope")
    }

    /// Get the current scope.
    fn current_scope_mut(&mut self) -> &mut Scope {
        self.scopes.last_mut().expect("expected scope")
    }

    /// Get the top-level scope’s info.
    fn current_top_level_info(&self) -> Option<&Info> {
        if let Some(scope) = self.scopes.get(1) {
            scope.info.as_ref()
        } else {
            None
        }
    }

    /// Get the top-level scope’s info, mutably.
    fn current_top_level_info_mut(&mut self) -> Option<&mut Info> {
        if let Some(scope) = self.scopes.get_mut(1) {
            scope.info.as_mut()
        } else {
            None
        }
    }

    /// Check if `id` is in scope.
    fn in_scope(&self, id: &String) -> bool {
        let mut index = self.scopes.len();

        while index > 0 {
            index -= 1;
            if self.scopes[index].defined.contains(id) {
                return true;
            }
        }

        false
    }

    /// Add an identifier to a scope.
    fn add_id(&mut self, id: String, block: bool) {
        let scope = if block {
            self.current_scope_mut()
        } else {
            self.current_fn_scope_mut()
        };
        scope.defined.push(id);
    }

    // Add a pattern to a scope.
    fn add_pat(&mut self, pat: &swc_ecma_ast::Pat, block: bool) {
        match pat {
            // `x`
            swc_ecma_ast::Pat::Ident(d) => self.add_id(d.id.sym.to_string(), block),
            // `...x`
            swc_ecma_ast::Pat::Array(d) => {
                let mut index = 0;
                while index < d.elems.len() {
                    if let Some(d) = &d.elems[index] {
                        self.add_pat(d, block);
                    }
                    index += 1;
                }
            }
            // `...x`
            swc_ecma_ast::Pat::Rest(d) => self.add_pat(&d.arg, block),
            // `{x=y}`
            swc_ecma_ast::Pat::Assign(d) => self.add_pat(&d.left, block),
            swc_ecma_ast::Pat::Object(d) => {
                let mut index = 0;
                while index < d.props.len() {
                    match &d.props[index] {
                        // `{...x}`
                        swc_ecma_ast::ObjectPatProp::Rest(d) => {
                            self.add_pat(&d.arg, block);
                        }
                        // `{key: value}`
                        swc_ecma_ast::ObjectPatProp::KeyValue(d) => {
                            self.add_pat(&d.value, block);
                        }
                        // `{key}` or `{key = value}`
                        swc_ecma_ast::ObjectPatProp::Assign(d) => {
                            self.add_id(d.key.to_string(), block);
                        }
                    }
                    index += 1;
                }
            }
            // Ignore `Invalid` / `Expr`.
            _ => {}
        }
    }
}

impl<'a> VisitMut for State<'a> {
    noop_visit_mut_type!();

    /// Rewrite JSX identifiers.
    fn visit_mut_jsx_element(&mut self, node: &mut swc_ecma_ast::JSXElement) {
        // If there is a top-level, non-global, scope which is a function.
        if let Some(info) = self.current_top_level_info() {
            // Rewrite only if we can rewrite.
            if is_props_receiving_fn(&info.name) || self.provider {
                let position = span_to_position(&node.span, self.location);
                match &node.opening.name {
                    // `<x.y>`, `<Foo.Bar>`, `<x.y.z>`.
                    swc_ecma_ast::JSXElementName::JSXMemberExpr(d) => {
                        let mut ids = vec![];
                        let mut mem = d;
                        loop {
                            ids.push(mem.prop.sym.to_string());
                            match &mem.obj {
                                swc_ecma_ast::JSXObject::Ident(d) => {
                                    ids.push(d.sym.to_string());
                                    break;
                                }
                                swc_ecma_ast::JSXObject::JSXMemberExpr(d) => {
                                    mem = d;
                                }
                            }
                        }
                        ids.reverse();
                        let primary_id = ids.first().unwrap().clone();
                        let in_scope = self.in_scope(&primary_id);

                        if !in_scope {
                            let info_mut = self.current_top_level_info_mut().unwrap();

                            let mut index = 1;
                            while index <= ids.len() {
                                let full_id = ids[0..index].join(".");
                                let component = index == ids.len();
                                if let Some(reference) =
                                    info_mut.references.iter_mut().find(|d| d.0 == full_id)
                                {
                                    if component {
                                        reference.1 = true;
                                    }
                                } else {
                                    info_mut
                                        .references
                                        .push((full_id, component, position.clone()))
                                }
                                index += 1;
                            }

                            if !info_mut.objects.contains(&primary_id) {
                                info_mut.objects.push(primary_id);
                            }
                        }
                    }
                    // `<foo>`, `<Foo>`, `<$>`, `<_bar>`, `<a_b>`.
                    swc_ecma_ast::JSXElementName::Ident(d) => {
                        // If the name is a valid ES identifier, and it doesn’t
                        // start with a lowercase letter, it’s a component.
                        // For example, `$foo`, `_bar`, `Baz` are all component
                        // names.
                        // But `foo` and `b-ar` are tag names.
                        let id = d.sym.to_string();

                        if is_literal_name(&id) {
                            // To do: ignore explicit JSX?

                            let mut invalid = None;

                            let name = if is_identifier_name(&id) {
                                swc_ecma_ast::JSXElementName::JSXMemberExpr(
                                    swc_ecma_ast::JSXMemberExpr {
                                        obj: swc_ecma_ast::JSXObject::Ident(create_ident(
                                            "_components",
                                        )),
                                        prop: create_ident(&id),
                                    },
                                )
                            } else {
                                let name = if let Some(invalid_ref) =
                                    info.aliases.iter().find(|d| d.0 == id)
                                {
                                    invalid_ref.1.clone()
                                } else {
                                    let name = format!("_component{}", info.aliases.len());
                                    invalid = Some((id.clone(), name.clone()));
                                    name
                                };

                                swc_ecma_ast::JSXElementName::Ident(create_ident(&name))
                            };

                            let info_mut = self.current_top_level_info_mut().unwrap();

                            if !info_mut.tags.contains(&id) {
                                info_mut.tags.push(id);
                            }

                            if let Some(invalid) = invalid {
                                info_mut.aliases.push(invalid)
                            }

                            if let Some(closing) = node.closing.as_mut() {
                                closing.name = name.clone();
                            }

                            node.opening.name = name;
                        } else {
                            let mut is_layout = false;

                            // The MDXLayout is wrapped in a
                            if let Some(name) = &info.name {
                                if name == "MDXContent" && id == "MDXLayout" {
                                    is_layout = true;
                                }
                            }

                            if !self.in_scope(&id) {
                                let info_mut = self.current_top_level_info_mut().unwrap();

                                if !is_layout {
                                    if let Some(reference) =
                                        info_mut.references.iter_mut().find(|d| d.0 == id)
                                    {
                                        reference.1 = true;
                                    } else {
                                        info_mut.references.push((id.clone(), true, position))
                                    }
                                }

                                if !info_mut.components.contains(&id) {
                                    info_mut.components.push(id);
                                }
                            }
                        }
                    }
                    // `<xml:thing>`.
                    swc_ecma_ast::JSXElementName::JSXNamespacedName(_) => {
                        // Ignore.
                    }
                }
            }
        }

        node.visit_mut_children_with(self);
    }

    /// Add specifiers of import declarations.
    fn visit_mut_import_decl(&mut self, node: &mut swc_ecma_ast::ImportDecl) {
        let mut index = 0;
        while index < node.specifiers.len() {
            let ident = match &node.specifiers[index] {
                swc_ecma_ast::ImportSpecifier::Default(x) => &x.local.sym,
                swc_ecma_ast::ImportSpecifier::Namespace(x) => &x.local.sym,
                swc_ecma_ast::ImportSpecifier::Named(x) => &x.local.sym,
            };
            self.add_id(ident.to_string(), false);
            index += 1;
        }

        node.visit_mut_children_with(self);
    }

    /// Add patterns of variable declarations.
    fn visit_mut_var_decl(&mut self, node: &mut swc_ecma_ast::VarDecl) {
        let block = node.kind != swc_ecma_ast::VarDeclKind::Var;
        let mut index = 0;
        while index < node.decls.len() {
            self.add_pat(&node.decls[index].name, block);
            index += 1;
        }
        node.visit_mut_children_with(self);
    }

    /// Add identifier of class declaration.
    fn visit_mut_class_decl(&mut self, node: &mut swc_ecma_ast::ClassDecl) {
        self.add_id(node.ident.sym.to_string(), false);
        node.visit_mut_children_with(self);
    }

    /// On function declarations, add name, create scope, add parameters.
    fn visit_mut_fn_decl(&mut self, node: &mut swc_ecma_ast::FnDecl) {
        let id = node.ident.sym.to_string();
        self.add_id(id.clone(), false);
        self.enter(Some(Info {
            name: Some(id),
            ..Default::default()
        }));
        let mut index = 0;
        while index < node.function.params.len() {
            self.add_pat(&node.function.params[index].pat, false);
            index += 1;
        }
        node.visit_mut_children_with(self);
        // Rewrite.
        self.exit_func(Func::Decl(node));
    }

    /// On function expressions, add name, create scope, add parameters.
    fn visit_mut_fn_expr(&mut self, node: &mut swc_ecma_ast::FnExpr) {
        // Note: `periscopic` adds the ID to the newly generated scope, for
        // fn expressions.
        // That seems wrong?
        let name = if let Some(ident) = &node.ident {
            let id = ident.sym.to_string();
            self.add_id(id.clone(), false);
            Some(id)
        } else {
            None
        };

        self.enter(Some(Info {
            name,
            ..Default::default()
        }));
        let mut index = 0;
        while index < node.function.params.len() {
            self.add_pat(&node.function.params[index].pat, false);
            index += 1;
        }
        node.visit_mut_children_with(self);
        self.exit_func(Func::Expr(node));
    }

    /// On arrow functions, create scope, add parameters.
    fn visit_mut_arrow_expr(&mut self, node: &mut swc_ecma_ast::ArrowExpr) {
        self.enter(Some(Info::default()));
        let mut index = 0;
        while index < node.params.len() {
            self.add_pat(&node.params[index], false);
            index += 1;
        }
        node.visit_mut_children_with(self);
        self.exit_func(Func::Arrow(node));
    }

    // Blocks.
    // Not sure why `periscopic` only does `For`/`ForIn`/`ForOf`/`Block`.
    // I added `While`/`DoWhile` here just to be sure.
    // But there are more.
    /// On for statements, create scope.
    fn visit_mut_for_stmt(&mut self, node: &mut swc_ecma_ast::ForStmt) {
        self.enter(None);
        node.visit_mut_children_with(self);
        self.exit();
    }
    /// On for/in statements, create scope.
    fn visit_mut_for_in_stmt(&mut self, node: &mut swc_ecma_ast::ForInStmt) {
        self.enter(None);
        node.visit_mut_children_with(self);
        self.exit();
    }
    /// On for/of statements, create scope.
    fn visit_mut_for_of_stmt(&mut self, node: &mut swc_ecma_ast::ForOfStmt) {
        self.enter(None);
        node.visit_mut_children_with(self);
        self.exit();
    }
    /// On while statements, create scope.
    fn visit_mut_while_stmt(&mut self, node: &mut swc_ecma_ast::WhileStmt) {
        self.enter(None);
        node.visit_mut_children_with(self);
        self.exit();
    }
    /// On do/while statements, create scope.
    fn visit_mut_do_while_stmt(&mut self, node: &mut swc_ecma_ast::DoWhileStmt) {
        self.enter(None);
        node.visit_mut_children_with(self);
        self.exit();
    }
    /// On block statements, create scope.
    fn visit_mut_block_stmt(&mut self, node: &mut swc_ecma_ast::BlockStmt) {
        self.enter(None);
        node.visit_mut_children_with(self);
        self.exit();
    }

    /// On catch clauses, create scope, add param.
    fn visit_mut_catch_clause(&mut self, node: &mut swc_ecma_ast::CatchClause) {
        self.enter(None);
        if let Some(pat) = &node.param {
            self.add_pat(pat, true);
        }
        node.visit_mut_children_with(self);
        self.exit();
    }
}

/// Generate an import provider.
///
/// ```js
/// import { useMDXComponents as _provideComponents } from "x"
/// ```
fn create_import_provider(source: &str) -> swc_ecma_ast::ModuleItem {
    swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::Import(
        swc_ecma_ast::ImportDecl {
            specifiers: vec![swc_ecma_ast::ImportSpecifier::Named(
                swc_ecma_ast::ImportNamedSpecifier {
                    local: create_ident("_provideComponents"),
                    imported: Some(swc_ecma_ast::ModuleExportName::Ident(create_ident(
                        "useMDXComponents",
                    ))),
                    span: swc_common::DUMMY_SP,
                    is_type_only: false,
                },
            )],
            src: Box::new(swc_ecma_ast::Str {
                value: source.into(),
                span: swc_common::DUMMY_SP,
                raw: None,
            }),
            type_only: false,
            asserts: None,
            span: swc_common::DUMMY_SP,
        },
    ))
}

/// Generate an error helper.
///
/// ```js
/// function _missingMdxReference(id, component) {
///   throw new Error("Expected " + (component ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it.");
/// }
/// ```
fn create_error_helper(development: bool, path: Option<String>) -> swc_ecma_ast::ModuleItem {
    let mut parameters = vec![
        swc_ecma_ast::Param {
            pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent {
                id: create_ident("id"),
                type_ann: None,
            }),
            decorators: vec![],
            span: swc_common::DUMMY_SP,
        },
        swc_ecma_ast::Param {
            pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent {
                id: create_ident("component"),
                type_ann: None,
            }),
            decorators: vec![],
            span: swc_common::DUMMY_SP,
        },
    ];

    // Accept a source location (which might be undefiend).
    if development {
        parameters.push(swc_ecma_ast::Param {
            pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent {
                id: create_ident("place"),
                type_ann: None,
            }),
            decorators: vec![],
            span: swc_common::DUMMY_SP,
        })
    }

    let mut message = vec![
        swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str {
            value: "Expected ".into(),
            span: swc_common::DUMMY_SP,
            raw: None,
        })),
        // `component ? "component" : "object"`
        swc_ecma_ast::Expr::Paren(swc_ecma_ast::ParenExpr {
            expr: Box::new(swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr {
                test: Box::new(create_ident_expression("component")),
                cons: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(
                    swc_ecma_ast::Str {
                        value: "component".into(),
                        span: swc_common::DUMMY_SP,
                        raw: None,
                    },
                ))),
                alt: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(
                    swc_ecma_ast::Str {
                        value: "object".into(),
                        span: swc_common::DUMMY_SP,
                        raw: None,
                    },
                ))),
                span: swc_common::DUMMY_SP,
            })),
            span: swc_common::DUMMY_SP,
        }),
        swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str {
            value: " `".into(),
            span: swc_common::DUMMY_SP,
            raw: None,
        })),
        create_ident_expression("id"),
        swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str {
            value: "` to be defined: you likely forgot to import, pass, or provide it.".into(),
            span: swc_common::DUMMY_SP,
            raw: None,
        })),
    ];

    // `place ? "\nIt’s referenced in your code at `" + place+ "`" : ""`
    if development {
        message.push(swc_ecma_ast::Expr::Paren(swc_ecma_ast::ParenExpr {
            expr: Box::new(swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr {
                test: Box::new(create_ident_expression("place")),
                cons: Box::new(create_binary_expression(
                    vec![
                        swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str {
                            value: "\nIt’s referenced in your code at `".into(),
                            span: swc_common::DUMMY_SP,
                            raw: None,
                        })),
                        create_ident_expression("place"),
                        swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str {
                            value: if let Some(path) = path {
                                format!("` in `{}`", path).into()
                            } else {
                                "`".into()
                            },
                            span: swc_common::DUMMY_SP,
                            raw: None,
                        })),
                    ],
                    swc_ecma_ast::BinaryOp::Add,
                )),
                alt: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(
                    swc_ecma_ast::Str {
                        value: "".into(),
                        span: swc_common::DUMMY_SP,
                        raw: None,
                    },
                ))),
                span: swc_common::DUMMY_SP,
            })),
            span: swc_common::DUMMY_SP,
        }))
    }

    swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Fn(
        swc_ecma_ast::FnDecl {
            ident: create_ident("_missingMdxReference"),
            declare: false,
            function: Box::new(swc_ecma_ast::Function {
                params: parameters,
                decorators: vec![],
                body: Some(swc_ecma_ast::BlockStmt {
                    stmts: vec![swc_ecma_ast::Stmt::Throw(swc_ecma_ast::ThrowStmt {
                        arg: Box::new(swc_ecma_ast::Expr::New(swc_ecma_ast::NewExpr {
                            callee: Box::new(create_ident_expression("Error")),
                            args: Some(vec![swc_ecma_ast::ExprOrSpread {
                                spread: None,
                                expr: Box::new(create_binary_expression(
                                    message,
                                    swc_ecma_ast::BinaryOp::Add,
                                )),
                            }]),
                            span: swc_common::DUMMY_SP,
                            type_args: None,
                        })),
                        span: swc_common::DUMMY_SP,
                    })],
                    span: swc_common::DUMMY_SP,
                }),
                is_generator: false,
                is_async: false,
                type_params: None,
                return_type: None,
                span: swc_common::DUMMY_SP,
            }),
        },
    )))
}

/// Check if this function is a props receiving component: it’s one of ours.
fn is_props_receiving_fn(name: &Option<String>) -> bool {
    if let Some(name) = name {
        name == "_createMdxContent" || name == "MDXContent"
    } else {
        false
    }
}

/// Check if a name is a literal tag name or an identifier to a component.
fn is_literal_name(name: &str) -> bool {
    matches!(name.as_bytes().first(), Some(b'a'..=b'z')) || !is_identifier_name(name)
}

// Check if a name is a valid identifier name.
fn is_identifier_name(name: &str) -> bool {
    for (index, char) in name.chars().enumerate() {
        if if index == 0 {
            !id_start(char)
        } else {
            !id_cont(char, false)
        } {
            return false;
        }
    }

    true
}