Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/tests/Module.test.cpp
2723 views
1
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
2
#include "Luau/Clone.h"
3
#include "Luau/Common.h"
4
#include "Luau/Module.h"
5
#include "Luau/Parser.h"
6
7
#include "Fixture.h"
8
9
#include "ScopedFlags.h"
10
#include "doctest.h"
11
12
using namespace Luau;
13
14
LUAU_FASTFLAG(DebugLuauForceOldSolver)
15
LUAU_FASTFLAG(DebugLuauFreezeArena)
16
LUAU_FASTINT(LuauTypeCloneIterationLimit)
17
18
TEST_SUITE_BEGIN("ModuleTests");
19
20
TEST_CASE_FIXTURE(Fixture, "is_within_comment")
21
{
22
check(R"(
23
--!strict
24
local foo = {}
25
function foo:bar() end
26
27
--[[
28
foo:
29
]] foo:bar()
30
31
--[[]]--[[]] -- Two distinct comments that have zero characters of space between them.
32
)");
33
34
SourceModule* sm = getMainSourceModule();
35
36
CHECK_EQ(5, sm->commentLocations.size());
37
38
CHECK(isWithinComment(*sm, Position{1, 15}));
39
CHECK(isWithinComment(*sm, Position{6, 16}));
40
CHECK(isWithinComment(*sm, Position{9, 13}));
41
CHECK(isWithinComment(*sm, Position{9, 14}));
42
43
CHECK(!isWithinComment(*sm, Position{2, 15}));
44
CHECK(!isWithinComment(*sm, Position{7, 10}));
45
CHECK(!isWithinComment(*sm, Position{7, 11}));
46
}
47
48
TEST_CASE_FIXTURE(Fixture, "is_within_comment_parse_result")
49
{
50
std::string src = R"(
51
--!strict
52
local foo = {}
53
function foo:bar() end
54
55
--[[
56
foo:
57
]] foo:bar()
58
59
--[[]]--[[]] -- Two distinct comments that have zero characters of space between them.
60
)";
61
62
Luau::Allocator alloc;
63
Luau::AstNameTable names{alloc};
64
Luau::ParseOptions parseOptions;
65
parseOptions.captureComments = true;
66
Luau::ParseResult parseResult = Luau::Parser::parse(src.data(), src.size(), names, alloc, parseOptions);
67
68
CHECK_EQ(5, parseResult.commentLocations.size());
69
70
CHECK(isWithinComment(parseResult, Position{1, 15}));
71
CHECK(isWithinComment(parseResult, Position{6, 16}));
72
CHECK(isWithinComment(parseResult, Position{9, 13}));
73
CHECK(isWithinComment(parseResult, Position{9, 14}));
74
75
CHECK(!isWithinComment(parseResult, Position{2, 15}));
76
CHECK(!isWithinComment(parseResult, Position{7, 10}));
77
CHECK(!isWithinComment(parseResult, Position{7, 11}));
78
}
79
80
TEST_CASE_FIXTURE(Fixture, "dont_clone_persistent_primitive")
81
{
82
TypeArena dest;
83
CloneState cloneState{getBuiltins()};
84
85
// numberType is persistent. We leave it as-is.
86
TypeId newNumber = clone(getBuiltins()->numberType, dest, cloneState);
87
CHECK_EQ(newNumber, getBuiltins()->numberType);
88
}
89
90
TEST_CASE_FIXTURE(Fixture, "deepClone_non_persistent_primitive")
91
{
92
TypeArena dest;
93
CloneState cloneState{getBuiltins()};
94
95
// Create a new number type that isn't persistent
96
unfreeze(getFrontend().globals.globalTypes);
97
TypeId oldNumber = getFrontend().globals.globalTypes.addType(PrimitiveType{PrimitiveType::Number});
98
freeze(getFrontend().globals.globalTypes);
99
TypeId newNumber = clone(oldNumber, dest, cloneState);
100
101
CHECK_NE(newNumber, oldNumber);
102
CHECK_EQ("number", toString(oldNumber));
103
CHECK_EQ("number", toString(newNumber));
104
CHECK_EQ(1, dest.types.size());
105
}
106
107
TEST_CASE_FIXTURE(Fixture, "deepClone_cyclic_table")
108
{
109
// Under DCR, we don't seal the outer occurrance of the table `Cyclic` which
110
// breaks this test. I'm not sure if that behaviour change is important or
111
// not, but it's tangental to the core purpose of this test.
112
113
DOES_NOT_PASS_NEW_SOLVER_GUARD();
114
115
CheckResult result = check(R"(
116
local Cyclic = {}
117
function Cyclic.get()
118
return Cyclic
119
end
120
)");
121
122
LUAU_REQUIRE_NO_ERRORS(result);
123
124
/* The inferred type of Cyclic is {get: () -> Cyclic}
125
*
126
* Assert that the return type of get() is the same as the outer table.
127
*/
128
129
TypeId ty = requireType("Cyclic");
130
131
TypeArena dest;
132
CloneState cloneState{getBuiltins()};
133
TypeId cloneTy = clone(ty, dest, cloneState);
134
135
TableType* ttv = getMutable<TableType>(cloneTy);
136
REQUIRE(ttv != nullptr);
137
138
CHECK_EQ(std::optional<std::string>{"Cyclic"}, ttv->syntheticName);
139
140
TypeId methodType = *ttv->props["get"].readTy;
141
REQUIRE(methodType != nullptr);
142
143
const FunctionType* ftv = get<FunctionType>(methodType);
144
REQUIRE(ftv != nullptr);
145
146
std::optional<TypeId> methodReturnType = first(ftv->retTypes);
147
REQUIRE(methodReturnType);
148
149
CHECK_MESSAGE(methodReturnType == cloneTy, toString(methodType, {true}) << " should be pointer identical to " << toString(cloneTy, {true}));
150
CHECK_EQ(2, dest.typePacks.size()); // one for the function args, and another for its return type
151
CHECK_EQ(2, dest.types.size()); // One table and one function
152
}
153
154
TEST_CASE_FIXTURE(Fixture, "deepClone_cyclic_table_2")
155
{
156
TypeArena src;
157
158
TypeId tableTy = src.addType(TableType{});
159
TableType* tt = getMutable<TableType>(tableTy);
160
REQUIRE(tt);
161
162
TypeId methodTy = src.addType(FunctionType{src.addTypePack({}), src.addTypePack({tableTy})});
163
164
tt->props["get"].setType(methodTy);
165
166
TypeArena dest;
167
168
CloneState cloneState{getBuiltins()};
169
TypeId cloneTy = clone(tableTy, dest, cloneState);
170
TableType* ctt = getMutable<TableType>(cloneTy);
171
REQUIRE(ctt);
172
173
TypeId clonedMethodType = *ctt->props["get"].readTy;
174
REQUIRE(clonedMethodType);
175
176
const FunctionType* cmf = get<FunctionType>(clonedMethodType);
177
REQUIRE(cmf);
178
179
std::optional<TypeId> cloneMethodReturnType = first(cmf->retTypes);
180
REQUIRE(bool(cloneMethodReturnType));
181
182
CHECK(*cloneMethodReturnType == cloneTy);
183
}
184
185
TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_types_point_into_globalTypes_arena")
186
{
187
CheckResult result = check(R"(
188
return {sign=math.sign}
189
)");
190
dumpErrors(result);
191
LUAU_REQUIRE_NO_ERRORS(result);
192
193
ModulePtr module = getFrontend().moduleResolver.getModule("MainModule");
194
std::optional<TypeId> exports = first(module->returnType);
195
REQUIRE(bool(exports));
196
197
REQUIRE(isInArena(*exports, module->interfaceTypes));
198
199
TableType* exportsTable = getMutable<TableType>(*exports);
200
REQUIRE(exportsTable != nullptr);
201
202
TypeId signType = *exportsTable->props["sign"].readTy;
203
REQUIRE(signType != nullptr);
204
205
CHECK(!isInArena(signType, module->interfaceTypes));
206
CHECK(isInArena(signType, getFrontend().globals.globalTypes));
207
}
208
209
TEST_CASE_FIXTURE(Fixture, "deepClone_union")
210
{
211
TypeArena dest;
212
CloneState cloneState{getBuiltins()};
213
214
unfreeze(getFrontend().globals.globalTypes);
215
TypeId oldUnion = getFrontend().globals.globalTypes.addType(UnionType{{getBuiltins()->numberType, getBuiltins()->stringType}});
216
freeze(getFrontend().globals.globalTypes);
217
TypeId newUnion = clone(oldUnion, dest, cloneState);
218
219
CHECK_NE(newUnion, oldUnion);
220
CHECK_EQ("number | string", toString(newUnion));
221
CHECK_EQ(1, dest.types.size());
222
}
223
224
TEST_CASE_FIXTURE(Fixture, "deepClone_intersection")
225
{
226
TypeArena dest;
227
CloneState cloneState{getBuiltins()};
228
229
unfreeze(getFrontend().globals.globalTypes);
230
TypeId oldIntersection = getFrontend().globals.globalTypes.addType(IntersectionType{{getBuiltins()->numberType, getBuiltins()->stringType}});
231
freeze(getFrontend().globals.globalTypes);
232
TypeId newIntersection = clone(oldIntersection, dest, cloneState);
233
234
CHECK_NE(newIntersection, oldIntersection);
235
CHECK_EQ("number & string", toString(newIntersection));
236
CHECK_EQ(1, dest.types.size());
237
}
238
239
TEST_CASE_FIXTURE(Fixture, "clone_class")
240
{
241
Type exampleMetaClass{ExternType{
242
"ExampleClassMeta",
243
{
244
{"__add", {getBuiltins()->anyType}},
245
},
246
std::nullopt,
247
std::nullopt,
248
{},
249
{},
250
"Test",
251
{}
252
}};
253
Type exampleClass{ExternType{
254
"ExampleClass",
255
{
256
{"PropOne", {getBuiltins()->numberType}},
257
{"PropTwo", {getBuiltins()->stringType}},
258
},
259
std::nullopt,
260
&exampleMetaClass,
261
{},
262
{},
263
"Test",
264
{}
265
}};
266
267
TypeArena dest;
268
CloneState cloneState{getBuiltins()};
269
270
TypeId cloned = clone(&exampleClass, dest, cloneState);
271
const ExternType* etv = get<ExternType>(cloned);
272
REQUIRE(etv != nullptr);
273
274
REQUIRE(etv->metatable);
275
const ExternType* metatable = get<ExternType>(*etv->metatable);
276
REQUIRE(metatable);
277
278
CHECK_EQ("ExampleClass", etv->name);
279
CHECK_EQ("ExampleClassMeta", metatable->name);
280
}
281
282
TEST_CASE_FIXTURE(Fixture, "clone_free_types")
283
{
284
TypeArena arena;
285
TypeId freeTy = freshType(NotNull{&arena}, getBuiltins(), nullptr);
286
TypePackVar freeTp(FreeTypePack{TypeLevel{}});
287
288
TypeArena dest;
289
CloneState cloneState{getBuiltins()};
290
291
TypeId clonedTy = clone(freeTy, dest, cloneState);
292
CHECK(get<FreeType>(clonedTy));
293
294
cloneState = {getBuiltins()};
295
TypePackId clonedTp = clone(&freeTp, dest, cloneState);
296
CHECK(get<FreeTypePack>(clonedTp));
297
}
298
299
TEST_CASE_FIXTURE(Fixture, "clone_free_tables")
300
{
301
Type tableTy{TableType{}};
302
TableType* ttv = getMutable<TableType>(&tableTy);
303
ttv->state = TableState::Free;
304
305
TypeArena dest;
306
CloneState cloneState{getBuiltins()};
307
308
TypeId cloned = clone(&tableTy, dest, cloneState);
309
const TableType* clonedTtv = get<TableType>(cloned);
310
CHECK_EQ(clonedTtv->state, TableState::Free);
311
}
312
313
TEST_CASE_FIXTURE(BuiltinsFixture, "clone_self_property")
314
{
315
// CLI-117082 ModuleTests.clone_self_property we don't infer self correctly, instead replacing it with unknown.
316
if (!FFlag::DebugLuauForceOldSolver)
317
return;
318
fileResolver.source["Module/A"] = R"(
319
--!nonstrict
320
local a = {}
321
function a:foo(x: number)
322
return -x;
323
end
324
return a;
325
)";
326
327
CheckResult result = getFrontend().check("Module/A");
328
LUAU_REQUIRE_NO_ERRORS(result);
329
330
fileResolver.source["Module/B"] = R"(
331
--!nonstrict
332
local a = require(script.Parent.A)
333
return a.foo(5)
334
)";
335
336
result = getFrontend().check("Module/B");
337
338
LUAU_REQUIRE_ERROR_COUNT(1, result);
339
340
CHECK_EQ("This function must be called with self. Did you mean to use a colon instead of a dot?", toString(result.errors[0]));
341
}
342
343
TEST_CASE_FIXTURE(Fixture, "clone_iteration_limit")
344
{
345
ScopedFastInt sfi{FInt::LuauTypeCloneIterationLimit, 2000};
346
347
TypeArena src;
348
349
TypeId table = src.addType(TableType{});
350
TypeId nested = table;
351
352
int nesting = 2500;
353
for (int i = 0; i < nesting; i++)
354
{
355
TableType* ttv = getMutable<TableType>(nested);
356
ttv->props["a"].readTy = src.addType(TableType{});
357
ttv->props["a"].writeTy = ttv->props["a"].readTy;
358
nested = *ttv->props["a"].readTy;
359
}
360
361
TypeArena dest;
362
CloneState cloneState{getBuiltins()};
363
364
TypeId ty = clone(table, dest, cloneState);
365
CHECK(get<ErrorType>(ty));
366
367
// Cloning it again is an important test.
368
TypeId ty2 = clone(table, dest, cloneState);
369
CHECK(get<ErrorType>(ty2));
370
}
371
372
// Unions should never be cyclic, but we should clone them correctly even if
373
// they are.
374
TEST_CASE_FIXTURE(Fixture, "clone_cyclic_union")
375
{
376
TypeArena src;
377
378
TypeId u = src.addType(UnionType{{getBuiltins()->numberType, getBuiltins()->stringType}});
379
UnionType* uu = getMutable<UnionType>(u);
380
REQUIRE(uu);
381
382
uu->options.push_back(u);
383
384
TypeArena dest;
385
CloneState cloneState{getBuiltins()};
386
387
TypeId cloned = clone(u, dest, cloneState);
388
REQUIRE(cloned);
389
390
const UnionType* clonedUnion = get<UnionType>(cloned);
391
REQUIRE(clonedUnion);
392
REQUIRE(3 == clonedUnion->options.size());
393
394
CHECK(getBuiltins()->numberType == clonedUnion->options[0]);
395
CHECK(getBuiltins()->stringType == clonedUnion->options[1]);
396
CHECK(cloned == clonedUnion->options[2]);
397
}
398
399
TEST_CASE_FIXTURE(Fixture, "any_persistance_does_not_leak")
400
{
401
fileResolver.source["Module/A"] = R"(
402
export type A = B
403
type B = A
404
)";
405
406
FrontendOptions opts;
407
opts.retainFullTypeGraphs = false;
408
CheckResult result = getFrontend().check("Module/A", opts);
409
LUAU_REQUIRE_ERRORS(result);
410
411
auto mod = getFrontend().moduleResolver.getModule("Module/A");
412
auto it = mod->exportedTypeBindings.find("A");
413
REQUIRE(it != mod->exportedTypeBindings.end());
414
415
if (!FFlag::DebugLuauForceOldSolver)
416
CHECK(toString(it->second.type) == "any");
417
else
418
CHECK(toString(it->second.type) == "*error-type*");
419
}
420
421
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_reexports")
422
{
423
fileResolver.source["Module/A"] = R"(
424
export type A = {p : number}
425
return {}
426
)";
427
428
fileResolver.source["Module/B"] = R"(
429
local a = require(script.Parent.A)
430
export type B = {q : a.A}
431
return {}
432
)";
433
434
CheckResult result = getFrontend().check("Module/B");
435
LUAU_REQUIRE_NO_ERRORS(result);
436
437
ModulePtr modA = getFrontend().moduleResolver.getModule("Module/A");
438
ModulePtr modB = getFrontend().moduleResolver.getModule("Module/B");
439
REQUIRE(modA);
440
REQUIRE(modB);
441
auto modAiter = modA->exportedTypeBindings.find("A");
442
auto modBiter = modB->exportedTypeBindings.find("B");
443
REQUIRE(modAiter != modA->exportedTypeBindings.end());
444
REQUIRE(modBiter != modB->exportedTypeBindings.end());
445
TypeId typeA = modAiter->second.type;
446
TypeId typeB = modBiter->second.type;
447
TableType* tableB = getMutable<TableType>(typeB);
448
REQUIRE(tableB);
449
CHECK(typeA == tableB->props["q"].readTy);
450
}
451
452
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_types_of_reexported_values")
453
{
454
fileResolver.source["Module/A"] = R"(
455
local exports = {a={p=5}}
456
return exports
457
)";
458
459
fileResolver.source["Module/B"] = R"(
460
local a = require(script.Parent.A)
461
local exports = {b=a.a}
462
return exports
463
)";
464
465
CheckResult result = getFrontend().check("Module/B");
466
LUAU_REQUIRE_NO_ERRORS(result);
467
468
ModulePtr modA = getFrontend().moduleResolver.getModule("Module/A");
469
REQUIRE(modA);
470
ModulePtr modB = getFrontend().moduleResolver.getModule("Module/B");
471
REQUIRE(modB);
472
473
std::optional<TypeId> typeA = first(modA->returnType);
474
REQUIRE(typeA);
475
std::optional<TypeId> typeB = first(modB->returnType);
476
REQUIRE(typeB);
477
478
TableType* tableA = getMutable<TableType>(*typeA);
479
REQUIRE_MESSAGE(tableA, "Expected a table, but got " << toString(*typeA));
480
TableType* tableB = getMutable<TableType>(*typeB);
481
REQUIRE_MESSAGE(tableB, "Expected a table, but got " << toString(*typeB));
482
483
CHECK(tableA->props["a"].readTy == tableB->props["b"].readTy);
484
CHECK(tableA->props["a"].writeTy == tableB->props["b"].writeTy);
485
}
486
487
TEST_CASE_FIXTURE(BuiltinsFixture, "clone_table_bound_to_table_bound_to_table")
488
{
489
TypeArena arena;
490
491
TypeId a = arena.addType(TableType{TableState::Free, TypeLevel{}});
492
getMutable<TableType>(a)->name = "a";
493
494
TypeId b = arena.addType(TableType{TableState::Free, TypeLevel{}});
495
getMutable<TableType>(b)->name = "b";
496
497
TypeId c = arena.addType(TableType{TableState::Free, TypeLevel{}});
498
getMutable<TableType>(c)->name = "c";
499
500
getMutable<TableType>(a)->boundTo = b;
501
getMutable<TableType>(b)->boundTo = c;
502
503
TypeArena dest;
504
CloneState state{getBuiltins()};
505
TypeId res = clone(a, dest, state);
506
507
REQUIRE(dest.types.size() == 1);
508
509
auto tableA = get<TableType>(res);
510
REQUIRE_MESSAGE(tableA, "Expected table, got " << res);
511
REQUIRE(tableA->name == "c");
512
REQUIRE(!tableA->boundTo);
513
}
514
515
TEST_CASE_FIXTURE(BuiltinsFixture, "clone_a_bound_type_to_a_persistent_type")
516
{
517
TypeArena arena;
518
519
TypeId boundTo = arena.addType(BoundType{getBuiltins()->numberType});
520
REQUIRE(getBuiltins()->numberType->persistent);
521
522
TypeArena dest;
523
CloneState state{getBuiltins()};
524
TypeId res = clone(boundTo, dest, state);
525
526
REQUIRE(res == follow(boundTo));
527
}
528
529
TEST_CASE_FIXTURE(BuiltinsFixture, "clone_a_bound_typepack_to_a_persistent_typepack")
530
{
531
TypeArena arena;
532
533
TypePackId boundTo = arena.addTypePack(BoundTypePack{getBuiltins()->neverTypePack});
534
REQUIRE(getBuiltins()->neverTypePack->persistent);
535
536
TypeArena dest;
537
CloneState state{getBuiltins()};
538
TypePackId res = clone(boundTo, dest, state);
539
540
REQUIRE(res == follow(boundTo));
541
}
542
543
TEST_CASE_FIXTURE(Fixture, "old_solver_correctly_populates_child_scopes")
544
{
545
check(R"(
546
--!strict
547
if true then
548
end
549
550
if false then
551
end
552
553
if true then
554
else
555
end
556
557
local x = {}
558
for i,v in x do
559
end
560
)");
561
562
auto& module = getFrontend().moduleResolver.getModule("MainModule");
563
CHECK(module->getModuleScope()->children.size() == 7);
564
}
565
566
TEST_SUITE_END();
567
568