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