Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/tests/Frontend.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/AstQuery.h"
3
#include "Luau/BuiltinDefinitions.h"
4
#include "Luau/Common.h"
5
#include "Luau/DenseHash.h"
6
#include "Luau/Frontend.h"
7
#include "Luau/Parser.h"
8
#include "Luau/RequireTracer.h"
9
10
#include "Fixture.h"
11
12
#include "Luau/Type.h"
13
#include "doctest.h"
14
15
#include <algorithm>
16
17
using namespace Luau;
18
19
LUAU_FASTFLAG(DebugLuauForceOldSolver);
20
LUAU_FASTFLAG(DebugLuauFreezeArena)
21
LUAU_FASTFLAG(DebugLuauMagicTypes)
22
23
namespace
24
{
25
struct NaiveFileResolver : NullFileResolver
26
{
27
std::optional<ModuleInfo> resolveModule(const ModuleInfo* context, AstExpr* expr, const TypeCheckLimits& limits) override
28
{
29
if (AstExprGlobal* g = expr->as<AstExprGlobal>())
30
{
31
if (g->name == "Modules")
32
return ModuleInfo{"Modules"};
33
34
if (g->name == "game")
35
return ModuleInfo{"game"};
36
}
37
else if (AstExprIndexName* i = expr->as<AstExprIndexName>())
38
{
39
if (context)
40
return ModuleInfo{context->name + '/' + i->index.value, context->optional};
41
}
42
else if (AstExprCall* call = expr->as<AstExprCall>(); call && call->self && call->args.size >= 1 && context)
43
{
44
if (AstExprConstantString* index = call->args.data[0]->as<AstExprConstantString>())
45
{
46
AstName func = call->func->as<AstExprIndexName>()->index;
47
48
if (func == "GetService" && context->name == "game")
49
return ModuleInfo{"game/" + std::string(index->value.data, index->value.size)};
50
}
51
}
52
53
return std::nullopt;
54
}
55
};
56
57
} // namespace
58
59
struct FrontendFixture : BuiltinsFixture
60
{
61
FrontendFixture() = default;
62
63
Frontend& getFrontend() override
64
{
65
if (frontend)
66
return *frontend;
67
68
Frontend& f = BuiltinsFixture::getFrontend();
69
addGlobalBinding(f.globals, "game", f.builtinTypes->anyType, "@test");
70
addGlobalBinding(f.globals, "script", f.builtinTypes->anyType, "@test");
71
return *frontend;
72
}
73
74
Allocator allocator_;
75
NotNull<Allocator> allocator{&allocator_};
76
77
AstNameTable nameTable_{allocator_};
78
NotNull<AstNameTable> nameTable{&nameTable_};
79
80
TypeArena arena_;
81
NotNull<TypeArena> arena{&arena_};
82
83
TypeId parseType(std::string_view src)
84
{
85
return getFrontend().parseType(allocator, nameTable, NotNull{&getFrontend().iceHandler}, TypeCheckLimits{}, arena, src);
86
}
87
};
88
89
TEST_SUITE_BEGIN("FrontendTest");
90
91
TEST_CASE_FIXTURE(FrontendFixture, "find_a_require")
92
{
93
AstStatBlock* program = parse(R"(
94
local M = require(Modules.Foo.Bar)
95
)");
96
97
NaiveFileResolver naiveFileResolver;
98
99
auto res = traceRequires(&naiveFileResolver, program, "", {});
100
CHECK_EQ(1, res.requireList.size());
101
CHECK_EQ(res.requireList[0].first, "Modules/Foo/Bar");
102
}
103
104
// It could be argued that this should not work.
105
TEST_CASE_FIXTURE(FrontendFixture, "find_a_require_inside_a_function")
106
{
107
AstStatBlock* program = parse(R"(
108
function foo()
109
local M = require(Modules.Foo.Bar)
110
end
111
)");
112
113
NaiveFileResolver naiveFileResolver;
114
115
auto res = traceRequires(&naiveFileResolver, program, "", {});
116
CHECK_EQ(1, res.requireList.size());
117
}
118
119
TEST_CASE_FIXTURE(FrontendFixture, "real_source")
120
{
121
AstStatBlock* program = parse(R"(
122
return function()
123
local Modules = game:GetService("CoreGui").Gui.Modules
124
125
local Roact = require(Modules.Common.Roact)
126
local Rodux = require(Modules.Common.Rodux)
127
128
local AppReducer = require(Modules.LuaApp.AppReducer)
129
local AEAppReducer = require(Modules.LuaApp.Reducers.AEReducers.AEAppReducer)
130
local AETabList = require(Modules.LuaApp.Components.Avatar.UI.Views.Portrait.AETabList)
131
local mockServices = require(Modules.LuaApp.TestHelpers.mockServices)
132
local DeviceOrientationMode = require(Modules.LuaApp.DeviceOrientationMode)
133
local MockAvatarEditorTheme = require(Modules.LuaApp.TestHelpers.MockAvatarEditorTheming)
134
local FFlagAvatarEditorEnableThemes = settings():GetFFlag("AvatarEditorEnableThemes2")
135
end
136
)");
137
138
NaiveFileResolver naiveFileResolver;
139
140
auto res = traceRequires(&naiveFileResolver, program, "", {});
141
CHECK_EQ(8, res.requireList.size());
142
}
143
144
TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_dependent_scripts")
145
{
146
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
147
fileResolver.source["game/Gui/Modules/B"] = R"(
148
local Modules = game:GetService('Gui').Modules
149
local A = require(Modules.A)
150
return {b_value = A.hello}
151
)";
152
153
getFrontend().check("game/Gui/Modules/B");
154
155
ModulePtr bModule = getFrontend().moduleResolver.getModule("game/Gui/Modules/B");
156
REQUIRE(bModule != nullptr);
157
CHECK(bModule->errors.empty());
158
Luau::dumpErrors(bModule);
159
160
auto bExports = first(bModule->returnType);
161
REQUIRE(!!bExports);
162
163
CHECK_EQ("{ b_value: number }", toString(*bExports));
164
}
165
166
TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_cyclically_dependent_scripts")
167
{
168
fileResolver.source["game/Gui/Modules/A"] = R"(
169
local Modules = game:GetService('Gui').Modules
170
local B = require(Modules.B)
171
return {}
172
)";
173
fileResolver.source["game/Gui/Modules/B"] = R"(
174
local Modules = game:GetService('Gui').Modules
175
local A = require(Modules.A)
176
require(Modules.C)
177
return {}
178
)";
179
fileResolver.source["game/Gui/Modules/C"] = R"(
180
local Modules = game:GetService('Gui').Modules
181
do local A = require(Modules.A) end
182
return {}
183
)";
184
185
fileResolver.source["game/Gui/Modules/D"] = R"(
186
local Modules = game:GetService('Gui').Modules
187
do local A = require(Modules.A) end
188
return {}
189
)";
190
191
CheckResult result1 = getFrontend().check("game/Gui/Modules/B");
192
LUAU_REQUIRE_ERROR_COUNT(4, result1);
193
194
CHECK_MESSAGE(get<ModuleHasCyclicDependency>(result1.errors[0]), "Should have been a ModuleHasCyclicDependency: " << toString(result1.errors[0]));
195
196
CHECK_MESSAGE(get<ModuleHasCyclicDependency>(result1.errors[1]), "Should have been a ModuleHasCyclicDependency: " << toString(result1.errors[1]));
197
198
CHECK_MESSAGE(get<ModuleHasCyclicDependency>(result1.errors[2]), "Should have been a ModuleHasCyclicDependency: " << toString(result1.errors[2]));
199
200
CheckResult result2 = getFrontend().check("game/Gui/Modules/D");
201
LUAU_REQUIRE_ERROR_COUNT(0, result2);
202
}
203
204
TEST_CASE_FIXTURE(FrontendFixture, "any_annotation_breaks_cycle")
205
{
206
fileResolver.source["game/Gui/Modules/A"] = R"(
207
local Modules = game:GetService('Gui').Modules
208
local B = require(Modules.B)
209
return {hello = B.hello}
210
)";
211
fileResolver.source["game/Gui/Modules/B"] = R"(
212
local Modules = game:GetService('Gui').Modules
213
local A = require(Modules.A) :: any
214
return {hello = A.hello}
215
)";
216
217
CheckResult result = getFrontend().check("game/Gui/Modules/A");
218
LUAU_REQUIRE_NO_ERRORS(result);
219
}
220
221
TEST_CASE_FIXTURE(FrontendFixture, "nocheck_modules_are_typed")
222
{
223
fileResolver.source["game/Gui/Modules/A"] = R"(
224
--!nocheck
225
export type Foo = number
226
return {hello = "hi"}
227
)";
228
fileResolver.source["game/Gui/Modules/B"] = R"(
229
--!nonstrict
230
export type Foo = number
231
return {hello = "hi"}
232
)";
233
fileResolver.source["game/Gui/Modules/C"] = R"(
234
local Modules = game:GetService('Gui').Modules
235
local A = require(Modules.A)
236
local B = require(Modules.B)
237
local five : A.Foo = 5
238
)";
239
240
CheckResult result = getFrontend().check("game/Gui/Modules/C");
241
LUAU_REQUIRE_NO_ERRORS(result);
242
243
ModulePtr aModule = getFrontend().moduleResolver.getModule("game/Gui/Modules/A");
244
REQUIRE(bool(aModule));
245
246
std::optional<TypeId> aExports = first(aModule->returnType);
247
REQUIRE(bool(aExports));
248
249
ModulePtr bModule = getFrontend().moduleResolver.getModule("game/Gui/Modules/B");
250
REQUIRE(bool(bModule));
251
252
std::optional<TypeId> bExports = first(bModule->returnType);
253
REQUIRE(bool(bExports));
254
255
CHECK_EQ(toString(*aExports), toString(*bExports));
256
}
257
258
TEST_CASE_FIXTURE(FrontendFixture, "cycle_detection_between_check_and_nocheck")
259
{
260
fileResolver.source["game/Gui/Modules/A"] = R"(
261
local Modules = game:GetService('Gui').Modules
262
local B = require(Modules.B)
263
return {hello = B.hello}
264
)";
265
fileResolver.source["game/Gui/Modules/B"] = R"(
266
--!nocheck
267
local Modules = game:GetService('Gui').Modules
268
local A = require(Modules.A)
269
return {hello = A.hello}
270
)";
271
272
CheckResult result = getFrontend().check("game/Gui/Modules/A");
273
LUAU_REQUIRE_ERROR_COUNT(1, result);
274
}
275
276
TEST_CASE_FIXTURE(FrontendFixture, "nocheck_cycle_used_by_checked")
277
{
278
fileResolver.source["game/Gui/Modules/A"] = R"(
279
--!nocheck
280
local Modules = game:GetService('Gui').Modules
281
local B = require(Modules.B)
282
return {hello = B.hello}
283
)";
284
fileResolver.source["game/Gui/Modules/B"] = R"(
285
--!nocheck
286
local Modules = game:GetService('Gui').Modules
287
local A = require(Modules.A)
288
return {hello = A.hello}
289
)";
290
fileResolver.source["game/Gui/Modules/C"] = R"(
291
--!strict
292
local Modules = game:GetService('Gui').Modules
293
local A = require(Modules.A)
294
local B = require(Modules.B)
295
return {a=A, b=B}
296
)";
297
298
CheckResult result = getFrontend().check("game/Gui/Modules/C");
299
LUAU_REQUIRE_NO_ERRORS(result);
300
301
ModulePtr cModule = getFrontend().moduleResolver.getModule("game/Gui/Modules/C");
302
REQUIRE(bool(cModule));
303
304
std::optional<TypeId> cExports = first(cModule->returnType);
305
REQUIRE(bool(cExports));
306
307
CHECK("{ a: { hello: any }, b: { hello: any } }" == toString(*cExports));
308
}
309
310
TEST_CASE_FIXTURE(FrontendFixture, "cycle_detection_disabled_in_nocheck")
311
{
312
fileResolver.source["game/Gui/Modules/A"] = R"(
313
--!nocheck
314
local Modules = game:GetService('Gui').Modules
315
local B = require(Modules.B)
316
return {hello = B.hello}
317
)";
318
fileResolver.source["game/Gui/Modules/B"] = R"(
319
--!nocheck
320
local Modules = game:GetService('Gui').Modules
321
local A = require(Modules.A)
322
return {hello = A.hello}
323
)";
324
325
CheckResult result = getFrontend().check("game/Gui/Modules/A");
326
LUAU_REQUIRE_NO_ERRORS(result);
327
}
328
329
TEST_CASE_FIXTURE(FrontendFixture, "cycle_errors_can_be_fixed")
330
{
331
fileResolver.source["game/Gui/Modules/A"] = R"(
332
local Modules = game:GetService('Gui').Modules
333
local B = require(Modules.B)
334
return {hello = B.hello}
335
)";
336
fileResolver.source["game/Gui/Modules/B"] = R"(
337
local Modules = game:GetService('Gui').Modules
338
local A = require(Modules.A)
339
return {hello = A.hello}
340
)";
341
342
CheckResult result1 = getFrontend().check("game/Gui/Modules/A");
343
LUAU_REQUIRE_ERROR_COUNT(2, result1);
344
345
CHECK_MESSAGE(get<ModuleHasCyclicDependency>(result1.errors[0]), "Should have been a ModuleHasCyclicDependency: " << toString(result1.errors[0]));
346
347
CHECK_MESSAGE(get<ModuleHasCyclicDependency>(result1.errors[1]), "Should have been a ModuleHasCyclicDependency: " << toString(result1.errors[1]));
348
349
fileResolver.source["game/Gui/Modules/B"] = R"(
350
return {hello = 42}
351
)";
352
getFrontend().markDirty("game/Gui/Modules/B");
353
354
CheckResult result2 = getFrontend().check("game/Gui/Modules/A");
355
LUAU_REQUIRE_NO_ERRORS(result2);
356
}
357
358
TEST_CASE_FIXTURE(FrontendFixture, "cycle_error_paths")
359
{
360
fileResolver.source["game/Gui/Modules/A"] = R"(
361
local Modules = game:GetService('Gui').Modules
362
local B = require(Modules.B)
363
return {hello = B.hello}
364
)";
365
fileResolver.source["game/Gui/Modules/B"] = R"(
366
local Modules = game:GetService('Gui').Modules
367
local A = require(Modules.A)
368
return {hello = A.hello}
369
)";
370
371
CheckResult result = getFrontend().check("game/Gui/Modules/A");
372
LUAU_REQUIRE_ERROR_COUNT(2, result);
373
374
auto ce1 = get<ModuleHasCyclicDependency>(result.errors[0]);
375
REQUIRE(ce1);
376
CHECK_EQ(result.errors[0].moduleName, "game/Gui/Modules/B");
377
REQUIRE_EQ(ce1->cycle.size(), 2);
378
CHECK_EQ(ce1->cycle[0], "game/Gui/Modules/A");
379
CHECK_EQ(ce1->cycle[1], "game/Gui/Modules/B");
380
381
auto ce2 = get<ModuleHasCyclicDependency>(result.errors[1]);
382
REQUIRE(ce2);
383
CHECK_EQ(result.errors[1].moduleName, "game/Gui/Modules/A");
384
REQUIRE_EQ(ce2->cycle.size(), 2);
385
CHECK_EQ(ce2->cycle[0], "game/Gui/Modules/B");
386
CHECK_EQ(ce2->cycle[1], "game/Gui/Modules/A");
387
}
388
389
TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface")
390
{
391
fileResolver.source["game/A"] = R"(
392
return {hello = 2}
393
)";
394
395
CheckResult result = getFrontend().check("game/A");
396
LUAU_REQUIRE_NO_ERRORS(result);
397
398
fileResolver.source["game/A"] = R"(
399
local me = require(game.A)
400
return {hello = 2}
401
)";
402
getFrontend().markDirty("game/A");
403
404
result = getFrontend().check("game/A");
405
LUAU_REQUIRE_ERRORS(result);
406
407
auto ty = requireType("game/A", "me");
408
CHECK_EQ(toString(ty), "any");
409
}
410
411
TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_longer")
412
{
413
fileResolver.source["game/A"] = R"(
414
return {mod_a = 2}
415
)";
416
417
CheckResult result = getFrontend().check("game/A");
418
LUAU_REQUIRE_NO_ERRORS(result);
419
420
fileResolver.source["game/B"] = R"(
421
local me = require(game.A)
422
return {mod_b = 4}
423
)";
424
425
result = getFrontend().check("game/B");
426
LUAU_REQUIRE_NO_ERRORS(result);
427
428
fileResolver.source["game/A"] = R"(
429
local me = require(game.B)
430
return {mod_a_prime = 3}
431
)";
432
433
getFrontend().markDirty("game/A");
434
getFrontend().markDirty("game/B");
435
436
result = getFrontend().check("game/A");
437
LUAU_REQUIRE_ERRORS(result);
438
439
TypeId tyA = requireType("game/A", "me");
440
CHECK_EQ(toString(tyA), "any");
441
442
result = getFrontend().check("game/B");
443
LUAU_REQUIRE_ERRORS(result);
444
445
TypeId tyB = requireType("game/B", "me");
446
CHECK_EQ(toString(tyB), "any");
447
}
448
449
TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_exports")
450
{
451
fileResolver.source["game/A"] = R"(
452
local b = require(game.B)
453
export type atype = { x: b.btype }
454
return {mod_a = 1}
455
)";
456
457
fileResolver.source["game/B"] = R"(
458
export type btype = { x: number }
459
460
local function bf()
461
local a = require(game.A)
462
local bfl : a.atype = nil
463
return {bfl.x}
464
end
465
return {mod_b = 2}
466
)";
467
468
ToStringOptions opts;
469
opts.exhaustive = true;
470
471
CheckResult resultA = getFrontend().check("game/A");
472
LUAU_REQUIRE_ERRORS(resultA);
473
474
CheckResult resultB = getFrontend().check("game/B");
475
LUAU_REQUIRE_ERRORS(resultB);
476
477
TypeId tyB = requireExportedType("game/B", "btype");
478
CHECK_EQ(toString(tyB, opts), "{ x: number }");
479
480
TypeId tyA = requireExportedType("game/A", "atype");
481
CHECK_EQ(toString(tyA, opts), "{ x: any }");
482
483
getFrontend().markDirty("game/B");
484
resultB = getFrontend().check("game/B");
485
LUAU_REQUIRE_ERRORS(resultB);
486
487
tyB = requireExportedType("game/B", "btype");
488
CHECK_EQ(toString(tyB, opts), "{ x: number }");
489
490
tyA = requireExportedType("game/A", "atype");
491
CHECK_EQ(toString(tyA, opts), "{ x: any }");
492
}
493
494
TEST_CASE_FIXTURE(FrontendFixture, "dont_reparse_clean_file_when_linting")
495
{
496
fileResolver.source["Modules/A"] = R"(
497
local t = {}
498
499
for i=#t,1 do
500
end
501
502
for i=#t,1,-1 do
503
end
504
)";
505
506
configResolver.defaultConfig.enabledLint.enableWarning(LintWarning::Code_ForRange);
507
508
lintModule("Modules/A");
509
510
fileResolver.source["Modules/A"] = R"(
511
-- We have fixed the lint error, but we did not tell the Frontend that the file is changed!
512
-- Therefore, we expect Frontend to reuse the results from previous lint.
513
)";
514
515
LintResult lintResult = lintModule("Modules/A");
516
517
CHECK_EQ(1, lintResult.warnings.size());
518
}
519
520
TEST_CASE_FIXTURE(FrontendFixture, "dont_recheck_script_that_hasnt_been_marked_dirty")
521
{
522
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
523
fileResolver.source["game/Gui/Modules/B"] = R"(
524
local Modules = game:GetService('Gui').Modules
525
local A = require(Modules.A)
526
return {b_value = A.hello}
527
)";
528
529
getFrontend().check("game/Gui/Modules/B");
530
531
fileResolver.source["game/Gui/Modules/A"] =
532
"Massively incorrect syntax haha oops! However! The getFrontend().doesn't know that this file needs reparsing!";
533
534
getFrontend().check("game/Gui/Modules/B");
535
536
ModulePtr bModule = getFrontend().moduleResolver.getModule("game/Gui/Modules/B");
537
CHECK(bModule->errors.empty());
538
Luau::dumpErrors(bModule);
539
}
540
541
TEST_CASE_FIXTURE(FrontendFixture, "recheck_if_dependent_script_is_dirty")
542
{
543
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
544
fileResolver.source["game/Gui/Modules/B"] = R"(
545
local Modules = game:GetService('Gui').Modules
546
local A = require(Modules.A)
547
return {b_value = A.hello}
548
)";
549
550
getFrontend().check("game/Gui/Modules/B");
551
552
fileResolver.source["game/Gui/Modules/A"] = "return {hello='hi!'}";
553
getFrontend().markDirty("game/Gui/Modules/A");
554
555
getFrontend().check("game/Gui/Modules/B");
556
557
ModulePtr bModule = getFrontend().moduleResolver.getModule("game/Gui/Modules/B");
558
CHECK(bModule->errors.empty());
559
Luau::dumpErrors(bModule);
560
561
auto bExports = first(bModule->returnType);
562
REQUIRE(!!bExports);
563
564
CHECK_EQ("{ b_value: string }", toString(*bExports));
565
}
566
567
TEST_CASE_FIXTURE(FrontendFixture, "mark_non_immediate_reverse_deps_as_dirty")
568
{
569
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
570
fileResolver.source["game/Gui/Modules/B"] = R"(
571
return require(game:GetService('Gui').Modules.A)
572
)";
573
fileResolver.source["game/Gui/Modules/C"] = R"(
574
local Modules = game:GetService('Gui').Modules
575
local B = require(Modules.B)
576
return {c_value = B.hello}
577
)";
578
579
getFrontend().check("game/Gui/Modules/C");
580
581
std::vector<Luau::ModuleName> markedDirty;
582
getFrontend().markDirty("game/Gui/Modules/A", &markedDirty);
583
584
REQUIRE(markedDirty.size() == 3);
585
CHECK(std::find(markedDirty.begin(), markedDirty.end(), "game/Gui/Modules/A") != markedDirty.end());
586
CHECK(std::find(markedDirty.begin(), markedDirty.end(), "game/Gui/Modules/B") != markedDirty.end());
587
CHECK(std::find(markedDirty.begin(), markedDirty.end(), "game/Gui/Modules/C") != markedDirty.end());
588
}
589
590
#if 0
591
// Does not work yet. :(
592
TEST_CASE_FIXTURE(FrontendFixture, "recheck_if_dependent_script_has_a_parse_error")
593
{
594
fileResolver.source["Modules/A"] = "oh no a syntax error";
595
fileResolver.source["Modules/B"] = R"(
596
local Modules = {}
597
local A = require(Modules.A)
598
return {}
599
)";
600
601
CheckResult result = getFrontend().check("Modules/B");
602
LUAU_REQUIRE_ERROR_COUNT(1, result);
603
CHECK_EQ("Modules/A", result.errors[0].moduleName);
604
605
CheckResult result2 = getFrontend().check("Modules/B");
606
LUAU_REQUIRE_ERROR_COUNT(1, result2);
607
CHECK_EQ(result2.errors[0], result.errors[0]);
608
}
609
#endif
610
611
TEST_CASE_FIXTURE(FrontendFixture, "produce_errors_for_unchanged_file_with_a_syntax_error")
612
{
613
fileResolver.source["Modules/A"] = "oh no a blatant syntax error!!";
614
615
CheckResult one = getFrontend().check("Modules/A");
616
CheckResult two = getFrontend().check("Modules/A");
617
618
CHECK(!one.errors.empty());
619
CHECK(!two.errors.empty());
620
}
621
622
TEST_CASE_FIXTURE(FrontendFixture, "produce_errors_for_unchanged_file_with_errors")
623
{
624
fileResolver.source["Modules/A"] = "local p: number = 'oh no a type error'";
625
626
getFrontend().check("Modules/A");
627
628
fileResolver.source["Modules/A"] =
629
"local p = 4 -- We have fixed the problem, but we didn't tell the getFrontend(). so it will not recheck this file!";
630
CheckResult secondResult = getFrontend().check("Modules/A");
631
632
CHECK_EQ(1, secondResult.errors.size());
633
}
634
635
TEST_CASE_FIXTURE(FrontendFixture, "reports_errors_from_multiple_sources")
636
{
637
fileResolver.source["game/Gui/Modules/A"] = R"(
638
local a: number = 'oh no a type error'
639
return {a=a}
640
)";
641
642
fileResolver.source["game/Gui/Modules/B"] = R"(
643
local Modules = script.Parent
644
local A = require(Modules.A)
645
local b: number = 'another one! This is quite distressing!'
646
)";
647
648
CheckResult result = getFrontend().check("game/Gui/Modules/B");
649
LUAU_REQUIRE_ERROR_COUNT(2, result);
650
651
CHECK_EQ("game/Gui/Modules/A", result.errors[0].moduleName);
652
CHECK_EQ("game/Gui/Modules/B", result.errors[1].moduleName);
653
}
654
655
TEST_CASE_FIXTURE(FrontendFixture, "report_require_to_nonexistent_file")
656
{
657
fileResolver.source["Modules/A"] = R"(
658
local Modules = script
659
local B = require(Modules.B)
660
)";
661
662
CheckResult result = getFrontend().check("Modules/A");
663
LUAU_REQUIRE_ERROR_COUNT(1, result);
664
665
std::string s = toString(result.errors[0]);
666
CHECK_MESSAGE(get<UnknownRequire>(result.errors[0]), "Should have been an UnknownRequire: " << toString(result.errors[0]));
667
}
668
669
TEST_CASE_FIXTURE(FrontendFixture, "ignore_require_to_nonexistent_file")
670
{
671
fileResolver.source["Modules/A"] = R"(
672
local Modules = script
673
local B = require(Modules.B) :: any
674
)";
675
676
CheckResult result = getFrontend().check("Modules/A");
677
LUAU_REQUIRE_NO_ERRORS(result);
678
}
679
680
TEST_CASE_FIXTURE(FrontendFixture, "report_syntax_error_in_required_file")
681
{
682
fileResolver.source["Modules/A"] = "oh no a gross breach of syntax";
683
fileResolver.source["Modules/B"] = R"(
684
local Modules = script.Parent
685
local A = require(Modules.A)
686
)";
687
688
CheckResult result = getFrontend().check("Modules/B");
689
LUAU_REQUIRE_ERRORS(result);
690
691
CHECK_EQ("Modules/A", result.errors[0].moduleName);
692
693
bool b = std::any_of(
694
begin(result.errors),
695
end(result.errors),
696
[](auto&& e) -> bool
697
{
698
return get<SyntaxError>(e);
699
}
700
);
701
if (!b)
702
{
703
CHECK_MESSAGE(false, "Expected a syntax error!");
704
dumpErrors(result);
705
}
706
}
707
708
TEST_CASE_FIXTURE(FrontendFixture, "re_report_type_error_in_required_file")
709
{
710
fileResolver.source["Modules/A"] = R"(
711
local n: number = 'five'
712
return {n=n}
713
)";
714
715
fileResolver.source["Modules/B"] = R"(
716
local Modules = script.Parent
717
local A = require(Modules.A)
718
print(A.n)
719
)";
720
721
CheckResult result = getFrontend().check("Modules/B");
722
LUAU_REQUIRE_ERROR_COUNT(1, result);
723
724
CheckResult result2 = getFrontend().check("Modules/B");
725
LUAU_REQUIRE_ERROR_COUNT(1, result2);
726
727
CHECK_EQ("Modules/A", result.errors[0].moduleName);
728
}
729
730
TEST_CASE_FIXTURE(FrontendFixture, "accumulate_cached_errors")
731
{
732
fileResolver.source["Modules/A"] = R"(
733
local n: number = 'five'
734
return {n=n}
735
)";
736
737
fileResolver.source["Modules/B"] = R"(
738
local Modules = script.Parent
739
local A = require(Modules.A)
740
local b: number = 'seven'
741
print(A, b)
742
)";
743
744
CheckResult result1 = getFrontend().check("Modules/B");
745
746
LUAU_REQUIRE_ERROR_COUNT(2, result1);
747
748
CHECK_EQ("Modules/A", result1.errors[0].moduleName);
749
CHECK_EQ("Modules/B", result1.errors[1].moduleName);
750
751
CheckResult result2 = getFrontend().check("Modules/B");
752
753
LUAU_REQUIRE_ERROR_COUNT(2, result2);
754
755
CHECK_EQ("Modules/A", result2.errors[0].moduleName);
756
CHECK_EQ("Modules/B", result2.errors[1].moduleName);
757
}
758
759
TEST_CASE_FIXTURE(FrontendFixture, "accumulate_cached_errors_in_consistent_order")
760
{
761
fileResolver.source["Modules/A"] = R"(
762
a = 1
763
b = 2
764
local Modules = script.Parent
765
local A = require(Modules.B)
766
)";
767
768
fileResolver.source["Modules/B"] = R"(
769
d = 3
770
e = 4
771
return {}
772
)";
773
774
CheckResult result1 = getFrontend().check("Modules/A");
775
776
LUAU_REQUIRE_ERROR_COUNT(4, result1);
777
778
CHECK_EQ("Modules/A", result1.errors[2].moduleName);
779
CHECK_EQ("Modules/A", result1.errors[3].moduleName);
780
781
CHECK_EQ("Modules/B", result1.errors[0].moduleName);
782
CHECK_EQ("Modules/B", result1.errors[1].moduleName);
783
784
CheckResult result2 = getFrontend().check("Modules/A");
785
CHECK_EQ(4, result2.errors.size());
786
787
for (size_t i = 0; i < result1.errors.size(); ++i)
788
CHECK_EQ(result1.errors[i], result2.errors[i]);
789
}
790
791
TEST_CASE_FIXTURE(FrontendFixture, "test_pruneParentSegments")
792
{
793
CHECK_EQ(
794
std::optional<std::string>{"Modules/Enum/ButtonState"},
795
pathExprToModuleName("", {"Modules", "LuaApp", "DeprecatedDarkTheme", "Parent", "Parent", "Enum", "ButtonState"})
796
);
797
CHECK_EQ(std::optional<std::string>{"workspace/Foo/Bar/Baz"}, pathExprToModuleName("workspace/Foo/Quux", {"script", "Parent", "Bar", "Baz"}));
798
CHECK_EQ(std::nullopt, pathExprToModuleName("", {}));
799
CHECK_EQ(std::optional<std::string>{"script"}, pathExprToModuleName("", {"script"}));
800
CHECK_EQ(std::optional<std::string>{"script/Parent"}, pathExprToModuleName("", {"script", "Parent"}));
801
CHECK_EQ(std::optional<std::string>{"script"}, pathExprToModuleName("", {"script", "Parent", "Parent"}));
802
CHECK_EQ(std::optional<std::string>{"script"}, pathExprToModuleName("", {"script", "Test", "Parent"}));
803
CHECK_EQ(std::optional<std::string>{"script/Parent"}, pathExprToModuleName("", {"script", "Test", "Parent", "Parent"}));
804
CHECK_EQ(std::optional<std::string>{"script/Parent"}, pathExprToModuleName("", {"script", "Test", "Parent", "Test", "Parent", "Parent"}));
805
}
806
807
TEST_CASE_FIXTURE(FrontendFixture, "test_lint_uses_correct_config")
808
{
809
fileResolver.source["Module/A"] = R"(
810
local t = {}
811
812
for i=#t,1 do
813
end
814
)";
815
816
configResolver.configFiles["Module/A"].enabledLint.enableWarning(LintWarning::Code_ForRange);
817
818
auto result = lintModule("Module/A");
819
CHECK_EQ(1, result.warnings.size());
820
821
configResolver.configFiles["Module/A"].enabledLint.disableWarning(LintWarning::Code_ForRange);
822
getFrontend().markDirty("Module/A");
823
824
auto result2 = lintModule("Module/A");
825
CHECK_EQ(0, result2.warnings.size());
826
827
LintOptions overrideOptions;
828
829
overrideOptions.enableWarning(LintWarning::Code_ForRange);
830
getFrontend().markDirty("Module/A");
831
832
auto result3 = lintModule("Module/A", overrideOptions);
833
CHECK_EQ(1, result3.warnings.size());
834
835
overrideOptions.disableWarning(LintWarning::Code_ForRange);
836
getFrontend().markDirty("Module/A");
837
838
auto result4 = lintModule("Module/A", overrideOptions);
839
CHECK_EQ(0, result4.warnings.size());
840
}
841
842
TEST_CASE_FIXTURE(FrontendFixture, "lint_results_are_only_for_checked_module")
843
{
844
fileResolver.source["Module/A"] = R"(
845
local _ = 0b10000000000000000000000000000000000000000000000000000000000000000
846
)";
847
848
fileResolver.source["Module/B"] = R"(
849
require(script.Parent.A)
850
local _ = 0x10000000000000000
851
)";
852
853
LintResult lintResult = lintModule("Module/B");
854
CHECK_EQ(1, lintResult.warnings.size());
855
856
// Check cached result
857
lintResult = lintModule("Module/B");
858
CHECK_EQ(1, lintResult.warnings.size());
859
}
860
861
TEST_CASE_FIXTURE(FrontendFixture, "discard_type_graphs")
862
{
863
Frontend fe{&fileResolver, &configResolver, {false}};
864
865
fileResolver.source["Module/A"] = R"(
866
local a = {1,2,3,4,5}
867
)";
868
869
CheckResult result = fe.check("Module/A");
870
871
ModulePtr module = fe.moduleResolver.getModule("Module/A");
872
873
CHECK_EQ(0, module->internalTypes.types.size());
874
CHECK_EQ(0, module->internalTypes.typePacks.size());
875
CHECK_EQ(0, module->astTypes.size());
876
CHECK_EQ(0, module->astResolvedTypes.size());
877
CHECK_EQ(0, module->astResolvedTypePacks.size());
878
}
879
880
TEST_CASE_FIXTURE(FrontendFixture, "it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded")
881
{
882
Frontend fe{!FFlag::DebugLuauForceOldSolver ? SolverMode::New : SolverMode::Old, &fileResolver, &configResolver, {false}};
883
fileResolver.source["Module/A"] = R"(
884
--!strict
885
local a: {Count: number} = {count='five'}
886
)";
887
888
CheckResult result = fe.check("Module/A");
889
890
REQUIRE_EQ(1, result.errors.size());
891
892
// When this test fails, it is because the TypeIds needed by the error have been deallocated.
893
// It is thus basically impossible to predict what will happen when this assert is evaluated.
894
// It could segfault, or you could see weird type names like the empty string or <VALUELESS BY EXCEPTION>
895
if (!FFlag::DebugLuauForceOldSolver)
896
{
897
CHECK_EQ(
898
"Table type '{ count: string }' not compatible with type '{ Count: number }' because the former is missing field 'Count'",
899
toString(result.errors[0])
900
);
901
}
902
else
903
REQUIRE_EQ(
904
"Table type 'a' not compatible with type '{ Count: number }' because the former is missing field 'Count'", toString(result.errors[0])
905
);
906
}
907
908
TEST_CASE_FIXTURE(FrontendFixture, "trace_requires_in_nonstrict_mode")
909
{
910
// The new non-strict mode is not currently expected to signal any errors here.
911
if (!FFlag::DebugLuauForceOldSolver)
912
return;
913
914
fileResolver.source["Module/A"] = R"(
915
--!nonstrict
916
local module = {}
917
918
function module.f(arg: number)
919
print('f', arg)
920
end
921
922
return module
923
)";
924
925
fileResolver.source["Module/B"] = R"(
926
--!nonstrict
927
local A = require(script.Parent.A)
928
929
print(A.g(5)) -- Key 'g' not found
930
print(A.f('five')) -- Type mismatch number and string
931
print(A.f(5)) -- OK
932
)";
933
934
CheckResult result = getFrontend().check("Module/B");
935
936
LUAU_REQUIRE_ERROR_COUNT(2, result);
937
938
CHECK_EQ(4, result.errors[0].location.begin.line);
939
CHECK_EQ(5, result.errors[1].location.begin.line);
940
}
941
942
TEST_CASE_FIXTURE(FrontendFixture, "environments")
943
{
944
ScopePtr testScope = getFrontend().addEnvironment("test");
945
946
unfreeze(getFrontend().globals.globalTypes);
947
getFrontend().loadDefinitionFile(
948
getFrontend().globals,
949
testScope,
950
R"(
951
export type Foo = number | string
952
)",
953
"@test",
954
/* captureComments */ false
955
);
956
freeze(getFrontend().globals.globalTypes);
957
958
fileResolver.source["A"] = R"(
959
--!nonstrict
960
local foo: Foo = 1
961
)";
962
963
fileResolver.source["B"] = R"(
964
--!nonstrict
965
local foo: Foo = 1
966
)";
967
968
fileResolver.source["C"] = R"(
969
--!strict
970
local foo: Foo = 1
971
)";
972
973
fileResolver.environments["A"] = "test";
974
975
CheckResult resultA = getFrontend().check("A");
976
LUAU_REQUIRE_NO_ERRORS(resultA);
977
978
CheckResult resultB = getFrontend().check("B");
979
LUAU_REQUIRE_ERROR_COUNT(1, resultB);
980
981
CheckResult resultC = getFrontend().check("C");
982
LUAU_REQUIRE_ERROR_COUNT(1, resultC);
983
}
984
985
TEST_CASE_FIXTURE(FrontendFixture, "ast_node_at_position")
986
{
987
check(R"(
988
local t = {}
989
990
function t:aa() end
991
992
t:
993
)");
994
995
SourceModule* module = getMainSourceModule();
996
Position pos = module->root->location.end;
997
AstNode* node = findNodeAtPosition(*module, pos);
998
999
REQUIRE(node);
1000
REQUIRE(bool(node->asExpr()));
1001
1002
++pos.column;
1003
AstNode* node2 = findNodeAtPosition(*module, pos);
1004
CHECK_EQ(node, node2);
1005
}
1006
1007
TEST_CASE_FIXTURE(FrontendFixture, "stats_are_not_reset_between_checks")
1008
{
1009
fileResolver.source["Module/A"] = R"(
1010
--!strict
1011
local B = require(script.Parent.B)
1012
local foo = B.foo + 1
1013
)";
1014
1015
fileResolver.source["Module/B"] = R"(
1016
--!strict
1017
return {foo = 1}
1018
)";
1019
1020
CheckResult r1 = getFrontend().check("Module/A");
1021
LUAU_REQUIRE_NO_ERRORS(r1);
1022
1023
Frontend::Stats stats1 = getFrontend().stats;
1024
CHECK_EQ(2, stats1.files);
1025
1026
getFrontend().markDirty("Module/A");
1027
getFrontend().markDirty("Module/B");
1028
1029
CheckResult r2 = getFrontend().check("Module/A");
1030
LUAU_REQUIRE_NO_ERRORS(r2);
1031
Frontend::Stats stats2 = getFrontend().stats;
1032
1033
CHECK_EQ(4, stats2.files);
1034
}
1035
1036
TEST_CASE_FIXTURE(FrontendFixture, "clearStats")
1037
{
1038
fileResolver.source["Module/A"] = R"(
1039
--!strict
1040
local B = require(script.Parent.B)
1041
local foo = B.foo + 1
1042
)";
1043
1044
fileResolver.source["Module/B"] = R"(
1045
--!strict
1046
return {foo = 1}
1047
)";
1048
1049
CheckResult r1 = getFrontend().check("Module/A");
1050
LUAU_REQUIRE_NO_ERRORS(r1);
1051
1052
Frontend::Stats stats1 = getFrontend().stats;
1053
CHECK_EQ(2, stats1.files);
1054
1055
getFrontend().markDirty("Module/A");
1056
getFrontend().markDirty("Module/B");
1057
1058
getFrontend().clearStats();
1059
CheckResult r2 = getFrontend().check("Module/A");
1060
LUAU_REQUIRE_NO_ERRORS(r2);
1061
Frontend::Stats stats2 = getFrontend().stats;
1062
1063
CHECK_EQ(2, stats2.files);
1064
}
1065
1066
TEST_CASE_FIXTURE(FrontendFixture, "typecheck_twice_for_ast_types")
1067
{
1068
fileResolver.source["Module/A"] = R"(
1069
local a = 1
1070
)";
1071
1072
CheckResult result = getFrontend().check("Module/A");
1073
1074
ModulePtr module = getFrontend().moduleResolver.getModule("Module/A");
1075
1076
REQUIRE_EQ(module->astTypes.size(), 1);
1077
auto it = module->astTypes.begin();
1078
CHECK_EQ(toString(it->second), "number");
1079
}
1080
1081
TEST_CASE_FIXTURE(FrontendFixture, "imported_table_modification_2")
1082
{
1083
// This test describes non-strict mode behavior that is just not currently present in the new non-strict mode.
1084
if (!FFlag::DebugLuauForceOldSolver)
1085
return;
1086
1087
getFrontend().options.retainFullTypeGraphs = false;
1088
1089
fileResolver.source["Module/A"] = R"(
1090
--!nonstrict
1091
local a = {}
1092
a.x = 1
1093
return a;
1094
)";
1095
1096
fileResolver.source["Module/B"] = R"(
1097
--!nonstrict
1098
local a = require(script.Parent.A)
1099
local b = {}
1100
function a:b() end -- this should error, since A doesn't define a:b()
1101
return b
1102
)";
1103
1104
fileResolver.source["Module/C"] = R"(
1105
--!nonstrict
1106
local a = require(script.Parent.A)
1107
local b = require(script.Parent.B)
1108
a:b() -- this should error, since A doesn't define a:b()
1109
)";
1110
1111
CheckResult resultA = getFrontend().check("Module/A");
1112
LUAU_REQUIRE_NO_ERRORS(resultA);
1113
1114
CheckResult resultB = getFrontend().check("Module/B");
1115
LUAU_REQUIRE_ERRORS(resultB);
1116
1117
CheckResult resultC = getFrontend().check("Module/C");
1118
LUAU_REQUIRE_ERRORS(resultC);
1119
}
1120
1121
// This test does not use TEST_CASE_FIXTURE because we need to set a flag before
1122
// the fixture is constructed.
1123
TEST_CASE("no_use_after_free_with_type_fun_instantiation")
1124
{
1125
// This flag forces this test to crash if there's a UAF in this code.
1126
ScopedFastFlag sff_DebugLuauFreezeArena(FFlag::DebugLuauFreezeArena, true);
1127
1128
FrontendFixture fix;
1129
1130
fix.fileResolver.source["Module/A"] = R"(
1131
export type Foo<V> = typeof(setmetatable({}, {}))
1132
return false;
1133
)";
1134
1135
fix.fileResolver.source["Module/B"] = R"(
1136
local A = require(script.Parent.A)
1137
export type Foo<V> = A.Foo<V>
1138
return false;
1139
)";
1140
1141
// We don't care about the result. That we haven't crashed is enough.
1142
fix.getFrontend().check("Module/B");
1143
}
1144
1145
TEST_CASE("check_without_builtin_next")
1146
{
1147
TestFileResolver fileResolver;
1148
TestConfigResolver configResolver;
1149
Frontend frontend(&fileResolver, &configResolver);
1150
fileResolver.source["Module/A"] = "for k,v in 2 do end";
1151
fileResolver.source["Module/B"] = "return next";
1152
1153
// We don't care about the result. That we haven't crashed is enough.
1154
frontend.check("Module/A");
1155
frontend.check("Module/B");
1156
}
1157
1158
TEST_CASE_FIXTURE(BuiltinsFixture, "reexport_cyclic_type")
1159
{
1160
fileResolver.source["Module/A"] = R"(
1161
type F<T> = (set: G<T>) -> ()
1162
1163
export type G<T> = {
1164
forEach: (a: F<T>) -> (),
1165
}
1166
1167
function X<T>(a: F<T>): ()
1168
end
1169
1170
return X
1171
)";
1172
1173
fileResolver.source["Module/B"] = R"(
1174
--!strict
1175
local A = require(script.Parent.A)
1176
1177
export type G<T> = A.G<T>
1178
1179
return {
1180
A = A,
1181
}
1182
)";
1183
1184
CheckResult result = getFrontend().check("Module/B");
1185
1186
LUAU_REQUIRE_NO_ERRORS(result);
1187
}
1188
1189
TEST_CASE_FIXTURE(BuiltinsFixture, "reexport_type_alias")
1190
{
1191
fileResolver.source["Module/A"] = R"(
1192
type KeyOfTestEvents = "test-file-start" | "test-file-success" | "test-file-failure" | "test-case-result"
1193
type MyAny = any
1194
1195
export type TestFileEvent<T = KeyOfTestEvents> = (
1196
eventName: T,
1197
args: any --[[ ROBLOX TODO: Unhandled node for type: TSIndexedAccessType ]] --[[ TestEvents[T] ]]
1198
) -> MyAny
1199
1200
return {}
1201
)";
1202
1203
fileResolver.source["Module/B"] = R"(
1204
--!strict
1205
local A = require(script.Parent.A)
1206
1207
export type TestFileEvent = A.TestFileEvent
1208
)";
1209
1210
CheckResult result = getFrontend().check("Module/B");
1211
1212
LUAU_REQUIRE_NO_ERRORS(result);
1213
}
1214
1215
TEST_CASE_FIXTURE(BuiltinsFixture, "module_scope_check")
1216
{
1217
getFrontend().prepareModuleScope = [this](const ModuleName& name, const ScopePtr& scope, bool forAutocomplete)
1218
{
1219
scope->bindings[Luau::AstName{"x"}] = Luau::Binding{getFrontend().globals.builtinTypes->numberType};
1220
};
1221
1222
fileResolver.source["game/A"] = R"(
1223
local a = x
1224
)";
1225
1226
CheckResult result = getFrontend().check("game/A");
1227
LUAU_REQUIRE_NO_ERRORS(result);
1228
1229
auto ty = requireType("game/A", "a");
1230
CHECK_EQ(toString(ty), "number");
1231
}
1232
1233
TEST_CASE_FIXTURE(FrontendFixture, "parse_only")
1234
{
1235
fileResolver.source["game/Gui/Modules/A"] = R"(
1236
local a: number = 'oh no a type error'
1237
return {a=a}
1238
)";
1239
1240
fileResolver.source["game/Gui/Modules/B"] = R"(
1241
local Modules = script.Parent
1242
local A = require(Modules.A)
1243
local b: number = 2
1244
)";
1245
1246
getFrontend().parse("game/Gui/Modules/B");
1247
1248
REQUIRE(getFrontend().sourceNodes.count("game/Gui/Modules/A"));
1249
REQUIRE(getFrontend().sourceNodes.count("game/Gui/Modules/B"));
1250
1251
auto node = getFrontend().sourceNodes["game/Gui/Modules/B"];
1252
CHECK(node->requireSet.contains("game/Gui/Modules/A"));
1253
REQUIRE_EQ(node->requireLocations.size(), 1);
1254
CHECK_EQ(node->requireLocations[0].second, Luau::Location(Position(2, 18), Position(2, 36)));
1255
1256
// Early parse doesn't cause typechecking to be skipped
1257
CheckResult result = getFrontend().check("game/Gui/Modules/B");
1258
LUAU_REQUIRE_ERROR_COUNT(1, result);
1259
1260
CHECK_EQ("game/Gui/Modules/A", result.errors[0].moduleName);
1261
CHECK_EQ("Expected this to be 'number', but got 'string'", toString(result.errors[0]));
1262
}
1263
1264
TEST_CASE_FIXTURE(FrontendFixture, "markdirty_early_return")
1265
{
1266
constexpr char moduleName[] = "game/Gui/Modules/A";
1267
fileResolver.source[moduleName] = R"(
1268
return 1
1269
)";
1270
1271
{
1272
std::vector<ModuleName> markedDirty;
1273
getFrontend().markDirty(moduleName, &markedDirty);
1274
CHECK(markedDirty.empty());
1275
}
1276
1277
getFrontend().parse(moduleName);
1278
1279
{
1280
std::vector<ModuleName> markedDirty;
1281
getFrontend().markDirty(moduleName, &markedDirty);
1282
CHECK(!markedDirty.empty());
1283
}
1284
}
1285
1286
TEST_CASE_FIXTURE(FrontendFixture, "attribute_ices_to_the_correct_module")
1287
{
1288
ScopedFastFlag sff{FFlag::DebugLuauMagicTypes, true};
1289
1290
fileResolver.source["game/one"] = R"(
1291
require(game.two)
1292
)";
1293
1294
fileResolver.source["game/two"] = R"(
1295
local a: _luau_ice
1296
)";
1297
1298
try
1299
{
1300
getFrontend().check("game/one");
1301
}
1302
catch (InternalCompilerError& err)
1303
{
1304
CHECK("game/two" == err.moduleName);
1305
return;
1306
}
1307
1308
FAIL("Expected an InternalCompilerError!");
1309
}
1310
1311
TEST_CASE_FIXTURE(FrontendFixture, "checked_modules_have_the_correct_mode")
1312
{
1313
fileResolver.source["game/A"] = R"(
1314
--!nocheck
1315
local a: number = "five"
1316
)";
1317
1318
fileResolver.source["game/B"] = R"(
1319
--!nonstrict
1320
local a = math.abs("five")
1321
)";
1322
1323
fileResolver.source["game/C"] = R"(
1324
--!strict
1325
local a = 10
1326
)";
1327
1328
getFrontend().check("game/A");
1329
getFrontend().check("game/B");
1330
getFrontend().check("game/C");
1331
1332
ModulePtr moduleA = getFrontend().moduleResolver.getModule("game/A");
1333
REQUIRE(moduleA);
1334
CHECK(moduleA->mode == Mode::NoCheck);
1335
1336
ModulePtr moduleB = getFrontend().moduleResolver.getModule("game/B");
1337
REQUIRE(moduleB);
1338
CHECK(moduleB->mode == Mode::Nonstrict);
1339
1340
ModulePtr moduleC = getFrontend().moduleResolver.getModule("game/C");
1341
REQUIRE(moduleC);
1342
CHECK(moduleC->mode == Mode::Strict);
1343
}
1344
1345
TEST_CASE_FIXTURE(FrontendFixture, "separate_caches_for_autocomplete")
1346
{
1347
DOES_NOT_PASS_NEW_SOLVER_GUARD();
1348
1349
fileResolver.source["game/A"] = R"(
1350
--!nonstrict
1351
local exports = {}
1352
function exports.hello() end
1353
return exports
1354
)";
1355
1356
FrontendOptions opts;
1357
opts.forAutocomplete = true;
1358
getFrontend().setLuauSolverMode(!FFlag::DebugLuauForceOldSolver ? SolverMode::New : SolverMode::Old);
1359
getFrontend().check("game/A", opts);
1360
1361
CHECK(nullptr == getFrontend().moduleResolver.getModule("game/A"));
1362
1363
ModulePtr acModule = getFrontend().moduleResolverForAutocomplete.getModule("game/A");
1364
REQUIRE(acModule != nullptr);
1365
CHECK(acModule->mode == Mode::Strict);
1366
1367
getFrontend().check("game/A");
1368
1369
ModulePtr module = getFrontend().moduleResolver.getModule("game/A");
1370
1371
REQUIRE(module != nullptr);
1372
CHECK(module->mode == Mode::Nonstrict);
1373
}
1374
1375
TEST_CASE_FIXTURE(FrontendFixture, "no_separate_caches_with_the_new_solver")
1376
{
1377
ScopedFastFlag sff{FFlag::DebugLuauForceOldSolver, false};
1378
1379
fileResolver.source["game/A"] = R"(
1380
--!nonstrict
1381
local exports = {}
1382
function exports.hello() end
1383
return exports
1384
)";
1385
1386
FrontendOptions opts;
1387
opts.forAutocomplete = true;
1388
1389
getFrontend().check("game/A", opts);
1390
1391
CHECK(nullptr == getFrontend().moduleResolverForAutocomplete.getModule("game/A"));
1392
1393
ModulePtr module = getFrontend().moduleResolver.getModule("game/A");
1394
1395
REQUIRE(module != nullptr);
1396
CHECK(module->mode == Mode::Nonstrict);
1397
}
1398
1399
TEST_CASE_FIXTURE(Fixture, "exported_tables_have_position_metadata")
1400
{
1401
CheckResult result = check(R"(
1402
return { abc = 22 }
1403
)");
1404
1405
LUAU_REQUIRE_NO_ERRORS(result);
1406
1407
ModulePtr mm = getMainModule();
1408
1409
TypePackId retTp = mm->getModuleScope()->returnType;
1410
auto retHead = flatten(retTp).first;
1411
REQUIRE(1 == retHead.size());
1412
1413
const TableType* tt = get<TableType>(retHead[0]);
1414
REQUIRE(tt);
1415
1416
CHECK("MainModule" == tt->definitionModuleName);
1417
1418
CHECK(1 == tt->props.size());
1419
CHECK(tt->props.count("abc"));
1420
1421
const Property& prop = tt->props.find("abc")->second;
1422
1423
CHECK(Location{Position{1, 17}, Position{1, 20}} == prop.location);
1424
}
1425
1426
TEST_CASE_FIXTURE(FrontendFixture, "get_required_scripts")
1427
{
1428
fileResolver.source["game/workspace/MyScript"] = R"(
1429
local MyModuleScript = require(game.workspace.MyModuleScript)
1430
local MyModuleScript2 = require(game.workspace.MyModuleScript2)
1431
MyModuleScript.myPrint()
1432
)";
1433
1434
fileResolver.source["game/workspace/MyModuleScript"] = R"(
1435
local module = {}
1436
function module.myPrint()
1437
print("Hello World")
1438
end
1439
return module
1440
)";
1441
1442
fileResolver.source["game/workspace/MyModuleScript2"] = R"(
1443
local module = {}
1444
return module
1445
)";
1446
1447
// isDirty(name) is true, getRequiredScripts should not hit the cache.
1448
getFrontend().markDirty("game/workspace/MyScript");
1449
std::vector<ModuleName> requiredScripts = getFrontend().getRequiredScripts("game/workspace/MyScript", {});
1450
REQUIRE(requiredScripts.size() == 2);
1451
CHECK(requiredScripts[0] == "game/workspace/MyModuleScript");
1452
CHECK(requiredScripts[1] == "game/workspace/MyModuleScript2");
1453
1454
// Call getFrontend().check first, then getRequiredScripts should hit the cache because isDirty(name) is false.
1455
getFrontend().check("game/workspace/MyScript");
1456
requiredScripts = getFrontend().getRequiredScripts("game/workspace/MyScript", {});
1457
REQUIRE(requiredScripts.size() == 2);
1458
CHECK(requiredScripts[0] == "game/workspace/MyModuleScript");
1459
CHECK(requiredScripts[1] == "game/workspace/MyModuleScript2");
1460
}
1461
1462
TEST_CASE_FIXTURE(FrontendFixture, "get_required_scripts_dirty")
1463
{
1464
fileResolver.source["game/workspace/MyScript"] = R"(
1465
print("Hello World")
1466
)";
1467
1468
fileResolver.source["game/workspace/MyModuleScript"] = R"(
1469
local module = {}
1470
function module.myPrint()
1471
print("Hello World")
1472
end
1473
return module
1474
)";
1475
1476
getFrontend().check("game/workspace/MyScript");
1477
std::vector<ModuleName> requiredScripts = getFrontend().getRequiredScripts("game/workspace/MyScript", {});
1478
REQUIRE(requiredScripts.size() == 0);
1479
1480
fileResolver.source["game/workspace/MyScript"] = R"(
1481
local MyModuleScript = require(game.workspace.MyModuleScript)
1482
MyModuleScript.myPrint()
1483
)";
1484
1485
requiredScripts = getFrontend().getRequiredScripts("game/workspace/MyScript", {});
1486
REQUIRE(requiredScripts.size() == 0);
1487
1488
getFrontend().markDirty("game/workspace/MyScript");
1489
requiredScripts = getFrontend().getRequiredScripts("game/workspace/MyScript", {});
1490
REQUIRE(requiredScripts.size() == 1);
1491
CHECK(requiredScripts[0] == "game/workspace/MyModuleScript");
1492
}
1493
1494
TEST_CASE_FIXTURE(FrontendFixture, "check_module_references_allocator")
1495
{
1496
fileResolver.source["game/workspace/MyScript"] = R"(
1497
print("Hello World")
1498
)";
1499
1500
getFrontend().check("game/workspace/MyScript");
1501
1502
ModulePtr module = getFrontend().moduleResolver.getModule("game/workspace/MyScript");
1503
SourceModule* source = getFrontend().getSourceModule("game/workspace/MyScript");
1504
CHECK(module);
1505
CHECK(source);
1506
1507
CHECK_EQ(module->allocator.get(), source->allocator.get());
1508
CHECK_EQ(module->names.get(), source->names.get());
1509
}
1510
1511
TEST_CASE_FIXTURE(FrontendFixture, "check_module_references_correct_ast_root")
1512
{
1513
fileResolver.source["game/workspace/MyScript"] = R"(
1514
print("Hello World")
1515
)";
1516
1517
getFrontend().check("game/workspace/MyScript");
1518
1519
ModulePtr module = getFrontend().moduleResolver.getModule("game/workspace/MyScript");
1520
SourceModule* source = getFrontend().getSourceModule("game/workspace/MyScript");
1521
CHECK(module);
1522
CHECK(source);
1523
1524
CHECK_EQ(module->root, source->root);
1525
}
1526
1527
TEST_CASE_FIXTURE(FrontendFixture, "dfg_data_cleared_on_retain_type_graphs_unset")
1528
{
1529
ScopedFastFlag sff{FFlag::DebugLuauForceOldSolver, false};
1530
fileResolver.source["game/A"] = R"(
1531
local a = 1
1532
local b = 2
1533
local c = 3
1534
return {x = a, y = b, z = c}
1535
)";
1536
1537
getFrontend().options.retainFullTypeGraphs = true;
1538
getFrontend().check("game/A");
1539
1540
auto mod = getFrontend().moduleResolver.getModule("game/A");
1541
CHECK(!mod->defArena.allocator.empty());
1542
CHECK(!mod->keyArena.allocator.empty());
1543
1544
// We should check that the dfg arena is empty once retainFullTypeGraphs is unset
1545
getFrontend().options.retainFullTypeGraphs = false;
1546
getFrontend().markDirty("game/A");
1547
getFrontend().check("game/A");
1548
1549
mod = getFrontend().moduleResolver.getModule("game/A");
1550
CHECK(mod->defArena.allocator.empty());
1551
CHECK(mod->keyArena.allocator.empty());
1552
}
1553
1554
TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents")
1555
{
1556
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
1557
fileResolver.source["game/Gui/Modules/B"] = R"(
1558
return require(game:GetService('Gui').Modules.A)
1559
)";
1560
fileResolver.source["game/Gui/Modules/C"] = R"(
1561
local Modules = game:GetService('Gui').Modules
1562
local B = require(Modules.B)
1563
return {c_value = B.hello}
1564
)";
1565
fileResolver.source["game/Gui/Modules/D"] = R"(
1566
local Modules = game:GetService('Gui').Modules
1567
local C = require(Modules.C)
1568
return {d_value = C.c_value}
1569
)";
1570
1571
getFrontend().check("game/Gui/Modules/D");
1572
1573
std::vector<ModuleName> visited;
1574
getFrontend().traverseDependents(
1575
"game/Gui/Modules/B",
1576
[&visited](SourceNode& node)
1577
{
1578
visited.push_back(node.name);
1579
return true;
1580
}
1581
);
1582
1583
CHECK_EQ(std::vector<ModuleName>{"game/Gui/Modules/B", "game/Gui/Modules/C", "game/Gui/Modules/D"}, visited);
1584
}
1585
1586
TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents_early_exit")
1587
{
1588
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
1589
fileResolver.source["game/Gui/Modules/B"] = R"(
1590
return require(game:GetService('Gui').Modules.A)
1591
)";
1592
fileResolver.source["game/Gui/Modules/C"] = R"(
1593
local Modules = game:GetService('Gui').Modules
1594
local B = require(Modules.B)
1595
return {c_value = B.hello}
1596
)";
1597
1598
getFrontend().check("game/Gui/Modules/C");
1599
1600
std::vector<ModuleName> visited;
1601
getFrontend().traverseDependents(
1602
"game/Gui/Modules/A",
1603
[&visited](SourceNode& node)
1604
{
1605
visited.push_back(node.name);
1606
return node.name != "game/Gui/Modules/B";
1607
}
1608
);
1609
1610
CHECK_EQ(std::vector<ModuleName>{"game/Gui/Modules/A", "game/Gui/Modules/B"}, visited);
1611
}
1612
1613
TEST_CASE_FIXTURE(FrontendFixture, "test_dependents_stored_on_node_as_graph_updates")
1614
{
1615
auto updateSource = [&](const std::string& name, const std::string& source)
1616
{
1617
fileResolver.source[name] = source;
1618
getFrontend().markDirty(name);
1619
};
1620
1621
auto validateMatchesRequireLists = [&](const std::string& message)
1622
{
1623
DenseHashMap<ModuleName, std::vector<ModuleName>> dependents{{}};
1624
for (const auto& module : getFrontend().sourceNodes)
1625
{
1626
for (const auto& dep : module.second->requireSet)
1627
dependents[dep].push_back(module.first);
1628
}
1629
1630
for (const auto& module : getFrontend().sourceNodes)
1631
{
1632
Set<ModuleName>& dependentsForModule = module.second->dependents;
1633
for (const auto& dep : dependents[module.first])
1634
CHECK_MESSAGE(1 == dependentsForModule.count(dep), "Mismatch in dependents for " << module.first << ": " << message);
1635
}
1636
};
1637
1638
auto validateSecondDependsOnFirst = [&](const std::string& from, const std::string& to, bool expected)
1639
{
1640
SourceNode& fromNode = *getFrontend().sourceNodes[from];
1641
CHECK_MESSAGE(
1642
fromNode.dependents.count(to) == int(expected),
1643
"Expected " << from << " to " << (expected ? std::string() : std::string("not ")) << "have a reverse dependency on " << to
1644
);
1645
};
1646
1647
// C -> B -> A
1648
{
1649
updateSource("game/Gui/Modules/A", "return {hello=5, world=true}");
1650
updateSource("game/Gui/Modules/B", R"(
1651
return require(game:GetService('Gui').Modules.A)
1652
)");
1653
updateSource("game/Gui/Modules/C", R"(
1654
local Modules = game:GetService('Gui').Modules
1655
local B = require(Modules.B)
1656
return {c_value = B}
1657
)");
1658
getFrontend().check("game/Gui/Modules/C");
1659
1660
validateMatchesRequireLists("Initial check");
1661
1662
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/B", true);
1663
validateSecondDependsOnFirst("game/Gui/Modules/B", "game/Gui/Modules/C", true);
1664
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/A", false);
1665
}
1666
1667
// C -> B, A
1668
{
1669
updateSource("game/Gui/Modules/B", R"(
1670
return 1
1671
)");
1672
getFrontend().check("game/Gui/Modules/C");
1673
1674
validateMatchesRequireLists("Removing dependency B->A");
1675
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/B", false);
1676
}
1677
1678
// C -> B -> A
1679
{
1680
updateSource("game/Gui/Modules/B", R"(
1681
return require(game:GetService('Gui').Modules.A)
1682
)");
1683
getFrontend().check("game/Gui/Modules/C");
1684
1685
validateMatchesRequireLists("Adding back B->A");
1686
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/B", true);
1687
}
1688
1689
// C -> B -> A, D -> (C,B,A)
1690
{
1691
updateSource("game/Gui/Modules/D", R"(
1692
local C = require(game:GetService('Gui').Modules.C)
1693
local B = require(game:GetService('Gui').Modules.B)
1694
local A = require(game:GetService('Gui').Modules.A)
1695
return {d_value = C.c_value}
1696
)");
1697
getFrontend().check("game/Gui/Modules/D");
1698
1699
validateMatchesRequireLists("Adding D->C, D->B, D->A");
1700
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/D", true);
1701
validateSecondDependsOnFirst("game/Gui/Modules/B", "game/Gui/Modules/D", true);
1702
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/D", true);
1703
}
1704
1705
// B -> A, C <-> D
1706
{
1707
updateSource("game/Gui/Modules/D", "return require(game:GetService('Gui').Modules.C)");
1708
updateSource("game/Gui/Modules/C", "return require(game:GetService('Gui').Modules.D)");
1709
getFrontend().check("game/Gui/Modules/D");
1710
1711
validateMatchesRequireLists("Adding cycle D->C, C->D");
1712
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/D", true);
1713
validateSecondDependsOnFirst("game/Gui/Modules/D", "game/Gui/Modules/C", true);
1714
}
1715
1716
// B -> A, C -> D, D -> error
1717
{
1718
updateSource("game/Gui/Modules/D", "return require(game:GetService('Gui').Modules.C.)");
1719
getFrontend().check("game/Gui/Modules/D");
1720
1721
validateMatchesRequireLists("Adding error dependency D->C.");
1722
validateSecondDependsOnFirst("game/Gui/Modules/D", "game/Gui/Modules/C", true);
1723
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/D", false);
1724
}
1725
}
1726
1727
TEST_CASE_FIXTURE(FrontendFixture, "test_invalid_dependency_tracking_per_module_resolver")
1728
{
1729
ScopedFastFlag newSolver{FFlag::DebugLuauForceOldSolver, true};
1730
getFrontend().setLuauSolverMode(!FFlag::DebugLuauForceOldSolver ? SolverMode::New : SolverMode::Old);
1731
1732
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
1733
fileResolver.source["game/Gui/Modules/B"] = "return require(game:GetService('Gui').Modules.A)";
1734
1735
FrontendOptions opts;
1736
opts.forAutocomplete = false;
1737
1738
getFrontend().check("game/Gui/Modules/B", opts);
1739
CHECK(getFrontend().allModuleDependenciesValid("game/Gui/Modules/B", opts.forAutocomplete));
1740
CHECK(!getFrontend().allModuleDependenciesValid("game/Gui/Modules/B", !opts.forAutocomplete));
1741
1742
opts.forAutocomplete = true;
1743
getFrontend().check("game/Gui/Modules/A", opts);
1744
1745
CHECK(!getFrontend().allModuleDependenciesValid("game/Gui/Modules/B", opts.forAutocomplete));
1746
CHECK(getFrontend().allModuleDependenciesValid("game/Gui/Modules/B", !opts.forAutocomplete));
1747
CHECK(getFrontend().allModuleDependenciesValid("game/Gui/Modules/A", !opts.forAutocomplete));
1748
CHECK(getFrontend().allModuleDependenciesValid("game/Gui/Modules/A", opts.forAutocomplete));
1749
}
1750
1751
TEST_CASE_FIXTURE(FrontendFixture, "queue_check_simple")
1752
{
1753
fileResolver.source["game/Gui/Modules/A"] = R"(
1754
--!strict
1755
return {hello=5, world=true}
1756
)";
1757
fileResolver.source["game/Gui/Modules/B"] = R"(
1758
--!strict
1759
local Modules = game:GetService('Gui').Modules
1760
local A = require(Modules.A)
1761
return {b_value = A.hello}
1762
)";
1763
1764
getFrontend().queueModuleCheck("game/Gui/Modules/B");
1765
getFrontend().checkQueuedModules();
1766
1767
auto result = getFrontend().getCheckResult("game/Gui/Modules/B", true);
1768
REQUIRE(result);
1769
LUAU_REQUIRE_NO_ERRORS(*result);
1770
}
1771
1772
TEST_CASE_FIXTURE(FrontendFixture, "queue_check_cycle_instant")
1773
{
1774
fileResolver.source["game/Gui/Modules/A"] = R"(
1775
--!strict
1776
local Modules = game:GetService('Gui').Modules
1777
local B = require(Modules.B)
1778
return {a_value = B.hello}
1779
)";
1780
fileResolver.source["game/Gui/Modules/B"] = R"(
1781
--!strict
1782
local Modules = game:GetService('Gui').Modules
1783
local A = require(Modules.A)
1784
return {b_value = A.hello}
1785
)";
1786
1787
getFrontend().queueModuleCheck("game/Gui/Modules/B");
1788
getFrontend().checkQueuedModules();
1789
1790
auto result = getFrontend().getCheckResult("game/Gui/Modules/B", true);
1791
REQUIRE(result);
1792
LUAU_REQUIRE_ERROR_COUNT(2, *result);
1793
CHECK(toString(result->errors[0]) == "Cyclic module dependency: game/Gui/Modules/B -> game/Gui/Modules/A");
1794
CHECK(toString(result->errors[1]) == "Cyclic module dependency: game/Gui/Modules/A -> game/Gui/Modules/B");
1795
}
1796
1797
TEST_CASE_FIXTURE(FrontendFixture, "queue_check_cycle_delayed")
1798
{
1799
fileResolver.source["game/Gui/Modules/C"] = R"(
1800
--!strict
1801
return {c_value = 5}
1802
)";
1803
fileResolver.source["game/Gui/Modules/A"] = R"(
1804
--!strict
1805
local Modules = game:GetService('Gui').Modules
1806
local C = require(Modules.C)
1807
local B = require(Modules.B)
1808
return {a_value = B.hello + C.c_value}
1809
)";
1810
fileResolver.source["game/Gui/Modules/B"] = R"(
1811
--!strict
1812
local Modules = game:GetService('Gui').Modules
1813
local C = require(Modules.C)
1814
local A = require(Modules.A)
1815
return {b_value = A.hello + C.c_value}
1816
)";
1817
1818
getFrontend().queueModuleCheck("game/Gui/Modules/B");
1819
getFrontend().checkQueuedModules();
1820
1821
auto result = getFrontend().getCheckResult("game/Gui/Modules/B", true);
1822
REQUIRE(result);
1823
LUAU_REQUIRE_ERROR_COUNT(2, *result);
1824
CHECK(toString(result->errors[0]) == "Cyclic module dependency: game/Gui/Modules/B -> game/Gui/Modules/A");
1825
CHECK(toString(result->errors[1]) == "Cyclic module dependency: game/Gui/Modules/A -> game/Gui/Modules/B");
1826
}
1827
1828
TEST_CASE_FIXTURE(FrontendFixture, "queue_check_propagates_ice")
1829
{
1830
ScopedFastFlag sffs{FFlag::DebugLuauMagicTypes, true};
1831
1832
ModuleName mm = fromString("MainModule");
1833
fileResolver.source[mm] = R"(
1834
--!strict
1835
local a: _luau_ice = 55
1836
)";
1837
getFrontend().markDirty(mm);
1838
getFrontend().queueModuleCheck("MainModule");
1839
1840
CHECK_THROWS_AS(getFrontend().checkQueuedModules(), InternalCompilerError);
1841
}
1842
1843
TEST_CASE_FIXTURE(FrontendFixture, "parse_just_a_type")
1844
{
1845
std::string src = "(number, string) -> boolean?";
1846
1847
TypeArena arena;
1848
Allocator allocator;
1849
AstNameTable names{allocator};
1850
1851
BuiltinTypes builtinTypes;
1852
InternalErrorReporter iceHandler;
1853
TypeCheckLimits limits;
1854
1855
TypeId ty = getFrontend().parseType(NotNull{&allocator}, NotNull{&names}, NotNull{&iceHandler}, limits, NotNull{&arena}, src);
1856
1857
CHECK("(number, string) -> boolean?" == toString(ty));
1858
}
1859
1860
TEST_CASE_FIXTURE(FrontendFixture, "parse_types")
1861
{
1862
const TypeId ty1 = parseType("(number, boolean?) -> string");
1863
CHECK("(number, boolean?) -> string" == toString(ty1));
1864
1865
CHECK_THROWS_AS(parseType("illegal Luau Syntax here"), InternalCompilerError);
1866
1867
const TypeId ty3 = parseType("blah<blahblah, number>");
1868
CHECK(get<ErrorType>(ty3));
1869
1870
CHECK_THROWS_AS(parseType("number, boolean?) -> string"), InternalCompilerError);
1871
CHECK_THROWS_AS(parseType("{size: number?"), InternalCompilerError);
1872
}
1873
1874
TEST_SUITE_END();
1875
1876