mirror of
https://github.com/luau-lang/luau.git
synced 2024-12-13 13:30:40 +00:00
f5dabc2998
In this update we improve overall stability of the new type solver and address some type inference issues with it. If you use the new solver and want to use all new fixes included in this release, you have to reference an additional Luau flag: ```c++ LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) ``` And set its value to `644`: ```c++ DFInt::LuauTypeSolverRelease.value = 644; // Or a higher value for future updates ``` ## New Solver * Fixed a debug assertion failure in autocomplete (Fixes #1391) * Fixed type function distribution issue which transformed `len<>` and `unm<>` into `not<>` (Fixes #1416) * Placed a limit on the possible normalized table intersection size as a temporary measure to avoid hangs and out-of-memory issues for complex type refinements * Internal recursion limits are now respected in the subtyping operations and in autocomplete, to avoid stack overflow crashes * Fixed false positive errors on assignments to tables whose indexers are unions of strings * Fixed memory corruption crashes in subtyping of generic types containing other generic types in their bounds --- Internal Contributors: Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
2374 lines
70 KiB
C++
2374 lines
70 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
#include "Luau/Normalize.h"
|
|
#include "Luau/Scope.h"
|
|
#include "Luau/TypeInfer.h"
|
|
|
|
#include "Fixture.h"
|
|
|
|
#include "doctest.h"
|
|
|
|
LUAU_FASTFLAG(LuauSolverV2)
|
|
LUAU_FASTFLAG(LuauUseNormalizeIntersectionLimit)
|
|
|
|
using namespace Luau;
|
|
|
|
namespace
|
|
{
|
|
std::optional<WithPredicate<TypePackId>> magicFunctionInstanceIsA(
|
|
TypeChecker& typeChecker,
|
|
const ScopePtr& scope,
|
|
const AstExprCall& expr,
|
|
WithPredicate<TypePackId> withPredicate
|
|
)
|
|
{
|
|
if (expr.args.size != 1)
|
|
return std::nullopt;
|
|
|
|
auto index = expr.func->as<Luau::AstExprIndexName>();
|
|
auto str = expr.args.data[0]->as<Luau::AstExprConstantString>();
|
|
if (!index || !str)
|
|
return std::nullopt;
|
|
|
|
std::optional<LValue> lvalue = tryGetLValue(*index->expr);
|
|
std::optional<TypeFun> tfun = scope->lookupType(std::string(str->value.data, str->value.size));
|
|
if (!lvalue || !tfun)
|
|
return std::nullopt;
|
|
|
|
ModulePtr module = typeChecker.currentModule;
|
|
TypePackId booleanPack = module->internalTypes.addTypePack({typeChecker.booleanType});
|
|
return WithPredicate<TypePackId>{booleanPack, {IsAPredicate{std::move(*lvalue), expr.location, tfun->type}}};
|
|
}
|
|
|
|
void dcrMagicRefinementInstanceIsA(const MagicRefinementContext& ctx)
|
|
{
|
|
if (ctx.callSite->args.size != 1 || ctx.discriminantTypes.empty())
|
|
return;
|
|
|
|
auto index = ctx.callSite->func->as<Luau::AstExprIndexName>();
|
|
auto str = ctx.callSite->args.data[0]->as<Luau::AstExprConstantString>();
|
|
if (!index || !str)
|
|
return;
|
|
|
|
std::optional<TypeId> discriminantTy = ctx.discriminantTypes[0];
|
|
if (!discriminantTy)
|
|
return;
|
|
|
|
std::optional<TypeFun> tfun = ctx.scope->lookupType(std::string(str->value.data, str->value.size));
|
|
if (!tfun)
|
|
return;
|
|
|
|
LUAU_ASSERT(get<BlockedType>(*discriminantTy));
|
|
asMutable(*discriminantTy)->ty.emplace<BoundType>(tfun->type);
|
|
}
|
|
|
|
struct RefinementClassFixture : BuiltinsFixture
|
|
{
|
|
RefinementClassFixture()
|
|
{
|
|
TypeArena& arena = frontend.globals.globalTypes;
|
|
NotNull<Scope> scope{frontend.globals.globalScope.get()};
|
|
|
|
std::optional<TypeId> rootSuper = std::make_optional(builtinTypes->classType);
|
|
|
|
unfreeze(arena);
|
|
TypeId vec3 = arena.addType(ClassType{"Vector3", {}, rootSuper, std::nullopt, {}, nullptr, "Test", {}});
|
|
getMutable<ClassType>(vec3)->props = {
|
|
{"X", Property{builtinTypes->numberType}},
|
|
{"Y", Property{builtinTypes->numberType}},
|
|
{"Z", Property{builtinTypes->numberType}},
|
|
};
|
|
|
|
TypeId inst = arena.addType(ClassType{"Instance", {}, rootSuper, std::nullopt, {}, nullptr, "Test", {}});
|
|
|
|
TypePackId isAParams = arena.addTypePack({inst, builtinTypes->stringType});
|
|
TypePackId isARets = arena.addTypePack({builtinTypes->booleanType});
|
|
TypeId isA = arena.addType(FunctionType{isAParams, isARets});
|
|
getMutable<FunctionType>(isA)->magicFunction = magicFunctionInstanceIsA;
|
|
getMutable<FunctionType>(isA)->dcrMagicRefinement = dcrMagicRefinementInstanceIsA;
|
|
|
|
getMutable<ClassType>(inst)->props = {
|
|
{"Name", Property{builtinTypes->stringType}},
|
|
{"IsA", Property{isA}},
|
|
};
|
|
|
|
TypeId folder = frontend.globals.globalTypes.addType(ClassType{"Folder", {}, inst, std::nullopt, {}, nullptr, "Test", {}});
|
|
TypeId part = frontend.globals.globalTypes.addType(ClassType{"Part", {}, inst, std::nullopt, {}, nullptr, "Test", {}});
|
|
getMutable<ClassType>(part)->props = {
|
|
{"Position", Property{vec3}},
|
|
};
|
|
|
|
frontend.globals.globalScope->exportedTypeBindings["Vector3"] = TypeFun{{}, vec3};
|
|
frontend.globals.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, inst};
|
|
frontend.globals.globalScope->exportedTypeBindings["Folder"] = TypeFun{{}, folder};
|
|
frontend.globals.globalScope->exportedTypeBindings["Part"] = TypeFun{{}, part};
|
|
|
|
for (const auto& [name, ty] : frontend.globals.globalScope->exportedTypeBindings)
|
|
persist(ty.type);
|
|
|
|
freeze(frontend.globals.globalTypes);
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
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, "a_and_b_or_a_and_c")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(a: string?, b: number?, c: boolean)
|
|
if (a and b) or (a and c) then
|
|
local foo = a
|
|
local bar = b
|
|
local baz = c
|
|
else
|
|
local foo = a
|
|
local bar = b
|
|
local baz = c
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("number?", toString(requireTypeAtPosition({4, 28})));
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ("boolean", toString(requireTypeAtPosition({5, 28})));
|
|
else
|
|
CHECK_EQ("true", toString(requireTypeAtPosition({5, 28}))); // oh no! :(
|
|
|
|
CHECK_EQ("string?", toString(requireTypeAtPosition({7, 28})));
|
|
CHECK_EQ("number?", toString(requireTypeAtPosition({8, 28})));
|
|
CHECK_EQ("boolean", toString(requireTypeAtPosition({9, 28})));
|
|
}
|
|
|
|
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);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
CHECK_EQ("number?", toString(requireTypeAtPosition({3, 26})));
|
|
CHECK_EQ("string?", toString(requireTypeAtPosition({4, 26})));
|
|
}
|
|
else
|
|
{
|
|
// We're going to drop support for type refinements through type assertions.
|
|
CHECK_EQ("number", toString(requireTypeAtPosition({3, 26})));
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({4, 26})));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_in_if_condition_position")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(s: any, t: unknown)
|
|
if type(s) == "number" then
|
|
local n = s
|
|
end
|
|
if type(t) == "number" then
|
|
local n = t
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
// DCR changes refinements to preserve error suppression.
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ("*error-type* | number", toString(requireTypeAtPosition({3, 26})));
|
|
else
|
|
CHECK_EQ("number", toString(requireTypeAtPosition({3, 26})));
|
|
CHECK_EQ("number", toString(requireTypeAtPosition({6, 26})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_in_assert_position")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(a)
|
|
assert(type(a) == "number")
|
|
local b = a
|
|
return b
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK("<a>(a) -> a & number" == toString(requireType("f")));
|
|
else
|
|
CHECK("<a>(a) -> number" == toString(requireType("f")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table_then_test_a_prop")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: unknown): string?
|
|
if typeof(x) == "table" then
|
|
if typeof(x.foo) == "string" then
|
|
return x.foo
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
else
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
|
|
for (size_t i = 0; i < result.errors.size(); i++)
|
|
{
|
|
const UnknownProperty* up = get<UnknownProperty>(result.errors[i]);
|
|
REQUIRE_EQ("foo", up->key);
|
|
REQUIRE_EQ("unknown", toString(up->table));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table_then_test_a_nested_prop")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: unknown): string?
|
|
if typeof(x) == "table" then
|
|
-- this should error, `x.foo` is an unknown property
|
|
if typeof(x.foo.bar) == "string" then
|
|
return x.foo.bar
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
const UnknownProperty* up = get<UnknownProperty>(result.errors[0]);
|
|
REQUIRE_EQ("bar", up->key);
|
|
REQUIRE_EQ("unknown", toString(up->table));
|
|
}
|
|
else
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
|
|
for (size_t i = 0; i < result.errors.size(); i++)
|
|
{
|
|
const UnknownProperty* up = get<UnknownProperty>(result.errors[i]);
|
|
REQUIRE_EQ("foo", up->key);
|
|
REQUIRE_EQ("unknown", toString(up->table));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table_then_test_a_tested_nested_prop")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: unknown): string?
|
|
if typeof(x) == "table" then
|
|
if typeof(x.foo) == "table" and typeof(x.foo.bar) == "string" then
|
|
return x.foo.bar
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
else
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(3, result);
|
|
|
|
for (size_t i = 0; i < result.errors.size(); i++)
|
|
{
|
|
const UnknownProperty* up = get<UnknownProperty>(result.errors[i]);
|
|
REQUIRE_EQ("foo", up->key);
|
|
REQUIRE_EQ("unknown", toString(up->table));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "call_an_incompatible_function_after_using_typeguard")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: number)
|
|
return x
|
|
end
|
|
|
|
local function g(x: unknown)
|
|
if type(x) == "string" then
|
|
f(x)
|
|
end
|
|
end
|
|
|
|
local function h(x: any)
|
|
if type(x) == "string" then
|
|
f(x)
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
|
|
CHECK("Type 'string' could not be converted into 'number'" == toString(result.errors[0]));
|
|
CHECK(Location{{ 7, 18}, {7, 19}} == result.errors[0].location);
|
|
|
|
CHECK("Type 'string' could not be converted into 'number'" == toString(result.errors[1]));
|
|
CHECK(Location{{ 13, 18}, {13, 19}} == result.errors[1].location);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "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 t2 = t
|
|
local foo = t.x
|
|
end
|
|
|
|
local bar = t.x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
// CLI-115281 - Types produced by refinements don't always get simplified
|
|
CHECK("{ x: number? } & { x: ~(false?) }" == toString(requireTypeAtPosition({4, 23})));
|
|
CHECK("number" == toString(requireTypeAtPosition({5, 26})));
|
|
}
|
|
|
|
CHECK_EQ("number?", toString(requireType("bar")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "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(BuiltinsFixture, "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, "lvalue_is_equal_to_another_lvalue")
|
|
{
|
|
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);
|
|
|
|
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
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "lvalue_is_equal_to_a_term")
|
|
{
|
|
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);
|
|
|
|
CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "(number | string)?"); // a == 1;
|
|
CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "(number | string)?"); // a ~= 1
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "term_is_equal_to_an_lvalue")
|
|
{
|
|
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::LuauSolverV2)
|
|
{
|
|
CHECK_EQ(toString(requireTypeAtPosition({3, 28})), R"("hello")"); // a == "hello"
|
|
CHECK_EQ(toString(requireTypeAtPosition({5, 28})), R"(((string & ~"hello") | number)?)"); // a ~= "hello"
|
|
}
|
|
else
|
|
{
|
|
CHECK_EQ(toString(requireTypeAtPosition({3, 28})), R"("hello")"); // a == "hello"
|
|
CHECK_EQ(toString(requireTypeAtPosition({5, 28})), R"((number | string)?)"); // a ~= "hello"
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "lvalue_is_not_nil")
|
|
{
|
|
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);
|
|
|
|
CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "number | string"); // a ~= nil
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "nil"); // a == nil :)
|
|
else
|
|
CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "(number | string)?"); // a == nil
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "free_type_is_equal_to_an_lvalue")
|
|
{
|
|
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::LuauSolverV2)
|
|
CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "unknown"); // a == b
|
|
else
|
|
CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "a"); // 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")
|
|
{
|
|
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);
|
|
|
|
CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "any"); // a ~= b
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "{ x: number }?"); // a ~= b
|
|
else
|
|
CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "{| x: number |}?"); // a ~= b
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "string_not_equal_to_string_or_nil")
|
|
{
|
|
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::LuauSolverV2)
|
|
{
|
|
CHECK_EQ(toString(requireTypeAtPosition({8, 29})), "string?"); // a == b
|
|
CHECK_EQ(toString(requireTypeAtPosition({8, 32})), "string?"); // a == b
|
|
}
|
|
else
|
|
{
|
|
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(BuiltinsFixture, "type_narrow_to_vector")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x)
|
|
if type(x) == "vector" then
|
|
local foo = x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ("never", toString(requireTypeAtPosition({3, 28})));
|
|
else
|
|
CHECK_EQ("*error-type*", toString(requireTypeAtPosition({3, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "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);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
// CLI-115281 Types produced by refinements do not consistently get simplified
|
|
CHECK_EQ("(nil & string)?", toString(requireTypeAtPosition({4, 24}))); // type(v) == "nil"
|
|
CHECK_EQ("(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({6, 24}))); // type(v) ~= "nil"
|
|
|
|
CHECK_EQ("(nil & string)?", toString(requireTypeAtPosition({10, 24}))); // equivalent to type(v) == "nil"
|
|
CHECK_EQ("(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({12, 24}))); // equivalent to type(v) ~= "nil"
|
|
}
|
|
else
|
|
{
|
|
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(BuiltinsFixture, "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(BuiltinsFixture, "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);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ("{ x: number } | { y: boolean }", toString(requireTypeAtPosition({3, 28}))); // type(x) == "table"
|
|
else
|
|
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(BuiltinsFixture, "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"
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "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);
|
|
|
|
ToStringOptions opts;
|
|
opts.exhaustive = true;
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ("{ x: number } & { y: number }", toString(requireTypeAtPosition({4, 28}), opts));
|
|
else
|
|
CHECK_EQ("{| x: number |} & {| y: number |}", toString(requireTypeAtPosition({4, 28}), opts));
|
|
CHECK_EQ("nil", toString(requireTypeAtPosition({6, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "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(BuiltinsFixture, "type_guard_narrowed_into_nothingness")
|
|
{
|
|
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);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
// CLI-115281 Types produced by refinements do not consistently get simplified
|
|
CHECK_EQ("{ x: number } & ~table", toString(requireTypeAtPosition({3, 28})));
|
|
}
|
|
else
|
|
CHECK_EQ("never", 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(BuiltinsFixture, "either_number_or_string")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: any, y: unknown)
|
|
if type(x) == "number" or type(x) == "string" then
|
|
local foo = x
|
|
end
|
|
if type(y) == "number" or type(y) == "string" then
|
|
local foo = y
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ("*error-type* | number | string", toString(requireTypeAtPosition({3, 28})));
|
|
else
|
|
CHECK_EQ("number | string", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("number | string", toString(requireTypeAtPosition({6, 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);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
// CLI-115281 Types produced by refinements do not consistently get simplified
|
|
CHECK_EQ("({ x: boolean } & { x: ~(false?) })?", toString(requireTypeAtPosition({3, 28})));
|
|
}
|
|
else
|
|
CHECK_EQ("{| x: boolean |}?", toString(requireTypeAtPosition({3, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "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(BuiltinsFixture, "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(BuiltinsFixture, "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(BuiltinsFixture, "is_truthy_constraint_ifelse_expression")
|
|
{
|
|
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(BuiltinsFixture, "invert_is_truthy_constraint_ifelse_expression")
|
|
{
|
|
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(BuiltinsFixture, "type_comparison_ifelse_expression")
|
|
{
|
|
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
|
|
|
|
function g(v:unknown)
|
|
return if typeof(v) == "number" then v else returnOne(v)
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
CHECK_EQ("*error-type* | number", toString(requireTypeAtPosition({6, 49})));
|
|
CHECK_EQ("*error-type* | ~number", toString(requireTypeAtPosition({6, 66})));
|
|
}
|
|
else
|
|
{
|
|
CHECK_EQ("number", toString(requireTypeAtPosition({6, 49})));
|
|
CHECK_EQ("any", toString(requireTypeAtPosition({6, 66})));
|
|
}
|
|
|
|
CHECK_EQ("number", toString(requireTypeAtPosition({10, 49})));
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ("~number", toString(requireTypeAtPosition({10, 66})));
|
|
else
|
|
CHECK_EQ("unknown", toString(requireTypeAtPosition({10, 66})));
|
|
}
|
|
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "is_truthy_constraint_while_expression")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(v:string?)
|
|
while v do
|
|
local foo = v
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({3, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "invert_is_truthy_constraint_while_expression")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(v:string?)
|
|
while not v do
|
|
local foo = v
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("nil", toString(requireTypeAtPosition({3, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_the_correct_types_opposite_of_while_a_is_not_number_or_string")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(a: string | number | boolean)
|
|
while type(a) ~= "number" and type(a) ~= "string" do
|
|
local foo = a
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("boolean", toString(requireTypeAtPosition({3, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "correctly_lookup_a_shadowed_local_that_which_was_previously_refined")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local foo: string? = "hi"
|
|
assert(foo)
|
|
local foo: number = 5
|
|
print(foo:sub(1, 1))
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
CHECK_EQ("Type 'number' does not have key 'sub'", toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "correctly_lookup_property_whose_base_was_previously_refined")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type T = {x: string | number}
|
|
local t: T? = {x = "hi"}
|
|
if t then
|
|
if type(t.x) == "string" then
|
|
local foo = t.x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({5, 30})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "correctly_lookup_property_whose_base_was_previously_refined2")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type T = { x: { y: number }? }
|
|
|
|
local function f(t: T?)
|
|
if t and t.x then
|
|
local foo = t.x.y
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("number", toString(requireTypeAtPosition({5, 32})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "apply_refinements_on_astexprindexexpr_whose_subscript_expr_is_constant_string")
|
|
{
|
|
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_CASE_FIXTURE(Fixture, "discriminate_from_truthiness_of_x")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type T = {tag: "missing", x: nil} | {tag: "exists", x: string}
|
|
|
|
local function f(t: T)
|
|
if t.x then
|
|
local foo = t
|
|
else
|
|
local bar = t
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
// CLI-115281 Types produced by refinements do not consistently get simplified
|
|
CHECK("{ tag: \"exists\", x: string } & { x: ~(false?) }" == toString(requireTypeAtPosition({5, 28})));
|
|
CHECK("({ tag: \"exists\", x: string } & { x: ~~(false?) }) | { tag: \"missing\", x: nil }" == toString(requireTypeAtPosition({7, 28})));
|
|
}
|
|
else
|
|
{
|
|
CHECK_EQ(R"({| tag: "exists", x: string |})", toString(requireTypeAtPosition({5, 28})));
|
|
CHECK_EQ(R"({| tag: "exists", x: string |} | {| tag: "missing", x: nil |})", toString(requireTypeAtPosition({7, 28})));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "discriminate_tag")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Cat = {tag: "Cat", name: string, catfood: string}
|
|
type Dog = {tag: "Dog", name: string, dogfood: string}
|
|
type Animal = Cat | Dog
|
|
|
|
local function f(animal: Animal)
|
|
if animal.tag == "Cat" then
|
|
local cat = animal
|
|
elseif animal.tag == "Dog" then
|
|
local dog = animal
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("Cat", toString(requireTypeAtPosition({7, 33})));
|
|
CHECK_EQ("Dog", toString(requireTypeAtPosition({9, 33})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "discriminate_tag_with_implicit_else")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Cat = {tag: "Cat", name: string, catfood: string}
|
|
type Dog = {tag: "Dog", name: string, dogfood: string}
|
|
type Animal = Cat | Dog
|
|
|
|
local function f(animal: Animal)
|
|
if animal.tag == "Cat" then
|
|
local cat = animal
|
|
else
|
|
local dog = animal
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("Cat", toString(requireTypeAtPosition({7, 33})));
|
|
CHECK_EQ("Dog", toString(requireTypeAtPosition({9, 33})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "and_or_peephole_refinement")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function len(a: {any})
|
|
return a and #a or nil
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "narrow_boolean_to_true_or_false")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: boolean)
|
|
if x then
|
|
local foo = x
|
|
else
|
|
local foo = x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("true", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("false", toString(requireTypeAtPosition({5, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "discriminate_on_properties_of_disjoint_tables_where_that_property_is_true_or_false")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Ok<T> = { ok: true, value: T }
|
|
type Err<E> = { ok: false, error: E }
|
|
type Result<T, E> = Ok<T> | Err<E>
|
|
|
|
local function apply<T, E>(t: Result<T, E>, f: (T) -> (), g: (E) -> ())
|
|
if t.ok then
|
|
f(t.value)
|
|
else
|
|
g(t.error)
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "refine_a_property_not_to_be_nil_through_an_intersection_table")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type T = {} & {f: ((string) -> string)?}
|
|
local function f(t: T, x)
|
|
if t.f then
|
|
t.f(x)
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(RefinementClassFixture, "discriminate_from_isa_of_x")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type T = {tag: "Part", x: Part} | {tag: "Folder", x: Folder}
|
|
|
|
local function f(t: T)
|
|
if t.x:IsA("Part") then
|
|
local foo = t
|
|
else
|
|
local bar = t
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
CHECK(R"({ tag: "Part", x: Part })" == toString(requireTypeAtPosition({5, 28})));
|
|
CHECK(R"({ tag: "Folder", x: Folder })" == toString(requireTypeAtPosition({7, 28})));
|
|
}
|
|
else
|
|
{
|
|
CHECK_EQ(R"({| tag: "Part", x: Part |})", toString(requireTypeAtPosition({5, 28})));
|
|
CHECK_EQ(R"({| tag: "Folder", x: Folder |})", toString(requireTypeAtPosition({7, 28})));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(RefinementClassFixture, "typeguard_cast_free_table_to_vector")
|
|
{
|
|
// CLI-115286 - Refining via type(x) == 'vector' does not work in the new solver
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
|
|
|
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_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("Vector3", toString(requireTypeAtPosition({5, 28}))); // type(vec) == "vector"
|
|
|
|
CHECK_EQ("never", toString(requireTypeAtPosition({7, 28}))); // typeof(vec) == "Instance"
|
|
|
|
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, "type_narrow_but_the_discriminant_type_isnt_a_class")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: string | number | Instance | Vector3)
|
|
if type(x) == "any" then
|
|
local foo = x
|
|
else
|
|
local foo = x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
CHECK_EQ("never", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("Instance | Vector3 | number | string", toString(requireTypeAtPosition({5, 28})));
|
|
}
|
|
else
|
|
{
|
|
CHECK_EQ("*error-type*", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("*error-type*", 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_from_subclasses_of_instance_or_string_or_vector3")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: Part | Folder | string | Vector3)
|
|
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("Vector3 | string", toString(requireTypeAtPosition({5, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(RefinementClassFixture, "x_as_any_if_x_is_instance_elseif_x_is_table")
|
|
{
|
|
// CLI-117136 - this code doesn't finish constraint solving and has blocked types in the output
|
|
if (FFlag::LuauSolverV2)
|
|
return;
|
|
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);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
CHECK_EQ("Folder & Instance & {- -}", toString(requireTypeAtPosition({5, 28})));
|
|
CHECK_EQ("(~Folder | ~Instance) & {- -} & never", toString(requireTypeAtPosition({7, 28})));
|
|
}
|
|
else
|
|
{
|
|
CHECK_EQ("Folder", toString(requireTypeAtPosition({5, 28})));
|
|
CHECK_EQ("any", toString(requireTypeAtPosition({7, 28})));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(RefinementClassFixture, "refine_param_of_type_instance_without_using_typeof")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: Instance)
|
|
if 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({3, 28})));
|
|
CHECK_EQ("never", toString(requireTypeAtPosition({5, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(RefinementClassFixture, "refine_param_of_type_folder_or_part_without_using_typeof")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: Part | Folder)
|
|
if x:IsA("Folder") then
|
|
local foo = x
|
|
else
|
|
local foo = x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("Folder", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("Part", toString(requireTypeAtPosition({5, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(RefinementClassFixture, "isa_type_refinement_must_be_known_ahead_of_time")
|
|
{
|
|
// CLI-115087 - The new solver does not consistently combine tables with
|
|
// class types when they appear in the upper bounds of a free type.
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
|
|
|
CheckResult result = check(R"(
|
|
local function f(x): Instance
|
|
if x:IsA("Folder") then
|
|
local foo = x
|
|
else
|
|
local foo = x
|
|
end
|
|
|
|
return x
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("Instance", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("Instance", toString(requireTypeAtPosition({5, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(RefinementClassFixture, "x_is_not_instance_or_else_not_part")
|
|
{
|
|
// CLI-117135 - RefinementTests.x_is_not_instance_or_else_not_part not correctly applying refinements to a function parameter
|
|
if (FFlag::LuauSolverV2)
|
|
return;
|
|
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(BuiltinsFixture, "typeguard_doesnt_leak_to_elseif")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(a)
|
|
if type(a) == "boolean" then
|
|
local a1 = a
|
|
elseif a.fn() then
|
|
local a2 = a
|
|
else
|
|
local a3 = a
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknowns")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: unknown)
|
|
if type(x) == "string" then
|
|
local foo = x
|
|
else
|
|
local bar = x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("~string", toString(requireTypeAtPosition({5, 28})));
|
|
}
|
|
else
|
|
{
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("unknown", toString(requireTypeAtPosition({5, 28})));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_boolean")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: number | boolean)
|
|
if typeof(x) == "boolean" then
|
|
local foo = x
|
|
else
|
|
local foo = x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK_EQ("boolean", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("number", toString(requireTypeAtPosition({5, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_thread")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: number | thread)
|
|
if typeof(x) == "thread" then
|
|
local foo = x
|
|
else
|
|
local foo = x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK_EQ("thread", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("number", toString(requireTypeAtPosition({5, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_buffer")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: number | buffer)
|
|
if typeof(x) == "buffer" then
|
|
local foo = x
|
|
else
|
|
local foo = x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK_EQ("buffer", toString(requireTypeAtPosition({3, 28})));
|
|
CHECK_EQ("number", toString(requireTypeAtPosition({5, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "falsiness_of_TruthyPredicate_narrows_into_nil")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(t: {number})
|
|
local x = t[1]
|
|
if not x then
|
|
local foo = x
|
|
else
|
|
local bar = x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("nil", toString(requireTypeAtPosition({4, 28})));
|
|
CHECK_EQ("number", toString(requireTypeAtPosition({6, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "what_nonsensical_condition")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x)
|
|
if type(x) == "string" and type(x) == "number" then
|
|
local foo = x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("never", toString(requireTypeAtPosition({3, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "else_with_no_explicit_expression_should_also_refine_the_tagged_union")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Ok<T> = { tag: "ok", value: T }
|
|
type Err<E> = { tag: "err", err: E }
|
|
type Result<T, E> = Ok<T> | Err<E>
|
|
|
|
function and_then<T, U, E>(r: Result<T, E>, f: (T) -> U): Result<U, E>
|
|
if r.tag == "ok" then
|
|
return { tag = "ok", value = f(r.value) }
|
|
else
|
|
return r
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "fuzz_filtered_refined_types_are_followed")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local _
|
|
do
|
|
local _ = _ ~= _ or _ or _
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table_then_take_the_length")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: unknown)
|
|
if typeof(x) == "table" then
|
|
local len = #x
|
|
end
|
|
end
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK_EQ("table", toString(requireTypeAtPosition({3, 29})));
|
|
}
|
|
else
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ("unknown", toString(requireTypeAtPosition({3, 29})));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table_then_clone_it")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: unknown)
|
|
if typeof(x) == "table" then
|
|
local cloned: {} = table.clone(x)
|
|
end
|
|
end
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
else
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(RefinementClassFixture, "refine_a_param_that_got_resolved_during_constraint_solving_stage")
|
|
{
|
|
// CLI-117134 - Applying a refinement causes an optional value access error.
|
|
if (FFlag::LuauSolverV2)
|
|
return;
|
|
CheckResult result = check(R"(
|
|
type Id<T> = T
|
|
|
|
local function f(x: Id<Id<Part | Folder> | Id<string>>)
|
|
if typeof(x) ~= "string" and x:IsA("Part") then
|
|
local foo = x
|
|
else
|
|
local foo = x
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK_EQ("Part", toString(requireTypeAtPosition({5, 28})));
|
|
CHECK_EQ("Folder | string", toString(requireTypeAtPosition({7, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(RefinementClassFixture, "refine_a_param_that_got_resolved_during_constraint_solving_stage_2")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function hof(f: (Instance) -> ()) end
|
|
|
|
hof(function(inst)
|
|
if inst:IsA("Part") then
|
|
local foo = inst
|
|
else
|
|
local foo = inst
|
|
end
|
|
end)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK_EQ("Part", toString(requireTypeAtPosition({5, 28})));
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ("Instance & ~Part", toString(requireTypeAtPosition({7, 28})));
|
|
else
|
|
CHECK_EQ("Instance", toString(requireTypeAtPosition({7, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "refine_a_property_of_some_global")
|
|
{
|
|
CheckResult result = check(R"(
|
|
foo = { bar = 5 :: number? }
|
|
|
|
if foo.bar then
|
|
local bar = foo.bar
|
|
end
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(3, result);
|
|
|
|
CHECK_EQ("*error-type* | buffer | class | function | number | string | table | thread | true", toString(requireTypeAtPosition({4, 30})));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(t: {string}, s: string)
|
|
local v1 = t[5]
|
|
local v2 = v1
|
|
|
|
if typeof(v1) == "nil" then
|
|
local foo = v1
|
|
else
|
|
local foo = v1
|
|
end
|
|
|
|
if typeof(v2) == "nil" then
|
|
local foo = v2
|
|
else
|
|
local foo = v2
|
|
end
|
|
|
|
if typeof(s) == "nil" then
|
|
local foo = s -- line 18
|
|
else
|
|
local foo = s -- line 20
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("nil", toString(requireTypeAtPosition({6, 28})));
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({8, 28})));
|
|
|
|
CHECK_EQ("nil", toString(requireTypeAtPosition({12, 28})));
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({14, 28})));
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
// CLI-115281 - Types produced by refinements don't always get simplified
|
|
CHECK_EQ("nil & string", toString(requireTypeAtPosition({18, 28})));
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({20, 28})));
|
|
}
|
|
else
|
|
{
|
|
CHECK_EQ("nil", toString(requireTypeAtPosition({18, 28})));
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({20, 28})));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "cat_or_dog_through_a_local")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Cat = { tag: "cat", catfood: string }
|
|
type Dog = { tag: "dog", dogfood: string }
|
|
type Animal = Cat | Dog
|
|
|
|
local function f(animal: Animal)
|
|
local tag = animal.tag
|
|
if tag == "dog" then
|
|
local dog = animal
|
|
elseif tag == "cat" then
|
|
local cat = animal
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("Cat | Dog", toString(requireTypeAtPosition({8, 28})));
|
|
CHECK_EQ("Cat | Dog", toString(requireTypeAtPosition({10, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "prove_that_dataflow_analysis_isnt_doing_alias_tracking_yet")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(tag: "cat" | "dog")
|
|
local tag2 = tag
|
|
|
|
if tag2 == "cat" then
|
|
local foo = tag
|
|
else
|
|
local foo = tag
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ(R"("cat" | "dog")", toString(requireTypeAtPosition({5, 28})));
|
|
CHECK_EQ(R"("cat" | "dog")", toString(requireTypeAtPosition({7, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "fail_to_refine_a_property_of_subscript_expression")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Foo = { foo: number? }
|
|
local function f(t: {Foo})
|
|
if t[1].foo then
|
|
local foo = t[1].foo
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK_EQ("number?", toString(requireTypeAtPosition({4, 34})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "type_annotations_arent_relevant_when_doing_dataflow_analysis")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function s() return "hello" end
|
|
|
|
local function f(t: {string})
|
|
local s1: string = t[5]
|
|
local s2: string = s()
|
|
|
|
if typeof(s1) == "nil" and typeof(s2) == "nil" then
|
|
local foo = s1
|
|
local bar = s2
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("nil", toString(requireTypeAtPosition({8, 28})));
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
// CLI-115478 - This should be never
|
|
CHECK_EQ("nil", toString(requireTypeAtPosition({9, 28})));
|
|
}
|
|
else
|
|
CHECK_EQ("nil", toString(requireTypeAtPosition({9, 28})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "function_call_with_colon_after_refining_not_to_be_nil")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
export type Observer<T> = {
|
|
read complete: ((self: Observer<T>) -> ())?,
|
|
}
|
|
|
|
local function _f(handler: Observer<any>)
|
|
assert(handler.complete ~= nil)
|
|
handler:complete() -- incorrectly gives Value of type '((Observer<any>) -> ())?' could be nil
|
|
handler.complete(handler) -- works fine, both forms should avoid the error
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "refinements_should_not_affect_assignment")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: unknown = true
|
|
if a == true then
|
|
a = 'not even remotely similar to a boolean'
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refinements_should_preserve_error_suppression")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: any = {}
|
|
local b
|
|
if typeof(a) == "table" then
|
|
b = a.field
|
|
end
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
else
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "many_refinements_on_val")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function is_nan(val: any): boolean
|
|
return type(val) == "number" and val ~= val
|
|
end
|
|
|
|
local function is_js_boolean(val: any): boolean
|
|
return not not val and val ~= 0 and val ~= "" and not is_nan(val)
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("(any) -> boolean", toString(requireType("is_nan")));
|
|
CHECK_EQ("(any) -> boolean", toString(requireType("is_js_boolean")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
// this test is DCR-only as an instance of DCR fixing a bug in the old solver
|
|
|
|
CheckResult result = check(R"(
|
|
local function f(a: unknown)
|
|
if typeof(a) == "table" then
|
|
for i, v in a do
|
|
return i, v
|
|
end
|
|
end
|
|
|
|
error("")
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("(unknown) -> (unknown, unknown)", toString(requireType("f")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
CheckResult result = check(R"(
|
|
local function test(element: any?)
|
|
if element then
|
|
local owner = element._owner
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "globals_can_be_narrowed_too")
|
|
{
|
|
CheckResult result = check(R"(
|
|
if typeof(string) == 'string' then
|
|
local foo = string
|
|
end
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
// CLI-114134
|
|
CHECK("string & typeof(string)" == toString(requireTypeAtPosition(Position{2, 24})));
|
|
}
|
|
else
|
|
CHECK("never" == toString(requireTypeAtPosition(Position{2, 24})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function isIndexKey(k, contiguousLength)
|
|
return type(k) == "number"
|
|
and k <= contiguousLength -- nothing out of bounds
|
|
and 1 <= k -- nothing illegal for array indices
|
|
and math.floor(k) == k -- no float keys
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction_variant")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function isIndexKey(k, contiguousLength: number)
|
|
return type(k) == "number"
|
|
and k <= contiguousLength -- nothing out of bounds
|
|
and 1 <= k -- nothing illegal for array indices
|
|
and math.floor(k) == k -- no float keys
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "ex")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: string | number)
|
|
if typeof((x)) == "string" then
|
|
local y = x
|
|
end
|
|
end
|
|
)");
|
|
TypeId t = requireTypeAtPosition({3, 18});
|
|
CHECK("string" == toString(t));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(RefinementClassFixture, "mutate_prop_of_some_refined_symbol")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function instances(): {Instance} error("") end
|
|
local function vec3(x, y, z): Vector3 error("") end
|
|
|
|
for _, object in ipairs(instances()) do
|
|
if object:IsA("Part") then
|
|
object.Position = vec3(1, 2, 3)
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(RefinementClassFixture, "mutate_prop_of_some_refined_symbol_2")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Result<T, E> = never
|
|
| { tag: "ok", value: T }
|
|
| { tag: "err", error: E }
|
|
|
|
local function results(): {Result<number, string>} error("") end
|
|
|
|
for _, res in ipairs(results()) do
|
|
if res.tag == "ok" then
|
|
res.value = 7
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "ensure_t_after_return_references_all_reachable_points")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local t = {}
|
|
|
|
local function f(k: string)
|
|
if t[k] ~= nil then
|
|
return
|
|
end
|
|
|
|
t[k] = 5
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("{ [string]: number }", toString(requireTypeAtPosition({8, 12}), {true}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "long_disjunction_of_refinements_should_not_trip_recursion_counter")
|
|
{
|
|
CHECK_NOTHROW(check(R"(
|
|
function(obj)
|
|
if script.Parent.SeatNumber.Value == "1D" or
|
|
script.Parent.SeatNumber.Value == "2D" or
|
|
script.Parent.SeatNumber.Value == "3D" or
|
|
script.Parent.SeatNumber.Value == "4D" or
|
|
script.Parent.SeatNumber.Value == "5D" or
|
|
script.Parent.SeatNumber.Value == "6D" or
|
|
script.Parent.SeatNumber.Value == "7D" or
|
|
script.Parent.SeatNumber.Value == "8D" or
|
|
script.Parent.SeatNumber.Value == "9D" or
|
|
script.Parent.SeatNumber.Value == "10D" or
|
|
script.Parent.SeatNumber.Value == "11D" or
|
|
script.Parent.SeatNumber.Value == "12D" or
|
|
script.Parent.SeatNumber.Value == "13D" or
|
|
script.Parent.SeatNumber.Value == "14D" or
|
|
script.Parent.SeatNumber.Value == "15D" or
|
|
script.Parent.SeatNumber.Value == "16D" or
|
|
script.Parent.SeatNumber.Value == "1C" or
|
|
script.Parent.SeatNumber.Value == "2C" or
|
|
script.Parent.SeatNumber.Value == "3C" or
|
|
script.Parent.SeatNumber.Value == "4C" or
|
|
script.Parent.SeatNumber.Value == "5C" or
|
|
script.Parent.SeatNumber.Value == "6C" or
|
|
script.Parent.SeatNumber.Value == "7C" or
|
|
script.Parent.SeatNumber.Value == "8C" or
|
|
script.Parent.SeatNumber.Value == "9C" or
|
|
script.Parent.SeatNumber.Value == "10C" or
|
|
script.Parent.SeatNumber.Value == "11C" or
|
|
script.Parent.SeatNumber.Value == "12C" or
|
|
script.Parent.SeatNumber.Value == "13C" or
|
|
script.Parent.SeatNumber.Value == "14C" or
|
|
script.Parent.SeatNumber.Value == "15C" or
|
|
script.Parent.SeatNumber.Value == "16C" then
|
|
end
|
|
)"));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "more_complex_long_disjunction_of_refinements_shouldnt_trip_ice")
|
|
{
|
|
CHECK_NOTHROW(check(R"(
|
|
script:connect(function(obj)
|
|
if script.Parent.SeatNumber.Value == "1D" or
|
|
script.Parent.SeatNumber.Value == "2D" or
|
|
script.Parent.SeatNumber.Value == "3D" or
|
|
script.Parent.SeatNumber.Value == "4D" or
|
|
script.Parent.SeatNumber.Value == "5D" or
|
|
script.Parent.SeatNumber.Value == "6D" or
|
|
script.Parent.SeatNumber.Value == "7D" or
|
|
script.Parent.SeatNumber.Value == "8D" or
|
|
script.Parent.SeatNumber.Value == "9D" or
|
|
script.Parent.SeatNumber.Value == "10D" or
|
|
script.Parent.SeatNumber.Value == "11D" or
|
|
script.Parent.SeatNumber.Value == "12D" or
|
|
script.Parent.SeatNumber.Value == "13D" or
|
|
script.Parent.SeatNumber.Value == "14D" or
|
|
script.Parent.SeatNumber.Value == "15D" or
|
|
script.Parent.SeatNumber.Value == "16D" or
|
|
script.Parent.SeatNumber.Value == "1C" or
|
|
script.Parent.SeatNumber.Value == "2C" or
|
|
script.Parent.SeatNumber.Value == "3C" or
|
|
script.Parent.SeatNumber.Value == "4C" or
|
|
script.Parent.SeatNumber.Value == "5C" or
|
|
script.Parent.SeatNumber.Value == "6C" or
|
|
script.Parent.SeatNumber.Value == "7C" or
|
|
script.Parent.SeatNumber.Value == "8C" or
|
|
script.Parent.SeatNumber.Value == "9C" or
|
|
script.Parent.SeatNumber.Value == "10C" or
|
|
script.Parent.SeatNumber.Value == "11C" or
|
|
script.Parent.SeatNumber.Value == "12C" or
|
|
script.Parent.SeatNumber.Value == "13C" or
|
|
script.Parent.SeatNumber.Value == "14C" or
|
|
script.Parent.SeatNumber.Value == "15C" or
|
|
script.Parent.SeatNumber.Value == "16C" then
|
|
end)
|
|
)"));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "refinements_should_avoid_building_up_big_intersect_families")
|
|
{
|
|
CHECK_NOTHROW(check(R"(
|
|
script:connect(function(obj)
|
|
if script.Parent.SeatNumber.Value == "1D" or script.Parent.SeatNumber.Value == "2D" or script.Parent.SeatNumber.Value == "3D" or script.Parent.SeatNumber.Value == "4D" or script.Parent.SeatNumber.Value == "5D" or script.Parent.SeatNumber.Value == "6D" or script.Parent.SeatNumber.Value == "7D" or script.Parent.SeatNumber.Value == "8D" or script.Parent.SeatNumber.Value == "9D" or script.Parent.SeatNumber.Value == "10D" or script.Parent.SeatNumber.Value == "11D" or script.Parent.SeatNumber.Value == "12D" or script.Parent.SeatNumber.Value == "13D" or script.Parent.SeatNumber.Value == "14D" or script.Parent.SeatNumber.Value == "15D" or script.Parent.SeatNumber.Value == "16D" or script.Parent.SeatNumber.Value == "1C" or script.Parent.SeatNumber.Value == "2C" or script.Parent.SeatNumber.Value == "3C" or script.Parent.SeatNumber.Value == "4C" or script.Parent.SeatNumber.Value == "5C" or script.Parent.SeatNumber.Value == "6C" or script.Parent.SeatNumber.Value == "7C" or script.Parent.SeatNumber.Value == "8C" or script.Parent.SeatNumber.Value == "9C" or script.Parent.SeatNumber.Value == "10C" or script.Parent.SeatNumber.Value == "11C" or script.Parent.SeatNumber.Value == "12C" or script.Parent.SeatNumber.Value == "13C" or script.Parent.SeatNumber.Value == "14C" or script.Parent.SeatNumber.Value == "15C" or script.Parent.SeatNumber.Value == "16C" then
|
|
if p.Name == script.Parent.Parent.Parent.Parent.Parent.Parent.MainParts.CD.SurfaceGui[script.Parent.SeatNumber.Value].Player.Value or script.Parent.Parent.Parent.Parent.Parent.Parent.MainParts.CD.SurfaceGui[script.Parent.SeatNumber.Value].Player.Value == "" then
|
|
else
|
|
if script.Parent:FindFirstChild("SeatWeld") then
|
|
end
|
|
end
|
|
else
|
|
if p.Name == script.Parent.Parent.Parent.Parent.Parent.Parent.MainParts.AB.SurfaceGui[script.Parent.SeatNumber.Value].Player.Value or script.Parent.Parent.Parent.Parent.Parent.Parent.MainParts.AB.SurfaceGui[script.Parent.SeatNumber.Value].Player.Value == "" then
|
|
print("Allowed")
|
|
else
|
|
if script.Parent:FindFirstChild("SeatWeld") then
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
)"));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "refinements_table_intersection_limits" * doctest::timeout(0.5))
|
|
{
|
|
ScopedFastFlag LuauUseNormalizeIntersectionLimit{FFlag::LuauUseNormalizeIntersectionLimit, true};
|
|
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
type Dir = {
|
|
a: number?, b: number?, c: number?, d: number?, e: number?, f: number?,
|
|
g: number?, h: number?, i: number?, j: number?, k: number?, l: number?,
|
|
m: number?, n: number?, o: number?, p: number?, q: number?, r: number?,
|
|
}
|
|
|
|
local function test(dirs: {Dir})
|
|
for k, dir in dirs
|
|
local success, message = pcall(function()
|
|
assert(dir.a == nil or type(dir.a) == "number")
|
|
assert(dir.b == nil or type(dir.b) == "number")
|
|
assert(dir.c == nil or type(dir.c) == "number")
|
|
assert(dir.d == nil or type(dir.d) == "number")
|
|
assert(dir.e == nil or type(dir.e) == "number")
|
|
assert(dir.f == nil or type(dir.f) == "number")
|
|
assert(dir.g == nil or type(dir.g) == "number")
|
|
assert(dir.h == nil or type(dir.h) == "number")
|
|
assert(dir.i == nil or type(dir.i) == "number")
|
|
assert(dir.j == nil or type(dir.j) == "number")
|
|
assert(dir.k == nil or type(dir.k) == "number")
|
|
assert(dir.l == nil or type(dir.l) == "number")
|
|
assert(dir.m == nil or type(dir.m) == "number")
|
|
assert(dir.n == nil or type(dir.n) == "number")
|
|
assert(dir.o == nil or type(dir.o) == "number")
|
|
assert(dir.p == nil or type(dir.p) == "number")
|
|
assert(dir.q == nil or type(dir.q) == "number")
|
|
assert(dir.r == nil or type(dir.r) == "number")
|
|
assert(dir.t == nil or type(dir.t) == "number")
|
|
assert(dir.u == nil or type(dir.u) == "number")
|
|
assert(dir.v == nil or type(dir.v) == "number")
|
|
local checkpoint = dir
|
|
|
|
checkpoint.w = 1
|
|
end)
|
|
assert(success)
|
|
end
|
|
end
|
|
)");
|
|
}
|
|
|
|
TEST_SUITE_END();
|