// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Parser.h" #include "AstQueryDsl.h" #include "Fixture.h" #include "ScopedFlags.h" #include "doctest.h" #include using namespace Luau; namespace { struct Counter { static int instanceCount; int id; Counter() { ++instanceCount; id = instanceCount; } }; int Counter::instanceCount = 0; // TODO: delete this and replace all other use of this function with matchParseError std::string getParseError(const std::string& code) { Fixture f; try { f.parse(code); } catch (const Luau::ParseErrors& e) { // in general, tests check only the first error return e.getErrors().front().getMessage(); } throw std::runtime_error("Expected a parse error in '" + code + "'"); } } // namespace TEST_SUITE_BEGIN("AllocatorTests"); TEST_CASE("allocator_can_be_moved") { Counter* c = nullptr; auto inner = [&]() { Luau::Allocator allocator; c = allocator.alloc(); Luau::Allocator moved{std::move(allocator)}; return moved; }; Counter::instanceCount = 0; Luau::Allocator a{inner()}; CHECK_EQ(1, c->id); } TEST_CASE("moved_out_Allocator_can_still_be_used") { Luau::Allocator outer; Luau::Allocator inner{std::move(outer)}; int* i = outer.alloc(); REQUIRE(i != nullptr); *i = 55; REQUIRE_EQ(*i, 55); } TEST_CASE("aligns_things") { Luau::Allocator alloc; char* one = alloc.alloc(); double* two = alloc.alloc(); (void)one; CHECK_EQ(0, reinterpret_cast(two) & (alignof(double) - 1)); } TEST_CASE("initial_double_is_aligned") { Luau::Allocator alloc; double* one = alloc.alloc(); CHECK_EQ(0, reinterpret_cast(one) & (alignof(double) - 1)); } TEST_SUITE_END(); TEST_SUITE_BEGIN("ParserTests"); TEST_CASE_FIXTURE(Fixture, "basic_parse") { AstStat* stat = parse("print(\"Hello World!\")"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "can_haz_annotations") { AstStatBlock* block = parse("local foo: string = \"Hello Types!\""); REQUIRE(block != nullptr); } TEST_CASE_FIXTURE(Fixture, "local_cannot_have_annotation_with_extensions_disabled") { Luau::ParseOptions options; options.allowTypeAnnotations = false; CHECK_THROWS_AS(parse("local foo: string = \"Hello Types!\"", options), std::exception); } TEST_CASE_FIXTURE(Fixture, "local_with_annotation") { AstStatBlock* block = parse(R"( local foo: string = "Hello Types!" )"); REQUIRE(block != nullptr); REQUIRE(block->body.size > 0); AstStatLocal* local = block->body.data[0]->as(); REQUIRE(local != nullptr); REQUIRE_EQ(1, local->vars.size); AstLocal* l = local->vars.data[0]; REQUIRE(l->annotation != nullptr); REQUIRE_EQ(1, local->values.size); } TEST_CASE_FIXTURE(Fixture, "type_names_can_contain_dots") { AstStatBlock* block = parse(R"( local foo: SomeModule.CoolType )"); REQUIRE(block != nullptr); } TEST_CASE_FIXTURE(Fixture, "functions_cannot_have_return_annotations_if_extensions_are_disabled") { Luau::ParseOptions options; options.allowTypeAnnotations = false; CHECK_THROWS_AS(parse("function foo(): number return 55 end", options), std::exception); } TEST_CASE_FIXTURE(Fixture, "functions_can_have_return_annotations") { AstStatBlock* block = parse(R"( function foo(): number return 55 end )"); REQUIRE(block != nullptr); REQUIRE(block->body.size > 0); AstStatFunction* statFunction = block->body.data[0]->as(); REQUIRE(statFunction != nullptr); REQUIRE(statFunction->func->returnAnnotation.has_value()); CHECK_EQ(statFunction->func->returnAnnotation->types.size, 1); CHECK(statFunction->func->returnAnnotation->tailType == nullptr); } TEST_CASE_FIXTURE(Fixture, "functions_can_have_a_function_type_annotation") { AstStatBlock* block = parse(R"( function f(): (number) -> nil return nil end )"); REQUIRE(block != nullptr); REQUIRE(block->body.size > 0); AstStatFunction* statFunc = block->body.data[0]->as(); REQUIRE(statFunc != nullptr); REQUIRE(statFunc->func->returnAnnotation.has_value()); CHECK(statFunc->func->returnAnnotation->tailType == nullptr); AstArray& retTypes = statFunc->func->returnAnnotation->types; REQUIRE(retTypes.size == 1); AstTypeFunction* funTy = retTypes.data[0]->as(); REQUIRE(funTy != nullptr); } TEST_CASE_FIXTURE(Fixture, "function_return_type_should_disambiguate_from_function_type_and_multiple_returns") { AstStatBlock* block = parse(R"( function f(): (number, string) return 1, "foo" end )"); REQUIRE(block != nullptr); REQUIRE(block->body.size > 0); AstStatFunction* statFunc = block->body.data[0]->as(); REQUIRE(statFunc != nullptr); REQUIRE(statFunc->func->returnAnnotation.has_value()); CHECK(statFunc->func->returnAnnotation->tailType == nullptr); AstArray& retTypes = statFunc->func->returnAnnotation->types; REQUIRE(retTypes.size == 2); AstTypeReference* ty0 = retTypes.data[0]->as(); REQUIRE(ty0 != nullptr); REQUIRE(ty0->name == "number"); AstTypeReference* ty1 = retTypes.data[1]->as(); REQUIRE(ty1 != nullptr); REQUIRE(ty1->name == "string"); } TEST_CASE_FIXTURE(Fixture, "function_return_type_should_parse_as_function_type_annotation_with_no_args") { AstStatBlock* block = parse(R"( function f(): () -> nil return nil end )"); REQUIRE(block != nullptr); REQUIRE(block->body.size > 0); AstStatFunction* statFunc = block->body.data[0]->as(); REQUIRE(statFunc != nullptr); REQUIRE(statFunc->func->returnAnnotation.has_value()); CHECK(statFunc->func->returnAnnotation->tailType == nullptr); AstArray& retTypes = statFunc->func->returnAnnotation->types; REQUIRE(retTypes.size == 1); AstTypeFunction* funTy = retTypes.data[0]->as(); REQUIRE(funTy != nullptr); REQUIRE(funTy->argTypes.types.size == 0); CHECK(funTy->argTypes.tailType == nullptr); CHECK(funTy->returnTypes.tailType == nullptr); AstTypeReference* ty = funTy->returnTypes.types.data[0]->as(); REQUIRE(ty != nullptr); REQUIRE(ty->name == "nil"); } TEST_CASE_FIXTURE(Fixture, "annotations_can_be_tables") { AstStatBlock* stat = parse(R"( local zero: number local one: {x: number, y: string} )"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "tables_should_have_an_indexer_and_keys") { AstStatBlock* stat = parse(R"( local t: { [string]: number, f: () -> nil } )"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "tables_can_have_trailing_separator") { AstStatBlock* stat = parse(R"( local zero: number local one: {x: number, y: string, } )"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "tables_can_use_semicolons") { AstStatBlock* stat = parse(R"( local zero: number local one: {x: number; y: string; } )"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "other_places_where_type_annotations_are_allowed") { AstStatBlock* stat = parse(R"( for i: number = 0, 50 do end for i: number, s: string in expr() do end )"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "nil_is_a_valid_type_name") { AstStatBlock* stat = parse(R"( local n: nil )"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "function_type_annotation") { AstStatBlock* stat = parse(R"( local f: (number, string) -> nil )"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "functions_can_return_multiple_values") { AstStatBlock* stat = parse(R"( local f: (number) -> (number, number) )"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "functions_can_have_0_arguments") { AstStatBlock* stat = parse(R"( local f: () -> number )"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "functions_can_return_0_values") { AstStatBlock* block = parse(R"( local f: (number) -> () )"); REQUIRE(block != nullptr); } TEST_CASE_FIXTURE(Fixture, "intersection_of_two_function_types_if_no_returns") { AstStatBlock* block = parse(R"( local f: (string) -> () & (number) -> () )"); REQUIRE(block != nullptr); AstStatLocal* local = block->body.data[0]->as(); AstTypeIntersection* annotation = local->vars.data[0]->annotation->as(); REQUIRE(annotation != nullptr); CHECK(annotation->types.data[0]->as()); CHECK(annotation->types.data[1]->as()); } TEST_CASE_FIXTURE(Fixture, "intersection_of_two_function_types_if_two_or_more_returns") { AstStatBlock* block = parse(R"( local f: (string) -> (string, number) & (number) -> (number, string) )"); REQUIRE(block != nullptr); AstStatLocal* local = block->body.data[0]->as(); AstTypeIntersection* annotation = local->vars.data[0]->annotation->as(); REQUIRE(annotation != nullptr); CHECK(annotation->types.data[0]->as()); CHECK(annotation->types.data[1]->as()); } TEST_CASE_FIXTURE(Fixture, "return_type_is_an_intersection_type_if_led_with_one_parenthesized_type") { AstStatBlock* block = parse(R"( local f: (string) -> (string) & (number) -> (number) )"); REQUIRE(block != nullptr); AstStatLocal* local = block->body.data[0]->as(); AstTypeFunction* annotation = local->vars.data[0]->annotation->as(); REQUIRE(annotation != nullptr); AstTypeIntersection* returnAnnotation = annotation->returnTypes.types.data[0]->as(); REQUIRE(returnAnnotation != nullptr); CHECK(returnAnnotation->types.data[0]->as()); CHECK(returnAnnotation->types.data[1]->as()); } TEST_CASE_FIXTURE(Fixture, "illegal_type_alias_if_extensions_are_disabled") { Luau::ParseOptions options; options.allowTypeAnnotations = false; CHECK_THROWS_AS(parse("type A = number", options), std::exception); } TEST_CASE_FIXTURE(Fixture, "type_alias_to_a_typeof") { AstStatBlock* block = parse(R"( type A = typeof(1) )"); REQUIRE(block != nullptr); REQUIRE(block->body.size > 0); auto typeAliasStat = block->body.data[0]->as(); REQUIRE(typeAliasStat != nullptr); CHECK_EQ(typeAliasStat->location, (Location{{1, 8}, {1, 26}})); } TEST_CASE_FIXTURE(Fixture, "type_alias_should_point_to_string") { AstStatBlock* block = parse(R"( type A = string )"); REQUIRE(block != nullptr); REQUIRE(block->body.size > 0); REQUIRE(block->body.data[0]->is()); } TEST_CASE_FIXTURE(Fixture, "type_alias_should_not_interfere_with_type_function_call_or_assignment") { AstStatBlock* block = parse(R"( type("a") type = nil )"); REQUIRE(block != nullptr); REQUIRE(block->body.size > 0); AstStatExpr* stat = block->body.data[0]->as(); REQUIRE(stat != nullptr); REQUIRE(stat->expr->as()); REQUIRE(block->body.data[1]->is()); } TEST_CASE_FIXTURE(Fixture, "type_alias_should_work_when_name_is_also_local") { AstStatBlock* block = parse(R"( local A = nil type A = string )"); REQUIRE(block != nullptr); REQUIRE(block->body.size == 2); REQUIRE(block->body.data[0]->is()); REQUIRE(block->body.data[1]->is()); } TEST_CASE_FIXTURE(Fixture, "type_alias_span_is_correct") { AstStatBlock* block = parse(R"( type Packed1 = (T...) -> (T...) type Packed2 = (Packed1, T...) -> (Packed1, T...) )"); REQUIRE(block != nullptr); REQUIRE(2 == block->body.size); AstStatTypeAlias* t1 = block->body.data[0]->as(); REQUIRE(t1); REQUIRE(Location{Position{1, 8}, Position{1, 45}} == t1->location); AstStatTypeAlias* t2 = block->body.data[1]->as(); REQUIRE(t2); REQUIRE(Location{Position{2, 8}, Position{2, 75}} == t2->location); } TEST_CASE_FIXTURE(Fixture, "parse_error_messages") { CHECK_EQ(getParseError(R"( local a: (number, number) -> (string )"), "Expected ')' (to close '(' at line 2), got "); CHECK_EQ(getParseError(R"( local a: (number, number) -> ( string )"), "Expected ')' (to close '(' at line 2), got "); CHECK_EQ(getParseError(R"( local a: (number, number) )"), "Expected '->' when parsing function type, got "); CHECK_EQ(getParseError(R"( local a: (number, number )"), "Expected ')' (to close '(' at line 2), got "); CHECK_EQ(getParseError(R"( local a: {foo: string, )"), "Expected identifier when parsing table field, got "); CHECK_EQ(getParseError(R"( local a: {foo: string )"), "Expected '}' (to close '{' at line 2), got "); CHECK_EQ(getParseError(R"( local a: { [string]: number, [number]: string } )"), "Cannot have more than one table indexer"); CHECK_EQ(getParseError(R"( type T = foo )"), "Expected '(' when parsing function parameters, got 'foo'"); } TEST_CASE_FIXTURE(Fixture, "mixed_intersection_and_union_not_allowed") { matchParseError("type A = number & string | boolean", "Mixing union and intersection types is not allowed; consider wrapping in parentheses."); } TEST_CASE_FIXTURE(Fixture, "mixed_intersection_and_union_allowed_when_parenthesized") { try { parse("type A = (number & string) | boolean"); } catch (const ParseErrors& e) { FAIL(e.what()); } } TEST_CASE_FIXTURE(Fixture, "cannot_write_multiple_values_in_type_groups") { matchParseError("type F = ((string, number))", "Expected '->' when parsing function type, got ')'"); matchParseError("type F = () -> ((string, number))", "Expected '->' when parsing function type, got ')'"); } TEST_CASE_FIXTURE(Fixture, "type_alias_error_messages") { CHECK_EQ(getParseError("type 5 = number"), "Expected identifier when parsing type name, got '5'"); CHECK_EQ(getParseError("type A"), "Expected '=' when parsing type alias, got "); CHECK_EQ(getParseError("type A<"), "Expected identifier, got "); CHECK_EQ(getParseError("type A' (to close '<' at column 7), got "); } TEST_CASE_FIXTURE(Fixture, "type_assertion_expression") { (void)parse(R"( local a = something() :: any )"); } // The bug that motivated this test was an infinite loop. // TODO: Set a timer and crash if the timeout is exceeded. TEST_CASE_FIXTURE(Fixture, "last_line_does_not_have_to_be_blank") { (void)parse("-- print('hello')"); } TEST_CASE_FIXTURE(Fixture, "type_assertion_expression_binds_tightly") { AstStatBlock* stat = parse(R"( local a = one :: any + two :: any )"); REQUIRE(stat != nullptr); AstStatBlock* block = stat->as(); REQUIRE(block != nullptr); REQUIRE_EQ(1, block->body.size); AstStatLocal* local = block->body.data[0]->as(); REQUIRE(local != nullptr); REQUIRE_EQ(1, local->values.size); AstExprBinary* bin = local->values.data[0]->as(); REQUIRE(bin != nullptr); CHECK(nullptr != bin->left->as()); CHECK(nullptr != bin->right->as()); } TEST_CASE_FIXTURE(Fixture, "mode_is_unset_if_no_hot_comment") { ParseResult result = parseEx("print('Hello World!')"); CHECK(result.hotcomments.empty()); } TEST_CASE_FIXTURE(Fixture, "sense_hot_comment_on_first_line") { ParseOptions options; options.captureComments = true; ParseResult result = parseEx(" --!strict ", options); std::optional mode = parseMode(result.hotcomments); REQUIRE(bool(mode)); CHECK_EQ(int(*mode), int(Mode::Strict)); } TEST_CASE_FIXTURE(Fixture, "non_header_hot_comments") { ParseOptions options; options.captureComments = true; ParseResult result = parseEx("do end --!strict", options); std::optional mode = parseMode(result.hotcomments); REQUIRE(!mode); } TEST_CASE_FIXTURE(Fixture, "stop_if_line_ends_with_hyphen") { CHECK_THROWS_AS(parse(" -"), std::exception); } TEST_CASE_FIXTURE(Fixture, "nonstrict_mode") { ParseOptions options; options.captureComments = true; ParseResult result = parseEx("--!nonstrict", options); CHECK(result.errors.empty()); std::optional mode = parseMode(result.hotcomments); REQUIRE(bool(mode)); CHECK_EQ(int(*mode), int(Mode::Nonstrict)); } TEST_CASE_FIXTURE(Fixture, "nocheck_mode") { ParseOptions options; options.captureComments = true; ParseResult result = parseEx("--!nocheck", options); CHECK(result.errors.empty()); std::optional mode = parseMode(result.hotcomments); REQUIRE(bool(mode)); CHECK_EQ(int(*mode), int(Mode::NoCheck)); } TEST_CASE_FIXTURE(Fixture, "vertical_space") { ParseResult result = parseEx("a()\vb()"); CHECK(result.errors.empty()); } TEST_CASE_FIXTURE(Fixture, "parse_error_type_name") { CHECK_EQ(getParseError(R"( local a: Foo.= )"), "Expected identifier when parsing field name, got '='"); } TEST_CASE_FIXTURE(Fixture, "parse_numbers_decimal") { AstStat* stat = parse("return 1, .5, 1.5, 1e-5, 1.5e-5, 12_345.1_25"); REQUIRE(stat != nullptr); AstStatReturn* str = stat->as()->body.data[0]->as(); CHECK(str->list.size == 6); CHECK_EQ(str->list.data[0]->as()->value, 1.0); CHECK_EQ(str->list.data[1]->as()->value, 0.5); CHECK_EQ(str->list.data[2]->as()->value, 1.5); CHECK_EQ(str->list.data[3]->as()->value, 1.0e-5); CHECK_EQ(str->list.data[4]->as()->value, 1.5e-5); CHECK_EQ(str->list.data[5]->as()->value, 12345.125); } TEST_CASE_FIXTURE(Fixture, "parse_numbers_hexadecimal") { AstStat* stat = parse("return 0xab, 0XAB05, 0xff_ff, 0xffffffffffffffff"); REQUIRE(stat != nullptr); AstStatReturn* str = stat->as()->body.data[0]->as(); CHECK(str->list.size == 4); CHECK_EQ(str->list.data[0]->as()->value, 0xab); CHECK_EQ(str->list.data[1]->as()->value, 0xAB05); CHECK_EQ(str->list.data[2]->as()->value, 0xFFFF); CHECK_EQ(str->list.data[3]->as()->value, double(ULLONG_MAX)); } TEST_CASE_FIXTURE(Fixture, "parse_numbers_binary") { AstStat* stat = parse("return 0b1, 0b0, 0b101010, 0b1111111111111111111111111111111111111111111111111111111111111111"); REQUIRE(stat != nullptr); AstStatReturn* str = stat->as()->body.data[0]->as(); CHECK(str->list.size == 4); CHECK_EQ(str->list.data[0]->as()->value, 1); CHECK_EQ(str->list.data[1]->as()->value, 0); CHECK_EQ(str->list.data[2]->as()->value, 42); CHECK_EQ(str->list.data[3]->as()->value, double(ULLONG_MAX)); } TEST_CASE_FIXTURE(Fixture, "parse_numbers_error") { CHECK_EQ(getParseError("return 0b123"), "Malformed number"); CHECK_EQ(getParseError("return 123x"), "Malformed number"); CHECK_EQ(getParseError("return 0xg"), "Malformed number"); CHECK_EQ(getParseError("return 0x0x123"), "Malformed number"); CHECK_EQ(getParseError("return 0xffffffffffffffffffffllllllg"), "Malformed number"); CHECK_EQ(getParseError("return 0x0xffffffffffffffffffffffffffff"), "Malformed number"); } TEST_CASE_FIXTURE(Fixture, "break_return_not_last_error") { CHECK_EQ(getParseError("return 0 print(5)"), "Expected , got 'print'"); CHECK_EQ(getParseError("while true do break print(5) end"), "Expected 'end' (to close 'do' at column 12), got 'print'"); } TEST_CASE_FIXTURE(Fixture, "error_on_unicode") { CHECK_EQ(getParseError(R"( local ☃ = 10 )"), "Expected identifier when parsing variable name, got Unicode character U+2603"); } TEST_CASE_FIXTURE(Fixture, "allow_unicode_in_string") { ParseResult result = parseEx("local snowman = \"☃\""); CHECK(result.errors.empty()); } TEST_CASE_FIXTURE(Fixture, "error_on_confusable") { CHECK_EQ(getParseError(R"( local pi = 3․13 )"), "Expected identifier when parsing expression, got Unicode character U+2024 (did you mean '.'?)"); } TEST_CASE_FIXTURE(Fixture, "error_on_non_utf8_sequence") { const char* expected = "Expected identifier when parsing expression, got invalid UTF-8 sequence"; CHECK_EQ(getParseError("local pi = \xFF!"), expected); CHECK_EQ(getParseError("local pi = \xE2!"), expected); } TEST_CASE_FIXTURE(Fixture, "lex_broken_unicode") { const std::string testInput = std::string("\xFF\xFE☃․"); Luau::Allocator alloc; AstNameTable table(alloc); Lexer lexer(testInput.c_str(), testInput.size(), table); Lexeme lexeme = lexer.current(); lexeme = lexer.next(); CHECK_EQ(lexeme.type, Lexeme::BrokenUnicode); CHECK_EQ(lexeme.codepoint, 0); CHECK_EQ(lexeme.location, Luau::Location(Luau::Position(0, 0), Luau::Position(0, 1))); lexeme = lexer.next(); CHECK_EQ(lexeme.type, Lexeme::BrokenUnicode); CHECK_EQ(lexeme.codepoint, 0); CHECK_EQ(lexeme.location, Luau::Location(Luau::Position(0, 1), Luau::Position(0, 2))); lexeme = lexer.next(); CHECK_EQ(lexeme.type, Lexeme::BrokenUnicode); CHECK_EQ(lexeme.codepoint, 0x2603); CHECK_EQ(lexeme.location, Luau::Location(Luau::Position(0, 2), Luau::Position(0, 5))); lexeme = lexer.next(); CHECK_EQ(lexeme.type, Lexeme::BrokenUnicode); CHECK_EQ(lexeme.codepoint, 0x2024); CHECK_EQ(lexeme.location, Luau::Location(Luau::Position(0, 5), Luau::Position(0, 8))); lexeme = lexer.next(); CHECK_EQ(lexeme.type, Lexeme::Eof); } TEST_CASE_FIXTURE(Fixture, "parse_continue") { AstStatBlock* stat = parse(R"( while true do continue() continue = 5 continue, continue = continue continue end )"); REQUIRE(stat != nullptr); AstStatBlock* block = stat->as(); REQUIRE(block != nullptr); REQUIRE_EQ(1, block->body.size); AstStatWhile* wb = block->body.data[0]->as(); REQUIRE(wb != nullptr); AstStatBlock* wblock = wb->body->as(); REQUIRE(wblock != nullptr); REQUIRE_EQ(4, wblock->body.size); REQUIRE(wblock->body.data[0]->is()); REQUIRE(wblock->body.data[1]->is()); REQUIRE(wblock->body.data[2]->is()); REQUIRE(wblock->body.data[3]->is()); } TEST_CASE_FIXTURE(Fixture, "continue_not_last_error") { CHECK_EQ(getParseError("while true do continue print(5) end"), "Expected 'end' (to close 'do' at column 12), got 'print'"); } TEST_CASE_FIXTURE(Fixture, "parse_export_type") { AstStatBlock* stat = parse(R"( export() export = 5 export, export = export export type A = number type A = number )"); REQUIRE(stat != nullptr); AstStatBlock* block = stat->as(); REQUIRE(block != nullptr); REQUIRE_EQ(5, block->body.size); REQUIRE(block->body.data[0]->is()); REQUIRE(block->body.data[1]->is()); REQUIRE(block->body.data[2]->is()); REQUIRE(block->body.data[3]->is()); REQUIRE(block->body.data[4]->is()); } TEST_CASE_FIXTURE(Fixture, "export_is_an_identifier_only_when_followed_by_type") { try { parse(R"( export function a() end )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Incomplete statement: expected assignment or a function call", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "incomplete_statement_error") { CHECK_EQ(getParseError("fiddlesticks"), "Incomplete statement: expected assignment or a function call"); } TEST_CASE_FIXTURE(Fixture, "parse_compound_assignment") { AstStatBlock* block = parse(R"( a += 5 )"); REQUIRE(block != nullptr); REQUIRE(block->body.size == 1); REQUIRE(block->body.data[0]->is()); REQUIRE(block->body.data[0]->as()->op == AstExprBinary::Add); } TEST_CASE_FIXTURE(Fixture, "parse_compound_assignment_error_call") { try { parse(R"( a() += 5 )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Expected identifier when parsing expression, got '+='", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_compound_assignment_error_not_lvalue") { try { parse(R"( (a) += 5 )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Assigned expression must be a variable or a field", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_compound_assignment_error_multiple") { try { parse(R"( a, b += 5 )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Expected '=' when parsing assignment, got '+='", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_double_brace_begin") { try { parse(R"( _ = `{{oops}}` )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Double braces are not permitted within interpolated strings. Did you mean '\\{'?", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_double_brace_mid") { try { parse(R"( _ = `{nice} {{oops}}` )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Double braces are not permitted within interpolated strings. Did you mean '\\{'?", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_without_end_brace") { auto columnOfEndBraceError = [this](const char* code) { try { parse(code); FAIL("Expected ParseErrors to be thrown"); return UINT_MAX; } catch (const ParseErrors& e) { CHECK_EQ(e.getErrors().size(), 1); auto error = e.getErrors().front(); CHECK_EQ("Malformed interpolated string, did you forget to add a '}'?", error.getMessage()); return error.getLocation().begin.column; } }; // This makes sure that the error is coming from the brace itself CHECK_EQ(columnOfEndBraceError("_ = `{a`"), columnOfEndBraceError("_ = `{abcdefg`")); CHECK_NE(columnOfEndBraceError("_ = `{a`"), columnOfEndBraceError("_ = `{a`")); } TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_without_end_brace_in_table") { try { parse(R"( _ = { `{a` } )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ(e.getErrors().size(), 2); CHECK_EQ("Malformed interpolated string, did you forget to add a '}'?", e.getErrors().front().getMessage()); CHECK_EQ("Expected '}' (to close '{' at line 2), got ", e.getErrors().back().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_mid_without_end_brace_in_table") { try { parse(R"( _ = { `x {"y"} {z` } )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ(e.getErrors().size(), 2); CHECK_EQ("Malformed interpolated string, did you forget to add a '}'?", e.getErrors().front().getMessage()); CHECK_EQ("Expected '}' (to close '{' at line 2), got ", e.getErrors().back().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_as_type_fail") { try { parse(R"( local a: `what` = `???` local b: `what {"the"}` = `???` local c: `what {"the"} heck` = `???` )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& parseErrors) { CHECK_EQ(parseErrors.getErrors().size(), 3); for (ParseError error : parseErrors.getErrors()) CHECK_EQ(error.getMessage(), "Interpolated string literals cannot be used as types"); } } TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_call_without_parens") { try { parse(R"( _ = print `{42}` )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Expected identifier when parsing expression, got `{", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_without_expression") { ScopedFastFlag sff("LuauFixInterpStringMid", true); try { parse(R"( print(`{}`) )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Malformed interpolated string, expected expression inside '{}'", e.getErrors().front().getMessage()); } try { parse(R"( print(`{}{1}`) )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Malformed interpolated string, expected expression inside '{}'", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_nesting_based_end_detection") { try { parse(R"(-- i am line 1 function BottomUpTree(item, depth) if depth > 0 then local i = item + item depth = depth - 1 local left, right = BottomUpTree(i-1, depth), BottomUpTree(i, depth) return { item, left, right } else return { item } end function ItemCheck(tree) if tree[2] then return tree[1] + ItemCheck(tree[2]) - ItemCheck(tree[3]) else return tree[1] end end )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Expected 'end' (to close 'function' at line 2), got ; did you forget to close 'else' at line 8?", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_nesting_based_end_detection_single_line") { try { parse(R"(-- i am line 1 function ItemCheck(tree) if tree[2] then return tree[1] + ItemCheck(tree[2]) - ItemCheck(tree[3]) else return tree[1] end function BottomUpTree(item, depth) if depth > 0 then local i = item + item depth = depth - 1 local left, right = BottomUpTree(i-1, depth), BottomUpTree(i, depth) return { item, left, right } else return { item } end end )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Expected 'end' (to close 'function' at line 2), got ; did you forget to close 'else' at line 3?", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_nesting_based_end_detection_local_repeat") { try { parse(R"(-- i am line 1 repeat print(1) repeat print(2) print(3) until false )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Expected 'until' (to close 'repeat' at line 2), got ; did you forget to close 'repeat' at line 4?", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_nesting_based_end_detection_local_function") { try { parse(R"(-- i am line 1 local function BottomUpTree(item, depth) if depth > 0 then local i = item + item depth = depth - 1 local left, right = BottomUpTree(i-1, depth), BottomUpTree(i, depth) return { item, left, right } else return { item } end local function ItemCheck(tree) if tree[2] then return tree[1] + ItemCheck(tree[2]) - ItemCheck(tree[3]) else return tree[1] end end )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Expected 'end' (to close 'function' at line 2), got ; did you forget to close 'else' at line 8?", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_nesting_based_end_detection_failsafe_earlier") { try { parse(R"(-- i am line 1 local function ItemCheck(tree) if tree[2] then return tree[1] + ItemCheck(tree[2]) - ItemCheck(tree[3]) else return tree[1] end end local function BottomUpTree(item, depth) if depth > 0 then local i = item + item depth = depth - 1 local left, right = BottomUpTree(i-1, depth), BottomUpTree(i, depth) return { item, left, right } else return { item } end )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Expected 'end' (to close 'function' at line 10), got ", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_nesting_based_end_detection_nested") { try { parse(R"(-- i am line 1 function stringifyTable(t) local entries = {} for k, v in pairs(t) do -- if we find a nested table, convert that recursively if type(v) == "table" then v = stringifyTable(v) else v = tostring(v) k = tostring(k) -- add another entry to our stringified table entries[#entries + 1] = ("s = s"):format(k, v) end -- the memory location of the table local id = tostring(t):sub(8) return ("{s}@s"):format(table.concat(entries, ", "), id) end )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ("Expected 'end' (to close 'function' at line 2), got ; did you forget to close 'else' at line 8?", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_error_table_literal") { try { parse(R"( function stringifyTable(t) local foo = (name = t) return foo end )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ( "Expected ')' (to close '(' at column 17), got '='; did you mean to use '{' when defining a table?", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_error_function_call") { try { parse(R"( function stringifyTable(t) local foo = t:Parse 2 return foo end )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ(e.getErrors().front().getLocation().begin.line, 2); CHECK_EQ("Expected '(', '{' or when parsing function call, got '2'", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_error_function_call_newline") { try { parse(R"( function stringifyTable(t) local foo = t:Parse return foo end )"); FAIL("Expected ParseErrors to be thrown"); } catch (const ParseErrors& e) { CHECK_EQ(e.getErrors().front().getLocation().begin.line, 2); CHECK_EQ("Expected function call arguments after '('", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "parse_error_with_too_many_nested_type_group") { ScopedFastInt sfis{"LuauRecursionLimit", 20}; matchParseError( "function f(): (((((((((Fail))))))))) end", "Exceeded allowed recursion depth; simplify your type annotation to make the code compile"); matchParseError("function f(): () -> () -> () -> () -> () -> () -> () -> () -> () -> () -> () end", "Exceeded allowed recursion depth; simplify your type annotation to make the code compile"); matchParseError( "local t: {a: {b: {c: {d: {e: {f: {}}}}}}}", "Exceeded allowed recursion depth; simplify your type annotation to make the code compile"); } TEST_CASE_FIXTURE(Fixture, "parse_error_with_too_many_nested_if_statements") { ScopedFastInt sfis{"LuauRecursionLimit", 10}; matchParseErrorPrefix( "function f() if true then if true then if true then if true then if true then if true then if true then if true then if true " "then if true then if true then end end end end end end end end end end end end", "Exceeded allowed recursion depth;"); } TEST_CASE_FIXTURE(Fixture, "parse_error_with_too_many_changed_elseif_statements") { ScopedFastInt sfis{"LuauRecursionLimit", 10}; matchParseErrorPrefix( "function f() if false then elseif false then elseif false then elseif false then elseif false then elseif false then elseif " "false then elseif false then elseif false then elseif false then elseif false then end end", "Exceeded allowed recursion depth;"); } TEST_CASE_FIXTURE(Fixture, "parse_error_with_too_many_nested_ifelse_expressions1") { ScopedFastInt sfis{"LuauRecursionLimit", 10}; matchParseError("function f() return if true then 1 elseif true then 2 elseif true then 3 elseif true then 4 elseif true then 5 elseif true then " "6 elseif true then 7 elseif true then 8 elseif true then 9 elseif true then 10 else 11 end", "Exceeded allowed recursion depth; simplify your expression to make the code compile"); } TEST_CASE_FIXTURE(Fixture, "parse_error_with_too_many_nested_ifelse_expressions2") { ScopedFastInt sfis{"LuauRecursionLimit", 10}; matchParseError( "function f() return if if if if if if if if if if true then false else true then false else true then false else true then false else true " "then false else true then false else true then false else true then false else true then false else true then 1 else 2 end", "Exceeded allowed recursion depth; simplify your expression to make the code compile"); } TEST_CASE_FIXTURE(Fixture, "unparenthesized_function_return_type_list") { matchParseError( "function foo(): string, number end", "Expected a statement, got ','; did you forget to wrap the list of return types in parentheses?"); matchParseError("function foo(): (number) -> string, string", "Expected a statement, got ','; did you forget to wrap the list of return types in parentheses?"); // Will throw if the parse fails parse(R"( type Vector3MT = { __add: (Vector3MT, Vector3MT) -> Vector3MT, __mul: (Vector3MT, Vector3MT|number) -> Vector3MT } )"); } TEST_CASE_FIXTURE(Fixture, "short_array_types") { AstStatBlock* stat = parse(R"( local n: {string} )"); REQUIRE(stat != nullptr); AstStatLocal* local = stat->body.data[0]->as(); AstTypeTable* annotation = local->vars.data[0]->annotation->as(); REQUIRE(annotation != nullptr); CHECK(annotation->props.size == 0); REQUIRE(annotation->indexer); REQUIRE(annotation->indexer->indexType->is()); CHECK(annotation->indexer->indexType->as()->name == "number"); REQUIRE(annotation->indexer->resultType->is()); CHECK(annotation->indexer->resultType->as()->name == "string"); } TEST_CASE_FIXTURE(Fixture, "short_array_types_must_be_alone") { matchParseError("local n: {string, number}", "Expected '}' (to close '{' at column 10), got ','"); matchParseError("local n: {[number]: string, number}", "Expected ':' when parsing table field, got '}'"); matchParseError("local n: {x: string, number}", "Expected ':' when parsing table field, got '}'"); matchParseError("local n: {x: string, nil}", "Expected identifier when parsing table field, got 'nil'"); } TEST_CASE_FIXTURE(Fixture, "short_array_types_do_not_break_field_names") { AstStatBlock* stat = parse(R"( local n: {string: number} )"); REQUIRE(stat != nullptr); AstStatLocal* local = stat->body.data[0]->as(); AstTypeTable* annotation = local->vars.data[0]->annotation->as(); REQUIRE(annotation != nullptr); REQUIRE(annotation->props.size == 1); CHECK(!annotation->indexer); REQUIRE(annotation->props.data[0].name == "string"); REQUIRE(annotation->props.data[0].type->is()); REQUIRE(annotation->props.data[0].type->as()->name == "number"); } TEST_CASE_FIXTURE(Fixture, "short_array_types_are_not_field_names_when_complex") { matchParseError("local n: {string | number: number}", "Expected '}' (to close '{' at column 10), got ':'"); } TEST_CASE_FIXTURE(Fixture, "nil_can_not_be_a_field_name") { matchParseError("local n: {nil: number}", "Expected '}' (to close '{' at column 10), got ':'"); } TEST_CASE_FIXTURE(Fixture, "string_literal_call") { AstStatBlock* stat = parse("do foo 'bar' end"); REQUIRE(stat != nullptr); AstStatBlock* dob = stat->body.data[0]->as(); AstStatExpr* stc = dob->body.data[0]->as(); REQUIRE(stc != nullptr); AstExprCall* ec = stc->expr->as(); CHECK(ec->args.size == 1); AstExprConstantString* arg = ec->args.data[0]->as(); REQUIRE(arg != nullptr); CHECK(std::string(arg->value.data, arg->value.size) == "bar"); } TEST_CASE_FIXTURE(Fixture, "multiline_strings_newlines") { AstStatBlock* stat = parse("return [=[\nfoo\r\nbar\n\nbaz\n]=]"); REQUIRE(stat != nullptr); AstStatReturn* ret = stat->body.data[0]->as(); REQUIRE(ret != nullptr); AstExprConstantString* str = ret->list.data[0]->as(); REQUIRE(str != nullptr); CHECK(std::string(str->value.data, str->value.size) == "foo\nbar\n\nbaz\n"); } TEST_CASE_FIXTURE(Fixture, "string_literals_escape") { AstStatBlock* stat = parse(R"( return "foo\n\r", "foo\0324", "foo\x204", "foo\u{20}", "foo\u{0451}" )"); REQUIRE(stat != nullptr); AstStatReturn* ret = stat->body.data[0]->as(); REQUIRE(ret != nullptr); CHECK(ret->list.size == 5); AstExprConstantString* str; str = ret->list.data[0]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "foo\n\r"); str = ret->list.data[1]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "foo 4"); str = ret->list.data[2]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "foo 4"); str = ret->list.data[3]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "foo "); str = ret->list.data[4]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "foo\xd1\x91"); } TEST_CASE_FIXTURE(Fixture, "string_literals_escape_newline") { AstStatBlock* stat = parse("return \"foo\\z\n bar\", \"foo\\\n bar\", \"foo\\\r\nbar\""); REQUIRE(stat != nullptr); AstStatReturn* ret = stat->body.data[0]->as(); REQUIRE(ret != nullptr); CHECK(ret->list.size == 3); AstExprConstantString* str; str = ret->list.data[0]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "foobar"); str = ret->list.data[1]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "foo\n bar"); str = ret->list.data[2]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "foo\nbar"); } TEST_CASE_FIXTURE(Fixture, "string_literals_escapes") { AstStatBlock* stat = parse(R"( return "\xAB", "\u{2024}", "\121", "\1x", "\t", "\n" )"); REQUIRE(stat != nullptr); AstStatReturn* ret = stat->body.data[0]->as(); REQUIRE(ret != nullptr); CHECK(ret->list.size == 6); AstExprConstantString* str; str = ret->list.data[0]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "\xAB"); str = ret->list.data[1]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "\xE2\x80\xA4"); str = ret->list.data[2]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "\x79"); str = ret->list.data[3]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "\x01x"); str = ret->list.data[4]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "\t"); str = ret->list.data[5]->as(); REQUIRE(str != nullptr); CHECK_EQ(std::string(str->value.data, str->value.size), "\n"); } TEST_CASE_FIXTURE(Fixture, "parse_error_broken_comment") { const char* expected = "Expected identifier when parsing expression, got unfinished comment"; matchParseError("--[[unfinished work", expected); matchParseError("--!strict\n--[[unfinished work", expected); matchParseError("local x = 1 --[[unfinished work", expected); } TEST_CASE_FIXTURE(Fixture, "string_literals_escapes_broken") { const char* expected = "String literal contains malformed escape sequence"; matchParseError("return \"\\u{\"", expected); matchParseError("return \"\\u{FO}\"", expected); matchParseError("return \"\\u{123456789}\"", expected); matchParseError("return \"\\359\"", expected); matchParseError("return \"\\xFO\"", expected); matchParseError("return \"\\xF\"", expected); matchParseError("return \"\\x\"", expected); } TEST_CASE_FIXTURE(Fixture, "string_literals_broken") { matchParseError("return \"", "Malformed string"); matchParseError("return \"\\", "Malformed string"); matchParseError("return \"\r\r", "Malformed string"); } TEST_CASE_FIXTURE(Fixture, "number_literals") { AstStatBlock* stat = parse(R"( return 1, 1.5, .5, 12_34_56, 0x1234, 0b010101 )"); REQUIRE(stat != nullptr); AstStatReturn* ret = stat->body.data[0]->as(); REQUIRE(ret != nullptr); CHECK(ret->list.size == 6); AstExprConstantNumber* num; num = ret->list.data[0]->as(); REQUIRE(num != nullptr); CHECK_EQ(num->value, 1.0); num = ret->list.data[1]->as(); REQUIRE(num != nullptr); CHECK_EQ(num->value, 1.5); num = ret->list.data[2]->as(); REQUIRE(num != nullptr); CHECK_EQ(num->value, 0.5); num = ret->list.data[3]->as(); REQUIRE(num != nullptr); CHECK_EQ(num->value, 123456); num = ret->list.data[4]->as(); REQUIRE(num != nullptr); CHECK_EQ(num->value, 0x1234); num = ret->list.data[5]->as(); REQUIRE(num != nullptr); CHECK_EQ(num->value, 0x15); } TEST_CASE_FIXTURE(Fixture, "end_extent_of_functions_unions_and_intersections") { AstStatBlock* block = parse(R"( type F = (string) -> string type G = string | number | boolean type H = string & number & boolean print('hello') )"); REQUIRE_EQ(4, block->body.size); CHECK_EQ((Position{1, 35}), block->body.data[0]->location.end); CHECK_EQ((Position{2, 42}), block->body.data[1]->location.end); CHECK_EQ((Position{3, 42}), block->body.data[2]->location.end); } TEST_CASE_FIXTURE(Fixture, "end_extent_doesnt_consume_comments") { AstStatBlock* block = parse(R"( type F = number --comment print('hello') )"); REQUIRE_EQ(2, block->body.size); CHECK_EQ((Position{1, 23}), block->body.data[0]->location.end); } TEST_CASE_FIXTURE(Fixture, "end_extent_doesnt_consume_comments_even_with_capture") { // Same should hold when comments are captured ParseOptions opts; opts.captureComments = true; AstStatBlock* block = parse(R"( type F = number --comment print('hello') )", opts); REQUIRE_EQ(2, block->body.size); CHECK_EQ((Position{1, 23}), block->body.data[0]->location.end); } TEST_CASE_FIXTURE(Fixture, "parse_error_loop_control") { matchParseError("break", "break statement must be inside a loop"); matchParseError("repeat local function a() break end until false", "break statement must be inside a loop"); matchParseError("continue", "continue statement must be inside a loop"); matchParseError("repeat local function a() continue end until false", "continue statement must be inside a loop"); } TEST_CASE_FIXTURE(Fixture, "parse_error_confusing_function_call") { auto result1 = matchParseError(R"( function add(x, y) return x + y end add (4, 7) )", "Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of new statement; use ';' to separate " "statements"); CHECK(result1.errors.size() == 1); auto result2 = matchParseError(R"( function add(x, y) return x + y end local f = add (f :: any)['x'] = 2 )", "Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of new statement; use ';' to separate " "statements"); CHECK(result2.errors.size() == 1); auto result3 = matchParseError(R"( local x = {} function x:add(a, b) return a + b end x:add (1, 2) )", "Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of new statement; use ';' to separate " "statements"); CHECK(result3.errors.size() == 1); auto result4 = matchParseError(R"( local t = {} function f() return t end t.x, (f) ().y = 5, 6 )", "Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of new statement; use ';' to separate " "statements"); CHECK(result4.errors.size() == 1); } TEST_CASE_FIXTURE(Fixture, "parse_error_varargs") { matchParseError("function add(x, y) return ... end", "Cannot use '...' outside of a vararg function"); } TEST_CASE_FIXTURE(Fixture, "parse_error_assignment_lvalue") { matchParseError(R"( local a, b (2), b = b, a )", "Assigned expression must be a variable or a field"); matchParseError(R"( local a, b a, (3) = b, a )", "Assigned expression must be a variable or a field"); } TEST_CASE_FIXTURE(Fixture, "parse_error_type_annotation") { matchParseError("local a : 2 = 2", "Expected type, got '2'"); } TEST_CASE_FIXTURE(Fixture, "parse_error_missing_type_annotation") { { ParseResult result = tryParse("local x:"); CHECK(result.errors.size() == 1); Position begin = result.errors[0].getLocation().begin; Position end = result.errors[0].getLocation().end; CHECK(begin.line == end.line); int width = end.column - begin.column; CHECK(width == 0); CHECK(result.errors[0].getMessage() == "Expected type, got "); } { ParseResult result = tryParse(R"( local x:=42 )"); CHECK(result.errors.size() == 1); Position begin = result.errors[0].getLocation().begin; Position end = result.errors[0].getLocation().end; CHECK(begin.line == end.line); int width = end.column - begin.column; CHECK(width == 1); // Length of `=` CHECK(result.errors[0].getMessage() == "Expected type, got '='"); } { ParseResult result = tryParse(R"( function func():end )"); CHECK(result.errors.size() == 1); Position begin = result.errors[0].getLocation().begin; Position end = result.errors[0].getLocation().end; CHECK(begin.line == end.line); int width = end.column - begin.column; CHECK(width == 3); // Length of `end` CHECK(result.errors[0].getMessage() == "Expected type, got 'end'"); } } TEST_CASE_FIXTURE(Fixture, "parse_declarations") { AstStatBlock* stat = parseEx(R"( declare foo: number declare function bar(x: number): string declare function var(...: any) )") .root; REQUIRE(stat); REQUIRE_EQ(stat->body.size, 3); AstStatDeclareGlobal* global = stat->body.data[0]->as(); REQUIRE(global); CHECK(global->name == "foo"); CHECK(global->type); AstStatDeclareFunction* func = stat->body.data[1]->as(); REQUIRE(func); CHECK(func->name == "bar"); REQUIRE_EQ(func->params.types.size, 1); REQUIRE_EQ(func->retTypes.types.size, 1); AstStatDeclareFunction* varFunc = stat->body.data[2]->as(); REQUIRE(varFunc); CHECK(varFunc->name == "var"); CHECK(varFunc->params.tailType); matchParseError("declare function foo(x)", "All declaration parameters must be annotated"); matchParseError("declare foo", "Expected ':' when parsing global variable declaration, got "); } TEST_CASE_FIXTURE(Fixture, "parse_class_declarations") { AstStatBlock* stat = parseEx(R"( declare class Foo prop: number function method(self, foo: number): string end declare class Bar extends Foo prop2: string end )") .root; REQUIRE_EQ(stat->body.size, 2); AstStatDeclareClass* declaredClass = stat->body.data[0]->as(); REQUIRE(declaredClass); CHECK(declaredClass->name == "Foo"); CHECK(!declaredClass->superName); REQUIRE_EQ(declaredClass->props.size, 2); AstDeclaredClassProp& prop = declaredClass->props.data[0]; CHECK(prop.name == "prop"); CHECK(prop.ty->is()); AstDeclaredClassProp& method = declaredClass->props.data[1]; CHECK(method.name == "method"); CHECK(method.ty->is()); AstStatDeclareClass* subclass = stat->body.data[1]->as(); REQUIRE(subclass); REQUIRE(subclass->superName); CHECK(subclass->name == "Bar"); CHECK(*subclass->superName == "Foo"); REQUIRE_EQ(subclass->props.size, 1); AstDeclaredClassProp& prop2 = subclass->props.data[0]; CHECK(prop2.name == "prop2"); CHECK(prop2.ty->is()); } TEST_CASE_FIXTURE(Fixture, "class_method_properties") { const ParseResult p1 = matchParseError(R"( declare class Foo -- method's first parameter must be 'self' function method(foo: number) function method2(self) end )", "'self' must be present as the unannotated first parameter"); REQUIRE_EQ(1, p1.root->body.size); AstStatDeclareClass* klass = p1.root->body.data[0]->as(); REQUIRE(klass != nullptr); CHECK_EQ(2, klass->props.size); const ParseResult p2 = matchParseError(R"( declare class Foo function method(self, foo) function method2() end )", "All declaration parameters aside from 'self' must be annotated"); REQUIRE_EQ(1, p2.root->body.size); AstStatDeclareClass* klass2 = p2.root->body.data[0]->as(); REQUIRE(klass2 != nullptr); CHECK_EQ(2, klass2->props.size); } TEST_CASE_FIXTURE(Fixture, "parse_variadics") { //clang-format off AstStatBlock* stat = parseEx(R"( function foo(bar, ...: number): ...string end type Foo = (string, number, ...number) -> ...boolean type Bar = () -> (number, ...boolean) )") .root; //clang-format on REQUIRE(stat); REQUIRE_EQ(stat->body.size, 3); AstStatFunction* fn = stat->body.data[0]->as(); REQUIRE(fn); CHECK(fn->func->vararg); CHECK(fn->func->varargAnnotation); AstStatTypeAlias* foo = stat->body.data[1]->as(); REQUIRE(foo); AstTypeFunction* fnFoo = foo->type->as(); REQUIRE(fnFoo); CHECK_EQ(fnFoo->argTypes.types.size, 2); CHECK(fnFoo->argTypes.tailType); CHECK_EQ(fnFoo->returnTypes.types.size, 0); CHECK(fnFoo->returnTypes.tailType); AstStatTypeAlias* bar = stat->body.data[2]->as(); REQUIRE(bar); AstTypeFunction* fnBar = bar->type->as(); REQUIRE(fnBar); CHECK_EQ(fnBar->argTypes.types.size, 0); CHECK(!fnBar->argTypes.tailType); CHECK_EQ(fnBar->returnTypes.types.size, 1); CHECK(fnBar->returnTypes.tailType); } TEST_CASE_FIXTURE(Fixture, "variadics_must_be_last") { matchParseError("function foo(): (...number, string) end", "Expected ')' (to close '(' at column 17), got ','"); matchParseError("type Foo = (...number, string) -> (...string, number)", "Expected ')' (to close '(' at column 12), got ','"); } TEST_CASE_FIXTURE(Fixture, "variadic_definition_parsing") { AstStatBlock* stat = parseEx(R"( declare function foo(...: string): ...string declare class Foo function a(self, ...: string): ...string end )") .root; REQUIRE(stat != nullptr); matchParseError("declare function foo(...)", "All declaration parameters must be annotated"); matchParseError("declare class Foo function a(self, ...) end", "All declaration parameters aside from 'self' must be annotated"); } TEST_CASE_FIXTURE(Fixture, "generic_pack_parsing") { ParseResult result = parseEx(R"( function f(...: a...) end type A = (a...) -> b... )"); AstStatBlock* stat = result.root; REQUIRE(stat != nullptr); AstStatFunction* fn = stat->body.data[0]->as(); REQUIRE(fn != nullptr); REQUIRE(fn->func->varargAnnotation != nullptr); AstTypePackGeneric* annot = fn->func->varargAnnotation->as(); REQUIRE(annot != nullptr); CHECK(annot->genericName == "a"); AstStatTypeAlias* alias = stat->body.data[1]->as(); REQUIRE(alias != nullptr); AstTypeFunction* fnTy = alias->type->as(); REQUIRE(fnTy != nullptr); AstTypePackGeneric* argAnnot = fnTy->argTypes.tailType->as(); REQUIRE(argAnnot != nullptr); CHECK(argAnnot->genericName == "a"); AstTypePackGeneric* retAnnot = fnTy->returnTypes.tailType->as(); REQUIRE(retAnnot != nullptr); CHECK(retAnnot->genericName == "b"); } TEST_CASE_FIXTURE(Fixture, "generic_function_declaration_parsing") { ParseResult result = parseEx(R"( declare function f() )"); AstStatBlock* stat = result.root; REQUIRE(stat != nullptr); AstStatDeclareFunction* decl = stat->body.data[0]->as(); REQUIRE(decl != nullptr); REQUIRE_EQ(decl->generics.size, 2); REQUIRE_EQ(decl->genericPacks.size, 1); } TEST_CASE_FIXTURE(Fixture, "function_type_named_arguments") { { ParseResult result = parseEx("type MyFunc = (a: number, b: string, c: number) -> string"); AstStatBlock* stat = result.root; REQUIRE(stat != nullptr); AstStatTypeAlias* decl = stat->body.data[0]->as(); REQUIRE(decl != nullptr); AstTypeFunction* func = decl->type->as(); REQUIRE(func != nullptr); REQUIRE_EQ(func->argTypes.types.size, 3); REQUIRE_EQ(func->argNames.size, 3); REQUIRE(func->argNames.data[2]); CHECK_EQ(func->argNames.data[2]->first, "c"); } { ParseResult result = parseEx("type MyFunc = (a: number, string, c: number) -> string"); AstStatBlock* stat = result.root; REQUIRE(stat != nullptr); AstStatTypeAlias* decl = stat->body.data[0]->as(); REQUIRE(decl != nullptr); AstTypeFunction* func = decl->type->as(); REQUIRE(func != nullptr); REQUIRE_EQ(func->argTypes.types.size, 3); REQUIRE_EQ(func->argNames.size, 3); REQUIRE(!func->argNames.data[1]); REQUIRE(func->argNames.data[2]); CHECK_EQ(func->argNames.data[2]->first, "c"); } { ParseResult result = parseEx("type MyFunc = (a: number, string, number) -> string"); AstStatBlock* stat = result.root; REQUIRE(stat != nullptr); AstStatTypeAlias* decl = stat->body.data[0]->as(); REQUIRE(decl != nullptr); AstTypeFunction* func = decl->type->as(); REQUIRE(func != nullptr); REQUIRE_EQ(func->argTypes.types.size, 3); REQUIRE_EQ(func->argNames.size, 3); REQUIRE(!func->argNames.data[1]); REQUIRE(!func->argNames.data[2]); } { ParseResult result = parseEx("type MyFunc = (a: number, b: string, c: number) -> (d: number, e: string, f: number) -> string"); AstStatBlock* stat = result.root; REQUIRE(stat != nullptr); AstStatTypeAlias* decl = stat->body.data[0]->as(); REQUIRE(decl != nullptr); AstTypeFunction* func = decl->type->as(); REQUIRE(func != nullptr); REQUIRE_EQ(func->argTypes.types.size, 3); REQUIRE_EQ(func->argNames.size, 3); REQUIRE(func->argNames.data[2]); CHECK_EQ(func->argNames.data[2]->first, "c"); AstTypeFunction* funcRet = func->returnTypes.types.data[0]->as(); REQUIRE(funcRet != nullptr); REQUIRE_EQ(funcRet->argTypes.types.size, 3); REQUIRE_EQ(funcRet->argNames.size, 3); REQUIRE(func->argNames.data[2]); CHECK_EQ(funcRet->argNames.data[2]->first, "f"); } matchParseError("type MyFunc = (a: number, b: string, c: number) -> (d: number, e: string, f: number)", "Expected '->' when parsing function type, got "); matchParseError("type MyFunc = (number) -> (d: number) -> number", "Expected '->' when parsing function type, got '<'"); } TEST_CASE_FIXTURE(Fixture, "function_type_matching_parenthesis") { matchParseError("local a: (number -> string", "Expected ')' (to close '(' at column 13), got '->'"); } TEST_CASE_FIXTURE(Fixture, "parse_type_alias_default_type") { AstStat* stat = parse(R"( type A = {} type B = {} type C = {} type D = {} type E = {} type F = (T...) -> U... type G = (U...) -> T... )"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "parse_type_alias_default_type_errors") { matchParseError("type Y = {}", "Expected default type after type name", Location{{0, 20}, {0, 21}}); matchParseError("type Y = {}", "Expected default type pack after type pack name", Location{{0, 29}, {0, 30}}); matchParseError("type Y number> = {}", "Expected type pack after '=', got type", Location{{0, 14}, {0, 32}}); } TEST_CASE_FIXTURE(Fixture, "parse_type_pack_errors") { matchParseError("type Y = {a: T..., b: number}", "Unexpected '...' after type name; type pack is not allowed in this context", Location{{0, 20}, {0, 23}}); matchParseError("type Y = {a: (number | string)...", "Unexpected '...' after type annotation", Location{{0, 36}, {0, 39}}); } TEST_CASE_FIXTURE(Fixture, "parse_if_else_expression") { { AstStat* stat = parse("return if true then 1 else 2"); REQUIRE(stat != nullptr); AstStatReturn* str = stat->as()->body.data[0]->as(); REQUIRE(str != nullptr); CHECK(str->list.size == 1); auto* ifElseExpr = str->list.data[0]->as(); REQUIRE(ifElseExpr != nullptr); } { AstStat* stat = parse("return if true then 1 elseif true then 2 else 3"); REQUIRE(stat != nullptr); AstStatReturn* str = stat->as()->body.data[0]->as(); REQUIRE(str != nullptr); CHECK(str->list.size == 1); auto* ifElseExpr1 = str->list.data[0]->as(); REQUIRE(ifElseExpr1 != nullptr); auto* ifElseExpr2 = ifElseExpr1->falseExpr->as(); REQUIRE(ifElseExpr2 != nullptr); } // Use "else if" as opposed to elseif { AstStat* stat = parse("return if true then 1 else if true then 2 else 3"); REQUIRE(stat != nullptr); AstStatReturn* str = stat->as()->body.data[0]->as(); REQUIRE(str != nullptr); CHECK(str->list.size == 1); auto* ifElseExpr1 = str->list.data[0]->as(); REQUIRE(ifElseExpr1 != nullptr); auto* ifElseExpr2 = ifElseExpr1->falseExpr->as(); REQUIRE(ifElseExpr2 != nullptr); } // Use an if-else expression as the conditional expression of an if-else expression { AstStat* stat = parse("return if if true then false else true then 1 else 2"); REQUIRE(stat != nullptr); AstStatReturn* str = stat->as()->body.data[0]->as(); REQUIRE(str != nullptr); CHECK(str->list.size == 1); auto* ifElseExpr = str->list.data[0]->as(); REQUIRE(ifElseExpr != nullptr); auto* nestedIfElseExpr = ifElseExpr->condition->as(); REQUIRE(nestedIfElseExpr != nullptr); } } TEST_CASE_FIXTURE(Fixture, "parse_type_pack_type_parameters") { AstStat* stat = parse(R"( type Packed = () -> T... type A = Packed type B = Packed<...number> type C = Packed<(number, X...)> )"); REQUIRE(stat != nullptr); } TEST_CASE_FIXTURE(Fixture, "invalid_type_forms") { matchParseError("type A = (b: number)", "Expected '->' when parsing function type, got "); matchParseError("type P = () -> T... type B = P<(x: number, y: string)>", "Expected '->' when parsing function type, got '>'"); matchParseError("type F = (T...) -> ()", "Expected '->' when parsing function type, got '>'"); } TEST_SUITE_END(); TEST_SUITE_BEGIN("ParseErrorRecovery"); TEST_CASE_FIXTURE(Fixture, "multiple_parse_errors") { try { parse(R"( local a = 3 * ( return a + )"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ(2, e.getErrors().size()); } } // check that we are not skipping tokens that weren't processed at all TEST_CASE_FIXTURE(Fixture, "statement_error_recovery_expected") { try { parse(R"( function a(a, b) return a + b end some a(2, 5) )"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ(1, e.getErrors().size()); } } TEST_CASE_FIXTURE(Fixture, "statement_error_recovery_unexpected") { try { parse(R"(+)"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ(1, e.getErrors().size()); } } TEST_CASE_FIXTURE(Fixture, "extra_token_in_consume") { try { parse(R"( function test + (a, f) return a + f end return test(2, 3) )"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ(1, e.getErrors().size()); CHECK_EQ("Expected '(' when parsing function, got '+'", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "extra_token_in_consume_match") { try { parse(R"( function test(a, f+) return a + f end return test(2, 3) )"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ(1, e.getErrors().size()); CHECK_EQ("Expected ')' (to close '(' at column 14), got '+'", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "extra_token_in_consume_match_end") { try { parse(R"( if true then return 12 then end )"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ(1, e.getErrors().size()); CHECK_EQ("Expected 'end' (to close 'then' at line 2), got 'then'", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "extra_table_indexer_recovery") { try { parse(R"( local a : { [string] : number, [number] : string, count: number } )"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ(1, e.getErrors().size()); } } TEST_CASE_FIXTURE(Fixture, "recovery_error_limit_1") { ScopedFastInt luauParseErrorLimit("LuauParseErrorLimit", 1); try { parse("local a = "); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ(1, e.getErrors().size()); CHECK_EQ(e.getErrors().front().getMessage(), e.what()); } } TEST_CASE_FIXTURE(Fixture, "recovery_error_limit_2") { ScopedFastInt luauParseErrorLimit("LuauParseErrorLimit", 2); try { parse("escape escape escape"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ(3, e.getErrors().size()); CHECK_EQ("3 parse errors", std::string(e.what())); CHECK_EQ("Reached error limit (2)", e.getErrors().back().getMessage()); } } class CountAstNodes : public AstVisitor { public: bool visit(AstNode* node) override { count++; return true; } unsigned count = 0; }; TEST_CASE_FIXTURE(Fixture, "recovery_of_parenthesized_expressions") { auto checkAstEquivalence = [this](const char* codeWithErrors, const char* code) { try { parse(codeWithErrors); } catch (const Luau::ParseErrors&) { } CountAstNodes counterWithErrors; sourceModule->root->visit(&counterWithErrors); parse(code); CountAstNodes counter; sourceModule->root->visit(&counter); CHECK_EQ(counterWithErrors.count, counter.count); }; auto checkRecovery = [this, checkAstEquivalence](const char* codeWithErrors, const char* code, unsigned expectedErrorCount) { try { parse(codeWithErrors); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ(expectedErrorCount, e.getErrors().size()); checkAstEquivalence(codeWithErrors, code); } }; checkRecovery("function foo(a, b. c) return a + b end", "function foo(a, b) return a + b end", 1); checkRecovery("function foo(a, b: { a: number, b: number. c:number }) return a + b end", "function foo(a, b: { a: number, b: number }) return a + b end", 1); checkRecovery("function foo(a, b): (number -> number return a + b end", "function foo(a, b): (number) -> number return a + b end", 1); checkRecovery("function foo(a, b): (number, number -> number return a + b end", "function foo(a, b): (number) -> number return a + b end", 1); checkRecovery("function foo(a, b): (number; number) -> number return a + b end", "function foo(a, b): (number) -> number return a + b end", 1); checkRecovery("function foo(a, b): (number, number return a + b end", "function foo(a, b): (number, number) end", 1); checkRecovery("local function foo(a, b): (number, number return a + b end", "local function foo(a, b): (number, number) end", 1); // These tests correctly recovered before the changes and we test that new recovery didn't make them worse // (by skipping more tokens necessary) checkRecovery("type F = (number, number -> number", "type F = (number, number) -> number", 1); checkRecovery("function foo(a, b: { a: number, b: number) return a + b end", "function foo(a, b: { a: number, b: number }) return a + b end", 1); checkRecovery("function foo(a, b: { [number: number}) return a + b end", "function foo(a, b: { [number]: number}) return a + b end", 1); checkRecovery("local n: (string | number = 2", "local n: (string | number) = 2", 1); // Check that we correctly stop at the end of a line checkRecovery(R"( function foo(a, b return a + b end )", "function foo(a, b) return a + b end", 1); } TEST_CASE_FIXTURE(Fixture, "incomplete_method_call") { const std::string_view source = R"( function howdy() return game: end )"; SourceModule sourceModule; ParseResult result = Parser::parse(source.data(), source.size(), *sourceModule.names, *sourceModule.allocator, {}); REQUIRE_EQ(1, result.root->body.size); AstStatFunction* howdyFunction = result.root->body.data[0]->as(); REQUIRE(howdyFunction != nullptr); AstStatBlock* body = howdyFunction->func->body; REQUIRE_EQ(1, body->body.size); AstStatReturn* ret = body->body.data[0]->as(); REQUIRE(ret != nullptr); REQUIRE_GT(howdyFunction->location.end, body->location.end); } TEST_CASE_FIXTURE(Fixture, "incomplete_method_call_2") { const std::string_view source = R"( local game = { GetService=function(s) return 'hello' end } function a() game:a end )"; SourceModule sourceModule; ParseResult result = Parser::parse(source.data(), source.size(), *sourceModule.names, *sourceModule.allocator, {}); REQUIRE_EQ(2, result.root->body.size); AstStatFunction* howdyFunction = result.root->body.data[1]->as(); REQUIRE(howdyFunction != nullptr); AstStatBlock* body = howdyFunction->func->body; REQUIRE_EQ(1, body->body.size); AstStatError* ret = body->body.data[0]->as(); REQUIRE(ret != nullptr); REQUIRE_GT(howdyFunction->location.end, body->location.end); } TEST_CASE_FIXTURE(Fixture, "incomplete_method_call_still_yields_an_AstExprIndexName") { ParseResult result = tryParse(R"( game: )"); REQUIRE_EQ(1, result.root->body.size); AstStatError* stat = result.root->body.data[0]->as(); REQUIRE(stat); AstExprError* expr = stat->expressions.data[0]->as(); REQUIRE(expr); AstExprIndexName* indexName = expr->expressions.data[0]->as(); REQUIRE(indexName); } TEST_CASE_FIXTURE(Fixture, "recover_confusables") { // Binary matchParseError("local a = 4 != 10", "Unexpected '!=', did you mean '~='?"); matchParseError("local a = true && false", "Unexpected '&&', did you mean 'and'?"); matchParseError("local a = false || true", "Unexpected '||', did you mean 'or'?"); // Unary matchParseError("local a = !false", "Unexpected '!', did you mean 'not'?"); // Check that separate tokens are not considered as a single one matchParseError("local a = 4 ! = 10", "Expected identifier when parsing expression, got '!'"); matchParseError("local a = true & & false", "Expected identifier when parsing expression, got '&'"); matchParseError("local a = false | | true", "Expected identifier when parsing expression, got '|'"); } TEST_CASE_FIXTURE(Fixture, "capture_comments") { ParseOptions options; options.captureComments = true; ParseResult result = parseEx(R"( --!strict local a = 5 -- comment one local b = 8 -- comment two --[[ Multi line comment ]] local c = 'see' )", options); CHECK(result.errors.empty()); CHECK_EQ(4, result.commentLocations.size()); CHECK_EQ((Location{{1, 8}, {1, 17}}), result.commentLocations[0].location); CHECK_EQ((Location{{3, 20}, {3, 34}}), result.commentLocations[1].location); CHECK_EQ((Location{{4, 20}, {4, 34}}), result.commentLocations[2].location); CHECK_EQ((Location{{5, 8}, {7, 10}}), result.commentLocations[3].location); } TEST_CASE_FIXTURE(Fixture, "capture_broken_comment_at_the_start_of_the_file") { ParseOptions options; options.captureComments = true; ParseResult result = tryParse(R"( --[[ )", options); CHECK_EQ(1, result.commentLocations.size()); CHECK_EQ((Location{{1, 8}, {2, 4}}), result.commentLocations[0].location); } TEST_CASE_FIXTURE(Fixture, "capture_broken_comment") { ParseOptions options; options.captureComments = true; ParseResult result = tryParse(R"( local a = "test" --[[broken! )", options); CHECK_EQ(1, result.commentLocations.size()); CHECK_EQ((Location{{3, 8}, {4, 4}}), result.commentLocations[0].location); } TEST_CASE_FIXTURE(Fixture, "empty_function_type_error_recovery") { try { parse(R"( type Fn = ( any, string | number | () ) -> any )"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ("Expected '->' after '()' when parsing function type; did you mean 'nil'?", e.getErrors().front().getMessage()); } // If we have arguments or generics, don't use special case try { parse(R"(type Fn = (any, string | number | (number, number)) -> any)"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ("Expected '->' when parsing function type, got ')'", e.getErrors().front().getMessage()); } try { parse(R"(type Fn = (any, string | number | ()) -> any)"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ("Expected '->' when parsing function type, got ')'", e.getErrors().front().getMessage()); } try { parse(R"(type Fn = (any, string | number | ()) -> any)"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ("Expected '->' when parsing function type, got ')'", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "AstName_comparison") { CHECK(!(AstName() < AstName())); AstName one{"one"}; AstName two{"two"}; CHECK_NE((one < two), (two < one)); } TEST_CASE_FIXTURE(Fixture, "generic_type_list_recovery") { try { parse(R"( local function foo(a: U, ...: T...): (U, ...T) return a, ... end return foo(1, 2 -- to check for a second error after recovery )"); FAIL("Expected ParseErrors to be thrown"); } catch (const Luau::ParseErrors& e) { CHECK_EQ(2, e.getErrors().size()); CHECK_EQ("Generic types come before generic type packs", e.getErrors().front().getMessage()); } } TEST_CASE_FIXTURE(Fixture, "recover_index_name_keyword") { ParseResult result = tryParse(R"( local b local a = b.do )"); CHECK_EQ(1, result.errors.size()); result = tryParse(R"( local b local a = b. do end )"); CHECK_EQ(1, result.errors.size()); } TEST_CASE_FIXTURE(Fixture, "recover_self_call_keyword") { ParseResult result = tryParse(R"( local b local a = b:do )"); CHECK_EQ(2, result.errors.size()); result = tryParse(R"( local b local a = b: do end )"); CHECK_EQ(2, result.errors.size()); } TEST_CASE_FIXTURE(Fixture, "recover_type_index_name_keyword") { ParseResult result = tryParse(R"( local A local b : A.do )"); CHECK_EQ(1, result.errors.size()); result = tryParse(R"( local A local b : A.do do end )"); CHECK_EQ(1, result.errors.size()); } TEST_CASE_FIXTURE(Fixture, "recover_expected_type_pack") { ParseResult result = tryParse(R"( type Y = (T...) -> U... )"); CHECK_EQ(1, result.errors.size()); } TEST_CASE_FIXTURE(Fixture, "recover_unexpected_type_pack") { ParseResult result = tryParse(R"( type X = { a: T..., b: number } type Y = { a: T..., b: number } type Z = { a: string | T..., b: number } )"); REQUIRE_EQ(3, result.errors.size()); } TEST_CASE_FIXTURE(Fixture, "recover_function_return_type_annotations") { ParseResult result = tryParse(R"( type Custom = { x: A, y: B, z: C } type Packed = { x: (A...) -> () } type F = (number): Custom type G = Packed<(number): (string, number, boolean)> local function f(x: number) -> Custom end )"); REQUIRE_EQ(3, result.errors.size()); CHECK_EQ(result.errors[0].getMessage(), "Return types in function type annotations are written after '->' instead of ':'"); CHECK_EQ(result.errors[1].getMessage(), "Return types in function type annotations are written after '->' instead of ':'"); CHECK_EQ(result.errors[2].getMessage(), "Function return type annotations are written after ':' instead of '->'"); } TEST_CASE_FIXTURE(Fixture, "error_message_for_using_function_as_type_annotation") { ParseResult result = tryParse(R"( type Foo = function )"); REQUIRE_EQ(1, result.errors.size()); CHECK_EQ("Using 'function' as a type annotation is not supported, consider replacing with a function type annotation e.g. '(...any) -> ...any'", result.errors[0].getMessage()); } TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the_end_of_a_function_argument_list") { ParseResult result = tryParse(R"( foo(a, b, c,) )"); REQUIRE(1 == result.errors.size()); CHECK(Location({1, 20}, {1, 21}) == result.errors[0].getLocation()); CHECK("Expected expression after ',' but got ')' instead" == result.errors[0].getMessage()); } TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the_end_of_a_function_parameter_list") { ParseResult result = tryParse(R"( export type VisitFn = ( any, Array>, -- extra comma here ) -> any )"); REQUIRE(1 == result.errors.size()); CHECK(Location({4, 8}, {4, 9}) == result.errors[0].getLocation()); CHECK("Expected type after ',' but got ')' instead" == result.errors[0].getMessage()); } TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the_end_of_a_generic_parameter_list") { ParseResult result = tryParse(R"( export type VisitFn = (a: A, b: B) -> () )"); REQUIRE(1 == result.errors.size()); CHECK(Location({1, 36}, {1, 37}) == result.errors[0].getLocation()); CHECK("Expected type after ',' but got '>' instead" == result.errors[0].getMessage()); REQUIRE(1 == result.root->body.size); AstStatTypeAlias* t = result.root->body.data[0]->as(); REQUIRE(t != nullptr); AstTypeFunction* f = t->type->as(); REQUIRE(f != nullptr); CHECK(2 == f->generics.size); } TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_no_comma_between_table_members") { ParseResult result = tryParse(R"( local t = { first = 1 second = 2, third = 3, fouth = 4, } )"); REQUIRE(1 == result.errors.size()); CHECK(Location({3, 12}, {3, 18}) == result.errors[0].getLocation()); CHECK("Expected ',' after table constructor element" == result.errors[0].getMessage()); REQUIRE(1 == result.root->body.size); AstExprTable* table = Luau::query(result.root); REQUIRE(table); CHECK(table->items.size == 4); } TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_no_comma_after_last_table_member") { ParseResult result = tryParse(R"( local t = { first = 1 local ok = true local good = ok == true )"); REQUIRE(1 == result.errors.size()); CHECK(Location({4, 8}, {4, 13}) == result.errors[0].getLocation()); CHECK("Expected '}' (to close '{' at line 2), got 'local'" == result.errors[0].getMessage()); REQUIRE(3 == result.root->body.size); AstExprTable* table = Luau::query(result.root); REQUIRE(table); CHECK(table->items.size == 1); } TEST_CASE_FIXTURE(Fixture, "missing_default_type_pack_argument_after_variadic_type_parameter") { ScopedFastFlag sff{"LuauParserErrorsOnMissingDefaultTypePackArgument", true}; ParseResult result = tryParse(R"( type Foo = nil )"); REQUIRE_EQ(2, result.errors.size()); CHECK_EQ(Location{{1, 23}, {1, 25}}, result.errors[0].getLocation()); CHECK_EQ("Expected type, got '>'", result.errors[0].getMessage()); CHECK_EQ(Location{{1, 23}, {1, 24}}, result.errors[1].getLocation()); CHECK_EQ("Expected type pack after '=', got type", result.errors[1].getMessage()); } TEST_SUITE_END();