Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/Analysis/src/Linter.cpp
2725 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
4
#include "Luau/AstQuery.h"
5
#include "Luau/Module.h"
6
#include "Luau/Scope.h"
7
#include "Luau/TypeInfer.h"
8
#include "Luau/StringUtils.h"
9
#include "Luau/Common.h"
10
11
#include <algorithm>
12
#include <cmath>
13
#include <climits>
14
15
LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4)
16
LUAU_FASTFLAGVARIABLE(LuauLinterVectorPrimitive)
17
18
namespace Luau
19
{
20
21
struct LintContext
22
{
23
struct Global
24
{
25
TypeId type = nullptr;
26
std::optional<const char*> deprecated;
27
};
28
29
std::vector<LintWarning> result;
30
LintOptions options;
31
32
AstStat* root;
33
34
AstName placeholder;
35
DenseHashMap<AstName, Global> builtinGlobals;
36
ScopePtr scope;
37
const Module* module;
38
39
LintContext()
40
: root(nullptr)
41
, builtinGlobals(AstName())
42
, module(nullptr)
43
{
44
}
45
46
bool warningEnabled(LintWarning::Code code)
47
{
48
return (options.warningMask & (1ull << code)) != 0;
49
}
50
51
std::optional<TypeId> getType(AstExpr* expr)
52
{
53
if (!module)
54
return std::nullopt;
55
56
auto it = module->astTypes.find(expr);
57
if (!it)
58
return std::nullopt;
59
60
return *it;
61
}
62
};
63
64
struct WarningComparator
65
{
66
int compare(const Position& lhs, const Position& rhs) const
67
{
68
if (lhs.line != rhs.line)
69
return lhs.line < rhs.line ? -1 : 1;
70
if (lhs.column != rhs.column)
71
return lhs.column < rhs.column ? -1 : 1;
72
return 0;
73
}
74
75
int compare(const Location& lhs, const Location& rhs) const
76
{
77
if (int c = compare(lhs.begin, rhs.begin))
78
return c;
79
if (int c = compare(lhs.end, rhs.end))
80
return c;
81
return 0;
82
}
83
84
bool operator()(const LintWarning& lhs, const LintWarning& rhs) const
85
{
86
if (int c = compare(lhs.location, rhs.location))
87
return c < 0;
88
89
return lhs.code < rhs.code;
90
}
91
};
92
93
LUAU_PRINTF_ATTR(4, 5)
94
static void emitWarning(LintContext& context, LintWarning::Code code, const Location& location, const char* format, ...)
95
{
96
if (!context.warningEnabled(code))
97
return;
98
99
va_list args;
100
va_start(args, format);
101
std::string message = vformat(format, args);
102
va_end(args);
103
104
LintWarning warning = {code, location, std::move(message)};
105
context.result.push_back(warning);
106
}
107
108
static bool similar(AstExpr* lhs, AstExpr* rhs)
109
{
110
if (lhs->classIndex != rhs->classIndex)
111
return false;
112
113
#define CASE(T) else if (T* le = lhs->as<T>(), *re = rhs->as<T>(); le && re)
114
115
if (false)
116
return false;
117
CASE(AstExprGroup) return similar(le->expr, re->expr);
118
CASE(AstExprConstantNil) return true;
119
CASE(AstExprConstantBool) return le->value == re->value;
120
CASE(AstExprConstantNumber) return le->value == re->value;
121
CASE(AstExprConstantInteger) return le->value == re->value;
122
CASE(AstExprConstantString) return le->value.size == re->value.size && memcmp(le->value.data, re->value.data, le->value.size) == 0;
123
CASE(AstExprLocal) return le->local == re->local;
124
CASE(AstExprGlobal) return le->name == re->name;
125
CASE(AstExprVarargs) return true;
126
CASE(AstExprIndexName) return le->index == re->index && similar(le->expr, re->expr);
127
CASE(AstExprIndexExpr) return similar(le->expr, re->expr) && similar(le->index, re->index);
128
CASE(AstExprFunction) return false; // rarely meaningful in context of this pass, avoids having to process statement nodes
129
CASE(AstExprUnary) return le->op == re->op && similar(le->expr, re->expr);
130
CASE(AstExprBinary) return le->op == re->op && similar(le->left, re->left) && similar(le->right, re->right);
131
CASE(AstExprTypeAssertion) return le->expr == re->expr; // the type doesn't affect execution semantics, avoids having to process type nodes
132
CASE(AstExprError) return false;
133
CASE(AstExprCall)
134
{
135
if (le->args.size != re->args.size || le->self != re->self)
136
return false;
137
138
if (!similar(le->func, re->func))
139
return false;
140
141
for (size_t i = 0; i < le->args.size; ++i)
142
if (!similar(le->args.data[i], re->args.data[i]))
143
return false;
144
145
return true;
146
}
147
CASE(AstExprTable)
148
{
149
if (le->items.size != re->items.size)
150
return false;
151
152
for (size_t i = 0; i < le->items.size; ++i)
153
{
154
const AstExprTable::Item& li = le->items.data[i];
155
const AstExprTable::Item& ri = re->items.data[i];
156
157
if (li.kind != ri.kind)
158
return false;
159
160
if (bool(li.key) != bool(ri.key))
161
return false;
162
else if (li.key && !similar(li.key, ri.key))
163
return false;
164
165
if (!similar(li.value, ri.value))
166
return false;
167
}
168
169
return true;
170
}
171
CASE(AstExprIfElse) return similar(le->condition, re->condition) && similar(le->trueExpr, re->trueExpr) && similar(le->falseExpr, re->falseExpr);
172
CASE(AstExprInterpString)
173
{
174
if (le->strings.size != re->strings.size)
175
return false;
176
177
if (le->expressions.size != re->expressions.size)
178
return false;
179
180
for (size_t i = 0; i < le->strings.size; ++i)
181
if (le->strings.data[i].size != re->strings.data[i].size ||
182
memcmp(le->strings.data[i].data, re->strings.data[i].data, le->strings.data[i].size) != 0)
183
return false;
184
185
for (size_t i = 0; i < le->expressions.size; ++i)
186
if (!similar(le->expressions.data[i], re->expressions.data[i]))
187
return false;
188
189
return true;
190
}
191
CASE(AstExprInstantiate)
192
{
193
return similar(le->expr, re->expr);
194
}
195
else
196
{
197
LUAU_ASSERT(!"Unknown expression type");
198
return false;
199
}
200
201
#undef CASE
202
}
203
204
class LintGlobalLocal : AstVisitor
205
{
206
public:
207
LUAU_NOINLINE static void process(LintContext& context)
208
{
209
LintGlobalLocal pass;
210
pass.context = &context;
211
212
for (auto& global : context.builtinGlobals)
213
{
214
Global& g = pass.globals[global.first];
215
216
g.builtin = true;
217
g.deprecated = global.second.deprecated;
218
}
219
220
context.root->visit(&pass);
221
222
pass.report();
223
}
224
225
private:
226
struct FunctionInfo
227
{
228
explicit FunctionInfo(AstExprFunction* ast)
229
: ast(ast)
230
, dominatedGlobals({})
231
, conditionalExecution(false)
232
{
233
}
234
235
AstExprFunction* ast;
236
DenseHashSet<AstName> dominatedGlobals;
237
bool conditionalExecution;
238
};
239
240
struct Global
241
{
242
AstExprGlobal* firstRef = nullptr;
243
244
std::vector<AstExprFunction*> functionRef;
245
246
bool assigned = false;
247
bool builtin = false;
248
bool definedInModuleScope = false;
249
bool definedAsFunction = false;
250
bool readBeforeWritten = false;
251
std::optional<const char*> deprecated;
252
};
253
254
LintContext* context;
255
256
DenseHashMap<AstName, Global> globals;
257
std::vector<AstExprGlobal*> globalRefs;
258
std::vector<FunctionInfo> functionStack;
259
260
261
LintGlobalLocal()
262
: globals(AstName())
263
{
264
}
265
266
void report()
267
{
268
for (size_t i = 0; i < globalRefs.size(); ++i)
269
{
270
AstExprGlobal* gv = globalRefs[i];
271
Global* g = globals.find(gv->name);
272
273
if (!g || (!g->assigned && !g->builtin))
274
emitWarning(
275
*context, LintWarning::Code_UnknownGlobal, gv->location, "Unknown global '%s'; consider assigning to it first", gv->name.value
276
);
277
else if (g->deprecated)
278
{
279
if (const char* replacement = *g->deprecated; replacement && strlen(replacement))
280
emitWarning(
281
*context,
282
LintWarning::Code_DeprecatedGlobal,
283
gv->location,
284
"Global '%s' is deprecated, use '%s' instead",
285
gv->name.value,
286
replacement
287
);
288
else
289
emitWarning(*context, LintWarning::Code_DeprecatedGlobal, gv->location, "Global '%s' is deprecated", gv->name.value);
290
}
291
}
292
293
for (auto& global : globals)
294
{
295
const Global& g = global.second;
296
297
if (g.functionRef.size() && g.assigned && g.firstRef->name != context->placeholder)
298
{
299
AstExprFunction* top = g.functionRef.back();
300
301
if (top->debugname.value)
302
emitWarning(
303
*context,
304
LintWarning::Code_GlobalUsedAsLocal,
305
g.firstRef->location,
306
"Global '%s' is only used in the enclosing function '%s'; consider changing it to local",
307
g.firstRef->name.value,
308
top->debugname.value
309
);
310
else
311
emitWarning(
312
*context,
313
LintWarning::Code_GlobalUsedAsLocal,
314
g.firstRef->location,
315
"Global '%s' is only used in the enclosing function defined at line %d; consider changing it to local",
316
g.firstRef->name.value,
317
top->location.begin.line + 1
318
);
319
}
320
else if (g.assigned && !g.readBeforeWritten && !g.definedInModuleScope && g.firstRef->name != context->placeholder)
321
{
322
emitWarning(
323
*context,
324
LintWarning::Code_GlobalUsedAsLocal,
325
g.firstRef->location,
326
"Global '%s' is never read before being written. Consider changing it to local",
327
g.firstRef->name.value
328
);
329
}
330
}
331
}
332
333
bool visit(AstExprFunction* node) override
334
{
335
functionStack.emplace_back(node);
336
337
node->body->visit(this);
338
339
functionStack.pop_back();
340
341
return false;
342
}
343
344
bool visit(AstExprGlobal* node) override
345
{
346
if (!functionStack.empty() && !functionStack.back().dominatedGlobals.contains(node->name))
347
{
348
Global& g = globals[node->name];
349
g.readBeforeWritten = true;
350
}
351
trackGlobalRef(node);
352
353
if (node->name == context->placeholder)
354
emitWarning(
355
*context, LintWarning::Code_PlaceholderRead, node->location, "Placeholder value '_' is read here; consider using a named variable"
356
);
357
358
return true;
359
}
360
361
bool visit(AstExprLocal* node) override
362
{
363
if (node->local->name == context->placeholder)
364
emitWarning(
365
*context, LintWarning::Code_PlaceholderRead, node->location, "Placeholder value '_' is read here; consider using a named variable"
366
);
367
368
return true;
369
}
370
371
bool visit(AstStatAssign* node) override
372
{
373
for (size_t i = 0; i < node->vars.size; ++i)
374
{
375
AstExpr* var = node->vars.data[i];
376
377
if (AstExprGlobal* gv = var->as<AstExprGlobal>())
378
{
379
Global& g = globals[gv->name];
380
381
if (functionStack.empty())
382
{
383
g.definedInModuleScope = true;
384
}
385
else
386
{
387
if (!functionStack.back().conditionalExecution)
388
{
389
functionStack.back().dominatedGlobals.insert(gv->name);
390
}
391
}
392
393
if (g.builtin)
394
emitWarning(
395
*context,
396
LintWarning::Code_BuiltinGlobalWrite,
397
gv->location,
398
"Built-in global '%s' is overwritten here; consider using a local or changing the name",
399
gv->name.value
400
);
401
else
402
g.assigned = true;
403
404
trackGlobalRef(gv);
405
}
406
else if (var->is<AstExprLocal>())
407
{
408
// We don't visit locals here because it's a local *write*, and visit(AstExprLocal*) assumes it's a local *read*
409
}
410
else
411
{
412
var->visit(this);
413
}
414
}
415
416
for (size_t i = 0; i < node->values.size; ++i)
417
node->values.data[i]->visit(this);
418
419
return false;
420
}
421
422
bool visit(AstStatFunction* node) override
423
{
424
if (AstExprGlobal* gv = node->name->as<AstExprGlobal>())
425
{
426
Global& g = globals[gv->name];
427
428
if (g.builtin)
429
emitWarning(
430
*context,
431
LintWarning::Code_BuiltinGlobalWrite,
432
gv->location,
433
"Built-in global '%s' is overwritten here; consider using a local or changing the name",
434
gv->name.value
435
);
436
else
437
{
438
g.assigned = true;
439
g.definedAsFunction = true;
440
g.definedInModuleScope = functionStack.empty();
441
}
442
443
trackGlobalRef(gv);
444
}
445
446
return true;
447
}
448
449
class HoldConditionalExecution
450
{
451
public:
452
HoldConditionalExecution(LintGlobalLocal& p)
453
: p(p)
454
{
455
if (!p.functionStack.empty() && !p.functionStack.back().conditionalExecution)
456
{
457
resetToFalse = true;
458
p.functionStack.back().conditionalExecution = true;
459
}
460
}
461
~HoldConditionalExecution()
462
{
463
if (resetToFalse)
464
p.functionStack.back().conditionalExecution = false;
465
}
466
467
private:
468
bool resetToFalse = false;
469
LintGlobalLocal& p;
470
};
471
472
bool visit(AstStatIf* node) override
473
{
474
HoldConditionalExecution ce(*this);
475
node->condition->visit(this);
476
node->thenbody->visit(this);
477
if (node->elsebody)
478
node->elsebody->visit(this);
479
480
return false;
481
}
482
483
bool visit(AstStatWhile* node) override
484
{
485
HoldConditionalExecution ce(*this);
486
node->condition->visit(this);
487
node->body->visit(this);
488
489
return false;
490
}
491
492
bool visit(AstStatRepeat* node) override
493
{
494
HoldConditionalExecution ce(*this);
495
node->condition->visit(this);
496
node->body->visit(this);
497
498
return false;
499
}
500
501
bool visit(AstStatFor* node) override
502
{
503
HoldConditionalExecution ce(*this);
504
node->from->visit(this);
505
node->to->visit(this);
506
507
if (node->step)
508
node->step->visit(this);
509
510
node->body->visit(this);
511
512
return false;
513
}
514
515
bool visit(AstStatForIn* node) override
516
{
517
HoldConditionalExecution ce(*this);
518
for (AstExpr* expr : node->values)
519
expr->visit(this);
520
521
node->body->visit(this);
522
523
return false;
524
}
525
526
void trackGlobalRef(AstExprGlobal* node)
527
{
528
Global& g = globals[node->name];
529
530
globalRefs.push_back(node);
531
532
if (!g.firstRef)
533
{
534
g.firstRef = node;
535
536
// to reduce the cost of tracking we only track this for user globals
537
if (!g.builtin)
538
{
539
g.functionRef.clear();
540
g.functionRef.reserve(functionStack.size());
541
for (const FunctionInfo& entry : functionStack)
542
{
543
g.functionRef.push_back(entry.ast);
544
}
545
}
546
}
547
else
548
{
549
// to reduce the cost of tracking we only track this for user globals
550
if (!g.builtin)
551
{
552
// we need to find a common prefix between all uses of a global
553
size_t prefix = 0;
554
555
while (prefix < g.functionRef.size() && prefix < functionStack.size() && g.functionRef[prefix] == functionStack[prefix].ast)
556
prefix++;
557
558
g.functionRef.resize(prefix);
559
}
560
}
561
}
562
};
563
564
class LintSameLineStatement : AstVisitor
565
{
566
public:
567
LUAU_NOINLINE static void process(LintContext& context)
568
{
569
LintSameLineStatement pass;
570
571
pass.context = &context;
572
pass.lastLine = ~0u;
573
574
context.root->visit(&pass);
575
}
576
577
private:
578
LintContext* context;
579
unsigned int lastLine;
580
581
bool visit(AstStatBlock* node) override
582
{
583
for (size_t i = 1; i < node->body.size; ++i)
584
{
585
const Location& last = node->body.data[i - 1]->location;
586
const Location& location = node->body.data[i]->location;
587
588
if (location.begin.line != last.end.line)
589
continue;
590
591
// We warn once per line with multiple statements
592
if (location.begin.line == lastLine)
593
continue;
594
595
// There's a common pattern where local variables are computed inside a do block that starts on the same line; we white-list this pattern
596
if (node->body.data[i - 1]->is<AstStatLocal>() && node->body.data[i]->is<AstStatBlock>())
597
continue;
598
599
// Another common pattern is using multiple statements on the same line with semi-colons on each of them. White-list this pattern too.
600
if (node->body.data[i - 1]->hasSemicolon)
601
continue;
602
603
emitWarning(
604
*context,
605
LintWarning::Code_SameLineStatement,
606
location,
607
"A new statement is on the same line; add semi-colon on previous statement to silence"
608
);
609
610
lastLine = location.begin.line;
611
}
612
613
return true;
614
}
615
};
616
617
class LintMultiLineStatement : AstVisitor
618
{
619
public:
620
LUAU_NOINLINE static void process(LintContext& context)
621
{
622
LintMultiLineStatement pass;
623
pass.context = &context;
624
625
context.root->visit(&pass);
626
}
627
628
private:
629
LintContext* context;
630
631
struct Statement
632
{
633
Location start;
634
unsigned int lastLine;
635
bool flagged;
636
};
637
638
std::vector<Statement> stack;
639
640
bool visit(AstExpr* node) override
641
{
642
Statement& top = stack.back();
643
644
if (!top.flagged)
645
{
646
Location location = node->location;
647
648
if (location.begin.line > top.lastLine)
649
{
650
top.lastLine = location.begin.line;
651
652
if (location.begin.column <= top.start.begin.column)
653
{
654
emitWarning(
655
*context, LintWarning::Code_MultiLineStatement, location, "Statement spans multiple lines; use indentation to silence"
656
);
657
658
top.flagged = true;
659
}
660
}
661
}
662
663
return true;
664
}
665
666
bool visit(AstExprTable* node) override
667
{
668
(void)node;
669
670
return false;
671
}
672
673
bool visit(AstStatRepeat* node) override
674
{
675
node->body->visit(this);
676
677
return false;
678
}
679
680
bool visit(AstStatBlock* node) override
681
{
682
for (size_t i = 0; i < node->body.size; ++i)
683
{
684
AstStat* stmt = node->body.data[i];
685
686
Statement s = {stmt->location, stmt->location.begin.line, false};
687
stack.push_back(s);
688
689
stmt->visit(this);
690
691
stack.pop_back();
692
}
693
694
return false;
695
}
696
};
697
698
class LintLocalHygiene : AstVisitor
699
{
700
public:
701
LUAU_NOINLINE static void process(LintContext& context)
702
{
703
LintLocalHygiene pass;
704
pass.context = &context;
705
706
for (auto& global : context.builtinGlobals)
707
pass.globals[global.first].builtin = true;
708
709
context.root->visit(&pass);
710
711
pass.report();
712
}
713
714
private:
715
LintContext* context;
716
717
struct Local
718
{
719
AstNode* defined = nullptr;
720
bool function;
721
bool import;
722
bool used;
723
bool arg;
724
};
725
726
struct Global
727
{
728
bool used;
729
bool builtin;
730
AstExprGlobal* firstRef;
731
};
732
733
DenseHashMap<AstLocal*, Local> locals;
734
DenseHashMap<AstName, AstLocal*> imports;
735
DenseHashMap<AstName, Global> globals;
736
737
LintLocalHygiene()
738
: locals(NULL)
739
, imports(AstName())
740
, globals(AstName())
741
{
742
}
743
744
void report()
745
{
746
for (auto& l : locals)
747
{
748
if (l.second.used)
749
reportUsedLocal(l.first, l.second);
750
else if (l.second.defined)
751
reportUnusedLocal(l.first, l.second);
752
}
753
}
754
755
void reportUsedLocal(AstLocal* local, const Local& info)
756
{
757
if (AstLocal* shadow = local->shadow)
758
{
759
// LintDuplicateFunctions will catch this.
760
Local* shadowLocal = locals.find(shadow);
761
if (context->options.isEnabled(LintWarning::Code_DuplicateFunction) && info.function && shadowLocal && shadowLocal->function)
762
return;
763
764
// LintDuplicateLocal will catch this.
765
if (context->options.isEnabled(LintWarning::Code_DuplicateLocal) && shadowLocal && shadowLocal->defined == info.defined)
766
return;
767
768
// don't warn on inter-function shadowing since it is much more fragile wrt refactoring
769
if (shadow->functionDepth == local->functionDepth)
770
emitWarning(
771
*context,
772
LintWarning::Code_LocalShadow,
773
local->location,
774
"Variable '%s' shadows previous declaration at line %d",
775
local->name.value,
776
shadow->location.begin.line + 1
777
);
778
}
779
else if (Global* global = globals.find(local->name))
780
{
781
if (global->builtin)
782
; // there are many builtins with common names like 'table'; some of them are deprecated as well
783
else if (global->firstRef)
784
{
785
emitWarning(
786
*context,
787
LintWarning::Code_LocalShadow,
788
local->location,
789
"Variable '%s' shadows a global variable used at line %d",
790
local->name.value,
791
global->firstRef->location.begin.line + 1
792
);
793
}
794
else
795
{
796
emitWarning(*context, LintWarning::Code_LocalShadow, local->location, "Variable '%s' shadows a global variable", local->name.value);
797
}
798
}
799
}
800
801
void reportUnusedLocal(AstLocal* local, const Local& info)
802
{
803
if (local->name.value[0] == '_')
804
return;
805
806
if (info.function)
807
emitWarning(
808
*context,
809
LintWarning::Code_FunctionUnused,
810
local->location,
811
"Function '%s' is never used; prefix with '_' to silence",
812
local->name.value
813
);
814
else if (info.import)
815
emitWarning(
816
*context, LintWarning::Code_ImportUnused, local->location, "Import '%s' is never used; prefix with '_' to silence", local->name.value
817
);
818
else
819
emitWarning(
820
*context, LintWarning::Code_LocalUnused, local->location, "Variable '%s' is never used; prefix with '_' to silence", local->name.value
821
);
822
}
823
824
bool isRequireCall(AstExpr* expr)
825
{
826
AstExprCall* call = expr->as<AstExprCall>();
827
if (!call)
828
return false;
829
830
AstExprGlobal* glob = call->func->as<AstExprGlobal>();
831
if (!glob)
832
return false;
833
834
return glob->name == "require";
835
}
836
837
bool visit(AstStatAssign* node) override
838
{
839
for (AstExpr* var : node->vars)
840
{
841
// We don't visit locals here because it's a local *write*, and visit(AstExprLocal*) assumes it's a local *read*
842
if (!var->is<AstExprLocal>())
843
var->visit(this);
844
}
845
846
for (AstExpr* value : node->values)
847
value->visit(this);
848
849
return false;
850
}
851
852
bool visit(AstStatLocal* node) override
853
{
854
if (node->vars.size == 1 && node->values.size == 1)
855
{
856
Local& l = locals[node->vars.data[0]];
857
858
l.defined = node;
859
l.import = isRequireCall(node->values.data[0]);
860
861
if (l.import)
862
imports[node->vars.data[0]->name] = node->vars.data[0];
863
}
864
else
865
{
866
for (size_t i = 0; i < node->vars.size; ++i)
867
{
868
Local& l = locals[node->vars.data[i]];
869
870
l.defined = node;
871
}
872
}
873
874
return true;
875
}
876
877
bool visit(AstStatLocalFunction* node) override
878
{
879
Local& l = locals[node->name];
880
881
l.defined = node;
882
l.function = true;
883
884
return true;
885
}
886
887
bool visit(AstExprLocal* node) override
888
{
889
Local& l = locals[node->local];
890
891
l.used = true;
892
893
return true;
894
}
895
896
bool visit(AstExprGlobal* node) override
897
{
898
Global& global = globals[node->name];
899
900
global.used = true;
901
if (!global.firstRef)
902
global.firstRef = node;
903
904
return true;
905
}
906
907
bool visit(AstType* node) override
908
{
909
return true;
910
}
911
912
bool visit(AstTypePack* node) override
913
{
914
return true;
915
}
916
917
bool visit(AstTypeReference* node) override
918
{
919
if (!node->prefix)
920
return true;
921
922
if (!imports.contains(*node->prefix))
923
return true;
924
925
AstLocal* astLocal = imports[*node->prefix];
926
Local& local = locals[astLocal];
927
LUAU_ASSERT(local.import);
928
local.used = true;
929
930
return true;
931
}
932
933
bool visit(AstExprFunction* node) override
934
{
935
if (node->self)
936
locals[node->self].arg = true;
937
938
for (size_t i = 0; i < node->args.size; ++i)
939
locals[node->args.data[i]].arg = true;
940
941
return true;
942
}
943
};
944
945
class LintUnusedFunction : AstVisitor
946
{
947
public:
948
LUAU_NOINLINE static void process(LintContext& context)
949
{
950
LintUnusedFunction pass;
951
pass.context = &context;
952
953
context.root->visit(&pass);
954
955
pass.report();
956
}
957
958
private:
959
LintContext* context;
960
961
struct Global
962
{
963
Location location;
964
bool function;
965
bool used;
966
};
967
968
DenseHashMap<AstName, Global> globals;
969
970
LintUnusedFunction()
971
: globals(AstName())
972
{
973
}
974
975
void report()
976
{
977
for (auto& g : globals)
978
{
979
if (g.second.function && !g.second.used && g.first.value[0] != '_')
980
emitWarning(
981
*context,
982
LintWarning::Code_FunctionUnused,
983
g.second.location,
984
"Function '%s' is never used; prefix with '_' to silence",
985
g.first.value
986
);
987
}
988
}
989
990
bool visit(AstStatFunction* node) override
991
{
992
if (AstExprGlobal* expr = node->name->as<AstExprGlobal>())
993
{
994
Global& g = globals[expr->name];
995
996
g.function = true;
997
g.location = expr->location;
998
999
node->func->visit(this);
1000
1001
return false;
1002
}
1003
1004
return true;
1005
}
1006
1007
bool visit(AstExprGlobal* node) override
1008
{
1009
Global& g = globals[node->name];
1010
1011
g.used = true;
1012
1013
return true;
1014
}
1015
};
1016
1017
class LintUnreachableCode : AstVisitor
1018
{
1019
public:
1020
LUAU_NOINLINE static void process(LintContext& context)
1021
{
1022
LintUnreachableCode pass;
1023
pass.context = &context;
1024
1025
pass.analyze(context.root);
1026
context.root->visit(&pass);
1027
}
1028
1029
private:
1030
LintContext* context;
1031
1032
// Note: this enum is order-sensitive!
1033
// The order is in the "severity" of the termination and affects merging of status codes from different branches
1034
// For example, if one branch breaks and one returns, the merged result is "break"
1035
enum Status
1036
{
1037
Unknown,
1038
Continue,
1039
Break,
1040
Return,
1041
Error,
1042
};
1043
1044
const char* getReason(Status status)
1045
{
1046
switch (status)
1047
{
1048
case Continue:
1049
return "continue";
1050
1051
case Break:
1052
return "break";
1053
1054
case Return:
1055
return "return";
1056
1057
case Error:
1058
return "error";
1059
1060
default:
1061
return "unknown";
1062
}
1063
}
1064
1065
Status analyze(AstStat* node)
1066
{
1067
if (AstStatBlock* stat = node->as<AstStatBlock>())
1068
{
1069
for (size_t i = 0; i < stat->body.size; ++i)
1070
{
1071
AstStat* si = stat->body.data[i];
1072
Status step = analyze(si);
1073
1074
if (step != Unknown)
1075
{
1076
if (i + 1 == stat->body.size)
1077
return step;
1078
1079
AstStat* next = stat->body.data[i + 1];
1080
1081
// silence the warning for common pattern of Error (coming from error()) + Return
1082
if (step == Error && si->is<AstStatExpr>() && next->is<AstStatReturn>() && i + 2 == stat->body.size)
1083
return Error;
1084
1085
emitWarning(
1086
*context,
1087
LintWarning::Code_UnreachableCode,
1088
next->location,
1089
"Unreachable code (previous statement always %ss)",
1090
getReason(step)
1091
);
1092
return step;
1093
}
1094
}
1095
1096
return Unknown;
1097
}
1098
else if (AstStatIf* stat = node->as<AstStatIf>())
1099
{
1100
Status ifs = analyze(stat->thenbody);
1101
Status elses = stat->elsebody ? analyze(stat->elsebody) : Unknown;
1102
1103
return std::min(ifs, elses);
1104
}
1105
else if (AstStatWhile* stat = node->as<AstStatWhile>())
1106
{
1107
analyze(stat->body);
1108
1109
return Unknown;
1110
}
1111
else if (AstStatRepeat* stat = node->as<AstStatRepeat>())
1112
{
1113
analyze(stat->body);
1114
1115
return Unknown;
1116
}
1117
else if (node->is<AstStatBreak>())
1118
{
1119
return Break;
1120
}
1121
else if (node->is<AstStatContinue>())
1122
{
1123
return Continue;
1124
}
1125
else if (node->is<AstStatReturn>())
1126
{
1127
return Return;
1128
}
1129
else if (AstStatExpr* stat = node->as<AstStatExpr>())
1130
{
1131
if (AstExprCall* call = stat->expr->as<AstExprCall>())
1132
if (doesCallError(call))
1133
return Error;
1134
1135
return Unknown;
1136
}
1137
else if (AstStatFor* stat = node->as<AstStatFor>())
1138
{
1139
analyze(stat->body);
1140
1141
return Unknown;
1142
}
1143
else if (AstStatForIn* stat = node->as<AstStatForIn>())
1144
{
1145
analyze(stat->body);
1146
1147
return Unknown;
1148
}
1149
else
1150
{
1151
return Unknown;
1152
}
1153
}
1154
1155
bool visit(AstExprFunction* node) override
1156
{
1157
analyze(node->body);
1158
1159
return true;
1160
}
1161
};
1162
1163
class LintUnknownType : AstVisitor
1164
{
1165
public:
1166
LUAU_NOINLINE static void process(LintContext& context)
1167
{
1168
LintUnknownType pass;
1169
pass.context = &context;
1170
1171
context.root->visit(&pass);
1172
}
1173
1174
private:
1175
LintContext* context;
1176
1177
enum TypeKind
1178
{
1179
Kind_Unknown,
1180
Kind_Primitive, // primitive type supported by VM - boolean/userdata/etc. No differentiation between types of userdata.
1181
Kind_Vector, // 'vector' but only used when type is used. Remove when `LuauLinterVectorPrimitive` is clipped
1182
Kind_Userdata, // custom userdata type
1183
};
1184
1185
TypeKind getTypeKind(const std::string& name)
1186
{
1187
if (name == "nil" || name == "boolean" || name == "userdata" || name == "number" || name == "string" || name == "table" ||
1188
name == "function" || name == "thread" || name == "buffer")
1189
return Kind_Primitive;
1190
1191
if (name == "vector")
1192
{
1193
if (FFlag::LuauLinterVectorPrimitive)
1194
return Kind_Primitive;
1195
else
1196
return Kind_Vector;
1197
}
1198
1199
if (std::optional<TypeFun> maybeTy = context->scope->lookupType(name))
1200
return Kind_Userdata;
1201
1202
return Kind_Unknown;
1203
}
1204
1205
void validateType(AstExprConstantString* expr, std::initializer_list<TypeKind> expected, const char* expectedString)
1206
{
1207
std::string name(expr->value.data, expr->value.size);
1208
TypeKind kind = getTypeKind(name);
1209
1210
if (kind == Kind_Unknown)
1211
{
1212
emitWarning(*context, LintWarning::Code_UnknownType, expr->location, "Unknown type '%s'", name.c_str());
1213
return;
1214
}
1215
1216
for (TypeKind ek : expected)
1217
{
1218
if (kind == ek)
1219
return;
1220
}
1221
1222
emitWarning(*context, LintWarning::Code_UnknownType, expr->location, "Unknown type '%s' (expected %s)", name.c_str(), expectedString);
1223
}
1224
1225
bool visit(AstExprBinary* node) override
1226
{
1227
if (node->op == AstExprBinary::CompareNe || node->op == AstExprBinary::CompareEq)
1228
{
1229
AstExpr* lhs = node->left;
1230
AstExpr* rhs = node->right;
1231
1232
if (!rhs->is<AstExprConstantString>())
1233
std::swap(lhs, rhs);
1234
1235
AstExprCall* call = lhs->as<AstExprCall>();
1236
AstExprConstantString* arg = rhs->as<AstExprConstantString>();
1237
1238
if (call && arg)
1239
{
1240
AstExprGlobal* g = call->func->as<AstExprGlobal>();
1241
1242
if (g && g->name == "type")
1243
{
1244
validateType(arg, {Kind_Primitive, Kind_Vector}, "primitive type");
1245
}
1246
else if (g && g->name == "typeof")
1247
{
1248
validateType(arg, {Kind_Primitive, Kind_Userdata}, "primitive or userdata type");
1249
}
1250
}
1251
}
1252
1253
return true;
1254
}
1255
};
1256
1257
class LintForRange : AstVisitor
1258
{
1259
public:
1260
LUAU_NOINLINE static void process(LintContext& context)
1261
{
1262
LintForRange pass;
1263
pass.context = &context;
1264
1265
context.root->visit(&pass);
1266
}
1267
1268
private:
1269
LintContext* context;
1270
1271
double getLoopEnd(double from, double to)
1272
{
1273
return from + floor(to - from);
1274
}
1275
1276
bool visit(AstStatFor* node) override
1277
{
1278
// note: we silence all warnings below if *any* step is specified, assuming that the user knows best
1279
if (!node->step)
1280
{
1281
AstExprConstantNumber* fc = node->from->as<AstExprConstantNumber>();
1282
AstExprUnary* fu = node->from->as<AstExprUnary>();
1283
AstExprConstantNumber* tc = node->to->as<AstExprConstantNumber>();
1284
AstExprUnary* tu = node->to->as<AstExprUnary>();
1285
1286
Location rangeLocation(node->from->location, node->to->location);
1287
1288
// for i=#t,1 do
1289
if (fu && fu->op == AstExprUnary::Len && tc && tc->value == 1.0)
1290
emitWarning(
1291
*context, LintWarning::Code_ForRange, rangeLocation, "For loop should iterate backwards; did you forget to specify -1 as step?"
1292
);
1293
// for i=8,1 do
1294
else if (fc && tc && fc->value > tc->value)
1295
emitWarning(
1296
*context, LintWarning::Code_ForRange, rangeLocation, "For loop should iterate backwards; did you forget to specify -1 as step?"
1297
);
1298
// for i=1,8.75 do
1299
else if (fc && tc && getLoopEnd(fc->value, tc->value) != tc->value)
1300
emitWarning(
1301
*context,
1302
LintWarning::Code_ForRange,
1303
rangeLocation,
1304
"For loop ends at %g instead of %g; did you forget to specify step?",
1305
getLoopEnd(fc->value, tc->value),
1306
tc->value
1307
);
1308
// for i=0,#t do
1309
else if (fc && tu && fc->value == 0.0 && tu->op == AstExprUnary::Len)
1310
emitWarning(*context, LintWarning::Code_ForRange, rangeLocation, "For loop starts at 0, but arrays start at 1");
1311
// for i=#t,0 do
1312
else if (fu && fu->op == AstExprUnary::Len && tc && tc->value == 0.0)
1313
emitWarning(
1314
*context,
1315
LintWarning::Code_ForRange,
1316
rangeLocation,
1317
"For loop should iterate backwards; did you forget to specify -1 as step? Also consider changing 0 to 1 since arrays start at 1"
1318
);
1319
}
1320
1321
return true;
1322
}
1323
};
1324
1325
class LintUnbalancedAssignment : AstVisitor
1326
{
1327
public:
1328
LUAU_NOINLINE static void process(LintContext& context)
1329
{
1330
LintUnbalancedAssignment pass;
1331
pass.context = &context;
1332
1333
context.root->visit(&pass);
1334
}
1335
1336
private:
1337
LintContext* context;
1338
1339
void assign(size_t vars, const AstArray<AstExpr*>& values, const Location& location)
1340
{
1341
if (vars != values.size && values.size > 0)
1342
{
1343
AstExpr* last = values.data[values.size - 1];
1344
1345
if (vars < values.size)
1346
emitWarning(
1347
*context,
1348
LintWarning::Code_UnbalancedAssignment,
1349
location,
1350
"Assigning %d values to %d variables leaves some values unused",
1351
int(values.size),
1352
int(vars)
1353
);
1354
else if (last->is<AstExprCall>() || last->is<AstExprVarargs>())
1355
; // we don't know how many values the last expression returns
1356
else if (last->is<AstExprConstantNil>())
1357
; // last expression is nil which explicitly silences the nil-init warning
1358
else
1359
emitWarning(
1360
*context,
1361
LintWarning::Code_UnbalancedAssignment,
1362
location,
1363
"Assigning %d values to %d variables initializes extra variables with nil; add 'nil' to value list to silence",
1364
int(values.size),
1365
int(vars)
1366
);
1367
}
1368
}
1369
1370
bool visit(AstStatLocal* node) override
1371
{
1372
assign(node->vars.size, node->values, node->location);
1373
1374
return true;
1375
}
1376
1377
bool visit(AstStatAssign* node) override
1378
{
1379
assign(node->vars.size, node->values, node->location);
1380
1381
return true;
1382
}
1383
};
1384
1385
class LintImplicitReturn : AstVisitor
1386
{
1387
public:
1388
LUAU_NOINLINE static void process(LintContext& context)
1389
{
1390
LintImplicitReturn pass;
1391
pass.context = &context;
1392
1393
context.root->visit(&pass);
1394
}
1395
1396
private:
1397
LintContext* context;
1398
1399
Location getEndLocation(const AstStat* node)
1400
{
1401
Location loc = node->location;
1402
1403
if (node->is<AstStatExpr>() || node->is<AstStatAssign>() || node->is<AstStatLocal>())
1404
return loc;
1405
1406
if (loc.begin.line == loc.end.line)
1407
return loc;
1408
1409
// assume that we're in context of a statement that has an "end" block
1410
return Location(Position(loc.end.line, std::max(0, int(loc.end.column) - 3)), loc.end);
1411
}
1412
1413
AstStatReturn* getValueReturn(AstStat* node)
1414
{
1415
struct Visitor : AstVisitor
1416
{
1417
AstStatReturn* result = nullptr;
1418
1419
bool visit(AstExpr* node) override
1420
{
1421
(void)node;
1422
return false;
1423
}
1424
1425
bool visit(AstStatReturn* node) override
1426
{
1427
if (!result && node->list.size > 0)
1428
result = node;
1429
1430
return false;
1431
}
1432
};
1433
1434
Visitor visitor;
1435
node->visit(&visitor);
1436
return visitor.result;
1437
}
1438
1439
bool visit(AstExprFunction* node) override
1440
{
1441
const AstStat* bodyf = getFallthrough(node->body);
1442
AstStat* vret = getValueReturn(node->body);
1443
1444
if (bodyf && vret)
1445
{
1446
Location location = getEndLocation(bodyf);
1447
1448
if (node->debugname.value)
1449
emitWarning(
1450
*context,
1451
LintWarning::Code_ImplicitReturn,
1452
location,
1453
"Function '%s' can implicitly return no values even though there's an explicit return at line %d; add explicit return to silence",
1454
node->debugname.value,
1455
vret->location.begin.line + 1
1456
);
1457
else
1458
emitWarning(
1459
*context,
1460
LintWarning::Code_ImplicitReturn,
1461
location,
1462
"Function can implicitly return no values even though there's an explicit return at line %d; add explicit return to silence",
1463
vret->location.begin.line + 1
1464
);
1465
}
1466
1467
return true;
1468
}
1469
};
1470
1471
class LintFormatString : AstVisitor
1472
{
1473
public:
1474
LUAU_NOINLINE static void process(LintContext& context)
1475
{
1476
LintFormatString pass;
1477
pass.context = &context;
1478
1479
context.root->visit(&pass);
1480
}
1481
1482
static void fuzz(const char* data, size_t size)
1483
{
1484
LintContext context;
1485
1486
LintFormatString pass;
1487
pass.context = &context;
1488
1489
pass.checkStringFormat(data, size);
1490
pass.checkStringPack(data, size, false);
1491
pass.checkStringMatch(data, size);
1492
pass.checkStringReplace(data, size, -1);
1493
pass.checkDateFormat(data, size);
1494
}
1495
1496
private:
1497
LintContext* context;
1498
1499
static inline bool isAlpha(char ch)
1500
{
1501
// use or trick to convert to lower case and unsigned comparison to do range check
1502
return unsigned((ch | ' ') - 'a') < 26;
1503
}
1504
1505
static inline bool isDigit(char ch)
1506
{
1507
// use unsigned comparison to do range check for performance
1508
return unsigned(ch - '0') < 10;
1509
}
1510
1511
const char* checkStringFormat(const char* data, size_t size)
1512
{
1513
const char* flags = "-+ #0";
1514
const char* options = "cdiouxXeEfgGqs*";
1515
1516
for (size_t i = 0; i < size; ++i)
1517
{
1518
if (data[i] == '%')
1519
{
1520
i++;
1521
1522
// escaped % doesn't allow for flags/etc.
1523
if (i < size && data[i] == '%')
1524
continue;
1525
1526
// skip flags
1527
while (i < size && strchr(flags, data[i]))
1528
i++;
1529
1530
// skip width (up to two digits)
1531
if (i < size && isDigit(data[i]))
1532
i++;
1533
if (i < size && isDigit(data[i]))
1534
i++;
1535
1536
// skip precision
1537
if (i < size && data[i] == '.')
1538
{
1539
i++;
1540
1541
// up to two digits
1542
if (i < size && isDigit(data[i]))
1543
i++;
1544
if (i < size && isDigit(data[i]))
1545
i++;
1546
}
1547
1548
if (i == size)
1549
return "unfinished format specifier";
1550
1551
if (!strchr(options, data[i]))
1552
return "invalid format specifier: must be a string format specifier or %";
1553
}
1554
}
1555
1556
return nullptr;
1557
}
1558
1559
const char* checkStringPack(const char* data, size_t size, bool fixed)
1560
{
1561
const char* options = "<>=!bBhHlLjJTiIfdnczsxX ";
1562
const char* unsized = "<>=!zX ";
1563
1564
for (size_t i = 0; i < size; ++i)
1565
{
1566
if (!strchr(options, data[i]))
1567
return "unexpected character; must be a pack specifier or space";
1568
1569
if (data[i] == 'c' && (i + 1 == size || !isDigit(data[i + 1])))
1570
return "fixed-sized string format must specify the size";
1571
1572
if (data[i] == 'X' && (i + 1 == size || strchr(unsized, data[i + 1])))
1573
return "X must be followed by a size specifier";
1574
1575
if (fixed && (data[i] == 'z' || data[i] == 's'))
1576
return "pack specifier must be fixed-size";
1577
1578
if ((data[i] == '!' || data[i] == 'i' || data[i] == 'I' || data[i] == 'c' || data[i] == 's') && i + 1 < size && isDigit(data[i + 1]))
1579
{
1580
bool isc = data[i] == 'c';
1581
1582
unsigned int v = 0;
1583
while (i + 1 < size && isDigit(data[i + 1]) && v <= (INT_MAX - 9) / 10)
1584
{
1585
v = v * 10 + (data[i + 1] - '0');
1586
i++;
1587
}
1588
1589
if (i + 1 < size && isDigit(data[i + 1]))
1590
return "size specifier is too large";
1591
1592
if (!isc && (v == 0 || v > 16))
1593
return "integer size must be in range [1,16]";
1594
}
1595
}
1596
1597
return nullptr;
1598
}
1599
1600
const char* checkStringMatchSet(const char* data, size_t size, const char* magic, const char* classes)
1601
{
1602
for (size_t i = 0; i < size; ++i)
1603
{
1604
if (data[i] == '%')
1605
{
1606
i++;
1607
1608
if (i == size)
1609
return "unfinished character class";
1610
1611
if (isDigit(data[i]))
1612
{
1613
return "sets can not contain capture references";
1614
}
1615
else if (isAlpha(data[i]))
1616
{
1617
// lower case lookup - upper case for every character class is defined as its inverse
1618
if (!strchr(classes, data[i] | ' '))
1619
return "invalid character class, must refer to a defined class or its inverse";
1620
}
1621
else
1622
{
1623
// technically % can escape any non-alphanumeric character but this is error-prone
1624
if (!strchr(magic, data[i]))
1625
return "expected a magic character after %";
1626
}
1627
1628
if (i + 1 < size && data[i + 1] == '-')
1629
return "character range can't include character sets";
1630
}
1631
else if (data[i] == '-')
1632
{
1633
if (i + 1 < size && data[i + 1] == '%')
1634
return "character range can't include character sets";
1635
}
1636
}
1637
1638
return nullptr;
1639
}
1640
1641
const char* checkStringMatch(const char* data, size_t size, int* outCaptures = nullptr)
1642
{
1643
const char* magic = "^$()%.[]*+-?)";
1644
const char* classes = "acdglpsuwxz";
1645
1646
std::vector<int> openCaptures;
1647
int totalCaptures = 0;
1648
1649
for (size_t i = 0; i < size; ++i)
1650
{
1651
if (data[i] == '%')
1652
{
1653
i++;
1654
1655
if (i == size)
1656
return "unfinished character class";
1657
1658
if (isDigit(data[i]))
1659
{
1660
if (data[i] == '0')
1661
return "invalid capture reference, must be 1-9";
1662
1663
int captureIndex = data[i] - '0';
1664
1665
if (captureIndex > totalCaptures)
1666
return "invalid capture reference, must refer to a valid capture";
1667
1668
for (int open : openCaptures)
1669
if (open == captureIndex)
1670
return "invalid capture reference, must refer to a closed capture";
1671
}
1672
else if (isAlpha(data[i]))
1673
{
1674
if (data[i] == 'b')
1675
{
1676
if (i + 2 >= size)
1677
return "missing brace characters for balanced match";
1678
1679
i += 2;
1680
}
1681
else if (data[i] == 'f')
1682
{
1683
if (i + 1 >= size || data[i + 1] != '[')
1684
return "missing set after a frontier pattern";
1685
1686
// we can parse the set with the regular logic
1687
}
1688
else
1689
{
1690
// lower case lookup - upper case for every character class is defined as its inverse
1691
if (!strchr(classes, data[i] | ' '))
1692
return "invalid character class, must refer to a defined class or its inverse";
1693
}
1694
}
1695
else
1696
{
1697
// technically % can escape any non-alphanumeric character but this is error-prone
1698
if (!strchr(magic, data[i]))
1699
return "expected a magic character after %";
1700
}
1701
}
1702
else if (data[i] == '[')
1703
{
1704
size_t j = i + 1;
1705
1706
// empty patterns don't exist as per grammar rules, so we skip leading ^ and ]
1707
if (j < size && data[j] == '^')
1708
j++;
1709
1710
if (j < size && data[j] == ']')
1711
j++;
1712
1713
// scan for the end of the pattern
1714
while (j < size && data[j] != ']')
1715
{
1716
// % escapes the next character
1717
if (j + 1 < size && data[j] == '%')
1718
j++;
1719
1720
j++;
1721
}
1722
1723
if (j == size)
1724
return "expected ] at the end of the string to close a set";
1725
1726
if (const char* error = checkStringMatchSet(data + i + 1, j - i - 1, magic, classes))
1727
return error;
1728
1729
LUAU_ASSERT(data[j] == ']');
1730
i = j;
1731
}
1732
else if (data[i] == '(')
1733
{
1734
totalCaptures++;
1735
openCaptures.push_back(totalCaptures);
1736
}
1737
else if (data[i] == ')')
1738
{
1739
if (openCaptures.empty())
1740
return "unexpected ) without a matching (";
1741
openCaptures.pop_back();
1742
}
1743
}
1744
1745
if (!openCaptures.empty())
1746
return "expected ) at the end of the string to close a capture";
1747
1748
if (outCaptures)
1749
*outCaptures = totalCaptures;
1750
1751
return nullptr;
1752
}
1753
1754
const char* checkStringReplace(const char* data, size_t size, int captures)
1755
{
1756
for (size_t i = 0; i < size; ++i)
1757
{
1758
if (data[i] == '%')
1759
{
1760
i++;
1761
1762
if (i == size)
1763
return "unfinished replacement";
1764
1765
if (data[i] != '%' && !isDigit(data[i]))
1766
return "unexpected replacement character; must be a digit or %";
1767
1768
if (isDigit(data[i]) && captures >= 0 && data[i] - '0' > captures)
1769
return "invalid capture index, must refer to pattern capture";
1770
}
1771
}
1772
1773
return nullptr;
1774
}
1775
1776
const char* checkDateFormat(const char* data, size_t size)
1777
{
1778
const char* options = "aAbBcdHIjmMpSUwWxXyYzZ";
1779
1780
for (size_t i = 0; i < size; ++i)
1781
{
1782
if (data[i] == '%')
1783
{
1784
i++;
1785
1786
if (i == size)
1787
return "unfinished replacement";
1788
1789
if (data[i] != '%' && !strchr(options, data[i]))
1790
return "unexpected replacement character; must be a date format specifier or %";
1791
}
1792
1793
if (data[i] == 0)
1794
return "date format can not contain null characters";
1795
}
1796
1797
return nullptr;
1798
}
1799
1800
void matchStringCall(AstName name, AstExpr* self, AstArray<AstExpr*> args)
1801
{
1802
if (name == "format")
1803
{
1804
if (AstExprConstantString* fmt = self->as<AstExprConstantString>())
1805
if (const char* error = checkStringFormat(fmt->value.data, fmt->value.size))
1806
emitWarning(*context, LintWarning::Code_FormatString, fmt->location, "Invalid format string: %s", error);
1807
}
1808
else if (name == "pack" || name == "packsize" || name == "unpack")
1809
{
1810
if (AstExprConstantString* fmt = self->as<AstExprConstantString>())
1811
if (const char* error = checkStringPack(fmt->value.data, fmt->value.size, name == "packsize"))
1812
emitWarning(*context, LintWarning::Code_FormatString, fmt->location, "Invalid pack format: %s", error);
1813
}
1814
else if ((name == "match" || name == "gmatch") && args.size > 0)
1815
{
1816
if (AstExprConstantString* pat = args.data[0]->as<AstExprConstantString>())
1817
if (const char* error = checkStringMatch(pat->value.data, pat->value.size))
1818
emitWarning(*context, LintWarning::Code_FormatString, pat->location, "Invalid match pattern: %s", error);
1819
}
1820
else if (name == "find" && args.size > 0 && args.size <= 2)
1821
{
1822
if (AstExprConstantString* pat = args.data[0]->as<AstExprConstantString>())
1823
if (const char* error = checkStringMatch(pat->value.data, pat->value.size))
1824
emitWarning(*context, LintWarning::Code_FormatString, pat->location, "Invalid match pattern: %s", error);
1825
}
1826
else if (name == "find" && args.size >= 3)
1827
{
1828
AstExprConstantBool* mode = args.data[2]->as<AstExprConstantBool>();
1829
1830
// find(_, _, _, true) is a raw string find, not a pattern match
1831
if (mode && !mode->value)
1832
if (AstExprConstantString* pat = args.data[0]->as<AstExprConstantString>())
1833
if (const char* error = checkStringMatch(pat->value.data, pat->value.size))
1834
emitWarning(*context, LintWarning::Code_FormatString, pat->location, "Invalid match pattern: %s", error);
1835
}
1836
else if (name == "gsub" && args.size > 1)
1837
{
1838
int captures = -1;
1839
1840
if (AstExprConstantString* pat = args.data[0]->as<AstExprConstantString>())
1841
if (const char* error = checkStringMatch(pat->value.data, pat->value.size, &captures))
1842
emitWarning(*context, LintWarning::Code_FormatString, pat->location, "Invalid match pattern: %s", error);
1843
1844
if (AstExprConstantString* rep = args.data[1]->as<AstExprConstantString>())
1845
if (const char* error = checkStringReplace(rep->value.data, rep->value.size, captures))
1846
emitWarning(*context, LintWarning::Code_FormatString, rep->location, "Invalid match replacement: %s", error);
1847
}
1848
}
1849
1850
void matchCall(AstExprCall* node)
1851
{
1852
AstExprIndexName* func = node->func->as<AstExprIndexName>();
1853
if (!func)
1854
return;
1855
1856
if (node->self)
1857
{
1858
AstExprGroup* group = func->expr->as<AstExprGroup>();
1859
AstExpr* self = group ? group->expr : func->expr;
1860
1861
if (self->is<AstExprConstantString>())
1862
matchStringCall(func->index, self, node->args);
1863
else if (std::optional<TypeId> type = context->getType(self))
1864
if (isString(*type))
1865
matchStringCall(func->index, self, node->args);
1866
return;
1867
}
1868
1869
AstExprGlobal* lib = func->expr->as<AstExprGlobal>();
1870
if (!lib)
1871
return;
1872
1873
if (lib->name == "string")
1874
{
1875
if (node->args.size > 0)
1876
{
1877
AstArray<AstExpr*> rest = {node->args.data + 1, node->args.size - 1};
1878
1879
matchStringCall(func->index, node->args.data[0], rest);
1880
}
1881
}
1882
else if (lib->name == "os")
1883
{
1884
if (func->index == "date" && node->args.size > 0)
1885
{
1886
if (AstExprConstantString* fmt = node->args.data[0]->as<AstExprConstantString>())
1887
if (const char* error = checkDateFormat(fmt->value.data, fmt->value.size))
1888
emitWarning(*context, LintWarning::Code_FormatString, fmt->location, "Invalid date format: %s", error);
1889
}
1890
}
1891
}
1892
1893
bool visit(AstExprCall* node) override
1894
{
1895
matchCall(node);
1896
return true;
1897
}
1898
};
1899
1900
class LintTableLiteral : AstVisitor
1901
{
1902
public:
1903
LUAU_NOINLINE static void process(LintContext& context)
1904
{
1905
LintTableLiteral pass;
1906
pass.context = &context;
1907
1908
context.root->visit(&pass);
1909
}
1910
1911
private:
1912
LintContext* context;
1913
1914
bool visit(AstExprTable* node) override
1915
{
1916
int count = 0;
1917
1918
for (const AstExprTable::Item& item : node->items)
1919
if (item.kind == AstExprTable::Item::List)
1920
count++;
1921
1922
DenseHashMap<AstArray<char>*, int, AstArrayPredicate, AstArrayPredicate> names(nullptr);
1923
DenseHashMap<int, int> indices(-1);
1924
1925
for (const AstExprTable::Item& item : node->items)
1926
{
1927
if (!item.key)
1928
continue;
1929
1930
if (AstExprConstantString* expr = item.key->as<AstExprConstantString>())
1931
{
1932
int& line = names[&expr->value];
1933
1934
if (line)
1935
emitWarning(
1936
*context,
1937
LintWarning::Code_TableLiteral,
1938
expr->location,
1939
"Table field '%.*s' is a duplicate; previously defined at line %d",
1940
int(expr->value.size),
1941
expr->value.data,
1942
line
1943
);
1944
else
1945
line = expr->location.begin.line + 1;
1946
}
1947
else if (AstExprConstantNumber* expr = item.key->as<AstExprConstantNumber>())
1948
{
1949
if (expr->value >= 1 && expr->value <= double(count) && double(int(expr->value)) == expr->value)
1950
emitWarning(
1951
*context,
1952
LintWarning::Code_TableLiteral,
1953
expr->location,
1954
"Table index %d is a duplicate; previously defined as a list entry",
1955
int(expr->value)
1956
);
1957
else if (expr->value >= 0 && expr->value <= double(INT_MAX) && double(int(expr->value)) == expr->value)
1958
{
1959
int& line = indices[int(expr->value)];
1960
1961
if (line)
1962
emitWarning(
1963
*context,
1964
LintWarning::Code_TableLiteral,
1965
expr->location,
1966
"Table index %d is a duplicate; previously defined at line %d",
1967
int(expr->value),
1968
line
1969
);
1970
else
1971
line = expr->location.begin.line + 1;
1972
}
1973
}
1974
}
1975
1976
return true;
1977
}
1978
1979
bool visit(AstType* node) override
1980
{
1981
return true;
1982
}
1983
1984
bool visit(AstTypePack* node) override
1985
{
1986
return true;
1987
}
1988
1989
bool visit(AstTypeTable* node) override
1990
{
1991
struct Rec
1992
{
1993
AstTableAccess access;
1994
Location location;
1995
};
1996
1997
if (context->module->checkedInNewSolver)
1998
{
1999
DenseHashMap<AstName, Rec> names(AstName{});
2000
2001
for (const AstTableProp& item : node->props)
2002
{
2003
Rec* rec = names.find(item.name);
2004
if (!rec)
2005
{
2006
names[item.name] = Rec{item.access, item.location};
2007
continue;
2008
}
2009
2010
if (int(rec->access) & int(item.access))
2011
{
2012
if (rec->access == item.access)
2013
emitWarning(
2014
*context,
2015
LintWarning::Code_TableLiteral,
2016
item.location,
2017
"Table type field '%s' is a duplicate; previously defined at line %d",
2018
item.name.value,
2019
rec->location.begin.line + 1
2020
);
2021
else if (rec->access == AstTableAccess::ReadWrite)
2022
emitWarning(
2023
*context,
2024
LintWarning::Code_TableLiteral,
2025
item.location,
2026
"Table type field '%s' is already read-write; previously defined at line %d",
2027
item.name.value,
2028
rec->location.begin.line + 1
2029
);
2030
else if (rec->access == AstTableAccess::Read)
2031
emitWarning(
2032
*context,
2033
LintWarning::Code_TableLiteral,
2034
rec->location,
2035
"Table type field '%s' already has a read type defined at line %d",
2036
item.name.value,
2037
rec->location.begin.line + 1
2038
);
2039
else if (rec->access == AstTableAccess::Write)
2040
emitWarning(
2041
*context,
2042
LintWarning::Code_TableLiteral,
2043
rec->location,
2044
"Table type field '%s' already has a write type defined at line %d",
2045
item.name.value,
2046
rec->location.begin.line + 1
2047
);
2048
else
2049
LUAU_ASSERT(!"Unreachable");
2050
}
2051
else
2052
rec->access = AstTableAccess(int(rec->access) | int(item.access));
2053
}
2054
2055
return true;
2056
}
2057
2058
DenseHashMap<AstName, int> names(AstName{});
2059
2060
for (const AstTableProp& item : node->props)
2061
{
2062
int& line = names[item.name];
2063
2064
if (line)
2065
emitWarning(
2066
*context,
2067
LintWarning::Code_TableLiteral,
2068
item.location,
2069
"Table type field '%s' is a duplicate; previously defined at line %d",
2070
item.name.value,
2071
line
2072
);
2073
else
2074
line = item.location.begin.line + 1;
2075
}
2076
2077
return true;
2078
}
2079
2080
struct AstArrayPredicate
2081
{
2082
size_t operator()(const AstArray<char>* value) const
2083
{
2084
return hashRange(value->data, value->size);
2085
}
2086
2087
bool operator()(const AstArray<char>* lhs, const AstArray<char>* rhs) const
2088
{
2089
return (lhs && rhs) ? lhs->size == rhs->size && memcmp(lhs->data, rhs->data, lhs->size) == 0 : lhs == rhs;
2090
}
2091
};
2092
};
2093
2094
class LintUninitializedLocal : AstVisitor
2095
{
2096
public:
2097
LUAU_NOINLINE static void process(LintContext& context)
2098
{
2099
LintUninitializedLocal pass;
2100
pass.context = &context;
2101
2102
context.root->visit(&pass);
2103
2104
pass.report();
2105
}
2106
2107
private:
2108
struct Local
2109
{
2110
bool defined;
2111
bool initialized;
2112
bool assigned;
2113
AstExprLocal* firstUse;
2114
};
2115
2116
LintContext* context;
2117
DenseHashMap<AstLocal*, Local> locals;
2118
2119
LintUninitializedLocal()
2120
: locals(NULL)
2121
{
2122
}
2123
2124
void report()
2125
{
2126
for (auto& lp : locals)
2127
{
2128
AstLocal* local = lp.first;
2129
const Local& l = lp.second;
2130
2131
if (l.defined && !l.initialized && !l.assigned && l.firstUse)
2132
{
2133
emitWarning(
2134
*context,
2135
LintWarning::Code_UninitializedLocal,
2136
l.firstUse->location,
2137
"Variable '%s' defined at line %d is never initialized or assigned; initialize with 'nil' to silence",
2138
local->name.value,
2139
local->location.begin.line + 1
2140
);
2141
}
2142
}
2143
}
2144
2145
bool visit(AstStatLocal* node) override
2146
{
2147
AstExpr* last = node->values.size ? node->values.data[node->values.size - 1] : nullptr;
2148
bool vararg = last && (last->is<AstExprVarargs>() || last->is<AstExprCall>());
2149
2150
for (size_t i = 0; i < node->vars.size; ++i)
2151
{
2152
Local& l = locals[node->vars.data[i]];
2153
2154
l.defined = true;
2155
l.initialized = vararg || i < node->values.size;
2156
}
2157
2158
return true;
2159
}
2160
2161
bool visit(AstStatAssign* node) override
2162
{
2163
for (size_t i = 0; i < node->vars.size; ++i)
2164
visitAssign(node->vars.data[i]);
2165
2166
for (size_t i = 0; i < node->values.size; ++i)
2167
node->values.data[i]->visit(this);
2168
2169
return false;
2170
}
2171
2172
bool visit(AstStatFunction* node) override
2173
{
2174
visitAssign(node->name);
2175
node->func->visit(this);
2176
2177
return false;
2178
}
2179
2180
bool visit(AstExprLocal* node) override
2181
{
2182
Local& l = locals[node->local];
2183
2184
if (!l.firstUse)
2185
l.firstUse = node;
2186
2187
return false;
2188
}
2189
2190
void visitAssign(AstExpr* var)
2191
{
2192
if (AstExprLocal* lv = var->as<AstExprLocal>())
2193
{
2194
Local& l = locals[lv->local];
2195
2196
l.assigned = true;
2197
}
2198
else
2199
{
2200
var->visit(this);
2201
}
2202
}
2203
};
2204
2205
class LintDuplicateFunction : AstVisitor
2206
{
2207
public:
2208
LUAU_NOINLINE static void process(LintContext& context)
2209
{
2210
LintDuplicateFunction pass{&context};
2211
context.root->visit(&pass);
2212
}
2213
2214
private:
2215
LintContext* context;
2216
DenseHashMap<std::string, Location> defns;
2217
2218
LintDuplicateFunction(LintContext* context)
2219
: context(context)
2220
, defns("")
2221
{
2222
}
2223
2224
bool visit(AstStatBlock* block) override
2225
{
2226
defns.clear();
2227
2228
for (AstStat* stat : block->body)
2229
{
2230
if (AstStatFunction* func = stat->as<AstStatFunction>())
2231
trackFunction(func->name->location, buildName(func->name));
2232
else if (AstStatLocalFunction* func = stat->as<AstStatLocalFunction>())
2233
trackFunction(func->name->location, func->name->name.value);
2234
}
2235
2236
return true;
2237
}
2238
2239
void trackFunction(Location location, const std::string& name)
2240
{
2241
if (name.empty())
2242
return;
2243
2244
Location& defn = defns[name];
2245
2246
if (defn.end.line == 0 && defn.end.column == 0)
2247
defn = location;
2248
else
2249
report(name, location, defn);
2250
}
2251
2252
std::string buildName(AstExpr* expr)
2253
{
2254
if (AstExprLocal* local = expr->as<AstExprLocal>())
2255
return local->local->name.value;
2256
else if (AstExprGlobal* global = expr->as<AstExprGlobal>())
2257
return global->name.value;
2258
else if (AstExprIndexName* indexName = expr->as<AstExprIndexName>())
2259
{
2260
std::string lhs = buildName(indexName->expr);
2261
if (lhs.empty())
2262
return lhs;
2263
2264
lhs += '.';
2265
lhs += indexName->index.value;
2266
return lhs;
2267
}
2268
else
2269
return std::string();
2270
}
2271
2272
void report(const std::string& name, Location location, Location otherLocation)
2273
{
2274
emitWarning(
2275
*context,
2276
LintWarning::Code_DuplicateFunction,
2277
location,
2278
"Duplicate function definition: '%s' also defined on line %d",
2279
name.c_str(),
2280
otherLocation.begin.line + 1
2281
);
2282
}
2283
};
2284
2285
class LintDeprecatedApi : AstVisitor
2286
{
2287
public:
2288
LUAU_NOINLINE static void process(LintContext& context)
2289
{
2290
LintDeprecatedApi pass{&context};
2291
context.root->visit(&pass);
2292
}
2293
2294
private:
2295
LintContext* context;
2296
2297
LintDeprecatedApi(LintContext* context)
2298
: context(context)
2299
{
2300
}
2301
2302
bool visit(AstExprLocal* node) override
2303
{
2304
const FunctionType* fty = getFunctionType(node);
2305
bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty);
2306
2307
if (shouldReport)
2308
{
2309
if (fty->deprecatedInfo != nullptr)
2310
{
2311
report(node->location, node->local->name.value, *fty->deprecatedInfo);
2312
}
2313
else
2314
{
2315
report(node->location, node->local->name.value);
2316
}
2317
}
2318
2319
return true;
2320
}
2321
2322
bool visit(AstExprGlobal* node) override
2323
{
2324
const FunctionType* fty = getFunctionType(node);
2325
bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty);
2326
2327
if (shouldReport)
2328
{
2329
if (fty->deprecatedInfo != nullptr)
2330
{
2331
report(node->location, node->name.value, *fty->deprecatedInfo);
2332
}
2333
else
2334
{
2335
report(node->location, node->name.value);
2336
}
2337
}
2338
2339
return true;
2340
}
2341
2342
bool visit(AstStatLocalFunction* node) override
2343
{
2344
check(node->func);
2345
return false;
2346
}
2347
2348
bool visit(AstStatFunction* node) override
2349
{
2350
check(node->func);
2351
return false;
2352
}
2353
2354
bool visit(AstExprIndexName* node) override
2355
{
2356
if (std::optional<TypeId> ty = context->getType(node->expr))
2357
check(node, follow(*ty));
2358
else if (AstExprGlobal* global = node->expr->as<AstExprGlobal>())
2359
check(node->location, global->name, node->index);
2360
2361
return true;
2362
}
2363
2364
bool visit(AstExprCall* node) override
2365
{
2366
// getfenv/setfenv are deprecated, however they are still used in some test frameworks and don't have a great general replacement
2367
// for now we warn about the deprecation only when they are used with a numeric first argument; this produces fewer warnings and makes use
2368
// of getfenv/setfenv a little more localized
2369
if (!node->self && node->args.size >= 1)
2370
{
2371
if (AstExprGlobal* fenv = node->func->as<AstExprGlobal>(); fenv && (fenv->name == "getfenv" || fenv->name == "setfenv"))
2372
{
2373
AstExpr* level = node->args.data[0];
2374
std::optional<TypeId> ty = context->getType(level);
2375
2376
if ((ty && isNumber(*ty)) || level->is<AstExprConstantNumber>())
2377
{
2378
// some common uses of getfenv(n) can be replaced by debug.info if the goal is to get the caller's identity
2379
const char* suggestion = (fenv->name == "getfenv") ? "; consider using 'debug.info' instead" : "";
2380
2381
emitWarning(
2382
*context, LintWarning::Code_DeprecatedApi, node->location, "Function '%s' is deprecated%s", fenv->name.value, suggestion
2383
);
2384
}
2385
}
2386
}
2387
2388
return true;
2389
}
2390
2391
void check(AstExprIndexName* node, TypeId ty)
2392
{
2393
if (const ExternType* cty = get<ExternType>(ty))
2394
{
2395
if (const Property* prop = lookupExternTypeProp(cty, node->index.value))
2396
{
2397
if (prop->deprecated)
2398
{
2399
report(node->location, *prop, cty->name.c_str(), node->index.value);
2400
}
2401
else if (std::optional<TypeId> ty = prop->readTy)
2402
{
2403
const FunctionType* fty = get<FunctionType>(follow(ty));
2404
bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty);
2405
2406
if (shouldReport)
2407
{
2408
const char* className = nullptr;
2409
if (AstExprGlobal* global = node->expr->as<AstExprGlobal>())
2410
className = global->name.value;
2411
2412
const char* functionName = node->index.value;
2413
if (fty->deprecatedInfo != nullptr)
2414
{
2415
report(node->location, className, functionName, *fty->deprecatedInfo);
2416
}
2417
else
2418
{
2419
report(node->location, className, functionName);
2420
}
2421
}
2422
}
2423
}
2424
}
2425
else if (const TableType* tty = get<TableType>(ty))
2426
{
2427
auto prop = tty->props.find(node->index.value);
2428
2429
if (prop != tty->props.end())
2430
{
2431
if (prop->second.deprecated)
2432
{
2433
// strip synthetic typeof() for builtin tables
2434
if (tty->name && tty->name->compare(0, 7, "typeof(") == 0 && tty->name->back() == ')')
2435
report(node->location, prop->second, tty->name->substr(7, tty->name->length() - 8).c_str(), node->index.value);
2436
else
2437
report(node->location, prop->second, tty->name ? tty->name->c_str() : nullptr, node->index.value);
2438
}
2439
else
2440
{
2441
if (std::optional<TypeId> ty = prop->second.readTy)
2442
{
2443
const FunctionType* fty = get<FunctionType>(follow(ty));
2444
bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty);
2445
2446
if (shouldReport)
2447
{
2448
const char* className = nullptr;
2449
if (AstExprGlobal* global = node->expr->as<AstExprGlobal>())
2450
className = global->name.value;
2451
2452
const char* functionName = node->index.value;
2453
2454
if (fty->deprecatedInfo != nullptr)
2455
{
2456
report(node->location, className, functionName, *fty->deprecatedInfo);
2457
}
2458
else
2459
{
2460
report(node->location, className, functionName);
2461
}
2462
}
2463
}
2464
}
2465
}
2466
}
2467
}
2468
2469
void check(const Location& location, AstName global, AstName index)
2470
{
2471
if (const LintContext::Global* gv = context->builtinGlobals.find(global))
2472
{
2473
if (const TableType* tty = get<TableType>(gv->type))
2474
{
2475
auto prop = tty->props.find(index.value);
2476
2477
if (prop != tty->props.end() && prop->second.deprecated)
2478
report(location, prop->second, global.value, index.value);
2479
}
2480
}
2481
}
2482
2483
void check(AstExprFunction* func)
2484
{
2485
LUAU_ASSERT(func);
2486
2487
const FunctionType* fty = getFunctionType(func);
2488
bool isDeprecated = fty && fty->isDeprecatedFunction;
2489
// If a function is deprecated, we don't want to flag its recursive uses.
2490
// So we push it on a stack while its body is being analyzed.
2491
// When a deprecated function is used, we check the stack to ensure that we are not inside that function.
2492
if (isDeprecated)
2493
pushScope(fty);
2494
2495
func->visit(this);
2496
2497
if (isDeprecated)
2498
popScope(fty);
2499
}
2500
2501
void report(const Location& location, const Property& prop, const char* container, const char* field)
2502
{
2503
std::string suggestion = prop.deprecatedSuggestion.empty() ? "" : format(", use '%s' instead", prop.deprecatedSuggestion.c_str());
2504
2505
if (container)
2506
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s.%s' is deprecated%s", container, field, suggestion.c_str());
2507
else
2508
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s' is deprecated%s", field, suggestion.c_str());
2509
}
2510
2511
void report(const Location& location, const char* tableName, const char* functionName)
2512
{
2513
if (tableName)
2514
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s.%s' is deprecated", tableName, functionName);
2515
else
2516
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s' is deprecated", functionName);
2517
}
2518
2519
void report(const Location& location, const char* tableName, const char* functionName, const AstAttr::DeprecatedInfo& info)
2520
{
2521
std::string usePart = info.use ? format(", use '%s' instead", info.use->c_str()) : "";
2522
std::string reasonPart = info.reason ? format(". %s", info.reason->c_str()) : "";
2523
if (tableName)
2524
emitWarning(
2525
*context,
2526
LintWarning::Code_DeprecatedApi,
2527
location,
2528
"Member '%s.%s' is deprecated%s%s",
2529
tableName,
2530
functionName,
2531
usePart.c_str(),
2532
reasonPart.c_str()
2533
);
2534
else
2535
emitWarning(
2536
*context,
2537
LintWarning::Code_DeprecatedApi,
2538
location,
2539
"Member '%s' is deprecated%s%s",
2540
functionName,
2541
usePart.c_str(),
2542
reasonPart.c_str()
2543
);
2544
}
2545
2546
void report(const Location& location, const char* functionName)
2547
{
2548
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Function '%s' is deprecated", functionName);
2549
}
2550
2551
void report(const Location& location, const char* functionName, const AstAttr::DeprecatedInfo& info)
2552
{
2553
std::string usePart = info.use ? format(", use '%s' instead", info.use->c_str()) : "";
2554
std::string reasonPart = info.reason ? format(". %s", info.reason->c_str()) : "";
2555
emitWarning(
2556
*context, LintWarning::Code_DeprecatedApi, location, "Function '%s' is deprecated%s%s", functionName, usePart.c_str(), reasonPart.c_str()
2557
);
2558
}
2559
2560
std::vector<const FunctionType*> functionTypeScopeStack;
2561
2562
void pushScope(const FunctionType* fty)
2563
{
2564
LUAU_ASSERT(fty);
2565
2566
functionTypeScopeStack.push_back(fty);
2567
}
2568
2569
void popScope(const FunctionType* fty)
2570
{
2571
LUAU_ASSERT(fty);
2572
2573
LUAU_ASSERT(fty == functionTypeScopeStack.back());
2574
functionTypeScopeStack.pop_back();
2575
}
2576
2577
bool inScope(const FunctionType* fty) const
2578
{
2579
LUAU_ASSERT(fty);
2580
2581
return std::find(functionTypeScopeStack.begin(), functionTypeScopeStack.end(), fty) != functionTypeScopeStack.end();
2582
}
2583
2584
const FunctionType* getFunctionType(AstExpr* node)
2585
{
2586
std::optional<TypeId> ty = context->getType(node);
2587
if (!ty)
2588
return nullptr;
2589
2590
const FunctionType* fty = get<FunctionType>(follow(ty));
2591
2592
return fty;
2593
}
2594
};
2595
2596
class LintTableOperations : AstVisitor
2597
{
2598
public:
2599
LUAU_NOINLINE static void process(LintContext& context)
2600
{
2601
if (!context.module)
2602
return;
2603
2604
LintTableOperations pass{&context};
2605
context.root->visit(&pass);
2606
}
2607
2608
private:
2609
LintContext* context;
2610
2611
LintTableOperations(LintContext* context)
2612
: context(context)
2613
{
2614
}
2615
2616
bool visit(AstExprUnary* node) override
2617
{
2618
if (node->op == AstExprUnary::Len)
2619
checkIndexer(node, node->expr, "#");
2620
2621
return true;
2622
}
2623
2624
bool visit(AstExprCall* node) override
2625
{
2626
if (AstExprGlobal* func = node->func->as<AstExprGlobal>())
2627
{
2628
if (func->name == "ipairs" && node->args.size == 1)
2629
checkIndexer(node, node->args.data[0], "ipairs");
2630
}
2631
else if (AstExprIndexName* func = node->func->as<AstExprIndexName>())
2632
{
2633
if (AstExprGlobal* tablib = func->expr->as<AstExprGlobal>(); tablib && tablib->name == "table")
2634
checkTableCall(node, func);
2635
}
2636
2637
return true;
2638
}
2639
2640
void checkIndexer(AstExpr* node, AstExpr* expr, const char* op)
2641
{
2642
std::optional<Luau::TypeId> ty = context->getType(expr);
2643
if (!ty)
2644
return;
2645
2646
const TableType* tty = get<TableType>(follow(*ty));
2647
if (!tty)
2648
return;
2649
2650
if (!tty->indexer && !tty->props.empty() && tty->state != TableState::Generic)
2651
emitWarning(
2652
*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table without an array part is likely a bug", op
2653
);
2654
else if (tty->indexer && isString(tty->indexer->indexType)) // note: to avoid complexity of subtype tests we just check if the key is a string
2655
emitWarning(*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table with string keys is likely a bug", op);
2656
}
2657
2658
void checkTableCall(AstExprCall* node, AstExprIndexName* func)
2659
{
2660
AstExpr** args = node->args.data;
2661
2662
if (func->index == "insert" && node->args.size == 2)
2663
{
2664
if (AstExprCall* tail = args[1]->as<AstExprCall>())
2665
{
2666
if (std::optional<TypeId> funty = context->getType(tail->func))
2667
{
2668
size_t ret = getReturnCount(follow(*funty));
2669
2670
if (ret > 1)
2671
emitWarning(
2672
*context,
2673
LintWarning::Code_TableOperations,
2674
tail->location,
2675
"table.insert may change behavior if the call returns more than one result; consider adding parentheses around second "
2676
"argument"
2677
);
2678
}
2679
}
2680
}
2681
2682
if (func->index == "insert" && node->args.size >= 3)
2683
{
2684
// table.insert(t, 0, ?)
2685
if (isConstant(args[1], 0.0))
2686
emitWarning(
2687
*context,
2688
LintWarning::Code_TableOperations,
2689
args[1]->location,
2690
"table.insert uses index 0 but arrays are 1-based; did you mean 1 instead?"
2691
);
2692
2693
// table.insert(t, #t, ?)
2694
if (isLength(args[1], args[0]))
2695
emitWarning(
2696
*context,
2697
LintWarning::Code_TableOperations,
2698
args[1]->location,
2699
"table.insert will insert the value before the last element, which is likely a bug; consider removing the second argument or "
2700
"wrap it in parentheses to silence"
2701
);
2702
2703
// table.insert(t, #t+1, ?)
2704
if (AstExprBinary* add = args[1]->as<AstExprBinary>();
2705
add && add->op == AstExprBinary::Add && isLength(add->left, args[0]) && isConstant(add->right, 1.0))
2706
emitWarning(
2707
*context,
2708
LintWarning::Code_TableOperations,
2709
args[1]->location,
2710
"table.insert will append the value to the table; consider removing the second argument for efficiency"
2711
);
2712
}
2713
2714
if (func->index == "remove" && node->args.size >= 2)
2715
{
2716
// table.remove(t, 0)
2717
if (isConstant(args[1], 0.0))
2718
emitWarning(
2719
*context,
2720
LintWarning::Code_TableOperations,
2721
args[1]->location,
2722
"table.remove uses index 0 but arrays are 1-based; did you mean 1 instead?"
2723
);
2724
2725
// note: it's tempting to check for table.remove(t, #t), which is equivalent to table.remove(t), but it's correct, occurs frequently,
2726
// and also reads better.
2727
2728
// table.remove(t, #t-1)
2729
if (AstExprBinary* sub = args[1]->as<AstExprBinary>();
2730
sub && sub->op == AstExprBinary::Sub && isLength(sub->left, args[0]) && isConstant(sub->right, 1.0))
2731
emitWarning(
2732
*context,
2733
LintWarning::Code_TableOperations,
2734
args[1]->location,
2735
"table.remove will remove the value before the last element, which is likely a bug; consider removing the second argument or "
2736
"wrap it in parentheses to silence"
2737
);
2738
}
2739
2740
if (func->index == "move" && node->args.size >= 4)
2741
{
2742
// table.move(t, 0, _, _)
2743
if (isConstant(args[1], 0.0))
2744
emitWarning(
2745
*context,
2746
LintWarning::Code_TableOperations,
2747
args[1]->location,
2748
"table.move uses index 0 but arrays are 1-based; did you mean 1 instead?"
2749
);
2750
2751
// table.move(t, _, _, 0)
2752
else if (isConstant(args[3], 0.0))
2753
emitWarning(
2754
*context,
2755
LintWarning::Code_TableOperations,
2756
args[3]->location,
2757
"table.move uses index 0 but arrays are 1-based; did you mean 1 instead?"
2758
);
2759
}
2760
2761
if (func->index == "create" && node->args.size == 2)
2762
{
2763
// table.create(n, {...})
2764
if (args[1]->is<AstExprTable>())
2765
emitWarning(
2766
*context,
2767
LintWarning::Code_TableOperations,
2768
args[1]->location,
2769
"table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"
2770
);
2771
2772
// table.create(n, {...} :: ?)
2773
if (AstExprTypeAssertion* as = args[1]->as<AstExprTypeAssertion>(); as && as->expr->is<AstExprTable>())
2774
emitWarning(
2775
*context,
2776
LintWarning::Code_TableOperations,
2777
as->expr->location,
2778
"table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"
2779
);
2780
}
2781
}
2782
2783
bool isConstant(AstExpr* expr, double value)
2784
{
2785
AstExprConstantNumber* n = expr->as<AstExprConstantNumber>();
2786
return n && n->value == value;
2787
}
2788
2789
bool isLength(AstExpr* expr, AstExpr* table)
2790
{
2791
AstExprUnary* n = expr->as<AstExprUnary>();
2792
return n && n->op == AstExprUnary::Len && similar(n->expr, table);
2793
}
2794
2795
size_t getReturnCount(TypeId ty)
2796
{
2797
if (auto ftv = get<FunctionType>(ty))
2798
return size(ftv->retTypes);
2799
2800
if (auto itv = get<IntersectionType>(ty))
2801
{
2802
// We don't process the type recursively to avoid having to deal with self-recursive intersection types
2803
size_t result = 0;
2804
2805
for (TypeId part : itv->parts)
2806
if (auto ftv = get<FunctionType>(follow(part)))
2807
result = std::max(result, size(ftv->retTypes));
2808
2809
return result;
2810
}
2811
2812
return 0;
2813
}
2814
};
2815
2816
class LintDuplicateCondition : AstVisitor
2817
{
2818
public:
2819
LUAU_NOINLINE static void process(LintContext& context)
2820
{
2821
LintDuplicateCondition pass{&context};
2822
context.root->visit(&pass);
2823
}
2824
2825
private:
2826
LintContext* context;
2827
2828
LintDuplicateCondition(LintContext* context)
2829
: context(context)
2830
{
2831
}
2832
2833
bool visit(AstStatIf* stat) override
2834
{
2835
if (!stat->elsebody)
2836
return true;
2837
2838
if (!stat->elsebody->is<AstStatIf>())
2839
return true;
2840
2841
// if..elseif chain detected, we need to unroll it
2842
std::vector<AstExpr*> conditions;
2843
conditions.reserve(2);
2844
2845
AstStatIf* head = stat;
2846
while (head)
2847
{
2848
head->condition->visit(this);
2849
head->thenbody->visit(this);
2850
2851
conditions.push_back(head->condition);
2852
2853
if (head->elsebody && head->elsebody->is<AstStatIf>())
2854
{
2855
head = head->elsebody->as<AstStatIf>();
2856
continue;
2857
}
2858
2859
if (head->elsebody)
2860
head->elsebody->visit(this);
2861
2862
break;
2863
}
2864
2865
detectDuplicates(conditions);
2866
2867
// block recursive visits so that we only analyze each chain once
2868
return false;
2869
}
2870
2871
bool visit(AstExprIfElse* expr) override
2872
{
2873
if (!expr->falseExpr->is<AstExprIfElse>())
2874
return true;
2875
2876
// if..elseif chain detected, we need to unroll it
2877
std::vector<AstExpr*> conditions;
2878
conditions.reserve(2);
2879
2880
AstExprIfElse* head = expr;
2881
while (head)
2882
{
2883
head->condition->visit(this);
2884
head->trueExpr->visit(this);
2885
2886
conditions.push_back(head->condition);
2887
2888
if (head->falseExpr->is<AstExprIfElse>())
2889
{
2890
head = head->falseExpr->as<AstExprIfElse>();
2891
continue;
2892
}
2893
2894
head->falseExpr->visit(this);
2895
break;
2896
}
2897
2898
detectDuplicates(conditions);
2899
2900
// block recursive visits so that we only analyze each chain once
2901
return false;
2902
}
2903
2904
bool visit(AstExprBinary* expr) override
2905
{
2906
if (expr->op != AstExprBinary::And && expr->op != AstExprBinary::Or)
2907
return true;
2908
2909
// for And expressions, it's idiomatic to use "a and a or b" as a ternary replacement, so we detect this pattern
2910
if (expr->op == AstExprBinary::Or)
2911
{
2912
AstExprBinary* la = expr->left->as<AstExprBinary>();
2913
2914
if (la && la->op == AstExprBinary::And)
2915
{
2916
AstExprBinary* lb = la->left->as<AstExprBinary>();
2917
AstExprBinary* rb = la->right->as<AstExprBinary>();
2918
2919
// check that the length of and-chain is exactly 2
2920
if (!(lb && lb->op == AstExprBinary::And) && !(rb && rb->op == AstExprBinary::And))
2921
{
2922
la->left->visit(this);
2923
la->right->visit(this);
2924
expr->right->visit(this);
2925
return false;
2926
}
2927
}
2928
}
2929
2930
// unroll condition chain
2931
std::vector<AstExpr*> conditions;
2932
conditions.reserve(2);
2933
2934
extractOpChain(conditions, expr, expr->op);
2935
2936
detectDuplicates(conditions);
2937
2938
// block recursive visits so that we only analyze each chain once
2939
return false;
2940
}
2941
2942
void extractOpChain(std::vector<AstExpr*>& conditions, AstExpr* expr, AstExprBinary::Op op)
2943
{
2944
if (AstExprBinary* bin = expr->as<AstExprBinary>(); bin && bin->op == op)
2945
{
2946
extractOpChain(conditions, bin->left, op);
2947
extractOpChain(conditions, bin->right, op);
2948
}
2949
else if (AstExprGroup* group = expr->as<AstExprGroup>())
2950
{
2951
extractOpChain(conditions, group->expr, op);
2952
}
2953
else
2954
{
2955
conditions.push_back(expr);
2956
}
2957
}
2958
2959
void detectDuplicates(const std::vector<AstExpr*>& conditions)
2960
{
2961
// Limit the distance at which we consider duplicates to reduce N^2 complexity to KN
2962
const size_t kMaxDistance = 5;
2963
2964
for (size_t i = 0; i < conditions.size(); ++i)
2965
{
2966
for (size_t j = std::max(i, kMaxDistance) - kMaxDistance; j < i; ++j)
2967
{
2968
if (similar(conditions[j], conditions[i]))
2969
{
2970
if (conditions[i]->location.begin.line == conditions[j]->location.begin.line)
2971
emitWarning(
2972
*context,
2973
LintWarning::Code_DuplicateCondition,
2974
conditions[i]->location,
2975
"Condition has already been checked on column %d",
2976
conditions[j]->location.begin.column + 1
2977
);
2978
else
2979
emitWarning(
2980
*context,
2981
LintWarning::Code_DuplicateCondition,
2982
conditions[i]->location,
2983
"Condition has already been checked on line %d",
2984
conditions[j]->location.begin.line + 1
2985
);
2986
break;
2987
}
2988
}
2989
}
2990
}
2991
};
2992
2993
class LintDuplicateLocal : AstVisitor
2994
{
2995
public:
2996
LUAU_NOINLINE static void process(LintContext& context)
2997
{
2998
LintDuplicateLocal pass;
2999
pass.context = &context;
3000
3001
context.root->visit(&pass);
3002
}
3003
3004
private:
3005
LintContext* context;
3006
3007
DenseHashMap<AstLocal*, AstNode*> locals;
3008
3009
LintDuplicateLocal()
3010
: locals(nullptr)
3011
{
3012
}
3013
3014
bool visit(AstStatLocal* node) override
3015
{
3016
// early out for performance
3017
if (node->vars.size == 1)
3018
return true;
3019
3020
for (size_t i = 0; i < node->vars.size; ++i)
3021
locals[node->vars.data[i]] = node;
3022
3023
for (size_t i = 0; i < node->vars.size; ++i)
3024
{
3025
AstLocal* local = node->vars.data[i];
3026
3027
if (local->shadow && locals[local->shadow] == node && !ignoreDuplicate(local))
3028
{
3029
if (local->shadow->location.begin.line == local->location.begin.line)
3030
emitWarning(
3031
*context,
3032
LintWarning::Code_DuplicateLocal,
3033
local->location,
3034
"Variable '%s' already defined on column %d",
3035
local->name.value,
3036
local->shadow->location.begin.column + 1
3037
);
3038
else
3039
emitWarning(
3040
*context,
3041
LintWarning::Code_DuplicateLocal,
3042
local->location,
3043
"Variable '%s' already defined on line %d",
3044
local->name.value,
3045
local->shadow->location.begin.line + 1
3046
);
3047
}
3048
}
3049
3050
return true;
3051
}
3052
3053
bool visit(AstExprFunction* node) override
3054
{
3055
if (node->self)
3056
locals[node->self] = node;
3057
3058
for (size_t i = 0; i < node->args.size; ++i)
3059
locals[node->args.data[i]] = node;
3060
3061
for (size_t i = 0; i < node->args.size; ++i)
3062
{
3063
AstLocal* local = node->args.data[i];
3064
3065
if (local->shadow && locals[local->shadow] == node && !ignoreDuplicate(local))
3066
{
3067
if (local->shadow == node->self)
3068
emitWarning(*context, LintWarning::Code_DuplicateLocal, local->location, "Function parameter 'self' already defined implicitly");
3069
else if (local->shadow->location.begin.line == local->location.begin.line)
3070
emitWarning(
3071
*context,
3072
LintWarning::Code_DuplicateLocal,
3073
local->location,
3074
"Function parameter '%s' already defined on column %d",
3075
local->name.value,
3076
local->shadow->location.begin.column + 1
3077
);
3078
else
3079
emitWarning(
3080
*context,
3081
LintWarning::Code_DuplicateLocal,
3082
local->location,
3083
"Function parameter '%s' already defined on line %d",
3084
local->name.value,
3085
local->shadow->location.begin.line + 1
3086
);
3087
}
3088
}
3089
3090
return true;
3091
}
3092
3093
bool ignoreDuplicate(AstLocal* local)
3094
{
3095
return local->name == "_";
3096
}
3097
};
3098
3099
class LintMisleadingAndOr : AstVisitor
3100
{
3101
public:
3102
LUAU_NOINLINE static void process(LintContext& context)
3103
{
3104
LintMisleadingAndOr pass;
3105
pass.context = &context;
3106
3107
context.root->visit(&pass);
3108
}
3109
3110
private:
3111
LintContext* context;
3112
3113
bool visit(AstExprBinary* node) override
3114
{
3115
if (node->op != AstExprBinary::Or)
3116
return true;
3117
3118
AstExprBinary* and_ = node->left->as<AstExprBinary>();
3119
if (!and_ || and_->op != AstExprBinary::And)
3120
return true;
3121
3122
const char* alt = nullptr;
3123
3124
if (and_->right->is<AstExprConstantNil>())
3125
alt = "nil";
3126
else if (AstExprConstantBool* c = and_->right->as<AstExprConstantBool>(); c && c->value == false)
3127
alt = "false";
3128
3129
if (alt)
3130
emitWarning(
3131
*context,
3132
LintWarning::Code_MisleadingAndOr,
3133
node->location,
3134
"The and-or expression always evaluates to the second alternative because the first alternative is %s; consider using if-then-else "
3135
"expression instead",
3136
alt
3137
);
3138
3139
return true;
3140
}
3141
};
3142
3143
class LintIntegerParsing : AstVisitor
3144
{
3145
public:
3146
LUAU_NOINLINE static void process(LintContext& context)
3147
{
3148
LintIntegerParsing pass;
3149
pass.context = &context;
3150
3151
context.root->visit(&pass);
3152
}
3153
3154
private:
3155
LintContext* context;
3156
3157
bool visit(AstExprConstantNumber* node) override
3158
{
3159
switch (node->parseResult)
3160
{
3161
case ConstantNumberParseResult::Ok:
3162
case ConstantNumberParseResult::Malformed:
3163
break;
3164
case ConstantNumberParseResult::Imprecise:
3165
emitWarning(
3166
*context,
3167
LintWarning::Code_IntegerParsing,
3168
node->location,
3169
"Number literal exceeded available precision and was truncated to closest representable number"
3170
);
3171
break;
3172
case ConstantNumberParseResult::BinOverflow:
3173
emitWarning(
3174
*context,
3175
LintWarning::Code_IntegerParsing,
3176
node->location,
3177
"Binary number literal exceeded available precision and was truncated to 2^64"
3178
);
3179
break;
3180
case ConstantNumberParseResult::HexOverflow:
3181
emitWarning(
3182
*context,
3183
LintWarning::Code_IntegerParsing,
3184
node->location,
3185
"Hexadecimal number literal exceeded available precision and was truncated to 2^64"
3186
);
3187
break;
3188
case ConstantNumberParseResult::IntOverflow:
3189
emitWarning(*context, LintWarning::Code_IntegerParsing, node->location, "Integer number literal was clamped because it was out of range");
3190
break;
3191
}
3192
3193
return true;
3194
}
3195
};
3196
3197
class LintComparisonPrecedence : AstVisitor
3198
{
3199
public:
3200
LUAU_NOINLINE static void process(LintContext& context)
3201
{
3202
LintComparisonPrecedence pass;
3203
pass.context = &context;
3204
3205
context.root->visit(&pass);
3206
}
3207
3208
private:
3209
LintContext* context;
3210
3211
static bool isEquality(AstExprBinary::Op op)
3212
{
3213
return op == AstExprBinary::CompareNe || op == AstExprBinary::CompareEq;
3214
}
3215
3216
static bool isComparison(AstExprBinary::Op op)
3217
{
3218
return op == AstExprBinary::CompareNe || op == AstExprBinary::CompareEq || op == AstExprBinary::CompareLt || op == AstExprBinary::CompareLe ||
3219
op == AstExprBinary::CompareGt || op == AstExprBinary::CompareGe;
3220
}
3221
3222
static bool isNot(AstExpr* node)
3223
{
3224
AstExprUnary* expr = node->as<AstExprUnary>();
3225
3226
return expr && expr->op == AstExprUnary::Not;
3227
}
3228
3229
bool visit(AstExprBinary* node) override
3230
{
3231
if (!isComparison(node->op))
3232
return true;
3233
3234
// not X == Y; we silence this for not X == not Y as it's likely an intentional boolean comparison
3235
if (isNot(node->left) && !isNot(node->right))
3236
{
3237
std::string op = toString(node->op);
3238
3239
if (isEquality(node->op))
3240
emitWarning(
3241
*context,
3242
LintWarning::Code_ComparisonPrecedence,
3243
node->location,
3244
"not X %s Y is equivalent to (not X) %s Y; consider using X %s Y, or add parentheses to silence",
3245
op.c_str(),
3246
op.c_str(),
3247
node->op == AstExprBinary::CompareEq ? "~=" : "=="
3248
);
3249
else
3250
emitWarning(
3251
*context,
3252
LintWarning::Code_ComparisonPrecedence,
3253
node->location,
3254
"not X %s Y is equivalent to (not X) %s Y; add parentheses to silence",
3255
op.c_str(),
3256
op.c_str()
3257
);
3258
}
3259
else if (AstExprBinary* left = node->left->as<AstExprBinary>(); left && isComparison(left->op))
3260
{
3261
std::string lop = toString(left->op);
3262
std::string rop = toString(node->op);
3263
3264
if (isEquality(left->op) || isEquality(node->op))
3265
emitWarning(
3266
*context,
3267
LintWarning::Code_ComparisonPrecedence,
3268
node->location,
3269
"X %s Y %s Z is equivalent to (X %s Y) %s Z; add parentheses to silence",
3270
lop.c_str(),
3271
rop.c_str(),
3272
lop.c_str(),
3273
rop.c_str()
3274
);
3275
else
3276
emitWarning(
3277
*context,
3278
LintWarning::Code_ComparisonPrecedence,
3279
node->location,
3280
"X %s Y %s Z is equivalent to (X %s Y) %s Z; did you mean X %s Y and Y %s Z?",
3281
lop.c_str(),
3282
rop.c_str(),
3283
lop.c_str(),
3284
rop.c_str(),
3285
lop.c_str(),
3286
rop.c_str()
3287
);
3288
}
3289
3290
return true;
3291
}
3292
};
3293
3294
static void fillBuiltinGlobals(LintContext& context, const AstNameTable& names, const ScopePtr& env)
3295
{
3296
ScopePtr current = env;
3297
while (true)
3298
{
3299
for (auto& [global, binding] : current->bindings)
3300
{
3301
AstName name = names.get(global.c_str());
3302
3303
if (name.value)
3304
{
3305
auto& g = context.builtinGlobals[name];
3306
g.type = binding.typeId;
3307
if (binding.deprecated)
3308
g.deprecated = binding.deprecatedSuggestion.c_str();
3309
}
3310
}
3311
3312
if (current->parent)
3313
current = current->parent;
3314
else
3315
break;
3316
}
3317
}
3318
3319
static const char* fuzzyMatch(std::string_view str, const char* const* array, size_t size)
3320
{
3321
if (FInt::LuauSuggestionDistance == 0)
3322
return nullptr;
3323
3324
size_t bestDistance = FInt::LuauSuggestionDistance;
3325
size_t bestMatch = size;
3326
3327
for (size_t i = 0; i < size; ++i)
3328
{
3329
size_t ed = editDistance(str, array[i]);
3330
3331
if (ed <= bestDistance)
3332
{
3333
bestDistance = ed;
3334
bestMatch = i;
3335
}
3336
}
3337
3338
return bestMatch < size ? array[bestMatch] : nullptr;
3339
}
3340
3341
static void lintComments(LintContext& context, const std::vector<HotComment>& hotcomments)
3342
{
3343
bool seenMode = false;
3344
3345
for (const HotComment& hc : hotcomments)
3346
{
3347
// We reserve --!<space> for various informational (non-directive) comments
3348
if (hc.content.empty() || hc.content[0] == ' ' || hc.content[0] == '\t')
3349
continue;
3350
3351
if (!hc.header)
3352
{
3353
emitWarning(
3354
context,
3355
LintWarning::Code_CommentDirective,
3356
hc.location,
3357
"Comment directive is ignored because it is placed after the first non-comment token"
3358
);
3359
}
3360
else
3361
{
3362
size_t space = hc.content.find_first_of(" \t");
3363
std::string_view first = std::string_view(hc.content).substr(0, space);
3364
3365
if (first == "nolint")
3366
{
3367
size_t notspace = hc.content.find_first_not_of(" \t", space);
3368
3369
if (space == std::string::npos || notspace == std::string::npos)
3370
{
3371
// disables all lints
3372
}
3373
else if (LintWarning::parseName(hc.content.c_str() + notspace) == LintWarning::Code_Unknown)
3374
{
3375
const char* rule = hc.content.c_str() + notspace;
3376
3377
// skip Unknown
3378
if (const char* suggestion = fuzzyMatch(rule, kWarningNames + 1, LintWarning::Code__Count - 1))
3379
emitWarning(
3380
context,
3381
LintWarning::Code_CommentDirective,
3382
hc.location,
3383
"nolint directive refers to unknown lint rule '%s'; did you mean '%s'?",
3384
rule,
3385
suggestion
3386
);
3387
else
3388
emitWarning(
3389
context, LintWarning::Code_CommentDirective, hc.location, "nolint directive refers to unknown lint rule '%s'", rule
3390
);
3391
}
3392
}
3393
else if (first == "nocheck" || first == "nonstrict" || first == "strict")
3394
{
3395
if (space != std::string::npos)
3396
emitWarning(
3397
context,
3398
LintWarning::Code_CommentDirective,
3399
hc.location,
3400
"Comment directive with the type checking mode has extra symbols at the end of the line"
3401
);
3402
else if (seenMode)
3403
emitWarning(
3404
context,
3405
LintWarning::Code_CommentDirective,
3406
hc.location,
3407
"Comment directive with the type checking mode has already been used"
3408
);
3409
else
3410
seenMode = true;
3411
}
3412
else if (first == "optimize")
3413
{
3414
size_t notspace = hc.content.find_first_not_of(" \t", space);
3415
3416
if (space == std::string::npos || notspace == std::string::npos)
3417
emitWarning(context, LintWarning::Code_CommentDirective, hc.location, "optimize directive requires an optimization level");
3418
else
3419
{
3420
const char* level = hc.content.c_str() + notspace;
3421
3422
if (strcmp(level, "0") && strcmp(level, "1") && strcmp(level, "2"))
3423
emitWarning(
3424
context,
3425
LintWarning::Code_CommentDirective,
3426
hc.location,
3427
"optimize directive uses unknown optimization level '%s', 0..2 expected",
3428
level
3429
);
3430
}
3431
}
3432
else if (first == "native")
3433
{
3434
if (space != std::string::npos)
3435
emitWarning(
3436
context, LintWarning::Code_CommentDirective, hc.location, "native directive has extra symbols at the end of the line"
3437
);
3438
}
3439
else
3440
{
3441
static const char* kHotComments[] = {
3442
"nolint",
3443
"nocheck",
3444
"nonstrict",
3445
"strict",
3446
"optimize",
3447
"native",
3448
};
3449
3450
if (const char* suggestion = fuzzyMatch(first, kHotComments, std::size(kHotComments)))
3451
emitWarning(
3452
context,
3453
LintWarning::Code_CommentDirective,
3454
hc.location,
3455
"Unknown comment directive '%.*s'; did you mean '%s'?",
3456
int(first.size()),
3457
first.data(),
3458
suggestion
3459
);
3460
else
3461
emitWarning(
3462
context, LintWarning::Code_CommentDirective, hc.location, "Unknown comment directive '%.*s'", int(first.size()), first.data()
3463
);
3464
}
3465
}
3466
}
3467
}
3468
3469
static bool hasNativeCommentDirective(const std::vector<HotComment>& hotcomments)
3470
{
3471
for (const HotComment& hc : hotcomments)
3472
{
3473
if (hc.content.empty() || hc.content[0] == ' ' || hc.content[0] == '\t')
3474
continue;
3475
3476
if (hc.header)
3477
{
3478
size_t space = hc.content.find_first_of(" \t");
3479
std::string_view first = std::string_view(hc.content).substr(0, space);
3480
3481
if (first == "native")
3482
return true;
3483
}
3484
}
3485
3486
return false;
3487
}
3488
3489
struct LintRedundantNativeAttribute : AstVisitor
3490
{
3491
public:
3492
LUAU_NOINLINE static void process(LintContext& context)
3493
{
3494
LintRedundantNativeAttribute pass;
3495
pass.context = &context;
3496
context.root->visit(&pass);
3497
}
3498
3499
private:
3500
LintContext* context;
3501
3502
bool visit(AstExprFunction* node) override
3503
{
3504
node->body->visit(this);
3505
3506
for (const auto attribute : node->attributes)
3507
{
3508
if (attribute->type == AstAttr::Type::Native)
3509
{
3510
emitWarning(
3511
*context,
3512
LintWarning::Code_RedundantNativeAttribute,
3513
attribute->location,
3514
"native attribute on a function is redundant in a native module; consider removing it"
3515
);
3516
}
3517
}
3518
3519
return false;
3520
}
3521
};
3522
3523
std::vector<LintWarning> lint(
3524
AstStat* root,
3525
const AstNameTable& names,
3526
const ScopePtr& env,
3527
const Module* module,
3528
const std::vector<HotComment>& hotcomments,
3529
const LintOptions& options
3530
)
3531
{
3532
LintContext context;
3533
3534
context.options = options;
3535
context.root = root;
3536
context.placeholder = names.get("_");
3537
context.scope = env;
3538
context.module = module;
3539
3540
fillBuiltinGlobals(context, names, env);
3541
3542
if (context.warningEnabled(LintWarning::Code_UnknownGlobal) || context.warningEnabled(LintWarning::Code_DeprecatedGlobal) ||
3543
context.warningEnabled(LintWarning::Code_GlobalUsedAsLocal) || context.warningEnabled(LintWarning::Code_PlaceholderRead) ||
3544
context.warningEnabled(LintWarning::Code_BuiltinGlobalWrite))
3545
{
3546
LintGlobalLocal::process(context);
3547
}
3548
3549
if (context.warningEnabled(LintWarning::Code_MultiLineStatement))
3550
LintMultiLineStatement::process(context);
3551
3552
if (context.warningEnabled(LintWarning::Code_SameLineStatement))
3553
LintSameLineStatement::process(context);
3554
3555
if (context.warningEnabled(LintWarning::Code_LocalShadow) || context.warningEnabled(LintWarning::Code_FunctionUnused) ||
3556
context.warningEnabled(LintWarning::Code_ImportUnused) || context.warningEnabled(LintWarning::Code_LocalUnused))
3557
{
3558
LintLocalHygiene::process(context);
3559
}
3560
3561
if (context.warningEnabled(LintWarning::Code_FunctionUnused))
3562
LintUnusedFunction::process(context);
3563
3564
if (context.warningEnabled(LintWarning::Code_UnreachableCode))
3565
LintUnreachableCode::process(context);
3566
3567
if (context.warningEnabled(LintWarning::Code_UnknownType))
3568
LintUnknownType::process(context);
3569
3570
if (context.warningEnabled(LintWarning::Code_ForRange))
3571
LintForRange::process(context);
3572
3573
if (context.warningEnabled(LintWarning::Code_UnbalancedAssignment))
3574
LintUnbalancedAssignment::process(context);
3575
3576
if (context.warningEnabled(LintWarning::Code_ImplicitReturn))
3577
LintImplicitReturn::process(context);
3578
3579
if (context.warningEnabled(LintWarning::Code_FormatString))
3580
LintFormatString::process(context);
3581
3582
if (context.warningEnabled(LintWarning::Code_TableLiteral))
3583
LintTableLiteral::process(context);
3584
3585
if (context.warningEnabled(LintWarning::Code_UninitializedLocal))
3586
LintUninitializedLocal::process(context);
3587
3588
if (context.warningEnabled(LintWarning::Code_DuplicateFunction))
3589
LintDuplicateFunction::process(context);
3590
3591
if (context.warningEnabled(LintWarning::Code_DeprecatedApi))
3592
LintDeprecatedApi::process(context);
3593
3594
if (context.warningEnabled(LintWarning::Code_TableOperations))
3595
LintTableOperations::process(context);
3596
3597
if (context.warningEnabled(LintWarning::Code_DuplicateCondition))
3598
LintDuplicateCondition::process(context);
3599
3600
if (context.warningEnabled(LintWarning::Code_DuplicateLocal))
3601
LintDuplicateLocal::process(context);
3602
3603
if (context.warningEnabled(LintWarning::Code_MisleadingAndOr))
3604
LintMisleadingAndOr::process(context);
3605
3606
if (context.warningEnabled(LintWarning::Code_CommentDirective))
3607
lintComments(context, hotcomments);
3608
3609
if (context.warningEnabled(LintWarning::Code_IntegerParsing))
3610
LintIntegerParsing::process(context);
3611
3612
if (context.warningEnabled(LintWarning::Code_ComparisonPrecedence))
3613
LintComparisonPrecedence::process(context);
3614
3615
if (context.warningEnabled(LintWarning::Code_RedundantNativeAttribute))
3616
{
3617
if (hasNativeCommentDirective(hotcomments))
3618
LintRedundantNativeAttribute::process(context);
3619
}
3620
3621
std::sort(context.result.begin(), context.result.end(), WarningComparator());
3622
3623
return context.result;
3624
}
3625
3626
std::vector<AstName> getDeprecatedGlobals(const AstNameTable& names)
3627
{
3628
LintContext context;
3629
3630
std::vector<AstName> result;
3631
result.reserve(context.builtinGlobals.size());
3632
3633
for (auto& p : context.builtinGlobals)
3634
if (p.second.deprecated)
3635
result.push_back(p.first);
3636
3637
return result;
3638
}
3639
3640
void fuzzFormatString(const char* data, size_t size)
3641
{
3642
LintFormatString::fuzz(data, size);
3643
}
3644
3645
} // namespace Luau
3646
3647