full_moon 2.2.0

A lossless Lua parser
Documentation
use codespan_reporting::{
    diagnostic::{Diagnostic, Label},
    files::SimpleFiles,
};
use full_moon::{
    ast::{AstResult, LuaVersion},
    tokenizer::{self, LexerResult},
};
use insta::{assert_snapshot, assert_yaml_snapshot};
use std::{fs, path::Path};

mod common;
use common::run_test_folder;

fn process_fail_case(path: &Path, source: &str, lua_version: LuaVersion) {
    let result = full_moon::parse_fallible(source, lua_version);

    if result.errors().is_empty() {
        panic!("fail case passed for {path:?}");
    }

    assert_yaml_snapshot!("errors", result.errors());
    assert_yaml_snapshot!("ast", result.ast());

    process_codespan_display(source, &result);

    let ast = result.into_ast().update_positions();
    assert_yaml_snapshot!("ast_to_string", ast.to_string());
}

fn process_codespan_display(source: &str, result: &AstResult) {
    let mut files = SimpleFiles::new();

    let file_id = files.add("source.lua", source);

    let config = codespan_reporting::term::Config::default();
    let mut output = termcolor::NoColor::new(Vec::new());

    for error in result.errors() {
        let range = error.range();

        let diagnostic = Diagnostic::error()
            .with_message(error.error_message())
            .with_code(match error {
                full_moon::Error::AstError(_) => "ast",
                full_moon::Error::TokenizerError(_) => "tokenizer",
            })
            .with_labels(vec![Label::primary(
                file_id,
                (range.0.bytes())..(range.1.bytes()),
            )]);

        codespan_reporting::term::emit(&mut output, &config, &files, &diagnostic).unwrap();
    }

    assert_snapshot!(
        "error_display",
        String::from_utf8(output.into_inner()).unwrap()
    );
}

fn run_parser_fail_cases(folder: &str, lua_version: LuaVersion) {
    run_test_folder(folder, |path| {
        let source = fs::read_to_string(path.join("source.lua")).expect("couldn't read source.lua");
        let tokens = tokenizer::Lexer::new(&source, lua_version)
            .collect()
            .unwrap();
        assert_yaml_snapshot!("tokens", tokens);

        process_fail_case(path, &source, lua_version);
    });
}

#[test]
#[cfg(not(feature = "luau"))] // exclude extra nodes added to yaml
#[cfg_attr(feature = "no-source-tests", ignore)]
fn test_parser_fail_cases() {
    run_parser_fail_cases("./tests/cases/fail/parser", LuaVersion::lua51());
}

#[test]
#[cfg(not(feature = "luau"))] // exclude extra nodes added to yaml
#[cfg_attr(feature = "no-source-tests", ignore)]
fn test_tokenizer_fail_cases() {
    run_test_folder("./tests/cases/fail/tokenizer", |path| {
        let source = fs::read_to_string(path.join("source.lua")).expect("couldn't read source.lua");

        let tokens = tokenizer::Lexer::new(&source, LuaVersion::lua51()).collect();
        assert!(!matches!(tokens, LexerResult::Ok(_)));
        assert_yaml_snapshot!("tokens_result", tokens);

        process_fail_case(path, &source, LuaVersion::lua51());
    });
}

#[test]
#[cfg(feature = "luau")]
#[cfg_attr(feature = "no-source-tests", ignore)]
fn test_roblox_parser_fail_cases() {
    run_parser_fail_cases("./tests/roblox_cases/fail/parser", LuaVersion::luau());
}

#[test]
#[cfg(feature = "luau")]
#[cfg_attr(feature = "no-source-tests", ignore)]
fn test_roblox_tokenizer_fail_cases() {
    run_test_folder("./tests/roblox_cases/fail/tokenizer", |path| {
        let source = fs::read_to_string(path.join("source.lua")).expect("couldn't read source.lua");

        let tokens = tokenizer::Lexer::new(&source, LuaVersion::luau()).collect();
        assert!(!matches!(tokens, LexerResult::Ok(_)));
        assert_yaml_snapshot!("tokens_result", tokens);

        process_fail_case(path, &source, LuaVersion::luau());
    })
}

#[test]
#[cfg(feature = "lua52")]
#[cfg_attr(feature = "no-source-tests", ignore)]
fn test_lua52_parser_fail_cases() {
    run_parser_fail_cases("./tests/lua52_cases/fail/parser", LuaVersion::lua52());
}

#[test]
#[cfg(feature = "lua53")]
#[cfg_attr(feature = "no-source-tests", ignore)]
fn test_lua53_parser_fail_cases() {
    run_parser_fail_cases("./tests/lua53_cases/fail/parser", LuaVersion::lua53());
}

#[test]
#[cfg(feature = "lua54")]
#[cfg_attr(feature = "no-source-tests", ignore)]
fn test_lua54_parser_fail_cases() {
    run_parser_fail_cases("./tests/lua54_cases/fail/parser", LuaVersion::lua54());
}

#[test]
#[cfg(feature = "cfxlua")]
#[cfg_attr(feature = "no-source-tests", ignore)]
fn test_cfxlua_parser_fail_cases() {
    run_parser_fail_cases("./tests/cfxlua_cases/fail/parser", LuaVersion::cfxlua());
}

#[test]
#[cfg(feature = "cfxlua")]
#[cfg_attr(feature = "no-source-tests", ignore)]
fn test_cfxlua_tokenizer_fail_cases() {
    run_test_folder("./tests/cfxlua_cases/fail/tokenizer", |path| {
        let source = fs::read_to_string(path.join("source.lua")).expect("couldn't read source.lua");

        let tokens = tokenizer::Lexer::new(&source, LuaVersion::cfxlua()).collect();
        assert!(!matches!(tokens, LexerResult::Ok(_)));
        assert_yaml_snapshot!("tokens_result", tokens);

        process_fail_case(path, &source, LuaVersion::cfxlua());
    })
}