// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Scope.h" #include "Luau/TypeInfer.h" #include "Fixture.h" #include "doctest.h" LUAU_FASTFLAG(LuauWeakEqConstraint) LUAU_FASTFLAG(LuauQuantifyInPlace2) using namespace Luau; TEST_SUITE_BEGIN("RefinementTest"); TEST_CASE_FIXTURE(Fixture, "is_truthy_constraint") { CheckResult result = check(R"( function f(v: string?) if v then local s = v else local s = v end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("string", toString(requireTypeAtPosition({3, 26}))); CHECK_EQ("nil", toString(requireTypeAtPosition({5, 26}))); } TEST_CASE_FIXTURE(Fixture, "invert_is_truthy_constraint") { CheckResult result = check(R"( function f(v: string?) if not v then local s = v else local s = v end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("nil", toString(requireTypeAtPosition({3, 26}))); CHECK_EQ("string", toString(requireTypeAtPosition({5, 26}))); } TEST_CASE_FIXTURE(Fixture, "parenthesized_expressions_are_followed_through") { CheckResult result = check(R"( function f(v: string?) if (not v) then local s = v else local s = v end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("nil", toString(requireTypeAtPosition({3, 26}))); CHECK_EQ("string", toString(requireTypeAtPosition({5, 26}))); } TEST_CASE_FIXTURE(Fixture, "and_constraint") { CheckResult result = check(R"( function f(a: string?, b: number?) if a and b then local x = a local y = b else local x = a local y = b end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("string", toString(requireTypeAtPosition({3, 26}))); CHECK_EQ("number", toString(requireTypeAtPosition({4, 26}))); CHECK_EQ("string?", toString(requireTypeAtPosition({6, 26}))); CHECK_EQ("number?", toString(requireTypeAtPosition({7, 26}))); } TEST_CASE_FIXTURE(Fixture, "not_and_constraint") { CheckResult result = check(R"( function f(a: string?, b: number?) if not (a and b) then local x = a local y = b else local x = a local y = b end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("string?", toString(requireTypeAtPosition({3, 26}))); CHECK_EQ("number?", toString(requireTypeAtPosition({4, 26}))); CHECK_EQ("string", toString(requireTypeAtPosition({6, 26}))); CHECK_EQ("number", toString(requireTypeAtPosition({7, 26}))); } TEST_CASE_FIXTURE(Fixture, "or_predicate_with_truthy_predicates") { CheckResult result = check(R"( function f(a: string?, b: number?) if a or b then local x = a local y = b else local x = a local y = b end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("string?", toString(requireTypeAtPosition({3, 26}))); CHECK_EQ("number?", toString(requireTypeAtPosition({4, 26}))); CHECK_EQ("nil", toString(requireTypeAtPosition({6, 26}))); CHECK_EQ("nil", toString(requireTypeAtPosition({7, 26}))); } TEST_CASE_FIXTURE(Fixture, "type_assertion_expr_carry_its_constraints") { CheckResult result = check(R"( function g(a: number?, b: string?) if (a :: any) and (b :: any) then local x = a local y = b end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("number", toString(requireTypeAtPosition({3, 26}))); CHECK_EQ("string", toString(requireTypeAtPosition({4, 26}))); } TEST_CASE_FIXTURE(Fixture, "typeguard_in_if_condition_position") { CheckResult result = check(R"( function f(s: any) if type(s) == "number" then local n = s end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("number", toString(requireTypeAtPosition({3, 26}))); } TEST_CASE_FIXTURE(Fixture, "typeguard_in_assert_position") { CheckResult result = check(R"( local a assert(type(a) == "number") local b = a )"); LUAU_REQUIRE_NO_ERRORS(result); REQUIRE_EQ("number", toString(requireType("b"))); } TEST_CASE_FIXTURE(Fixture, "typeguard_only_look_up_types_from_global_scope") { CheckResult result = check(R"( type ActuallyString = string do -- Necessary. Otherwise toposort has ActuallyString come after string type alias. type string = number local foo: string = 1 if type(foo) == "string" then local bar: ActuallyString = foo local baz: boolean = foo end end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ("Type 'number' has no overlap with 'string'", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "call_a_more_specific_function_using_typeguard") { CheckResult result = check(R"( local function f(x: number) return x end local function g(x: any) if type(x) == "string" then f(x) end end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "impossible_type_narrow_is_not_an_error") { // This unit test serves as a reminder to not implement this warning until Luau is intelligent enough. // For instance, getting a value out of the indexer and checking whether the value exists is not an error. CheckResult result = check(R"( local t: {string} = {"a", "b", "c"} local v = t[4] if not v then t[4] = "d" else print(v) end )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(Fixture, "truthy_constraint_on_properties") { CheckResult result = check(R"( local t: {x: number?} = {x = 1} if t.x then local foo: number = t.x end local bar = t.x )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("number?", toString(requireType("bar"))); } TEST_CASE_FIXTURE(Fixture, "index_on_a_refined_property") { CheckResult result = check(R"( local t: {x: {y: string}?} = {x = {y = "hello!"}} if t.x then print(t.x.y) end )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(Fixture, "assert_non_binary_expressions_actually_resolve_constraints") { CheckResult result = check(R"( local foo: string? = "hello" assert(foo) local bar: string = foo )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_type_is_illegal") { ScopedFastFlag LuauTableSubtypingVariance2{"LuauTableSubtypingVariance2", true}; ScopedFastFlag luauExtendedTypeMismatchError{"LuauExtendedTypeMismatchError", true}; CheckResult result = check(R"( local t: {x: number?} = {x = nil} if t.x then local u: {x: number} = t end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ(R"(Type '{| x: number? |}' could not be converted into '{| x: number |}' caused by: Property 'x' is not compatible. Type 'number?' could not be converted into 'number')", toString(result.errors[0])); } TEST_CASE_FIXTURE(Fixture, "lvalue_is_equal_to_another_lvalue") { ScopedFastFlag sff1{"LuauEqConstraint", true}; CheckResult result = check(R"( local function f(a: (string | number)?, b: boolean?) if a == b then local foo, bar = a, b else local foo, bar = a, b end end )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauWeakEqConstraint) { CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "(number | string)?"); // a == b CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "boolean?"); // a == b CHECK_EQ(toString(requireTypeAtPosition({5, 33})), "(number | string)?"); // a ~= b CHECK_EQ(toString(requireTypeAtPosition({5, 36})), "boolean?"); // a ~= b } else { CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "nil"); // a == b CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "nil"); // a == b CHECK_EQ(toString(requireTypeAtPosition({5, 33})), "(number | string)?"); // a ~= b CHECK_EQ(toString(requireTypeAtPosition({5, 36})), "boolean?"); // a ~= b } } TEST_CASE_FIXTURE(Fixture, "lvalue_is_equal_to_a_term") { ScopedFastFlag sff1{"LuauEqConstraint", true}; CheckResult result = check(R"( local function f(a: (string | number)?) if a == 1 then local foo = a else local foo = a end end )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauWeakEqConstraint) { CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "(number | string)?"); // a == 1 CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "(number | string)?"); // a ~= 1 } else { CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "number"); // a == 1 CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "(number | string)?"); // a ~= 1 } } TEST_CASE_FIXTURE(Fixture, "term_is_equal_to_an_lvalue") { ScopedFastFlag sff1{"LuauEqConstraint", true}; CheckResult result = check(R"( local function f(a: (string | number)?) if "hello" == a then local foo = a else local foo = a end end )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauWeakEqConstraint) { CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "(number | string)?"); // a == "hello" CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "(number | string)?"); // a ~= "hello" } else { CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "string"); // a == "hello" CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "(number | string)?"); // a ~= "hello" } } TEST_CASE_FIXTURE(Fixture, "lvalue_is_not_nil") { ScopedFastFlag sff1{"LuauEqConstraint", true}; CheckResult result = check(R"( local function f(a: (string | number)?) if a ~= nil then local foo = a else local foo = a end end )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauWeakEqConstraint) { CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "number | string"); // a ~= nil CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "(number | string)?"); // a == nil } else { CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "number | string"); // a ~= nil CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "nil"); // a == nil } } TEST_CASE_FIXTURE(Fixture, "free_type_is_equal_to_an_lvalue") { ScopedFastFlag sff1{"LuauEqConstraint", true}; CheckResult result = check(R"( local function f(a, b: string?) if a == b then local foo, bar = a, b end end )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauWeakEqConstraint) { CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "a"); // a == b CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "string?"); // a == b } else { CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "string?"); // a == b CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "string?"); // a == b } } TEST_CASE_FIXTURE(Fixture, "unknown_lvalue_is_not_synonymous_with_other_on_not_equal") { ScopedFastFlag sff1{"LuauEqConstraint", true}; CheckResult result = check(R"( local function f(a: any, b: {x: number}?) if a ~= b then local foo, bar = a, b end end )"); LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::LuauWeakEqConstraint) { CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "any"); // a ~= b CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "{| x: number |}?"); // a ~= b } else { CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "any"); // a ~= b CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "{| x: number |}"); // a ~= b } } TEST_CASE_FIXTURE(Fixture, "string_not_equal_to_string_or_nil") { ScopedFastFlag sff1{"LuauEqConstraint", true}; CheckResult result = check(R"( local t: {string} = {"hello"} local a: string = t[1] local b: string? = nil if a ~= b then local foo, bar = a, b else local foo, bar = a, b end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ(toString(requireTypeAtPosition({6, 29})), "string"); // a ~= b CHECK_EQ(toString(requireTypeAtPosition({6, 32})), "string?"); // a ~= b if (FFlag::LuauWeakEqConstraint) { CHECK_EQ(toString(requireTypeAtPosition({8, 29})), "string"); // a == b CHECK_EQ(toString(requireTypeAtPosition({8, 32})), "string?"); // a == b } else { // This is technically not wrong, but it's also wrong at the same time. // The refinement code is none the wiser about the fact we pulled a string out of an array, so it has no choice but to narrow as just string. CHECK_EQ(toString(requireTypeAtPosition({8, 29})), "string"); // a == b CHECK_EQ(toString(requireTypeAtPosition({8, 32})), "string"); // a == b } } TEST_CASE_FIXTURE(Fixture, "narrow_property_of_a_bounded_variable") { CheckResult result = check(R"( local t local u: {x: number?} = {x = nil} t = u if t.x then local foo: number = t.x end )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(Fixture, "type_narrow_to_vector") { CheckResult result = check(R"( local function f(x) if type(x) == "vector" then local foo = x end end )"); // This is kinda weird to see, but this actually only happens in Luau without Roblox type bindings because we don't have a Vector3 type. LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ("Unknown type 'Vector3'", toString(result.errors[0])); CHECK_EQ("*unknown*", toString(requireTypeAtPosition({3, 28}))); } TEST_CASE_FIXTURE(Fixture, "nonoptional_type_can_narrow_to_nil_if_sense_is_true") { CheckResult result = check(R"( local t = {"hello"} local v = t[2] if type(v) == "nil" then local foo = v else local foo = v end if not (type(v) ~= "nil") then local foo = v else local foo = v end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("nil", toString(requireTypeAtPosition({4, 24}))); // type(v) == "nil" CHECK_EQ("string", toString(requireTypeAtPosition({6, 24}))); // type(v) ~= "nil" CHECK_EQ("nil", toString(requireTypeAtPosition({10, 24}))); // equivalent to type(v) == "nil" CHECK_EQ("string", toString(requireTypeAtPosition({12, 24}))); // equivalent to type(v) ~= "nil" } TEST_CASE_FIXTURE(Fixture, "typeguard_not_to_be_string") { CheckResult result = check(R"( local function f(x: string | number | boolean) if type(x) ~= "string" then local foo = x else local foo = x end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("boolean | number", toString(requireTypeAtPosition({3, 28}))); // type(x) ~= "string" CHECK_EQ("string", toString(requireTypeAtPosition({5, 28}))); // type(x) == "string" } TEST_CASE_FIXTURE(Fixture, "typeguard_narrows_for_table") { CheckResult result = check(R"( local function f(x: string | {x: number} | {y: boolean}) if type(x) == "table" then local foo = x else local foo = x end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("{| x: number |} | {| y: boolean |}", toString(requireTypeAtPosition({3, 28}))); // type(x) == "table" CHECK_EQ("string", toString(requireTypeAtPosition({5, 28}))); // type(x) ~= "table" } TEST_CASE_FIXTURE(Fixture, "typeguard_narrows_for_functions") { CheckResult result = check(R"( local function weird(x: string | ((number) -> string)) if type(x) == "function" then local foo = x else local foo = x end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("(number) -> string", toString(requireTypeAtPosition({3, 28}))); // type(x) == "function" CHECK_EQ("string", toString(requireTypeAtPosition({5, 28}))); // type(x) ~= "function" } namespace { std::optional> magicFunctionInstanceIsA( TypeChecker& typeChecker, const ScopePtr& scope, const AstExprCall& expr, ExprResult exprResult) { if (expr.args.size != 1) return std::nullopt; auto index = expr.func->as(); auto str = expr.args.data[0]->as(); if (!index || !str) return std::nullopt; std::optional lvalue = tryGetLValue(*index->expr); std::optional tfun = scope->lookupType(std::string(str->value.data, str->value.size)); if (!lvalue || !tfun) return std::nullopt; unfreeze(typeChecker.globalTypes); TypePackId booleanPack = typeChecker.globalTypes.addTypePack({typeChecker.booleanType}); freeze(typeChecker.globalTypes); return ExprResult{booleanPack, {IsAPredicate{std::move(*lvalue), expr.location, tfun->type}}}; } struct RefinementClassFixture : Fixture { RefinementClassFixture() { TypeArena& arena = typeChecker.globalTypes; unfreeze(arena); TypeId vec3 = arena.addType(ClassTypeVar{"Vector3", {}, std::nullopt, std::nullopt, {}, nullptr}); getMutable(vec3)->props = { {"X", Property{typeChecker.numberType}}, {"Y", Property{typeChecker.numberType}}, {"Z", Property{typeChecker.numberType}}, }; TypeId inst = arena.addType(ClassTypeVar{"Instance", {}, std::nullopt, std::nullopt, {}, nullptr}); TypePackId isAParams = arena.addTypePack({inst, typeChecker.stringType}); TypePackId isARets = arena.addTypePack({typeChecker.booleanType}); TypeId isA = arena.addType(FunctionTypeVar{isAParams, isARets}); getMutable(isA)->magicFunction = magicFunctionInstanceIsA; getMutable(inst)->props = { {"Name", Property{typeChecker.stringType}}, {"IsA", Property{isA}}, }; TypeId folder = typeChecker.globalTypes.addType(ClassTypeVar{"Folder", {}, inst, std::nullopt, {}, nullptr}); TypeId part = typeChecker.globalTypes.addType(ClassTypeVar{"Part", {}, inst, std::nullopt, {}, nullptr}); getMutable(part)->props = { {"Position", Property{vec3}}, }; typeChecker.globalScope->exportedTypeBindings["Vector3"] = TypeFun{{}, vec3}; typeChecker.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, inst}; typeChecker.globalScope->exportedTypeBindings["Folder"] = TypeFun{{}, folder}; typeChecker.globalScope->exportedTypeBindings["Part"] = TypeFun{{}, part}; freeze(typeChecker.globalTypes); } }; } // namespace TEST_CASE_FIXTURE(RefinementClassFixture, "typeguard_cast_free_table_to_vector") { CheckResult result = check(R"( local function f(vec) local X, Y, Z = vec.X, vec.Y, vec.Z if type(vec) == "vector" then local foo = vec elseif typeof(vec) == "Instance" then local foo = vec else local foo = vec end end )"); LUAU_REQUIRE_ERROR_COUNT(1, result); CHECK_EQ("Vector3", toString(requireTypeAtPosition({5, 28}))); // type(vec) == "vector" if (FFlag::LuauQuantifyInPlace2) CHECK_EQ("Type '{+ X: a, Y: b, Z: c +}' could not be converted into 'Instance'", toString(result.errors[0])); else CHECK_EQ("Type '{- X: a, Y: b, Z: c -}' could not be converted into 'Instance'", toString(result.errors[0])); CHECK_EQ("*unknown*", toString(requireTypeAtPosition({7, 28}))); // typeof(vec) == "Instance" if (FFlag::LuauQuantifyInPlace2) CHECK_EQ("{+ X: a, Y: b, Z: c +}", toString(requireTypeAtPosition({9, 28}))); // type(vec) ~= "vector" and typeof(vec) ~= "Instance" else CHECK_EQ("{- X: a, Y: b, Z: c -}", toString(requireTypeAtPosition({9, 28}))); // type(vec) ~= "vector" and typeof(vec) ~= "Instance" } TEST_CASE_FIXTURE(RefinementClassFixture, "typeguard_cast_instance_or_vector3_to_vector") { CheckResult result = check(R"( local function f(x: Instance | Vector3) if typeof(x) == "Vector3" then local foo = x else local foo = x end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("Vector3", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("Instance", toString(requireTypeAtPosition({5, 28}))); } TEST_CASE_FIXTURE(RefinementClassFixture, "type_narrow_for_all_the_userdata") { CheckResult result = check(R"( local function f(x: string | number | Instance | Vector3) if type(x) == "userdata" then local foo = x else local foo = x end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("Instance | Vector3", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("number | string", toString(requireTypeAtPosition({5, 28}))); } TEST_CASE_FIXTURE(RefinementClassFixture, "eliminate_subclasses_of_instance") { CheckResult result = check(R"( local function f(x: Part | Folder | string) if typeof(x) == "Instance" then local foo = x else local foo = x end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("Folder | Part", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("string", toString(requireTypeAtPosition({5, 28}))); } TEST_CASE_FIXTURE(RefinementClassFixture, "narrow_this_large_union") { CheckResult result = check(R"( local function f(x: Part | Folder | Instance | string | Vector3 | any) if typeof(x) == "Instance" then local foo = x else local foo = x end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("Folder | Instance | Part", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("Vector3 | any | string", toString(requireTypeAtPosition({5, 28}))); } TEST_CASE_FIXTURE(RefinementClassFixture, "x_as_any_if_x_is_instance_elseif_x_is_table") { CheckResult result = check(R"( --!nonstrict local function f(x) if typeof(x) == "Instance" and x:IsA("Folder") then local foo = x elseif typeof(x) == "table" then local foo = x end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("Folder", toString(requireTypeAtPosition({5, 28}))); CHECK_EQ("any", toString(requireTypeAtPosition({7, 28}))); } TEST_CASE_FIXTURE(RefinementClassFixture, "x_is_not_instance_or_else_not_part") { CheckResult result = check(R"( local function f(x: Part | Folder | string) if typeof(x) ~= "Instance" or not x:IsA("Part") then local foo = x else local foo = x end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("Folder | string", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("Part", toString(requireTypeAtPosition({5, 28}))); } TEST_CASE_FIXTURE(Fixture, "type_guard_can_filter_for_intersection_of_tables") { CheckResult result = check(R"( type XYCoord = {x: number} & {y: number} local function f(t: XYCoord?) if type(t) == "table" then local foo = t else local foo = t end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("{| x: number |} & {| y: number |}", toString(requireTypeAtPosition({4, 28}))); CHECK_EQ("nil", toString(requireTypeAtPosition({6, 28}))); } TEST_CASE_FIXTURE(Fixture, "type_guard_can_filter_for_overloaded_function") { CheckResult result = check(R"( type SomeOverloadedFunction = ((number) -> string) & ((string) -> number) local function f(g: SomeOverloadedFunction?) if type(g) == "function" then local foo = g else local foo = g end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("((number) -> string) & ((string) -> number)", toString(requireTypeAtPosition({4, 28}))); CHECK_EQ("nil", toString(requireTypeAtPosition({6, 28}))); } TEST_CASE_FIXTURE(Fixture, "type_guard_warns_on_no_overlapping_types_only_when_sense_is_true") { CheckResult result = check(R"( local function f(t: {x: number}) if type(t) ~= "table" then local foo = t error(("Expected a table, got %s"):format(type(t))) end return t.x + 1 end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("*unknown*", toString(requireTypeAtPosition({3, 28}))); } TEST_CASE_FIXTURE(Fixture, "not_a_or_not_b") { CheckResult result = check(R"( local function f(a: number?, b: number?) if (not a) or (not b) then local foo = a local bar = b end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("number?", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("number?", toString(requireTypeAtPosition({4, 28}))); } TEST_CASE_FIXTURE(Fixture, "not_a_or_not_b2") { CheckResult result = check(R"( local function f(a: number?, b: number?) if not (a and b) then local foo = a local bar = b end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("number?", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("number?", toString(requireTypeAtPosition({4, 28}))); } TEST_CASE_FIXTURE(Fixture, "not_a_and_not_b") { CheckResult result = check(R"( local function f(a: number?, b: number?) if (not a) and (not b) then local foo = a local bar = b end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("nil", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("nil", toString(requireTypeAtPosition({4, 28}))); } TEST_CASE_FIXTURE(Fixture, "not_a_and_not_b2") { CheckResult result = check(R"( local function f(a: number?, b: number?) if not (a or b) then local foo = a local bar = b end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("nil", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("nil", toString(requireTypeAtPosition({4, 28}))); } TEST_CASE_FIXTURE(Fixture, "either_number_or_string") { CheckResult result = check(R"( local function f(x: any) if type(x) == "number" or type(x) == "string" then local foo = x end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("number | string", toString(requireTypeAtPosition({3, 28}))); } TEST_CASE_FIXTURE(Fixture, "not_t_or_some_prop_of_t") { CheckResult result = check(R"( local function f(t: {x: boolean}?) if not t or t.x then local foo = t end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("{| x: boolean |}?", toString(requireTypeAtPosition({3, 28}))); } TEST_CASE_FIXTURE(Fixture, "assert_a_to_be_truthy_then_assert_a_to_be_number") { CheckResult result = check(R"( local a: (number | string)? assert(a) local b = a assert(type(a) == "number") local c = a )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("number | string", toString(requireTypeAtPosition({3, 18}))); CHECK_EQ("number", toString(requireTypeAtPosition({5, 18}))); } TEST_CASE_FIXTURE(Fixture, "merge_should_be_fully_agnostic_of_hashmap_ordering") { // This bug came up because there was a mistake in Luau::merge where zipping on two maps would produce the wrong merged result. CheckResult result = check(R"( local function f(b: string | { x: string }, a) assert(type(a) == "string") assert(type(b) == "string" or type(b) == "table") if type(b) == "string" then local foo = b end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("string", toString(requireTypeAtPosition({6, 28}))); } TEST_CASE_FIXTURE(Fixture, "refine_the_correct_types_opposite_of_when_a_is_not_number_or_string") { CheckResult result = check(R"( local function f(a: string | number | boolean) if type(a) ~= "number" and type(a) ~= "string" then local foo = a else local foo = a end end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("boolean", toString(requireTypeAtPosition({3, 28}))); CHECK_EQ("number | string", toString(requireTypeAtPosition({5, 28}))); } TEST_CASE_FIXTURE(Fixture, "is_truthy_constraint_ifelse_expression") { ScopedFastFlag sff1{"LuauIfElseExpressionBaseSupport", true}; ScopedFastFlag sff2{"LuauIfElseExpressionAnalysisSupport", true}; CheckResult result = check(R"( function f(v:string?) return if v then v else tostring(v) end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("string", toString(requireTypeAtPosition({2, 29}))); CHECK_EQ("nil", toString(requireTypeAtPosition({2, 45}))); } TEST_CASE_FIXTURE(Fixture, "invert_is_truthy_constraint_ifelse_expression") { ScopedFastFlag sff1{"LuauIfElseExpressionBaseSupport", true}; ScopedFastFlag sff2{"LuauIfElseExpressionAnalysisSupport", true}; CheckResult result = check(R"( function f(v:string?) return if not v then tostring(v) else v end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("nil", toString(requireTypeAtPosition({2, 42}))); CHECK_EQ("string", toString(requireTypeAtPosition({2, 50}))); } TEST_CASE_FIXTURE(Fixture, "type_comparison_ifelse_expression") { ScopedFastFlag sff1{"LuauIfElseExpressionBaseSupport", true}; ScopedFastFlag sff2{"LuauIfElseExpressionAnalysisSupport", true}; CheckResult result = check(R"( function returnOne(x) return 1 end function f(v:any) return if typeof(v) == "number" then v else returnOne(v) end )"); LUAU_REQUIRE_NO_ERRORS(result); CHECK_EQ("number", toString(requireTypeAtPosition({6, 49}))); CHECK_EQ("any", toString(requireTypeAtPosition({6, 66}))); } TEST_CASE_FIXTURE(Fixture, "apply_refinements_on_astexprindexexpr_whose_subscript_expr_is_constant_string") { ScopedFastFlag sff{"LuauRefiLookupFromIndexExpr", true}; CheckResult result = check(R"( type T = { [string]: { prop: number }? } local t: T = {} if t["hello"] then local foo = t["hello"].prop end )"); LUAU_REQUIRE_NO_ERRORS(result); } TEST_SUITE_END();