luau-syntax 0.732.0

Luau lexer, parser, AST, CST, and source utilities
Documentation
use super::super::common::*;
use luau_common::flags;

fn export_options() -> ParseOptions {
    ParseOptions::default()
}

// Parser.test.cpp: parse_attribute_on_export_function_stat
#[test]
fn parse_attribute_on_export_function_stat() {
    let _export = flags::LuauExportValueSyntax.scoped(true);
    let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
    with_parse(
        r#"
@checked
export function hello(x, y)
    return x + y
end
"#,
        ParseOptions::default(),
        |result| {
            let result = result.unwrap();

            let [statement] = statement_kinds(result.root.as_slice()).exact();
            let local_function = statement
                .as_local_function()
                .expect("expected exported local function");

            assert_eq!(local_function.location().begin, pos!(1, 0));
            assert!(local_function.name.is_exported);
            assert!(local_function.name.is_const);
            assert_eq!(local_function.function.attributes.len(), 1);
            assert_eq!(
                local_function.function.attributes[0].kind(),
                AttributeKind::Checked
            );
            assert_eq!(
                local_function.function.attributes[0].location,
                loc!(pos!(1, 0), pos!(1, 8))
            );
        },
    );
}

// Parser.test.cpp: export_value_rfc
#[test]
fn export_value_rfc() {
    let _export = flags::LuauExportValueSyntax.scoped(true);
    let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
    with_parse(
        r#"
export local version = "1.0.0"
export const TAU = math.pi * 2
export local settings: Settings = getSettings()
export local a, b, c = 1, 2, 3
export local d

export function add(a: number, b: number): number
    return a + b
end

export local f, g
function f()
    return g()
end

function g()
    return 42
end

local function ret(): (string, number, boolean)
    return "heh", 42, false
end
export local x, y, z = ret()
"#,
        ParseOptions::default(),
        |result| {
            let result = result.unwrap();
            let statements = statement_kinds(result.root.as_slice());
            assert_eq!(statements.len(), 11);

            let local = statements[0].as_local().expect("expected exported local");
            assert!(local.is_exported);
            assert!(!local.is_const);
            assert_eq!(local.bindings.len(), 1);
            assert!(local.bindings[0].is_exported);
            assert!(!local.bindings[0].is_const);

            let local = statements[1].as_local().expect("expected exported const");
            assert!(local.is_exported);
            assert!(local.is_const);
            assert_eq!(local.bindings.len(), 1);
            assert!(local.bindings[0].is_exported);
            assert!(local.bindings[0].is_const);

            let local = statements[2]
                .as_local()
                .expect("expected exported annotated local");
            assert!(local.is_exported);
            assert!(!local.is_const);
            assert_eq!(local.bindings.len(), 1);
            assert!(local.bindings[0].annotation.is_some());

            let local = statements[3]
                .as_local()
                .expect("expected exported local group");
            assert!(local.is_exported);
            assert!(!local.is_const);
            assert_eq!(local.bindings.len(), 3);
            assert!(
                local
                    .bindings
                    .iter()
                    .all(|binding| binding.is_exported && !binding.is_const)
            );

            let local = statements[4]
                .as_local()
                .expect("expected exported local declaration");
            assert!(local.is_exported);
            assert!(!local.is_const);
            assert_eq!(local.bindings.len(), 1);
            assert!(local.values.is_empty());
            assert!(local.bindings[0].is_exported);

            let local_function = statements[5]
                .as_local_function()
                .expect("expected exported function");
            assert!(local_function.name.is_exported);
            assert!(local_function.name.is_const);

            let local = statements[6]
                .as_local()
                .expect("expected exported forward declarations");
            assert!(local.is_exported);
            assert!(!local.is_const);
            assert!(local.values.is_empty());
            assert_eq!(local.bindings.len(), 2);
            assert!(
                local
                    .bindings
                    .iter()
                    .all(|binding| binding.is_exported && !binding.is_const)
            );

            assert_eq!(statements[7].tag, StatementTag::FunctionDeclaration);
            assert_eq!(statements[8].tag, StatementTag::FunctionDeclaration);

            let local = statements[10]
                .as_local()
                .expect("expected exported multret local group");
            assert!(local.is_exported);
            assert!(!local.is_const);
            assert_eq!(local.bindings.len(), 3);
            assert!(
                local
                    .bindings
                    .iter()
                    .all(|binding| binding.is_exported && !binding.is_const)
            );
        },
    );

    with_parse(
        r#"
export type Config = {
    debug: boolean,
    timeout: number,
}

return {
    debug = false,
    timeout = 5,
}
"#,
        export_options(),
        |result| {
            result.unwrap();
        },
    );
}

