ecma-parse-cat 0.1.0

ECMAScript parser consuming ecma-lex-cat tokens and producing ecma-syntax-cat Program ASTs. Comprehensive ES2024 surface (classes, modules, destructuring, async/await, optional chaining, templates). Recursive-descent with precedence climbing.
//! Integration tests covering statement parsing.

use ecma_lex_cat::lex;
use ecma_parse_cat::{Error, parse_script};
use ecma_syntax_cat::declaration::VariableKind;
use ecma_syntax_cat::program::ProgramKind;
use ecma_syntax_cat::statement::StatementKind;

fn first_statement_kind(source: &str) -> Result<StatementKind, Error> {
    let tokens = lex(source)?;
    let program = parse_script(&tokens)?;
    match program.value() {
        ProgramKind::Script { body } => {
            body.first()
                .map(|s| s.value().clone())
                .ok_or(Error::UnexpectedEof {
                    expected: "first statement",
                })
        }
        ProgramKind::Module { .. } => Err(Error::UnexpectedEof {
            expected: "script-mode program",
        }),
    }
}

#[test]
fn parses_empty_statement() -> Result<(), Error> {
    let kind = first_statement_kind(";")?;
    matches!(kind, StatementKind::Empty)
        .then_some(())
        .ok_or(Error::UnexpectedEof {
            expected: "empty statement",
        })
}

#[test]
fn parses_let_declaration() -> Result<(), Error> {
    let kind = first_statement_kind("let x = 1;")?;
    match kind {
        StatementKind::VariableDeclaration(decl) if decl.kind() == VariableKind::Let => Ok(()),
        _other => Err(Error::UnexpectedEof { expected: "let" }),
    }
}

#[test]
fn parses_const_destructuring() -> Result<(), Error> {
    let kind = first_statement_kind("const { a, b } = obj;")?;
    match kind {
        StatementKind::VariableDeclaration(decl)
            if decl.kind() == VariableKind::Const && decl.declarators().len() == 1 =>
        {
            Ok(())
        }
        _other => Err(Error::UnexpectedEof {
            expected: "const destructure",
        }),
    }
}

#[test]
fn parses_if_else() -> Result<(), Error> {
    let kind = first_statement_kind("if (x) { y; } else { z; }")?;
    match kind {
        StatementKind::If {
            alternate: Some(_), ..
        } => Ok(()),
        _other => Err(Error::UnexpectedEof {
            expected: "if-else",
        }),
    }
}

#[test]
fn parses_for_classic() -> Result<(), Error> {
    let kind = first_statement_kind("for (let i = 0; i < 10; i++) {}")?;
    match kind {
        StatementKind::For {
            init: Some(_),
            test: Some(_),
            update: Some(_),
            ..
        } => Ok(()),
        _other => Err(Error::UnexpectedEof {
            expected: "classic for loop",
        }),
    }
}

#[test]
fn parses_for_of() -> Result<(), Error> {
    let kind = first_statement_kind("for (const x of arr) {}")?;
    match kind {
        StatementKind::ForOf {
            is_await: false, ..
        } => Ok(()),
        _other => Err(Error::UnexpectedEof { expected: "for-of" }),
    }
}

#[test]
fn parses_for_in() -> Result<(), Error> {
    let kind = first_statement_kind("for (const k in obj) {}")?;
    match kind {
        StatementKind::ForIn { .. } => Ok(()),
        _other => Err(Error::UnexpectedEof { expected: "for-in" }),
    }
}

#[test]
fn parses_while() -> Result<(), Error> {
    let kind = first_statement_kind("while (x) { x = next(); }")?;
    matches!(kind, StatementKind::While { .. })
        .then_some(())
        .ok_or(Error::UnexpectedEof { expected: "while" })
}

#[test]
fn parses_try_catch_finally() -> Result<(), Error> {
    let kind = first_statement_kind("try { a; } catch (e) { b; } finally { c; }")?;
    match kind {
        StatementKind::Try {
            handler: Some(_),
            finalizer: Some(_),
            ..
        } => Ok(()),
        _other => Err(Error::UnexpectedEof {
            expected: "try with catch and finally",
        }),
    }
}

#[test]
fn parses_throw() -> Result<(), Error> {
    let kind = first_statement_kind("throw new Error();")?;
    matches!(kind, StatementKind::Throw { .. })
        .then_some(())
        .ok_or(Error::UnexpectedEof { expected: "throw" })
}

#[test]
fn parses_return_with_argument() -> Result<(), Error> {
    let tokens = lex("function f() { return 1; }")?;
    let program = parse_script(&tokens)?;
    match program.value() {
        ProgramKind::Script { body } => {
            match body.first().map(ecma_syntax_cat::span::Spanned::value) {
                Some(StatementKind::FunctionDeclaration(func)) => match func.body().first() {
                    Some(stmt) => match stmt.value() {
                        StatementKind::Return { argument: Some(_) } => Ok(()),
                        _other => Err(Error::UnexpectedEof {
                            expected: "return with argument",
                        }),
                    },
                    None => Err(Error::UnexpectedEof {
                        expected: "function body return statement",
                    }),
                },
                _other => Err(Error::UnexpectedEof {
                    expected: "function declaration",
                }),
            }
        }
        ProgramKind::Module { .. } => Err(Error::UnexpectedEof { expected: "script" }),
    }
}

#[test]
fn parses_switch() -> Result<(), Error> {
    let kind = first_statement_kind("switch (x) { case 1: a; break; default: b; }")?;
    match kind {
        StatementKind::Switch { cases, .. } if cases.len() == 2 => Ok(()),
        _other => Err(Error::UnexpectedEof {
            expected: "switch with two cases",
        }),
    }
}

#[test]
fn parses_class_declaration() -> Result<(), Error> {
    let kind =
        first_statement_kind("class Foo extends Bar { constructor() {} method() { return 1; } }")?;
    match kind {
        StatementKind::ClassDeclaration(class) if class.body().len() == 2 => Ok(()),
        _other => Err(Error::UnexpectedEof {
            expected: "class with two members",
        }),
    }
}

#[test]
fn parses_labeled_break() -> Result<(), Error> {
    let kind = first_statement_kind("outer: while (true) { break outer; }")?;
    matches!(kind, StatementKind::Labeled { .. })
        .then_some(())
        .ok_or(Error::UnexpectedEof {
            expected: "labeled statement",
        })
}