// 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 "Luau/TypeAttach.h" #include "Luau/TypeInfer.h" #include "Luau/Type.h" #include "Luau/Transpiler.h" #include "Fixture.h" #include "ScopedFlags.h" #include "doctest.h" using namespace Luau; LUAU_FASTFLAG(LuauUserDefinedTypeFunctions); TEST_SUITE_BEGIN("TranspilerTests"); TEST_CASE("test_1") { const std::string example = R"( local function isPortal(element) if type(element)~='table'then return false end return element.component == Core.Portal end )"; CHECK_EQ(example, transpile(example).code); } TEST_CASE("string_literals") { const std::string code = R"( local S='abcdef\n\f\a\020' )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("string_literals_containing_utf8") { const std::string code = R"( local S='lalala こんにちは' )"; // Konichiwa! CHECK_EQ(code, transpile(code).code); } TEST_CASE("elseif_chains_indent_sensibly") { const std::string code = R"( if This then Once() elseif That then Another() elseif SecondLast then Third() else IfAllElseFails() end )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("strips_type_annotations") { const std::string code = R"( local s: string= 'hello there' )"; const std::string expected = R"( local s = 'hello there' )"; CHECK_EQ(expected, transpile(code).code); } TEST_CASE("strips_type_assertion_expressions") { const std::string code = R"( local s= some_function() :: any+ something_else() :: number )"; const std::string expected = R"( local s= some_function() + something_else() )"; CHECK_EQ(expected, transpile(code).code); } TEST_CASE("function_taking_ellipsis") { const std::string code = R"( function F(...) end )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("omit_decimal_place_for_integers") { const std::string code = R"( local a=5, 6, 7, 3.141, 1.1290000000000002e+45 )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("for_loop") { const std::string one = R"( for i=1,10 do end )"; CHECK_EQ(one, transpile(one).code); const std::string code = R"( for i=5,6,7 do end )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("for_in_loop") { const std::string code = R"( for k, v in ipairs(x)do end )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("while_loop") { const std::string code = R"( while f(x)do print() end )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("repeat_until_loop") { const std::string code = R"( repeat print() until f(x) )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("lambda") { const std::string one = R"( local p=function(o, m, g) return 77 end )"; CHECK_EQ(one, transpile(one).code); const std::string two = R"( local p=function(o, m, g,...) return 77 end )"; CHECK_EQ(two, transpile(two).code); } TEST_CASE("local_function") { const std::string one = R"( local function p(o, m, g) return 77 end )"; CHECK_EQ(one, transpile(one).code); const std::string two = R"( local function p(o, m, g,...) return 77 end )"; CHECK_EQ(two, transpile(two).code); } TEST_CASE("function") { const std::string one = R"( function p(o, m, g) return 77 end )"; CHECK_EQ(one, transpile(one).code); const std::string two = R"( function p(o, m, g,...) return 77 end )"; CHECK_EQ(two, transpile(two).code); } TEST_CASE("table_literals") { const std::string code = R"( local t={1, 2, 3, foo='bar', baz=99,[5.5]='five point five', 'end'} )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("more_table_literals") { const std::string code = R"( local t={['Content-Type']='text/plain'} )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("table_literal_preserves_record_vs_general") { const std::string code = R"( local t={['foo']='bar',quux=42} )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("table_literal_with_numeric_key") { const std::string code = R"( local t={[5]='five',[6]='six'} )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("table_literal_with_keyword_key") { const std::string code = R"( local t={['nil']=nil,['true']=true} )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("table_literal_closing_brace_at_correct_position") { const std::string code = R"( local t={ eggs='Tasty', avocado='more like awesomecavo amirite' } )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("method_calls") { const std::string code = R"( foo.bar.baz:quux() )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("method_definitions") { const std::string code = R"( function foo.bar.baz:quux() end )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("spaces_between_keywords_even_if_it_pushes_the_line_estimation_off") { // Luau::Parser doesn't exactly preserve the string representation of numbers in Lua, so we can find ourselves // falling out of sync with the original code. We need to push keywords out so that there's at least one space between them. const std::string code = R"( if math.abs(raySlope) < .01 then return 0 end )"; const std::string expected = R"( if math.abs(raySlope) < 0.01 then return 0 end)"; CHECK_EQ(expected, transpile(code).code); } TEST_CASE("numbers") { const std::string code = R"( local a=2510238627 )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("infinity") { const std::string code = R"( local a = 1e500 local b = 1e400 )"; const std::string expected = R"( local a = 1e500 local b = 1e500 )"; CHECK_EQ(expected, transpile(code).code); } TEST_CASE("escaped_strings") { const std::string code = R"( local s='\\b\\t\\n\\\\' )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("escaped_strings_2") { const std::string code = R"( local s="\a\b\f\n\r\t\v\'\"\\" )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("need_a_space_between_number_literals_and_dots") { const std::string code = R"( return point and math.ceil(point* 100000* 100)/ 100000 .. '%'or '' )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("binary_keywords") { const std::string code = "local c = a0 ._ or b0 ._"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("do_blocks") { const std::string code = R"( foo() do local bar=baz() quux() end foo2() )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("emit_a_do_block_in_cases_of_potentially_ambiguous_syntax") { const std::string code = R"( f(); (g or f)() )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("roundtrip_types") { const std::string code = R"( local s:string='str' local t:{a:string,b:number,[string]:number} local fn:(string,string)->(number,number) local s2:typeof(s)='foo' local os:string? local sn:string|number local it:{x:number}&{y:number} )"; auto allocator = Allocator{}; auto names = AstNameTable{allocator}; ParseOptions options; ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, options); REQUIRE(parseResult.errors.empty()); CHECK_EQ(code, transpileWithTypes(*parseResult.root)); } TEST_CASE("roundtrip_generic_types") { const std::string code = R"( export type A = {v:T, next:A} )"; auto allocator = Allocator{}; auto names = AstNameTable{allocator}; ParseOptions options; ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, options); REQUIRE(parseResult.errors.empty()); CHECK_EQ(code, transpileWithTypes(*parseResult.root)); } TEST_CASE_FIXTURE(Fixture, "attach_types") { const std::string code = R"( local s='str' local t={a=1,b=false} local function fn() return 10 end )"; const std::string expected = R"( local s:string='str' local t:{a:number,b:boolean}={a=1,b=false} local function fn(): number return 10 end )"; CHECK_EQ(expected, decorateWithTypes(code)); } TEST_CASE("a_table_key_can_be_the_empty_string") { std::string code = "local T = {[''] = true}"; CHECK_EQ(code, transpile(code).code); } // There's a bit of login in the transpiler that always adds a space before a dot if the previous symbol ends in a digit. // This was surfacing an issue where we might not insert a space after the 'local' keyword. TEST_CASE("always_emit_a_space_after_local_keyword") { std::string code = "do local aZZZZ = Workspace.P1.Shape local bZZZZ = Enum.PartType.Cylinder end"; std::string expected = "do local aZZZZ = Workspace.P1 .Shape local bZZZZ= Enum.PartType.Cylinder end"; CHECK_EQ(expected, transpile(code).code); } TEST_CASE_FIXTURE(Fixture, "types_should_not_be_considered_cyclic_if_they_are_not_recursive") { std::string code = R"( local common: {foo:string} = {foo = 'foo'} local t = {} t.x = common t.y = common )"; std::string expected = R"( local common: {foo:string} = {foo = 'foo'} local t:{x:{foo:string},y:{foo:string}}={} t.x = common t.y = common )"; CHECK_EQ(expected, decorateWithTypes(code)); } TEST_CASE_FIXTURE(Fixture, "type_lists_should_be_emitted_correctly") { std::string code = R"( local a = function(a: string, b: number, ...: string): (string, ...number) end local b = function(...: string): ...number end local c = function() end )"; std::string expected = R"( local a:(string,number,...string)->(string,...number)=function(a:string,b:number,...:string): (string,...number) end local b:(...string)->(...number)=function(...:string): ...number end local c:()->()=function(): () end )"; std::string actual = decorateWithTypes(code); CHECK_EQ(expected, actual); } TEST_CASE_FIXTURE(Fixture, "function_type_location") { std::string code = R"( local function foo(x: number): number return x end local g: (number)->number = foo )"; std::string expected = R"( local function foo(x: number): number return x end local g: (number)->(number)=foo )"; std::string actual = decorateWithTypes(code); CHECK_EQ(expected, actual); } TEST_CASE_FIXTURE(Fixture, "transpile_type_assertion") { std::string code = "local a = 5 :: number"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else") { std::string code = "local a = if 1 then 2 else 3"; CHECK_EQ(code, transpile(code).code); } TEST_CASE_FIXTURE(Fixture, "transpile_type_reference_import") { fileResolver.source["game/A"] = R"( export type Type = { a: number } return {} )"; std::string code = R"( local Import = require(game.A) local a: Import.Type )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_type_packs") { std::string code = R"( type Packed = (T...)->(T...) local a: Packed<> local b: Packed<(number, string)> )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested") { std::string code = "local a: ((number)->(string))|((string)->(string))"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested_2") { std::string code = "local a: (number&string)|(string&boolean)"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested_3") { std::string code = "local a: nil | (string & number)"; CHECK_EQ("local a: ( string & number)?", transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_intersection_type_nested") { std::string code = "local a: ((number)->(string))&((string)->(string))"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_intersection_type_nested_2") { std::string code = "local a: (number|string)&(string|boolean)"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_varargs") { std::string code = "local function f(...) return ... end"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_index_expr") { std::string code = "local a = {1, 2, 3} local b = a[2]"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_unary") { std::string code = R"( local a = 1 local b = -1 local c = true local d = not c local e = 'hello' local d = #e )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_break_continue") { std::string code = R"( local a, b, c repeat if a then break end if b then continue end until c )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_compound_assignment") { std::string code = R"( local a = 1 a += 2 a -= 3 a *= 4 a /= 5 a //= 5 a %= 6 a ^= 7 a ..= ' - result' )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_assign_multiple") { std::string code = "a, b, c = 1, 2, 3"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_generic_function") { std::string code = R"( local function foo(a: T, ...: S...) return 1 end local f: (T, S...)->(number) = foo )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_union_reverse") { std::string code = "local a: nil | number"; CHECK_EQ("local a: number?", transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple") { std::string code = "for k,v in next,{}do print(k,v) end"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_error_expr") { std::string code = "local a = f:-"; auto allocator = Allocator{}; auto names = AstNameTable{allocator}; ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {}); CHECK_EQ("local a = (error-expr: f:%error-id%)-(error-expr)", transpileWithTypes(*parseResult.root)); } TEST_CASE_FIXTURE(Fixture, "transpile_error_stat") { std::string code = "-"; auto allocator = Allocator{}; auto names = AstNameTable{allocator}; ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {}); CHECK_EQ("(error-stat: (error-expr))", transpileWithTypes(*parseResult.root)); } TEST_CASE_FIXTURE(Fixture, "transpile_error_type") { std::string code = "local a: "; auto allocator = Allocator{}; auto names = AstNameTable{allocator}; ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {}); CHECK_EQ("local a:%error-type%", transpileWithTypes(*parseResult.root)); } TEST_CASE_FIXTURE(Fixture, "transpile_parse_error") { std::string code = "local a = -"; auto result = transpile(code); CHECK_EQ("", result.code); CHECK_EQ("Expected identifier when parsing expression, got ", result.parseError); } TEST_CASE_FIXTURE(Fixture, "transpile_to_string") { std::string code = "local a: string = 'hello'"; auto allocator = Allocator{}; auto names = AstNameTable{allocator}; ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {}); REQUIRE(parseResult.root); REQUIRE(parseResult.root->body.size == 1); AstStatLocal* statLocal = parseResult.root->body.data[0]->as(); REQUIRE(statLocal); CHECK_EQ("local a: string = 'hello'", toString(statLocal)); REQUIRE(statLocal->vars.size == 1); AstLocal* local = statLocal->vars.data[0]; REQUIRE(local->annotation); CHECK_EQ("string", toString(local->annotation)); REQUIRE(statLocal->values.size == 1); AstExpr* expr = statLocal->values.data[0]; CHECK_EQ("'hello'", toString(expr)); } TEST_CASE_FIXTURE(Fixture, "transpile_type_alias_default_type_parameters") { std::string code = R"( type Packed = (T, U, V...)->(W...) local a: Packed )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_singleton_types") { std::string code = R"( type t1 = 'hello' type t2 = true type t3 = '' type t4 = false )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_array_types") { std::string code = R"( type t1 = {number} type t2 = {[string]: number} )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple_types") { std::string code = "for k:string,v:boolean in next,{}do end"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_string_interp") { std::string code = R"( local _ = `hello {name}` )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_string_literal_escape") { std::string code = R"( local _ = ` bracket = \{, backtick = \` = {'ok'} ` )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_type_functions") { ScopedFastFlag sff{FFlag::LuauUserDefinedTypeFunctions, true}; std::string code = R"( type function foo(arg1, arg2) if arg1 == arg2 then return arg1 end return arg2 end )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_SUITE_END();