ecma-syntax-cat 0.1.0

ECMAScript abstract syntax tree as comp-cat-rs-idiomatic Rust types. ESTree-shaped, ES2024-complete, no panics, no Rc, no interior mutability. Foundation crate for boa-cat and related downstream tooling.
Documentation
//! ECMAScript module-level items: imports and exports.

use crate::expression::Expression;
use crate::identifier::Identifier;
use crate::span::Spanned;
use crate::statement::Statement;

/// One top-level item inside a module (either a statement or a module
/// declaration).
pub type ModuleItem = Spanned<ModuleItemKind>;

/// The shape of a module-level item.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModuleItemKind {
    /// A regular statement (variable declaration, function, expression
    /// statement, etc.).
    Statement(Statement),
    /// `import ... from "..."`
    Import(ImportDeclaration),
    /// `export { ... }`, `export let x = 1;`, etc.
    Export(ExportDeclaration),
}

impl std::fmt::Display for ModuleItemKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Statement(stmt) => write!(f, "{stmt}"),
            Self::Import(decl) => write!(f, "{decl}"),
            Self::Export(decl) => write!(f, "{decl}"),
        }
    }
}

/// An `import` declaration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportDeclaration {
    specifiers: Vec<ImportSpecifier>,
    source: String,
}

impl ImportDeclaration {
    /// Build an import declaration.  `source` is the module specifier
    /// string (e.g. `"./foo.js"`).
    #[must_use]
    pub fn new(specifiers: Vec<ImportSpecifier>, source: impl Into<String>) -> Self {
        Self {
            specifiers,
            source: source.into(),
        }
    }

    /// The import specifiers (named, default, namespace).
    #[must_use]
    pub fn specifiers(&self) -> &[ImportSpecifier] {
        &self.specifiers
    }

    /// The module specifier string.
    #[must_use]
    pub fn source(&self) -> &str {
        &self.source
    }
}

impl std::fmt::Display for ImportDeclaration {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let specs = self
            .specifiers
            .iter()
            .map(|s| format!("{s}"))
            .collect::<Vec<_>>()
            .join(", ");
        write!(f, "import {specs} from {:?};", self.source)
    }
}

/// One specifier inside an `import` declaration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportSpecifier {
    /// `import { name as local } from ...`.  When `imported == local` this
    /// is the unaliased form `import { name } from ...`.
    Named {
        /// The name exported by the source module.
        imported: Identifier,
        /// The local binding name in this module.
        local: Identifier,
    },
    /// `import defaultName from ...`.
    Default {
        /// The local binding name for the default export.
        local: Identifier,
    },
    /// `import * as ns from ...`.
    Namespace {
        /// The local binding name for the namespace object.
        local: Identifier,
    },
}

impl std::fmt::Display for ImportSpecifier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Named { imported, local } => {
                if imported == local {
                    write!(f, "{{ {imported} }}")
                } else {
                    write!(f, "{{ {imported} as {local} }}")
                }
            }
            Self::Default { local } => write!(f, "{local}"),
            Self::Namespace { local } => write!(f, "* as {local}"),
        }
    }
}

/// An `export` declaration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExportDeclaration {
    /// `export { a, b as c } from? "..."?`
    Named {
        /// Named specifiers.
        specifiers: Vec<ExportSpecifier>,
        /// Optional re-export source.
        source: Option<String>,
    },
    /// `export default expression` or `export default declaration`.
    Default {
        /// The exported expression or declaration.
        declaration: ExportDefault,
    },
    /// `export let x = ...` or `export function ...`.
    Declaration {
        /// The accompanying statement (typically a function/class/variable
        /// declaration).
        declaration: Statement,
    },
    /// `export * from "..."` or `export * as ns from "..."`.
    All {
        /// Optional namespace name for `export * as ns`.
        exported: Option<Identifier>,
        /// The source module specifier.
        source: String,
    },
}

impl std::fmt::Display for ExportDeclaration {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Named { specifiers, source } => {
                write_export_named(f, specifiers, source.as_deref())
            }
            Self::Default { declaration } => write!(f, "export default {declaration};"),
            Self::Declaration { declaration } => write!(f, "export {declaration}"),
            Self::All { exported, source } => write_export_all(f, exported.as_ref(), source),
        }
    }
}

fn write_export_named(
    f: &mut std::fmt::Formatter<'_>,
    specifiers: &[ExportSpecifier],
    source: Option<&str>,
) -> std::fmt::Result {
    let specs = specifiers
        .iter()
        .map(|s| format!("{s}"))
        .collect::<Vec<_>>()
        .join(", ");
    match source {
        Some(src) => write!(f, "export {{ {specs} }} from {src:?};"),
        None => write!(f, "export {{ {specs} }};"),
    }
}

fn write_export_all(
    f: &mut std::fmt::Formatter<'_>,
    exported: Option<&Identifier>,
    source: &str,
) -> std::fmt::Result {
    match exported {
        Some(name) => write!(f, "export * as {name} from {source:?};"),
        None => write!(f, "export * from {source:?};"),
    }
}

/// One specifier inside a named-export declaration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportSpecifier {
    local: Identifier,
    exported: Identifier,
}

impl ExportSpecifier {
    /// Build an export specifier.
    #[must_use]
    pub fn new(local: Identifier, exported: Identifier) -> Self {
        Self { local, exported }
    }

    /// The local binding being exported.
    #[must_use]
    pub fn local(&self) -> &Identifier {
        &self.local
    }

    /// The name under which it is exported.  When `local == exported` the
    /// `as` clause was omitted.
    #[must_use]
    pub fn exported(&self) -> &Identifier {
        &self.exported
    }
}

impl std::fmt::Display for ExportSpecifier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.local == self.exported {
            write!(f, "{}", self.local)
        } else {
            write!(f, "{} as {}", self.local, self.exported)
        }
    }
}

/// What may appear after `export default`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExportDefault {
    /// `export default <expression>;`
    Expression(Expression),
    /// `export default function ...` (may be anonymous).
    Function(crate::function::Function),
    /// `export default class ...` (may be anonymous).
    Class(crate::class::Class),
}

impl std::fmt::Display for ExportDefault {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Expression(expr) => write!(f, "{expr}"),
            Self::Function(func) => write!(f, "{func}"),
            Self::Class(class) => write!(f, "{class}"),
        }
    }
}