Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/tests/Linter.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/Linter.h"
3
#include "Luau/BuiltinDefinitions.h"
4
5
#include "Fixture.h"
6
#include "ScopedFlags.h"
7
8
#include "doctest.h"
9
10
LUAU_FASTFLAG(DebugLuauForceOldSolver)
11
LUAU_FASTFLAG(LuauLinterVectorPrimitive)
12
13
using namespace Luau;
14
15
TEST_SUITE_BEGIN("Linter");
16
17
TEST_CASE_FIXTURE(Fixture, "CleanCode")
18
{
19
LintResult result = lint(R"(
20
function fib(n)
21
return n < 2 and 1 or fib(n-1) + fib(n-2)
22
end
23
24
)");
25
26
REQUIRE(0 == result.warnings.size());
27
}
28
29
TEST_CASE_FIXTURE(Fixture, "type_function_fully_reduces")
30
{
31
LintResult result = lint(R"(
32
function fib(n)
33
return n < 2 or fib(n-2)
34
end
35
36
)");
37
38
REQUIRE(0 == result.warnings.size());
39
}
40
41
TEST_CASE_FIXTURE(Fixture, "UnknownGlobal")
42
{
43
LintResult result = lint("--!nocheck\nreturn foo");
44
45
REQUIRE(1 == result.warnings.size());
46
CHECK_EQ(result.warnings[0].text, "Unknown global 'foo'; consider assigning to it first");
47
}
48
49
TEST_CASE_FIXTURE(Fixture, "DeprecatedGlobal")
50
{
51
// Normally this would be defined externally, so hack it in for testing
52
addGlobalBinding(getFrontend().globals, "Wait", Binding{getBuiltins()->anyType, {}, true, "wait", "@test/global/Wait"});
53
54
LintResult result = lint("Wait(5)");
55
56
REQUIRE(1 == result.warnings.size());
57
CHECK_EQ(result.warnings[0].text, "Global 'Wait' is deprecated, use 'wait' instead");
58
}
59
60
TEST_CASE_FIXTURE(Fixture, "DeprecatedGlobalNoReplacement")
61
{
62
// Normally this would be defined externally, so hack it in for testing
63
const char* deprecationReplacementString = "";
64
addGlobalBinding(getFrontend().globals, "Version", Binding{getBuiltins()->anyType, {}, true, deprecationReplacementString});
65
66
LintResult result = lint("Version()");
67
68
REQUIRE(1 == result.warnings.size());
69
CHECK_EQ(result.warnings[0].text, "Global 'Version' is deprecated");
70
}
71
72
TEST_CASE_FIXTURE(Fixture, "PlaceholderRead")
73
{
74
LintResult result = lint(R"(
75
local _ = 5
76
return _
77
)");
78
79
REQUIRE(1 == result.warnings.size());
80
CHECK_EQ(result.warnings[0].text, "Placeholder value '_' is read here; consider using a named variable");
81
}
82
83
TEST_CASE_FIXTURE(Fixture, "PlaceholderReadGlobal")
84
{
85
LintResult result = lint(R"(
86
_ = 5
87
print(_)
88
)");
89
90
REQUIRE(1 == result.warnings.size());
91
CHECK_EQ(result.warnings[0].text, "Placeholder value '_' is read here; consider using a named variable");
92
}
93
94
TEST_CASE_FIXTURE(Fixture, "PlaceholderWrite")
95
{
96
LintResult result = lint(R"(
97
local _ = 5
98
_ = 6
99
)");
100
101
REQUIRE(0 == result.warnings.size());
102
}
103
104
TEST_CASE_FIXTURE(BuiltinsFixture, "BuiltinGlobalWrite")
105
{
106
LintResult result = lint(R"(
107
math = {}
108
109
function assert(x)
110
end
111
112
assert(5)
113
)");
114
115
REQUIRE(2 == result.warnings.size());
116
CHECK_EQ(result.warnings[0].text, "Built-in global 'math' is overwritten here; consider using a local or changing the name");
117
CHECK_EQ(result.warnings[1].text, "Built-in global 'assert' is overwritten here; consider using a local or changing the name");
118
}
119
120
TEST_CASE_FIXTURE(Fixture, "MultilineBlock")
121
{
122
LintResult result = lint(R"(
123
if true then print(1) print(2) print(3) end
124
)");
125
126
REQUIRE(1 == result.warnings.size());
127
CHECK_EQ(result.warnings[0].text, "A new statement is on the same line; add semi-colon on previous statement to silence");
128
}
129
130
TEST_CASE_FIXTURE(Fixture, "MultilineBlockSemicolonsWhitelisted")
131
{
132
LintResult result = lint(R"(
133
print(1); print(2); print(3)
134
)");
135
136
REQUIRE(0 == result.warnings.size());
137
}
138
139
TEST_CASE_FIXTURE(Fixture, "MultilineBlockMissedSemicolon")
140
{
141
LintResult result = lint(R"(
142
print(1); print(2) print(3)
143
)");
144
145
REQUIRE(1 == result.warnings.size());
146
CHECK_EQ(result.warnings[0].text, "A new statement is on the same line; add semi-colon on previous statement to silence");
147
}
148
149
TEST_CASE_FIXTURE(Fixture, "MultilineBlockLocalDo")
150
{
151
LintResult result = lint(R"(
152
local _x do
153
_x = 5
154
end
155
)");
156
157
REQUIRE(0 == result.warnings.size());
158
}
159
160
TEST_CASE_FIXTURE(Fixture, "ConfusingIndentation")
161
{
162
LintResult result = lint(R"(
163
print(math.max(1,
164
2))
165
)");
166
167
REQUIRE(1 == result.warnings.size());
168
CHECK_EQ(result.warnings[0].text, "Statement spans multiple lines; use indentation to silence");
169
}
170
171
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocal")
172
{
173
LintResult result = lint(R"(
174
function bar()
175
foo = 6
176
return foo
177
end
178
179
return bar()
180
)");
181
182
REQUIRE(1 == result.warnings.size());
183
CHECK_EQ(result.warnings[0].text, "Global 'foo' is only used in the enclosing function 'bar'; consider changing it to local");
184
}
185
186
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalMultiFx")
187
{
188
LintResult result = lint(R"(
189
function bar()
190
foo = 6
191
return foo
192
end
193
194
function baz()
195
foo = 6
196
return foo
197
end
198
199
return bar() + baz()
200
)");
201
202
REQUIRE(1 == result.warnings.size());
203
CHECK_EQ(result.warnings[0].text, "Global 'foo' is never read before being written. Consider changing it to local");
204
}
205
206
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalMultiFxWithRead")
207
{
208
LintResult result = lint(R"(
209
function bar()
210
foo = 6
211
return foo
212
end
213
214
function baz()
215
foo = 6
216
return foo
217
end
218
219
function read()
220
print(foo)
221
end
222
223
return bar() + baz() + read()
224
)");
225
226
REQUIRE(0 == result.warnings.size());
227
}
228
229
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalWithConditional")
230
{
231
LintResult result = lint(R"(
232
function bar()
233
if true then foo = 6 end
234
return foo
235
end
236
237
function baz()
238
foo = 6
239
return foo
240
end
241
242
return bar() + baz()
243
)");
244
245
REQUIRE(0 == result.warnings.size());
246
}
247
248
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocal3WithConditionalRead")
249
{
250
LintResult result = lint(R"(
251
function bar()
252
foo = 6
253
return foo
254
end
255
256
function baz()
257
foo = 6
258
return foo
259
end
260
261
function read()
262
if false then print(foo) end
263
end
264
265
return bar() + baz() + read()
266
)");
267
268
REQUIRE(0 == result.warnings.size());
269
}
270
271
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalInnerRead")
272
{
273
LintResult result = lint(R"(
274
function foo()
275
local f = function() return bar end
276
f()
277
bar = 42
278
end
279
280
function baz() bar = 0 end
281
282
return foo() + baz()
283
)");
284
285
REQUIRE(0 == result.warnings.size());
286
}
287
288
TEST_CASE_FIXTURE(Fixture, "GlobalAsLocalMulti")
289
{
290
LintResult result = lint(R"(
291
local createFunction = function(configValue)
292
-- Create an internal convenience function
293
local function internalLogic()
294
print(configValue) -- prints passed-in value
295
end
296
-- Here, we thought we were creating another internal convenience function
297
-- that closed over the passed-in configValue, but this is actually being
298
-- declared at module scope!
299
function moreInternalLogic()
300
print(configValue) -- nil!!!
301
end
302
return function()
303
internalLogic()
304
moreInternalLogic()
305
return nil
306
end
307
end
308
fnA = createFunction(true)
309
fnB = createFunction(false)
310
fnA() -- prints "true", "nil"
311
fnB() -- prints "false", "nil"
312
)");
313
314
REQUIRE(1 == result.warnings.size());
315
CHECK_EQ(
316
result.warnings[0].text, "Global 'moreInternalLogic' is only used in the enclosing function defined at line 2; consider changing it to local"
317
);
318
}
319
320
TEST_CASE_FIXTURE(Fixture, "LocalShadowLocal")
321
{
322
LintResult result = lint(R"(
323
local arg = 6
324
print(arg)
325
326
local arg = 5
327
print(arg)
328
)");
329
330
REQUIRE(1 == result.warnings.size());
331
CHECK_EQ(result.warnings[0].text, "Variable 'arg' shadows previous declaration at line 2");
332
}
333
334
TEST_CASE_FIXTURE(BuiltinsFixture, "LocalShadowGlobal")
335
{
336
LintResult result = lint(R"(
337
local math = math
338
global = math
339
340
function bar()
341
local global = math.max(5, 1)
342
return global
343
end
344
345
return bar()
346
)");
347
348
REQUIRE(1 == result.warnings.size());
349
CHECK_EQ(result.warnings[0].text, "Variable 'global' shadows a global variable used at line 3");
350
}
351
352
TEST_CASE_FIXTURE(Fixture, "LocalShadowArgument")
353
{
354
LintResult result = lint(R"(
355
function bar(a, b)
356
local a = b + 1
357
return a
358
end
359
360
return bar()
361
)");
362
363
REQUIRE(1 == result.warnings.size());
364
CHECK_EQ(result.warnings[0].text, "Variable 'a' shadows previous declaration at line 2");
365
}
366
367
TEST_CASE_FIXTURE(Fixture, "LocalUnused")
368
{
369
LintResult result = lint(R"(
370
local arg = 6
371
372
local function bar()
373
local arg = 5
374
local blarg = 6
375
if arg then
376
blarg = 42
377
end
378
end
379
380
return bar()
381
)");
382
383
REQUIRE(2 == result.warnings.size());
384
CHECK_EQ(result.warnings[0].text, "Variable 'arg' is never used; prefix with '_' to silence");
385
CHECK_EQ(result.warnings[1].text, "Variable 'blarg' is never used; prefix with '_' to silence");
386
}
387
388
TEST_CASE_FIXTURE(Fixture, "ImportUnused")
389
{
390
// Normally this would be defined externally, so hack it in for testing
391
addGlobalBinding(getFrontend().globals, "game", getBuiltins()->anyType, "@test");
392
393
LintResult result = lint(R"(
394
local Roact = require(game.Packages.Roact)
395
local _Roact = require(game.Packages.Roact)
396
)");
397
398
REQUIRE(1 == result.warnings.size());
399
CHECK_EQ(result.warnings[0].text, "Import 'Roact' is never used; prefix with '_' to silence");
400
}
401
402
TEST_CASE_FIXTURE(Fixture, "FunctionUnused")
403
{
404
LintResult result = lint(R"(
405
function bar()
406
end
407
408
local function qux()
409
end
410
411
function foo()
412
end
413
414
local function _unusedl()
415
end
416
417
function _unusedg()
418
end
419
420
return foo()
421
)");
422
423
REQUIRE(2 == result.warnings.size());
424
CHECK_EQ(result.warnings[0].text, "Function 'bar' is never used; prefix with '_' to silence");
425
CHECK_EQ(result.warnings[1].text, "Function 'qux' is never used; prefix with '_' to silence");
426
}
427
428
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeBasic")
429
{
430
LintResult result = lint(R"(
431
do
432
return 'ok'
433
end
434
435
print("hi!")
436
)");
437
438
REQUIRE(1 == result.warnings.size());
439
CHECK_EQ(result.warnings[0].location.begin.line, 5);
440
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always returns)");
441
}
442
443
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopBreak")
444
{
445
LintResult result = lint(R"(
446
while true do
447
do break end
448
print("nope")
449
end
450
451
print("hi!")
452
)");
453
454
REQUIRE(1 == result.warnings.size());
455
CHECK_EQ(result.warnings[0].location.begin.line, 3);
456
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always breaks)");
457
}
458
459
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopContinue")
460
{
461
LintResult result = lint(R"(
462
while true do
463
do continue end
464
print("nope")
465
end
466
467
print("hi!")
468
)");
469
470
REQUIRE(1 == result.warnings.size());
471
CHECK_EQ(result.warnings[0].location.begin.line, 3);
472
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always continues)");
473
}
474
475
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeIfMerge")
476
{
477
LintResult result = lint(R"(
478
function foo1(a)
479
if a then
480
return 'x'
481
else
482
return 'y'
483
end
484
return 'z'
485
end
486
487
function foo2(a)
488
if a then
489
return 'x'
490
end
491
return 'z'
492
end
493
494
function foo3(a)
495
if a then
496
return 'x'
497
else
498
print('y')
499
end
500
return 'z'
501
end
502
503
return { foo1, foo2, foo3 }
504
)");
505
506
REQUIRE(1 == result.warnings.size());
507
CHECK_EQ(result.warnings[0].location.begin.line, 7);
508
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always returns)");
509
}
510
511
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeErrorReturnSilent")
512
{
513
LintResult result = lint(R"(
514
function foo1(a)
515
if a then
516
error('x')
517
return 'z'
518
else
519
error('y')
520
end
521
end
522
523
return foo1
524
)");
525
526
REQUIRE(0 == result.warnings.size());
527
}
528
529
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeAssertFalseReturnSilent")
530
{
531
LintResult result = lint(R"(
532
function foo1(a)
533
if a then
534
return 'z'
535
end
536
537
assert(false)
538
end
539
540
return foo1
541
)");
542
543
REQUIRE(0 == result.warnings.size());
544
}
545
546
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeErrorReturnNonSilentBranchy")
547
{
548
LintResult result = lint(R"(
549
function foo1(a)
550
if a then
551
error('x')
552
else
553
error('y')
554
end
555
return 'z'
556
end
557
558
return foo1
559
)");
560
561
REQUIRE(1 == result.warnings.size());
562
CHECK_EQ(result.warnings[0].location.begin.line, 7);
563
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always errors)");
564
}
565
566
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeErrorReturnPropagate")
567
{
568
LintResult result = lint(R"(
569
function foo1(a)
570
if a then
571
error('x')
572
return 'z'
573
else
574
error('y')
575
end
576
return 'x'
577
end
578
579
return foo1
580
)");
581
582
REQUIRE(1 == result.warnings.size());
583
CHECK_EQ(result.warnings[0].location.begin.line, 8);
584
CHECK_EQ(result.warnings[0].text, "Unreachable code (previous statement always errors)");
585
}
586
587
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopWhile")
588
{
589
LintResult result = lint(R"(
590
function foo1(a)
591
while a do
592
return 'z'
593
end
594
return 'x'
595
end
596
597
return foo1
598
)");
599
600
REQUIRE(0 == result.warnings.size());
601
}
602
603
TEST_CASE_FIXTURE(Fixture, "UnreachableCodeLoopRepeat")
604
{
605
LintResult result = lint(R"(
606
function foo1(a)
607
repeat
608
return 'z'
609
until a
610
return 'x'
611
end
612
613
return foo1
614
)");
615
616
// this is technically a bug, since the repeat body always returns; fixing this bug is a bit more involved than I'd like
617
REQUIRE(0 == result.warnings.size());
618
}
619
620
TEST_CASE_FIXTURE(Fixture, "UnknownType")
621
{
622
unfreeze(getFrontend().globals.globalTypes);
623
TableType::Props instanceProps{
624
{"ClassName", {getBuiltins()->anyType}},
625
};
626
627
TableType instanceTable{instanceProps, std::nullopt, getFrontend().globals.globalScope->level, Luau::TableState::Sealed};
628
TypeId instanceType = getFrontend().globals.globalTypes.addType(instanceTable);
629
TypeFun instanceTypeFun{{}, instanceType};
630
631
getFrontend().globals.globalScope->exportedTypeBindings["Part"] = instanceTypeFun;
632
633
LintResult result = lint(R"(
634
local game = ...
635
local _e01 = type(game) == "Part"
636
local _e02 = typeof(game) == "Bar"
637
local _ok = typeof(game) == "vector"
638
639
local _o01 = type(game) == "number"
640
local _o02 = type(game) == "vector"
641
local _o03 = typeof(game) == "Part"
642
)");
643
644
if (FFlag::LuauLinterVectorPrimitive)
645
{
646
REQUIRE(2 == result.warnings.size());
647
CHECK_EQ(result.warnings[0].location.begin.line, 2);
648
CHECK_EQ(result.warnings[0].text, "Unknown type 'Part' (expected primitive type)");
649
CHECK_EQ(result.warnings[1].location.begin.line, 3);
650
CHECK_EQ(result.warnings[1].text, "Unknown type 'Bar'");
651
}
652
else
653
{
654
REQUIRE(3 == result.warnings.size());
655
CHECK_EQ(result.warnings[0].location.begin.line, 2);
656
CHECK_EQ(result.warnings[0].text, "Unknown type 'Part' (expected primitive type)");
657
CHECK_EQ(result.warnings[1].location.begin.line, 3);
658
CHECK_EQ(result.warnings[1].text, "Unknown type 'Bar'");
659
CHECK_EQ(result.warnings[2].location.begin.line, 4);
660
CHECK_EQ(result.warnings[2].text, "Unknown type 'vector' (expected primitive or userdata type)");
661
}
662
}
663
664
TEST_CASE_FIXTURE(Fixture, "ForRangeTable")
665
{
666
LintResult result = lint(R"(
667
local t = {}
668
669
for i=#t,1 do
670
end
671
672
for i=#t,1,-1 do
673
end
674
)");
675
676
REQUIRE(1 == result.warnings.size());
677
CHECK_EQ(result.warnings[0].location.begin.line, 3);
678
CHECK_EQ(result.warnings[0].text, "For loop should iterate backwards; did you forget to specify -1 as step?");
679
}
680
681
TEST_CASE_FIXTURE(Fixture, "ForRangeBackwards")
682
{
683
LintResult result = lint(R"(
684
for i=8,1 do
685
end
686
687
for i=8,1,-1 do
688
end
689
)");
690
691
REQUIRE(1 == result.warnings.size());
692
CHECK_EQ(result.warnings[0].location.begin.line, 1);
693
CHECK_EQ(result.warnings[0].text, "For loop should iterate backwards; did you forget to specify -1 as step?");
694
}
695
696
TEST_CASE_FIXTURE(Fixture, "ForRangeImprecise")
697
{
698
LintResult result = lint(R"(
699
for i=1.3,7.5 do
700
end
701
702
for i=1.3,7.5,1 do
703
end
704
)");
705
706
REQUIRE(1 == result.warnings.size());
707
CHECK_EQ(result.warnings[0].location.begin.line, 1);
708
CHECK_EQ(result.warnings[0].text, "For loop ends at 7.3 instead of 7.5; did you forget to specify step?");
709
}
710
711
TEST_CASE_FIXTURE(Fixture, "ForRangeZero")
712
{
713
LintResult result = lint(R"(
714
for i=0,#t do
715
end
716
717
for i=(0),#t do -- to silence
718
end
719
720
for i=#t,0 do
721
end
722
)");
723
724
REQUIRE(2 == result.warnings.size());
725
CHECK_EQ(result.warnings[0].location.begin.line, 1);
726
CHECK_EQ(result.warnings[0].text, "For loop starts at 0, but arrays start at 1");
727
CHECK_EQ(result.warnings[1].location.begin.line, 7);
728
CHECK_EQ(
729
result.warnings[1].text,
730
"For loop should iterate backwards; did you forget to specify -1 as step? Also consider changing 0 to 1 since arrays start at 1"
731
);
732
}
733
734
TEST_CASE_FIXTURE(Fixture, "UnbalancedAssignment")
735
{
736
LintResult result = lint(R"(
737
do
738
local _a,_b,_c = pcall()
739
end
740
do
741
local _a,_b,_c = pcall(), 5
742
end
743
do
744
local _a,_b,_c = pcall(), 5, 6
745
end
746
do
747
local _a,_b,_c = pcall(), 5, 6, 7
748
end
749
do
750
local _a,_b,_c = pcall(), nil
751
end
752
)");
753
754
REQUIRE(2 == result.warnings.size());
755
CHECK_EQ(result.warnings[0].location.begin.line, 5);
756
CHECK_EQ(result.warnings[0].text, "Assigning 2 values to 3 variables initializes extra variables with nil; add 'nil' to value list to silence");
757
CHECK_EQ(result.warnings[1].location.begin.line, 11);
758
CHECK_EQ(result.warnings[1].text, "Assigning 4 values to 3 variables leaves some values unused");
759
}
760
761
TEST_CASE_FIXTURE(Fixture, "ImplicitReturn")
762
{
763
LintResult result = lint(R"(
764
--!nonstrict
765
function f1(a)
766
if not a then
767
return 5
768
end
769
end
770
771
function f2(a)
772
if not a then
773
return
774
end
775
end
776
777
function f3(a)
778
if not a then
779
return 5
780
else
781
return
782
end
783
end
784
785
function f4(a)
786
for i in pairs(a) do
787
if i > 5 then
788
return i
789
end
790
end
791
792
print("element not found")
793
end
794
795
function f5(a)
796
for i in pairs(a) do
797
if i > 5 then
798
return i
799
end
800
end
801
802
error("element not found")
803
end
804
805
f6 = function(a)
806
if a == 0 then
807
return 42
808
end
809
end
810
811
function f7(a)
812
repeat
813
return 10
814
until a ~= nil
815
end
816
817
return f1,f2,f3,f4,f5,f6,f7
818
)");
819
820
REQUIRE(3 == result.warnings.size());
821
CHECK_EQ(result.warnings[0].location.begin.line, 5);
822
CHECK_EQ(
823
result.warnings[0].text,
824
"Function 'f1' can implicitly return no values even though there's an explicit return at line 5; add explicit return to silence"
825
);
826
CHECK_EQ(result.warnings[1].location.begin.line, 29);
827
CHECK_EQ(
828
result.warnings[1].text,
829
"Function 'f4' can implicitly return no values even though there's an explicit return at line 26; add explicit return to silence"
830
);
831
CHECK_EQ(result.warnings[2].location.begin.line, 45);
832
CHECK_EQ(
833
result.warnings[2].text,
834
"Function can implicitly return no values even though there's an explicit return at line 45; add explicit return to silence"
835
);
836
}
837
838
TEST_CASE_FIXTURE(Fixture, "ImplicitReturnInfiniteLoop")
839
{
840
LintResult result = lint(R"(
841
--!nonstrict
842
function f1(a)
843
while true do
844
if math.random() > 0.5 then
845
return 5
846
end
847
end
848
end
849
850
function f2(a)
851
repeat
852
if math.random() > 0.5 then
853
return 5
854
end
855
until false
856
end
857
858
function f3(a)
859
while true do
860
if math.random() > 0.5 then
861
return 5
862
end
863
if math.random() < 0.1 then
864
break
865
end
866
end
867
end
868
869
function f4(a)
870
repeat
871
if math.random() > 0.5 then
872
return 5
873
end
874
if math.random() < 0.1 then
875
break
876
end
877
until false
878
end
879
880
return f1,f2,f3,f4
881
)");
882
883
REQUIRE(2 == result.warnings.size());
884
CHECK_EQ(result.warnings[0].location.begin.line, 26);
885
CHECK_EQ(
886
result.warnings[0].text,
887
"Function 'f3' can implicitly return no values even though there's an explicit return at line 22; add explicit return to silence"
888
);
889
CHECK_EQ(result.warnings[1].location.begin.line, 37);
890
CHECK_EQ(
891
result.warnings[1].text,
892
"Function 'f4' can implicitly return no values even though there's an explicit return at line 33; add explicit return to silence"
893
);
894
}
895
896
TEST_CASE_FIXTURE(Fixture, "TypeAnnotationsShouldNotProduceWarnings")
897
{
898
LintResult result = lint(R"(--!strict
899
type InputData = {
900
id: number,
901
inputType: EnumItem,
902
inputState: EnumItem,
903
updated: number,
904
position: Vector3,
905
keyCode: EnumItem,
906
name: string
907
}
908
)");
909
910
REQUIRE(0 == result.warnings.size());
911
}
912
913
TEST_CASE_FIXTURE(Fixture, "BreakFromInfiniteLoopMakesStatementReachable")
914
{
915
LintResult result = lint(R"(
916
local bar = ...
917
918
repeat
919
if bar then
920
break
921
end
922
923
return 2
924
until true
925
926
return 1
927
)");
928
929
REQUIRE(0 == result.warnings.size());
930
}
931
932
TEST_CASE_FIXTURE(Fixture, "IgnoreLintAll")
933
{
934
LintResult result = lint(R"(
935
--!nolint
936
return foo
937
)");
938
939
REQUIRE(0 == result.warnings.size());
940
}
941
942
TEST_CASE_FIXTURE(Fixture, "IgnoreLintSpecific")
943
{
944
LintResult result = lint(R"(
945
--!nolint UnknownGlobal
946
local x = 1
947
return foo
948
)");
949
950
REQUIRE(1 == result.warnings.size());
951
CHECK_EQ(result.warnings[0].text, "Variable 'x' is never used; prefix with '_' to silence");
952
}
953
954
TEST_CASE_FIXTURE(Fixture, "FormatStringFormat")
955
{
956
LintResult result = lint(R"(
957
-- incorrect format strings
958
string.format("%")
959
string.format("%??d")
960
string.format("%Y")
961
962
-- incorrect format strings, self call
963
local _ = ("%"):format()
964
965
-- correct format strings, just to uh make sure
966
string.format("hello %+10d %.02f %%", 4, 5)
967
)");
968
969
REQUIRE(4 == result.warnings.size());
970
CHECK_EQ(result.warnings[0].text, "Invalid format string: unfinished format specifier");
971
CHECK_EQ(result.warnings[1].text, "Invalid format string: invalid format specifier: must be a string format specifier or %");
972
CHECK_EQ(result.warnings[2].text, "Invalid format string: invalid format specifier: must be a string format specifier or %");
973
CHECK_EQ(result.warnings[3].text, "Invalid format string: unfinished format specifier");
974
}
975
976
TEST_CASE_FIXTURE(Fixture, "FormatStringPack")
977
{
978
LintResult result = lint(R"(
979
-- incorrect pack specifiers
980
string.pack("?")
981
string.packsize("?")
982
string.unpack("?")
983
984
-- missing size
985
string.packsize("bc")
986
987
-- incorrect X alignment
988
string.packsize("X")
989
string.packsize("X i")
990
991
-- correct X alignment
992
string.packsize("Xi")
993
994
-- packsize can't be used with variable sized formats
995
string.packsize("s")
996
997
-- out of range size specifiers
998
string.packsize("i0")
999
string.packsize("i17")
1000
1001
-- a very very very out of range size specifier
1002
string.packsize("i99999999999999999999")
1003
string.packsize("c99999999999999999999")
1004
1005
-- correct format specifiers
1006
string.packsize("=!1bbbI3c42")
1007
)");
1008
1009
REQUIRE(11 == result.warnings.size());
1010
CHECK_EQ(result.warnings[0].text, "Invalid pack format: unexpected character; must be a pack specifier or space");
1011
CHECK_EQ(result.warnings[1].text, "Invalid pack format: unexpected character; must be a pack specifier or space");
1012
CHECK_EQ(result.warnings[2].text, "Invalid pack format: unexpected character; must be a pack specifier or space");
1013
CHECK_EQ(result.warnings[3].text, "Invalid pack format: fixed-sized string format must specify the size");
1014
CHECK_EQ(result.warnings[4].text, "Invalid pack format: X must be followed by a size specifier");
1015
CHECK_EQ(result.warnings[5].text, "Invalid pack format: X must be followed by a size specifier");
1016
CHECK_EQ(result.warnings[6].text, "Invalid pack format: pack specifier must be fixed-size");
1017
CHECK_EQ(result.warnings[7].text, "Invalid pack format: integer size must be in range [1,16]");
1018
CHECK_EQ(result.warnings[8].text, "Invalid pack format: integer size must be in range [1,16]");
1019
CHECK_EQ(result.warnings[9].text, "Invalid pack format: size specifier is too large");
1020
CHECK_EQ(result.warnings[10].text, "Invalid pack format: size specifier is too large");
1021
}
1022
1023
TEST_CASE_FIXTURE(Fixture, "FormatStringMatch")
1024
{
1025
LintResult result = lint(R"(
1026
local s = ...
1027
1028
-- incorrect character class specifiers
1029
string.match(s, "%q")
1030
string.gmatch(s, "%q")
1031
string.find(s, "%q")
1032
string.gsub(s, "%q", "")
1033
1034
-- various errors
1035
string.match(s, "%")
1036
string.match(s, "[%1]")
1037
string.match(s, "%0")
1038
string.match(s, "(%d)%2")
1039
string.match(s, "%bx")
1040
string.match(s, "%foo")
1041
string.match(s, '(%d))')
1042
string.match(s, '(%d')
1043
string.match(s, '[%d')
1044
string.match(s, '%,')
1045
1046
-- self call - not detected because we don't know the type!
1047
local _ = s:match("%q")
1048
1049
-- correct patterns
1050
string.match(s, "[A-Z]+(%d)%1")
1051
)");
1052
1053
REQUIRE(14 == result.warnings.size());
1054
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
1055
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
1056
CHECK_EQ(result.warnings[2].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
1057
CHECK_EQ(result.warnings[3].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
1058
CHECK_EQ(result.warnings[4].text, "Invalid match pattern: unfinished character class");
1059
CHECK_EQ(result.warnings[5].text, "Invalid match pattern: sets can not contain capture references");
1060
CHECK_EQ(result.warnings[6].text, "Invalid match pattern: invalid capture reference, must be 1-9");
1061
CHECK_EQ(result.warnings[7].text, "Invalid match pattern: invalid capture reference, must refer to a valid capture");
1062
CHECK_EQ(result.warnings[8].text, "Invalid match pattern: missing brace characters for balanced match");
1063
CHECK_EQ(result.warnings[9].text, "Invalid match pattern: missing set after a frontier pattern");
1064
CHECK_EQ(result.warnings[10].text, "Invalid match pattern: unexpected ) without a matching (");
1065
CHECK_EQ(result.warnings[11].text, "Invalid match pattern: expected ) at the end of the string to close a capture");
1066
CHECK_EQ(result.warnings[12].text, "Invalid match pattern: expected ] at the end of the string to close a set");
1067
CHECK_EQ(result.warnings[13].text, "Invalid match pattern: expected a magic character after %");
1068
}
1069
1070
TEST_CASE_FIXTURE(Fixture, "FormatStringMatchNested")
1071
{
1072
LintResult result = lint(R"~(
1073
local s = ...
1074
1075
-- correct reference to nested pattern
1076
string.match(s, "((a)%2)")
1077
1078
-- incorrect reference to nested pattern (not closed yet)
1079
string.match(s, "((a)%1)")
1080
1081
-- incorrect reference to nested pattern (index out of range)
1082
string.match(s, "((a)%3)")
1083
)~");
1084
1085
REQUIRE(2 == result.warnings.size());
1086
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid capture reference, must refer to a closed capture");
1087
CHECK_EQ(result.warnings[0].location.begin.line, 7);
1088
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid capture reference, must refer to a valid capture");
1089
CHECK_EQ(result.warnings[1].location.begin.line, 10);
1090
}
1091
1092
TEST_CASE_FIXTURE(Fixture, "FormatStringMatchSets")
1093
{
1094
LintResult result = lint(R"~(
1095
local s = ...
1096
1097
-- fake empty sets (but actually sets that aren't closed)
1098
string.match(s, "[]")
1099
string.match(s, "[^]")
1100
1101
-- character ranges in sets
1102
string.match(s, "[%a-b]")
1103
string.match(s, "[a-%b]")
1104
1105
-- invalid escapes
1106
string.match(s, "[%q]")
1107
string.match(s, "[%;]")
1108
1109
-- capture refs in sets
1110
string.match(s, "[%1]")
1111
1112
-- valid escapes and - at the end
1113
string.match(s, "[%]x-]")
1114
1115
-- % escapes itself
1116
string.match(s, "[%%]")
1117
1118
-- this abomination is a valid pattern due to rules wrt handling empty sets
1119
string.match(s, "[]|'[]")
1120
string.match(s, "[^]|'[]")
1121
)~");
1122
1123
REQUIRE(7 == result.warnings.size());
1124
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: expected ] at the end of the string to close a set");
1125
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: expected ] at the end of the string to close a set");
1126
CHECK_EQ(result.warnings[2].text, "Invalid match pattern: character range can't include character sets");
1127
CHECK_EQ(result.warnings[3].text, "Invalid match pattern: character range can't include character sets");
1128
CHECK_EQ(result.warnings[4].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
1129
CHECK_EQ(result.warnings[5].text, "Invalid match pattern: expected a magic character after %");
1130
CHECK_EQ(result.warnings[6].text, "Invalid match pattern: sets can not contain capture references");
1131
}
1132
1133
TEST_CASE_FIXTURE(Fixture, "FormatStringFindArgs")
1134
{
1135
LintResult result = lint(R"(
1136
local s = ...
1137
1138
-- incorrect character class specifier
1139
string.find(s, "%q")
1140
1141
-- raw string find
1142
string.find(s, "%q", 1, true)
1143
string.find(s, "%q", 1, math.random() < 0.5)
1144
1145
-- incorrect character class specifier
1146
string.find(s, "%q", 1, false)
1147
1148
-- missing arguments
1149
string.find()
1150
string.find("foo");
1151
("foo"):find()
1152
)");
1153
1154
REQUIRE(2 == result.warnings.size());
1155
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
1156
CHECK_EQ(result.warnings[0].location.begin.line, 4);
1157
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: invalid character class, must refer to a defined class or its inverse");
1158
CHECK_EQ(result.warnings[1].location.begin.line, 11);
1159
}
1160
1161
TEST_CASE_FIXTURE(Fixture, "FormatStringReplace")
1162
{
1163
LintResult result = lint(R"(
1164
local s = ...
1165
1166
-- incorrect replacements
1167
string.gsub(s, '(%d+)', "%")
1168
string.gsub(s, '(%d+)', "%x")
1169
string.gsub(s, '(%d+)', "%2")
1170
string.gsub(s, '', "%1")
1171
1172
-- correct replacements
1173
string.gsub(s, '[A-Z]+(%d)', "%0%1")
1174
string.gsub(s, 'foo', "%0")
1175
)");
1176
1177
REQUIRE(4 == result.warnings.size());
1178
CHECK_EQ(result.warnings[0].text, "Invalid match replacement: unfinished replacement");
1179
CHECK_EQ(result.warnings[1].text, "Invalid match replacement: unexpected replacement character; must be a digit or %");
1180
CHECK_EQ(result.warnings[2].text, "Invalid match replacement: invalid capture index, must refer to pattern capture");
1181
CHECK_EQ(result.warnings[3].text, "Invalid match replacement: invalid capture index, must refer to pattern capture");
1182
}
1183
1184
TEST_CASE_FIXTURE(Fixture, "FormatStringDate")
1185
{
1186
LintResult result = lint(R"(
1187
-- incorrect formats
1188
os.date("%")
1189
os.date("%L")
1190
os.date("%?")
1191
os.date("\0")
1192
1193
-- correct formats
1194
os.date("it's %c now")
1195
os.date("!*t")
1196
)");
1197
1198
REQUIRE(4 == result.warnings.size());
1199
CHECK_EQ(result.warnings[0].text, "Invalid date format: unfinished replacement");
1200
CHECK_EQ(result.warnings[1].text, "Invalid date format: unexpected replacement character; must be a date format specifier or %");
1201
CHECK_EQ(result.warnings[2].text, "Invalid date format: unexpected replacement character; must be a date format specifier or %");
1202
CHECK_EQ(result.warnings[3].text, "Invalid date format: date format can not contain null characters");
1203
}
1204
1205
TEST_CASE_FIXTURE(Fixture, "FormatStringTyped")
1206
{
1207
LintResult result = lint(R"~(
1208
local s: string, nons = ...
1209
1210
string.match(s, "[]")
1211
s:match("[]")
1212
1213
-- no warning here since we don't know that it's a string
1214
nons:match("[]")
1215
)~");
1216
1217
REQUIRE(2 == result.warnings.size());
1218
CHECK_EQ(result.warnings[0].text, "Invalid match pattern: expected ] at the end of the string to close a set");
1219
CHECK_EQ(result.warnings[0].location.begin.line, 3);
1220
CHECK_EQ(result.warnings[1].text, "Invalid match pattern: expected ] at the end of the string to close a set");
1221
CHECK_EQ(result.warnings[1].location.begin.line, 4);
1222
}
1223
1224
TEST_CASE_FIXTURE(Fixture, "TableLiteral")
1225
{
1226
LintResult result = lint(R"(-- line 1
1227
_ = {
1228
first = 1,
1229
second = 2,
1230
first = 3,
1231
}
1232
1233
_ = {
1234
first = 1,
1235
["first"] = 2,
1236
}
1237
1238
_ = {
1239
1, 2, 3,
1240
[1] = 42
1241
}
1242
1243
_ = {
1244
[3] = 42,
1245
1, 2, 3,
1246
}
1247
1248
local _: {
1249
first: number,
1250
second: string,
1251
first: boolean
1252
}
1253
1254
_ = {
1255
1, 2, 3,
1256
[0] = 42,
1257
[4] = 42,
1258
}
1259
1260
_ = {
1261
[1] = 1,
1262
[2] = 2,
1263
[1] = 3,
1264
}
1265
1266
function _foo(): { first: number, second: string, first: boolean }
1267
end
1268
)");
1269
1270
REQUIRE(7 == result.warnings.size());
1271
CHECK_EQ(result.warnings[0].text, "Table field 'first' is a duplicate; previously defined at line 3");
1272
CHECK_EQ(result.warnings[1].text, "Table field 'first' is a duplicate; previously defined at line 9");
1273
CHECK_EQ(result.warnings[2].text, "Table index 1 is a duplicate; previously defined as a list entry");
1274
CHECK_EQ(result.warnings[3].text, "Table index 3 is a duplicate; previously defined as a list entry");
1275
CHECK_EQ(result.warnings[4].text, "Table type field 'first' is a duplicate; previously defined at line 24");
1276
CHECK_EQ(result.warnings[5].text, "Table index 1 is a duplicate; previously defined at line 36");
1277
CHECK_EQ(result.warnings[6].text, "Table type field 'first' is a duplicate; previously defined at line 41");
1278
}
1279
1280
TEST_CASE_FIXTURE(Fixture, "read_write_table_props")
1281
{
1282
DOES_NOT_PASS_OLD_SOLVER_GUARD();
1283
1284
LintResult result = lint(R"(-- line 1
1285
type A = {x: number}
1286
type B = {read x: number, write x: number}
1287
type C = {x: number, read x: number} -- line 4
1288
type D = {x: number, write x: number}
1289
type E = {read x: number, x: boolean}
1290
type F = {read x: number, read x: number}
1291
type G = {write x: number, x: boolean}
1292
type H = {write x: number, write x: boolean}
1293
)");
1294
1295
REQUIRE(6 == result.warnings.size());
1296
CHECK(result.warnings[0].text == "Table type field 'x' is already read-write; previously defined at line 4");
1297
CHECK(result.warnings[1].text == "Table type field 'x' is already read-write; previously defined at line 5");
1298
CHECK(result.warnings[2].text == "Table type field 'x' already has a read type defined at line 6");
1299
CHECK(result.warnings[3].text == "Table type field 'x' is a duplicate; previously defined at line 7");
1300
CHECK(result.warnings[4].text == "Table type field 'x' already has a write type defined at line 8");
1301
CHECK(result.warnings[5].text == "Table type field 'x' is a duplicate; previously defined at line 9");
1302
}
1303
1304
TEST_CASE_FIXTURE(Fixture, "ImportOnlyUsedInTypeAnnotation")
1305
{
1306
LintResult result = lint(R"(
1307
local Foo = require(script.Parent.Foo)
1308
1309
local x: Foo.Y = 1
1310
)");
1311
1312
REQUIRE(1 == result.warnings.size());
1313
CHECK_EQ(result.warnings[0].text, "Variable 'x' is never used; prefix with '_' to silence");
1314
}
1315
1316
TEST_CASE_FIXTURE(Fixture, "ImportOnlyUsedInReturnType")
1317
{
1318
LintResult result = lint(R"(
1319
local Foo = require(script.Parent.Foo)
1320
1321
function foo(): Foo.Y
1322
end
1323
)");
1324
1325
REQUIRE(1 == result.warnings.size());
1326
CHECK_EQ(result.warnings[0].text, "Function 'foo' is never used; prefix with '_' to silence");
1327
}
1328
1329
TEST_CASE_FIXTURE(Fixture, "DisableUnknownGlobalWithTypeChecking")
1330
{
1331
LintResult result = lint(R"(
1332
--!strict
1333
unknownGlobal()
1334
)");
1335
1336
REQUIRE(0 == result.warnings.size());
1337
}
1338
1339
TEST_CASE_FIXTURE(Fixture, "no_spurious_warning_after_a_function_type_alias")
1340
{
1341
LintResult result = lint(R"(
1342
local exports = {}
1343
export type PathFunction<P> = (P?) -> string
1344
exports.tokensToFunction = function() end
1345
return exports
1346
)");
1347
1348
REQUIRE(0 == result.warnings.size());
1349
}
1350
1351
TEST_CASE_FIXTURE(Fixture, "use_all_parent_scopes_for_globals")
1352
{
1353
ScopePtr testScope = getFrontend().addEnvironment("Test");
1354
unfreeze(getFrontend().globals.globalTypes);
1355
getFrontend().loadDefinitionFile(
1356
getFrontend().globals,
1357
testScope,
1358
R"(
1359
declare Foo: number
1360
)",
1361
"@test",
1362
/* captureComments */ false
1363
);
1364
freeze(getFrontend().globals.globalTypes);
1365
1366
fileResolver.environments["A"] = "Test";
1367
1368
fileResolver.source["A"] = R"(
1369
local _foo: Foo = 123
1370
-- os.clock comes from the global scope, the parent of this module's environment
1371
local _bar: typeof(os.clock) = os.clock
1372
)";
1373
1374
LintResult result = lintModule("A");
1375
1376
REQUIRE(0 == result.warnings.size());
1377
}
1378
1379
TEST_CASE_FIXTURE(Fixture, "DeadLocalsUsed")
1380
{
1381
LintResult result = lint(R"(
1382
--!nolint LocalShadow
1383
do
1384
local x
1385
for x in pairs({}) do
1386
print(x)
1387
end
1388
print(x) -- x is not initialized
1389
end
1390
1391
do
1392
local a, b, c = 1, 2
1393
print(a, b, c) -- c is not initialized
1394
end
1395
1396
do
1397
local a, b, c = table.unpack({})
1398
print(a, b, c) -- no warning as we don't know anything about c
1399
end
1400
)");
1401
1402
REQUIRE(3 == result.warnings.size());
1403
CHECK_EQ(result.warnings[0].text, "Variable 'x' defined at line 4 is never initialized or assigned; initialize with 'nil' to silence");
1404
CHECK_EQ(result.warnings[1].text, "Assigning 2 values to 3 variables initializes extra variables with nil; add 'nil' to value list to silence");
1405
CHECK_EQ(result.warnings[2].text, "Variable 'c' defined at line 12 is never initialized or assigned; initialize with 'nil' to silence");
1406
}
1407
1408
TEST_CASE_FIXTURE(Fixture, "LocalFunctionNotDead")
1409
{
1410
LintResult result = lint(R"(
1411
local foo
1412
function foo() end
1413
)");
1414
1415
REQUIRE(0 == result.warnings.size());
1416
}
1417
1418
TEST_CASE_FIXTURE(Fixture, "DuplicateGlobalFunction")
1419
{
1420
LintResult result = lint(R"(
1421
function x() end
1422
1423
function x() end
1424
1425
return x
1426
)");
1427
1428
REQUIRE_EQ(1, result.warnings.size());
1429
1430
const auto& w = result.warnings[0];
1431
1432
CHECK_EQ(LintWarning::Code_DuplicateFunction, w.code);
1433
CHECK_EQ("Duplicate function definition: 'x' also defined on line 2", w.text);
1434
}
1435
1436
TEST_CASE_FIXTURE(Fixture, "DuplicateLocalFunction")
1437
{
1438
LintOptions options;
1439
options.setDefaults();
1440
options.enableWarning(LintWarning::Code_DuplicateFunction);
1441
options.enableWarning(LintWarning::Code_LocalShadow);
1442
1443
LintResult result = lint(
1444
R"(
1445
local function x() end
1446
1447
print(x)
1448
1449
local function x() end
1450
1451
return x
1452
)",
1453
options
1454
);
1455
1456
REQUIRE_EQ(1, result.warnings.size());
1457
1458
CHECK_EQ(LintWarning::Code_DuplicateFunction, result.warnings[0].code);
1459
}
1460
1461
TEST_CASE_FIXTURE(Fixture, "DuplicateMethod")
1462
{
1463
LintResult result = lint(R"(
1464
local T = {}
1465
function T:x() end
1466
1467
function T:x() end
1468
1469
return x
1470
)");
1471
1472
REQUIRE_EQ(1, result.warnings.size());
1473
1474
const auto& w = result.warnings[0];
1475
1476
CHECK_EQ(LintWarning::Code_DuplicateFunction, w.code);
1477
CHECK_EQ("Duplicate function definition: 'T.x' also defined on line 3", w.text);
1478
}
1479
1480
TEST_CASE_FIXTURE(Fixture, "DontTriggerTheWarningIfTheFunctionsAreInDifferentScopes")
1481
{
1482
LintResult result = lint(R"(
1483
if true then
1484
function c() end
1485
else
1486
function c() end
1487
end
1488
1489
return c
1490
)");
1491
1492
REQUIRE(0 == result.warnings.size());
1493
}
1494
1495
TEST_CASE_FIXTURE(Fixture, "LintHygieneUAF")
1496
{
1497
LintResult result = lint(R"(
1498
local Hooty = require(workspace.A)
1499
1500
local HoHooty = require(workspace.A)
1501
1502
local h: Hooty.Pointy = ruire(workspace.A)
1503
1504
local h: H
1505
local h: Hooty.Pointy = ruire(workspace.A)
1506
1507
local hh: Hooty.Pointy = ruire(workspace.A)
1508
1509
local h: Hooty.Pointy = ruire(workspace.A)
1510
1511
linooty.Pointy = ruire(workspace.A)
1512
1513
local hh: Hooty.Pointy = ruire(workspace.A)
1514
1515
local h: Hooty.Pointy = ruire(workspace.A)
1516
1517
linty = ruire(workspace.A)
1518
1519
local h: Hooty.Pointy = ruire(workspace.A)
1520
1521
local hh: Hooty.Pointy = ruire(workspace.A)
1522
1523
local h: Hooty.Pointy = ruire(workspace.A)
1524
1525
local h: Hooty.Pt
1526
)");
1527
1528
REQUIRE(12 == result.warnings.size());
1529
}
1530
1531
TEST_CASE_FIXTURE(BuiltinsFixture, "DeprecatedApiTyped")
1532
{
1533
unfreeze(getFrontend().globals.globalTypes);
1534
TypeId instanceType = getFrontend().globals.globalTypes.addType(ExternType{"Instance", {}, std::nullopt, std::nullopt, {}, {}, "Test", {}});
1535
persist(instanceType);
1536
getFrontend().globals.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, instanceType};
1537
1538
getMutable<ExternType>(instanceType)->props = {
1539
{"Name", {getBuiltins()->stringType}},
1540
{"DataCost", {getBuiltins()->numberType, /* deprecated= */ true}},
1541
{"Wait", {getBuiltins()->anyType, /* deprecated= */ true}},
1542
};
1543
1544
TypeId colorType =
1545
getFrontend().globals.globalTypes.addType(TableType{{}, std::nullopt, getFrontend().globals.globalScope->level, Luau::TableState::Sealed});
1546
1547
getMutable<TableType>(colorType)->props = {{"toHSV", {getBuiltins()->anyType, /* deprecated= */ true, "Color3:ToHSV"}}};
1548
1549
addGlobalBinding(getFrontend().globals, "Color3", Binding{colorType, {}});
1550
1551
if (TableType* ttv = getMutable<TableType>(getGlobalBinding(getFrontend().globals, "table")))
1552
{
1553
ttv->props["foreach"].deprecated = true;
1554
ttv->props["getn"].deprecated = true;
1555
ttv->props["getn"].deprecatedSuggestion = "#";
1556
}
1557
1558
freeze(getFrontend().globals.globalTypes);
1559
1560
LintResult result = lint(R"(
1561
return function (i: Instance)
1562
i:Wait(1.0)
1563
print(i.Name)
1564
print(Color3.toHSV())
1565
print(Color3.doesntexist, i.doesntexist) -- type error, but this verifies we correctly handle non-existent members
1566
print(table.getn({}))
1567
table.foreach({}, function() end)
1568
print(table.nogetn()) -- verify that we correctly handle non-existent members
1569
return i.DataCost
1570
end
1571
)");
1572
1573
REQUIRE(5 == result.warnings.size());
1574
CHECK_EQ(result.warnings[0].text, "Member 'Instance.Wait' is deprecated");
1575
CHECK_EQ(result.warnings[1].text, "Member 'toHSV' is deprecated, use 'Color3:ToHSV' instead");
1576
CHECK_EQ(result.warnings[2].text, "Member 'table.getn' is deprecated, use '#' instead");
1577
CHECK_EQ(result.warnings[3].text, "Member 'table.foreach' is deprecated");
1578
CHECK_EQ(result.warnings[4].text, "Member 'Instance.DataCost' is deprecated");
1579
}
1580
1581
TEST_CASE_FIXTURE(BuiltinsFixture, "DeprecatedApiUntyped")
1582
{
1583
if (TableType* ttv = getMutable<TableType>(getGlobalBinding(getFrontend().globals, "table")))
1584
{
1585
ttv->props["foreach"].deprecated = true;
1586
ttv->props["getn"].deprecated = true;
1587
ttv->props["getn"].deprecatedSuggestion = "#";
1588
}
1589
1590
LintResult result = lint(R"(
1591
-- TODO
1592
return function ()
1593
print(table.getn({}))
1594
table.foreach({}, function() end)
1595
print(table.nogetn()) -- verify that we correctly handle non-existent members
1596
end
1597
)");
1598
1599
REQUIRE(2 == result.warnings.size());
1600
CHECK_EQ(result.warnings[0].text, "Member 'table.getn' is deprecated, use '#' instead");
1601
CHECK_EQ(result.warnings[1].text, "Member 'table.foreach' is deprecated");
1602
}
1603
1604
TEST_CASE_FIXTURE(BuiltinsFixture, "DeprecatedApiFenv")
1605
{
1606
LintResult result = lint(R"(
1607
local f, g, h = ...
1608
1609
getfenv(1)
1610
getfenv(f :: () -> ())
1611
getfenv(g :: number)
1612
getfenv(h :: any)
1613
1614
setfenv(1, {})
1615
setfenv(f :: () -> (), {})
1616
setfenv(g :: number, {})
1617
setfenv(h :: any, {})
1618
)");
1619
1620
REQUIRE(4 == result.warnings.size());
1621
CHECK_EQ(result.warnings[0].text, "Function 'getfenv' is deprecated; consider using 'debug.info' instead");
1622
CHECK_EQ(result.warnings[0].location.begin.line + 1, 4);
1623
CHECK_EQ(result.warnings[1].text, "Function 'getfenv' is deprecated; consider using 'debug.info' instead");
1624
CHECK_EQ(result.warnings[1].location.begin.line + 1, 6);
1625
CHECK_EQ(result.warnings[2].text, "Function 'setfenv' is deprecated");
1626
CHECK_EQ(result.warnings[2].location.begin.line + 1, 9);
1627
CHECK_EQ(result.warnings[3].text, "Function 'setfenv' is deprecated");
1628
CHECK_EQ(result.warnings[3].location.begin.line + 1, 11);
1629
}
1630
1631
static void checkDeprecatedWarning(const Luau::LintWarning& warning, const Luau::Position& begin, const Luau::Position& end, const char* msg)
1632
{
1633
CHECK_EQ(warning.code, LintWarning::Code_DeprecatedApi);
1634
CHECK_EQ(warning.location, Location(begin, end));
1635
CHECK_EQ(warning.text, msg);
1636
}
1637
1638
TEST_CASE_FIXTURE(Fixture, "DeprecatedAttribute")
1639
{
1640
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
1641
1642
// @deprecated works on local functions
1643
{
1644
LintResult result = lint(R"(
1645
@deprecated
1646
local function testfun(x)
1647
return x + 1
1648
end
1649
1650
testfun(1)
1651
)");
1652
1653
REQUIRE(1 == result.warnings.size());
1654
checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated");
1655
}
1656
1657
// @deprecated works on globals functions
1658
{
1659
LintResult result = lint(R"(
1660
@deprecated
1661
function testfun(x)
1662
return x + 1
1663
end
1664
1665
testfun(1)
1666
)");
1667
1668
REQUIRE(1 == result.warnings.size());
1669
checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated");
1670
}
1671
1672
// @deprecated works on fully typed functions
1673
{
1674
LintResult result = lint(R"(
1675
@deprecated
1676
local function testfun(x:number):number
1677
return x + 1
1678
end
1679
1680
if math.random(2) == 2 then
1681
testfun(1)
1682
end
1683
)");
1684
1685
REQUIRE(1 == result.warnings.size());
1686
checkDeprecatedWarning(result.warnings[0], Position(7, 4), Position(7, 11), "Function 'testfun' is deprecated");
1687
}
1688
1689
// @deprecated works on functions without an explicit return type
1690
{
1691
LintResult result = lint(R"(
1692
@deprecated
1693
local function testfun(x:number)
1694
return x + 1
1695
end
1696
1697
g(testfun)
1698
)");
1699
1700
REQUIRE(1 == result.warnings.size());
1701
checkDeprecatedWarning(result.warnings[0], Position(6, 2), Position(6, 9), "Function 'testfun' is deprecated");
1702
}
1703
1704
// @deprecated works on functions without an explicit argument type
1705
{
1706
LintResult result = lint(R"(
1707
@deprecated
1708
local function testfun(x):number
1709
if x == 1 then
1710
return x
1711
else
1712
return 1 + testfun(x - 1)
1713
end
1714
end
1715
1716
testfun(1)
1717
)");
1718
1719
REQUIRE(1 == result.warnings.size());
1720
checkDeprecatedWarning(result.warnings[0], Position(10, 0), Position(10, 7), "Function 'testfun' is deprecated");
1721
}
1722
1723
// @deprecated works on inner functions
1724
{
1725
LintResult result = lint(R"(
1726
function flipFlop()
1727
local state = false
1728
1729
@deprecated
1730
local function invert()
1731
state = !state
1732
return state
1733
end
1734
1735
return invert
1736
end
1737
1738
f = flipFlop()
1739
assert(f() == true)
1740
)");
1741
1742
REQUIRE(2 == result.warnings.size());
1743
checkDeprecatedWarning(result.warnings[0], Position(10, 11), Position(10, 17), "Function 'invert' is deprecated");
1744
checkDeprecatedWarning(result.warnings[1], Position(14, 7), Position(14, 8), "Function 'f' is deprecated");
1745
}
1746
1747
// @deprecated does not automatically apply to inner functions
1748
{
1749
LintResult result = lint(R"(
1750
@deprecated
1751
function flipFlop()
1752
local state = false
1753
1754
local function invert()
1755
state = !state
1756
return state
1757
end
1758
1759
return invert
1760
end
1761
1762
f = flipFlop()
1763
assert(f() == true)
1764
)");
1765
1766
REQUIRE(1 == result.warnings.size());
1767
checkDeprecatedWarning(result.warnings[0], Position(13, 4), Position(13, 12), "Function 'flipFlop' is deprecated");
1768
}
1769
1770
// @deprecated works correctly if deprecated function is shadowed
1771
{
1772
LintResult result = lint(R"(
1773
@deprecated
1774
local function doTheThing()
1775
print("doing")
1776
end
1777
1778
doTheThing()
1779
1780
local function shadow()
1781
local function doTheThing()
1782
print("doing!")
1783
end
1784
1785
doTheThing()
1786
end
1787
1788
shadow()
1789
)");
1790
1791
REQUIRE(1 == result.warnings.size());
1792
checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 10), "Function 'doTheThing' is deprecated");
1793
}
1794
1795
// @deprecated does not issue warnings if a deprecated function uses itself
1796
{
1797
LintResult result = lint(R"(
1798
@deprecated
1799
function fibonacci(n)
1800
if n == 0 then
1801
return 0
1802
elseif n == 1 then
1803
return 1
1804
else
1805
return fibonacci(n - 1) + fibonacci(n - 2)
1806
end
1807
end
1808
1809
fibonacci(5)
1810
)");
1811
1812
REQUIRE(1 == result.warnings.size());
1813
checkDeprecatedWarning(result.warnings[0], Position(12, 0), Position(12, 9), "Function 'fibonacci' is deprecated");
1814
}
1815
1816
// @deprecated works for mutually recursive functions
1817
{
1818
LintResult result = lint(R"(
1819
@deprecated
1820
function odd(x)
1821
if x == 0 then
1822
return false
1823
else
1824
return even(x - 1)
1825
end
1826
end
1827
1828
@deprecated
1829
function even(x)
1830
if x == 0 then
1831
return true
1832
else
1833
return odd(x - 1)
1834
end
1835
end
1836
1837
assert(odd(1) == true)
1838
assert(even(0) == true)
1839
)");
1840
1841
REQUIRE(4 == result.warnings.size());
1842
checkDeprecatedWarning(result.warnings[0], Position(6, 15), Position(6, 19), "Function 'even' is deprecated");
1843
checkDeprecatedWarning(result.warnings[1], Position(15, 15), Position(15, 18), "Function 'odd' is deprecated");
1844
checkDeprecatedWarning(result.warnings[2], Position(19, 7), Position(19, 10), "Function 'odd' is deprecated");
1845
checkDeprecatedWarning(result.warnings[3], Position(20, 7), Position(20, 11), "Function 'even' is deprecated");
1846
}
1847
1848
// @deprecated works for methods with a literal class name
1849
{
1850
LintResult result = lint(R"(
1851
Account = { balance=0 }
1852
1853
@deprecated
1854
function Account:deposit(v)
1855
self.balance = self.balance + v
1856
end
1857
1858
Account:deposit(200.00)
1859
)");
1860
1861
REQUIRE(1 == result.warnings.size());
1862
checkDeprecatedWarning(result.warnings[0], Position(8, 0), Position(8, 15), "Member 'Account.deposit' is deprecated");
1863
}
1864
1865
// @deprecated works for methods with a compound expression class name
1866
{
1867
LintResult result = lint(R"(
1868
Account = { balance=0 }
1869
1870
function getAccount()
1871
return Account
1872
end
1873
1874
@deprecated
1875
function Account:deposit (v)
1876
self.balance = self.balance + v
1877
end
1878
1879
(getAccount()):deposit(200.00)
1880
)");
1881
1882
REQUIRE(1 == result.warnings.size());
1883
checkDeprecatedWarning(result.warnings[0], Position(12, 0), Position(12, 22), "Member 'deposit' is deprecated");
1884
}
1885
}
1886
1887
TEST_CASE_FIXTURE(Fixture, "DeprecatedAttributeWithParams")
1888
{
1889
// @deprecated works on local functions
1890
{
1891
LintResult result = lint(R"(
1892
@[deprecated{ use = "prodfun", reason = "Too old." }]
1893
local function testfun(x)
1894
return x + 1
1895
end
1896
1897
testfun(1)
1898
)");
1899
1900
REQUIRE(1 == result.warnings.size());
1901
checkDeprecatedWarning(
1902
result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated, use 'prodfun' instead. Too old."
1903
);
1904
}
1905
1906
// @deprecated works on globals functions
1907
{
1908
LintResult result = lint(R"(
1909
@[deprecated{ use = "prodfun", reason = "Too old." }]
1910
function testfun(x)
1911
return x + 1
1912
end
1913
1914
testfun(1)
1915
)");
1916
1917
REQUIRE(1 == result.warnings.size());
1918
checkDeprecatedWarning(
1919
result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated, use 'prodfun' instead. Too old."
1920
);
1921
}
1922
1923
// @deprecated with only 'use' works on local functions
1924
{
1925
LintResult result = lint(R"(
1926
@[deprecated{ use = "prodfun" }]
1927
local function testfun(x)
1928
return x + 1
1929
end
1930
1931
testfun(1)
1932
)");
1933
1934
REQUIRE(1 == result.warnings.size());
1935
checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated, use 'prodfun' instead");
1936
}
1937
1938
// @deprecated with only 'use' works on globals functions
1939
{
1940
LintResult result = lint(R"(
1941
@[deprecated{ use = "prodfun" }]
1942
function testfun(x)
1943
return x + 1
1944
end
1945
1946
testfun(1)
1947
)");
1948
REQUIRE(1 == result.warnings.size());
1949
checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated, use 'prodfun' instead");
1950
}
1951
1952
1953
// @deprecated with only 'reason' works on local functions
1954
{
1955
LintResult result = lint(R"(
1956
@[deprecated{ reason = "Too old." }]
1957
local function testfun(x)
1958
return x + 1
1959
end
1960
1961
testfun(1)
1962
)");
1963
1964
REQUIRE(1 == result.warnings.size());
1965
checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated. Too old.");
1966
}
1967
1968
// @deprecated with only 'reason' works on globals functions
1969
{
1970
LintResult result = lint(R"(
1971
@[deprecated{ reason = "Too old." }]
1972
function testfun(x)
1973
return x + 1
1974
end
1975
1976
testfun(1)
1977
)");
1978
1979
REQUIRE(1 == result.warnings.size());
1980
checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated. Too old.");
1981
}
1982
1983
// @deprecated works for methods with a literal class name
1984
{
1985
LintResult result = lint(R"(
1986
Account = { balance=0 }
1987
1988
@[deprecated{use = 'credit', reason = 'It sounds cool'}]
1989
function Account:deposit(v)
1990
self.balance = self.balance + v
1991
end
1992
1993
Account:deposit(200.00)
1994
)");
1995
1996
REQUIRE(1 == result.warnings.size());
1997
checkDeprecatedWarning(
1998
result.warnings[0], Position(8, 0), Position(8, 15), "Member 'Account.deposit' is deprecated, use 'credit' instead. It sounds cool"
1999
);
2000
}
2001
2002
// @deprecated works for methods with a compound expression class name
2003
{
2004
LintResult result = lint(R"(
2005
Account = { balance=0 }
2006
2007
function getAccount()
2008
return Account
2009
end
2010
2011
@[deprecated{use = 'credit', reason = 'It sounds cool'}]
2012
function Account:deposit (v)
2013
self.balance = self.balance + v
2014
end
2015
2016
(getAccount()):deposit(200.00)
2017
)");
2018
2019
REQUIRE(1 == result.warnings.size());
2020
checkDeprecatedWarning(
2021
result.warnings[0], Position(12, 0), Position(12, 22), "Member 'deposit' is deprecated, use 'credit' instead. It sounds cool"
2022
);
2023
}
2024
2025
{
2026
loadDefinition(R"(
2027
@[deprecated{use = 'foo', reason = 'Do better.'}] declare function bar(x: number): string
2028
)");
2029
2030
LintResult result = lint(R"(
2031
bar(2)
2032
)");
2033
2034
REQUIRE(1 == result.warnings.size());
2035
checkDeprecatedWarning(result.warnings[0], Position(1, 0), Position(1, 3), "Function 'bar' is deprecated, use 'foo' instead. Do better.");
2036
}
2037
2038
{
2039
loadDefinition(R"(
2040
declare Hooty : {
2041
tooty : @[deprecated{use = 'foo', reason = 'bar'}] @checked (number) -> number
2042
}
2043
)");
2044
LintResult result = lint(R"(
2045
print(Hooty:tooty(2.0))
2046
)");
2047
2048
REQUIRE(1 == result.warnings.size());
2049
checkDeprecatedWarning(result.warnings[0], Position(1, 6), Position(1, 17), "Member 'Hooty.tooty' is deprecated, use 'foo' instead. bar");
2050
}
2051
2052
{
2053
loadDefinition(R"(
2054
declare class Foo
2055
@[deprecated{use = 'foo', reason = 'baz'}]
2056
function bar(self, value: number) : number
2057
end
2058
2059
declare Foo: {
2060
new: () -> Foo
2061
}
2062
)");
2063
2064
LintResult result = lint(R"(
2065
local foo = Foo.new()
2066
print(foo:bar(2.0))
2067
)");
2068
2069
REQUIRE(1 == result.warnings.size());
2070
checkDeprecatedWarning(result.warnings[0], Position(2, 6), Position(2, 13), "Member 'bar' is deprecated, use 'foo' instead. baz");
2071
}
2072
}
2073
2074
TEST_CASE_FIXTURE(Fixture, "DeprecatedAttributeFunctionDeclaration")
2075
{
2076
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
2077
2078
// @deprecated works on function type declarations
2079
2080
loadDefinition(R"(
2081
@deprecated declare function bar(x: number): string
2082
)");
2083
2084
LintResult result = lint(R"(
2085
bar(2)
2086
)");
2087
2088
REQUIRE(1 == result.warnings.size());
2089
checkDeprecatedWarning(result.warnings[0], Position(1, 0), Position(1, 3), "Function 'bar' is deprecated");
2090
}
2091
2092
TEST_CASE_FIXTURE(Fixture, "DeprecatedAttributeTableDeclaration")
2093
{
2094
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
2095
2096
// @deprecated works on table type declarations
2097
2098
loadDefinition(R"(
2099
declare Hooty : {
2100
tooty : @deprecated @checked (number) -> number
2101
}
2102
)");
2103
2104
LintResult result = lint(R"(
2105
print(Hooty:tooty(2.0))
2106
)");
2107
2108
REQUIRE(1 == result.warnings.size());
2109
checkDeprecatedWarning(result.warnings[0], Position(1, 6), Position(1, 17), "Member 'Hooty.tooty' is deprecated");
2110
}
2111
2112
TEST_CASE_FIXTURE(Fixture, "DeprecatedAttributeMethodDeclaration")
2113
{
2114
ScopedFastFlag _{FFlag::DebugLuauForceOldSolver, false};
2115
2116
// @deprecated works on table type declarations
2117
2118
loadDefinition(R"(
2119
declare class Foo
2120
@deprecated
2121
function bar(self, value: number) : number
2122
end
2123
2124
declare Foo: {
2125
new: () -> Foo
2126
}
2127
)");
2128
2129
LintResult result = lint(R"(
2130
local foo = Foo.new()
2131
print(foo:bar(2.0))
2132
)");
2133
2134
REQUIRE(1 == result.warnings.size());
2135
checkDeprecatedWarning(result.warnings[0], Position(2, 6), Position(2, 13), "Member 'bar' is deprecated");
2136
}
2137
2138
TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperations")
2139
{
2140
LintResult result = lint(R"(
2141
local t = {}
2142
local tt = {}
2143
2144
table.insert(t, #t, 42)
2145
table.insert(t, (#t), 42) -- silenced
2146
2147
table.insert(t, #t + 1, 42)
2148
table.insert(t, #tt + 1, 42) -- different table, ok
2149
2150
table.insert(t, 0, 42)
2151
2152
table.remove(t, 0)
2153
2154
table.remove(t, #t-1)
2155
2156
table.insert(t, string.find("hello", "h"))
2157
2158
table.move(t, 0, #t, 1, tt)
2159
table.move(t, 1, #t, 0, tt)
2160
2161
table.create(42, {})
2162
table.create(42, {} :: {})
2163
)");
2164
2165
REQUIRE(10 == result.warnings.size());
2166
CHECK_EQ(
2167
result.warnings[0].text,
2168
"table.insert will insert the value before the last element, which is likely a bug; consider removing the "
2169
"second argument or wrap it in parentheses to silence"
2170
);
2171
CHECK_EQ(result.warnings[1].text, "table.insert will append the value to the table; consider removing the second argument for efficiency");
2172
CHECK_EQ(result.warnings[2].text, "table.insert uses index 0 but arrays are 1-based; did you mean 1 instead?");
2173
CHECK_EQ(result.warnings[3].text, "table.remove uses index 0 but arrays are 1-based; did you mean 1 instead?");
2174
CHECK_EQ(
2175
result.warnings[4].text,
2176
"table.remove will remove the value before the last element, which is likely a bug; consider removing the "
2177
"second argument or wrap it in parentheses to silence"
2178
);
2179
CHECK_EQ(
2180
result.warnings[5].text,
2181
"table.insert may change behavior if the call returns more than one result; consider adding parentheses around second argument"
2182
);
2183
CHECK_EQ(result.warnings[6].text, "table.move uses index 0 but arrays are 1-based; did you mean 1 instead?");
2184
CHECK_EQ(result.warnings[7].text, "table.move uses index 0 but arrays are 1-based; did you mean 1 instead?");
2185
CHECK_EQ(
2186
result.warnings[8].text, "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"
2187
);
2188
CHECK_EQ(
2189
result.warnings[9].text, "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"
2190
);
2191
}
2192
2193
TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperationsIndexer")
2194
{
2195
// CLI-116824 Linter incorrectly issues false positive when taking the length of a unannotated string function argument
2196
if (!FFlag::DebugLuauForceOldSolver)
2197
return;
2198
2199
LintResult result = lint(R"(
2200
local t1 = {} -- ok: empty
2201
local t2 = {1, 2} -- ok: array
2202
local t3 = { a = 1, b = 2 } -- not ok: dictionary
2203
local t4: {[number]: number} = {} -- ok: array
2204
local t5: {[string]: number} = {} -- not ok: dictionary
2205
local t6: typeof(setmetatable({1, 2}, {})) = {} -- ok: table with metatable
2206
local t7: string = "hello" -- ok: string
2207
local t8: {number} | {n: number} = {} -- ok: union
2208
2209
-- not ok
2210
print(#t3)
2211
print(#t5)
2212
ipairs(t5)
2213
2214
-- disabled
2215
-- ipairs(t3) adds indexer to t3, silencing error on #t3
2216
2217
-- ok
2218
print(#t1)
2219
print(#t2)
2220
print(#t4)
2221
print(#t6)
2222
print(#t7)
2223
print(#t8)
2224
2225
ipairs(t1)
2226
ipairs(t2)
2227
ipairs(t4)
2228
ipairs(t6)
2229
ipairs(t7)
2230
ipairs(t8)
2231
2232
-- ok, subtle: text is a string here implicitly, but the type annotation isn't available
2233
-- type checker assigns a type of generic table with the 'sub' member; we don't emit warnings on generic tables
2234
-- to avoid generating a false positive here
2235
function _impliedstring(element, text)
2236
for i = 1, #text do
2237
element:sendText(text:sub(i, i))
2238
end
2239
end
2240
)");
2241
2242
REQUIRE(3 == result.warnings.size());
2243
CHECK_EQ(result.warnings[0].location.begin.line + 1, 12);
2244
CHECK_EQ(result.warnings[0].text, "Using '#' on a table without an array part is likely a bug");
2245
CHECK_EQ(result.warnings[1].location.begin.line + 1, 13);
2246
CHECK_EQ(result.warnings[1].text, "Using '#' on a table with string keys is likely a bug");
2247
CHECK_EQ(result.warnings[2].location.begin.line + 1, 14);
2248
CHECK_EQ(result.warnings[2].text, "Using 'ipairs' on a table with string keys is likely a bug");
2249
}
2250
2251
TEST_CASE_FIXTURE(Fixture, "DuplicateConditions")
2252
{
2253
LintResult result = lint(R"(
2254
if true then
2255
elseif false then
2256
elseif true then -- duplicate
2257
end
2258
2259
if true then
2260
elseif false then
2261
else
2262
if true then -- duplicate
2263
end
2264
end
2265
2266
_ = true and true
2267
_ = true or true
2268
_ = (true and false) and true
2269
_ = (true and true) and true
2270
_ = (true and true) or true
2271
_ = (true and false) and (42 and false)
2272
2273
_ = true and true or false -- no warning since this is is a common pattern used as a ternary replacement
2274
2275
_ = if true then 1 elseif true then 2 else 3
2276
)");
2277
2278
REQUIRE(8 == result.warnings.size());
2279
CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 2");
2280
CHECK_EQ(result.warnings[0].location.begin.line + 1, 4);
2281
CHECK_EQ(result.warnings[1].text, "Condition has already been checked on column 5");
2282
CHECK_EQ(result.warnings[2].text, "Condition has already been checked on column 5");
2283
CHECK_EQ(result.warnings[3].text, "Condition has already been checked on column 6");
2284
CHECK_EQ(result.warnings[4].text, "Condition has already been checked on column 6");
2285
CHECK_EQ(result.warnings[5].text, "Condition has already been checked on column 6");
2286
CHECK_EQ(result.warnings[6].text, "Condition has already been checked on column 15");
2287
CHECK_EQ(result.warnings[6].location.begin.line + 1, 19);
2288
CHECK_EQ(result.warnings[7].text, "Condition has already been checked on column 8");
2289
}
2290
2291
TEST_CASE_FIXTURE(Fixture, "DuplicateConditionsExpr")
2292
{
2293
LintResult result = lint(R"(
2294
local correct, opaque = ...
2295
2296
if correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls", `string {opaque}`)}) then
2297
elseif correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls", `string {opaque}`)}) then
2298
elseif correct({a = 1, b = 2 * (-2), c = opaque.path['with']("calls", false)}) then
2299
end
2300
)");
2301
2302
REQUIRE(1 == result.warnings.size());
2303
CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 4");
2304
CHECK_EQ(result.warnings[0].location.begin.line + 1, 5);
2305
}
2306
2307
TEST_CASE_FIXTURE(Fixture, "DuplicateLocal")
2308
{
2309
LintResult result = lint(R"(
2310
function foo(a1, a2, a3, a1)
2311
end
2312
2313
local _, _, _ = ... -- ok!
2314
local a1, a2, a1 = ... -- not ok
2315
2316
local moo = {}
2317
function moo:bar(self)
2318
end
2319
2320
return foo, moo, a1, a2
2321
)");
2322
2323
REQUIRE(4 == result.warnings.size());
2324
CHECK_EQ(result.warnings[0].text, "Function parameter 'a1' already defined on column 14");
2325
CHECK_EQ(result.warnings[1].text, "Variable 'a1' is never used; prefix with '_' to silence");
2326
CHECK_EQ(result.warnings[2].text, "Variable 'a1' already defined on column 7");
2327
CHECK_EQ(result.warnings[3].text, "Function parameter 'self' already defined implicitly");
2328
}
2329
2330
TEST_CASE_FIXTURE(Fixture, "MisleadingAndOr")
2331
{
2332
LintResult result = lint(R"(
2333
_ = math.random() < 0.5 and true or 42
2334
_ = math.random() < 0.5 and false or 42 -- misleading
2335
_ = math.random() < 0.5 and nil or 42 -- misleading
2336
_ = math.random() < 0.5 and 0 or 42
2337
_ = (math.random() < 0.5 and false) or 42 -- currently ignored
2338
)");
2339
2340
REQUIRE(2 == result.warnings.size());
2341
CHECK_EQ(
2342
result.warnings[0].text,
2343
"The and-or expression always evaluates to the second alternative because the first alternative is false; "
2344
"consider using if-then-else expression instead"
2345
);
2346
CHECK_EQ(
2347
result.warnings[1].text,
2348
"The and-or expression always evaluates to the second alternative because the first alternative is nil; "
2349
"consider using if-then-else expression instead"
2350
);
2351
}
2352
2353
TEST_CASE_FIXTURE(Fixture, "WrongComment")
2354
{
2355
LintResult result = lint(R"(
2356
--!strict
2357
--!struct
2358
--!nolintGlobal
2359
--!nolint Global
2360
--!nolint KnownGlobal
2361
--!nolint UnknownGlobal
2362
--! no more lint
2363
--!strict here
2364
--!native on
2365
do end
2366
--!nolint
2367
)");
2368
2369
REQUIRE(7 == result.warnings.size());
2370
CHECK_EQ(result.warnings[0].text, "Unknown comment directive 'struct'; did you mean 'strict'?");
2371
CHECK_EQ(result.warnings[1].text, "Unknown comment directive 'nolintGlobal'");
2372
CHECK_EQ(result.warnings[2].text, "nolint directive refers to unknown lint rule 'Global'");
2373
CHECK_EQ(result.warnings[3].text, "nolint directive refers to unknown lint rule 'KnownGlobal'; did you mean 'UnknownGlobal'?");
2374
CHECK_EQ(result.warnings[4].text, "Comment directive with the type checking mode has extra symbols at the end of the line");
2375
CHECK_EQ(result.warnings[5].text, "native directive has extra symbols at the end of the line");
2376
CHECK_EQ(result.warnings[6].text, "Comment directive is ignored because it is placed after the first non-comment token");
2377
}
2378
2379
TEST_CASE_FIXTURE(Fixture, "WrongCommentMuteSelf")
2380
{
2381
LintResult result = lint(R"(
2382
--!nolint
2383
--!struct
2384
)");
2385
2386
REQUIRE(0 == result.warnings.size()); // --!nolint disables WrongComment lint :)
2387
}
2388
2389
TEST_CASE_FIXTURE(Fixture, "DuplicateConditionsIfStatAndExpr")
2390
{
2391
LintResult result = lint(R"(
2392
if if 1 then 2 else 3 then
2393
elseif if 1 then 2 else 3 then
2394
elseif if 0 then 5 else 4 then
2395
end
2396
)");
2397
2398
REQUIRE(1 == result.warnings.size());
2399
CHECK_EQ(result.warnings[0].text, "Condition has already been checked on line 2");
2400
}
2401
2402
TEST_CASE_FIXTURE(Fixture, "WrongCommentOptimize")
2403
{
2404
LintResult result = lint(R"(
2405
--!optimize
2406
--!optimize me
2407
--!optimize 100500
2408
--!optimize 2
2409
)");
2410
2411
REQUIRE(3 == result.warnings.size());
2412
CHECK_EQ(result.warnings[0].text, "optimize directive requires an optimization level");
2413
CHECK_EQ(result.warnings[1].text, "optimize directive uses unknown optimization level 'me', 0..2 expected");
2414
CHECK_EQ(result.warnings[2].text, "optimize directive uses unknown optimization level '100500', 0..2 expected");
2415
2416
result = lint("--!optimize ");
2417
REQUIRE(1 == result.warnings.size());
2418
CHECK_EQ(result.warnings[0].text, "optimize directive requires an optimization level");
2419
}
2420
2421
TEST_CASE_FIXTURE(Fixture, "TestStringInterpolation")
2422
{
2423
LintResult result = lint(R"(
2424
--!nocheck
2425
local _ = `unknown {foo}`
2426
)");
2427
2428
REQUIRE(1 == result.warnings.size());
2429
}
2430
2431
TEST_CASE_FIXTURE(Fixture, "IntegerParsing")
2432
{
2433
LintResult result = lint(R"(
2434
local _ = 0b10000000000000000000000000000000000000000000000000000000000000000
2435
local _ = 0x10000000000000000
2436
)");
2437
2438
REQUIRE(2 == result.warnings.size());
2439
CHECK_EQ(result.warnings[0].text, "Binary number literal exceeded available precision and was truncated to 2^64");
2440
CHECK_EQ(result.warnings[1].text, "Hexadecimal number literal exceeded available precision and was truncated to 2^64");
2441
}
2442
2443
TEST_CASE_FIXTURE(Fixture, "IntegerParsingDecimalImprecise")
2444
{
2445
LintResult result = lint(R"(
2446
local _ = 10000000000000000000000000000000000000000000000000000000000000000
2447
local _ = 10000000000000001
2448
local _ = -10000000000000001
2449
2450
-- 10^16 = 2^16 * 5^16, 5^16 only requires 38 bits
2451
local _ = 10000000000000000
2452
local _ = -10000000000000000
2453
2454
-- smallest possible number that is parsed imprecisely
2455
local _ = 9007199254740993
2456
local _ = -9007199254740993
2457
2458
-- note that numbers before and after parse precisely (number after is even => 1 more mantissa bit)
2459
local _ = 9007199254740992
2460
local _ = 9007199254740994
2461
2462
-- large powers of two should work as well (this is 2^63)
2463
local _ = -9223372036854775808
2464
)");
2465
2466
REQUIRE(5 == result.warnings.size());
2467
CHECK_EQ(result.warnings[0].text, "Number literal exceeded available precision and was truncated to closest representable number");
2468
CHECK_EQ(result.warnings[0].location.begin.line, 1);
2469
CHECK_EQ(result.warnings[1].text, "Number literal exceeded available precision and was truncated to closest representable number");
2470
CHECK_EQ(result.warnings[1].location.begin.line, 2);
2471
CHECK_EQ(result.warnings[2].text, "Number literal exceeded available precision and was truncated to closest representable number");
2472
CHECK_EQ(result.warnings[2].location.begin.line, 3);
2473
CHECK_EQ(result.warnings[3].text, "Number literal exceeded available precision and was truncated to closest representable number");
2474
CHECK_EQ(result.warnings[3].location.begin.line, 10);
2475
CHECK_EQ(result.warnings[4].text, "Number literal exceeded available precision and was truncated to closest representable number");
2476
CHECK_EQ(result.warnings[4].location.begin.line, 11);
2477
}
2478
2479
TEST_CASE_FIXTURE(Fixture, "IntegerParsingHexImprecise")
2480
{
2481
LintResult result = lint(R"(
2482
local _ = 0x1234567812345678
2483
2484
-- smallest possible number that is parsed imprecisely
2485
local _ = 0x20000000000001
2486
2487
-- note that numbers before and after parse precisely (number after is even => 1 more mantissa bit)
2488
local _ = 0x20000000000000
2489
local _ = 0x20000000000002
2490
2491
-- large powers of two should work as well (this is 2^63)
2492
local _ = 0x80000000000000
2493
)");
2494
2495
REQUIRE(2 == result.warnings.size());
2496
CHECK_EQ(result.warnings[0].text, "Number literal exceeded available precision and was truncated to closest representable number");
2497
CHECK_EQ(result.warnings[0].location.begin.line, 1);
2498
CHECK_EQ(result.warnings[1].text, "Number literal exceeded available precision and was truncated to closest representable number");
2499
CHECK_EQ(result.warnings[1].location.begin.line, 4);
2500
}
2501
2502
TEST_CASE_FIXTURE(Fixture, "ComparisonPrecedence")
2503
{
2504
LintResult result = lint(R"(
2505
local a, b = ...
2506
2507
local _ = not a == b
2508
local _ = not a ~= b
2509
local _ = not a <= b
2510
local _ = a <= b == 0
2511
local _ = a <= b <= 0
2512
2513
local _ = not a == not b -- weird but ok
2514
2515
-- silence tests for all of the above
2516
local _ = not (a == b)
2517
local _ = (not a) == b
2518
local _ = not (a ~= b)
2519
local _ = (not a) ~= b
2520
local _ = not (a <= b)
2521
local _ = (not a) <= b
2522
local _ = (a <= b) == 0
2523
local _ = a <= (b == 0)
2524
)");
2525
2526
REQUIRE(5 == result.warnings.size());
2527
CHECK_EQ(result.warnings[0].text, "not X == Y is equivalent to (not X) == Y; consider using X ~= Y, or add parentheses to silence");
2528
CHECK_EQ(result.warnings[1].text, "not X ~= Y is equivalent to (not X) ~= Y; consider using X == Y, or add parentheses to silence");
2529
CHECK_EQ(result.warnings[2].text, "not X <= Y is equivalent to (not X) <= Y; add parentheses to silence");
2530
CHECK_EQ(result.warnings[3].text, "X <= Y == Z is equivalent to (X <= Y) == Z; add parentheses to silence");
2531
CHECK_EQ(result.warnings[4].text, "X <= Y <= Z is equivalent to (X <= Y) <= Z; did you mean X <= Y and Y <= Z?");
2532
}
2533
2534
TEST_CASE_FIXTURE(Fixture, "RedundantNativeAttribute")
2535
{
2536
LintResult result = lint(R"(
2537
--!native
2538
2539
@native
2540
local function f(a)
2541
@native
2542
local function g(b)
2543
return (a + b)
2544
end
2545
return g
2546
end
2547
2548
f(3)(4)
2549
)");
2550
2551
REQUIRE(2 == result.warnings.size());
2552
2553
CHECK_EQ(result.warnings[0].text, "native attribute on a function is redundant in a native module; consider removing it");
2554
CHECK_EQ(result.warnings[0].location, Location(Position(3, 0), Position(3, 7)));
2555
2556
CHECK_EQ(result.warnings[1].text, "native attribute on a function is redundant in a native module; consider removing it");
2557
CHECK_EQ(result.warnings[1].location, Location(Position(5, 4), Position(5, 11)));
2558
}
2559
2560
TEST_CASE_FIXTURE(Fixture, "type_instantiation_lints")
2561
{
2562
LintResult result = lint(R"(
2563
local function a<b>(cool: b)
2564
print(cool)
2565
end
2566
2567
a<<"hi">>("hi")
2568
)");
2569
2570
REQUIRE(0 == result.warnings.size());
2571
}
2572
2573
TEST_SUITE_END();
2574
2575