// Parser.test.cpp: export_value_parse_failures
#[test]
fn export_value_parse_failures() {
    let _export = flags::LuauExportValueSyntax.scoped(true);
    let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
    for source in [
        r#"
export foo = 5
"#,
        r#"
export foo
"#,
        r#"
function foo()
end
export foo
"#,
        r#"
export local function foo()
end
"#,
    ] {
        assert!(!parse_errors_with_options(source, export_options()).is_empty());
    }

    let duplicate = parse_errors_with_options(
        r#"
export local foo = 1
export local foo = 2
"#,
        export_options(),
    );
    let duplicate_message = String::from_utf8_lossy(duplicate.first_error().message.as_bytes());
    assert!(duplicate_message.contains("foo"));

    for source in [
        r#"
export local answer = 42
return {answer = answer}
"#,
        r#"
if skip then
    return
end

export local answer = 42
"#,
        r#"
export class Player
    public health: number

    function setHealth(self, health: number)
        self.health = health
        return self
    end

    function getHealth(self): number
        return self.health
    end
end

return Player {health = 100}
"#,
    ] {
        parse_errors_with_options(source, export_options()).assert_first_message(
            "Exporting values is not compatible with top-level return (export/return conflict)",
        );
    }

    for source in [
        r#"
if true then
    export local insideIf = 1
end
"#,
        r#"
do
    export const insideDo = 1
end
"#,
        r#"
while true do
    export local insideWhile = 1
end
"#,
        r#"
repeat
    export local insideRepeat = 1
until true
"#,
        r#"
for i = 1, 1 do
    export local insideFor = i
end
"#,
        r#"
local function test()
    export local insideFunction = 1
end
"#,
    ] {
        parse_errors_with_options(source, export_options())
            .assert_first_message("'export' may only be applied to top-level statements");
    }
}

// Parser.test.cpp: export_value_parse_edge_cases
#[test]
fn export_value_parse_edge_cases() {
    let _export = flags::LuauExportValueSyntax.scoped(true);
    let _classes = flags::DebugLuauUserDefinedClasses.scoped(true);
    with_parse(
        r#"
export = 5
export += 1
export()
"#,
        export_options(),
        |result| {
            let result = result.unwrap();
            let statements = statement_kinds(result.root.as_slice());
            assert_eq!(statements.len(), 3);
            assert_eq!(statements[0].tag, StatementTag::Assign);
            assert_eq!(statements[1].tag, StatementTag::CompoundAssign);
            assert_eq!(statements[2].tag, StatementTag::Expression);
        },
    );

    with_parse("export local x = 5", export_options(), |result| {
        result.unwrap();
    });
    with_parse("export const x = 5", export_options(), |result| {
        result.unwrap();
    });
    with_parse(
        r#"
export function foo()
end
"#,
        export_options(),
        |result| {
            result.unwrap();
        },
    );

    parse_errors_with_options("export 42", export_options())
        .assert_first_message("Incomplete statement: expected assignment or a function call");
    parse_errors_with_options("export if true then end", export_options())
        .assert_first_message("Incomplete statement: expected assignment or a function call");
    parse_errors_with_options("export", export_options())
        .assert_first_message("Incomplete statement: expected assignment or a function call");
}