luau/Analysis/src/Linter.cpp

3103 lines
97 KiB
C++
Raw Normal View History

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Linter.h"
#include "Luau/AstQuery.h"
#include "Luau/Module.h"
#include "Luau/Scope.h"
#include "Luau/TypeInfer.h"
#include "Luau/StringUtils.h"
#include "Luau/Common.h"
#include <algorithm>
#include <math.h>
#include <limits.h>
2022-02-18 01:18:01 +00:00
LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4)
Sync to upstream/release/614 (#1173) # What's changed? Add program argument passing to scripts run using the Luau REPL! You can now pass `--program-args` (or shorthand `-a`) to the REPL which will treat all remaining arguments as arguments to pass to executed scripts. These values can be accessed through variadic argument expansion. You can read these values like so: ``` local args = {...} -- gets you an array of all the arguments ``` For example if we run the following script like `luau test.lua -a test1 test2 test3`: ``` -- test.lua print(...) ``` you should get the output: ``` test1 test2 test3 ``` ### Native Code Generation * Improve A64 lowering for vector operations by using vector instructions * Fix lowering issue in IR value location tracking! - A developer reported a divergence between code run in the VM and Native Code Generation which we have now fixed ### New Type Solver * Apply substitution to type families, and emit new constraints to reduce those further * More progress on reducing comparison (`lt/le`)type families * Resolve two major sources of cyclic types in the new solver ### Miscellaneous * Turned internal compiler errors (ICE's) into warnings and errors ------- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> --------- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: David Cope <dcope@roblox.com> Co-authored-by: Lily Brown <lbrown@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
2024-02-23 20:08:34 +00:00
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(LuauAttribute)
LUAU_FASTFLAG(LuauNativeAttribute)
LUAU_FASTFLAGVARIABLE(LintRedundantNativeAttribute, false)
namespace Luau
{
struct LintContext
{
struct Global
{
TypeId type = nullptr;
std::optional<const char*> deprecated;
};
std::vector<LintWarning> result;
LintOptions options;
AstStat* root;
AstName placeholder;
DenseHashMap<AstName, Global> builtinGlobals;
ScopePtr scope;
const Module* module;
LintContext()
: root(nullptr)
, builtinGlobals(AstName())
, module(nullptr)
{
}
bool warningEnabled(LintWarning::Code code)
{
return (options.warningMask & (1ull << code)) != 0;
}
std::optional<TypeId> getType(AstExpr* expr)
{
if (!module)
return std::nullopt;
auto it = module->astTypes.find(expr);
if (!it)
return std::nullopt;
return *it;
}
};
struct WarningComparator
{
int compare(const Position& lhs, const Position& rhs) const
{
if (lhs.line != rhs.line)
return lhs.line < rhs.line ? -1 : 1;
if (lhs.column != rhs.column)
return lhs.column < rhs.column ? -1 : 1;
return 0;
}
int compare(const Location& lhs, const Location& rhs) const
{
if (int c = compare(lhs.begin, rhs.begin))
return c;
if (int c = compare(lhs.end, rhs.end))
return c;
return 0;
}
bool operator()(const LintWarning& lhs, const LintWarning& rhs) const
{
if (int c = compare(lhs.location, rhs.location))
return c < 0;
return lhs.code < rhs.code;
}
};
LUAU_PRINTF_ATTR(4, 5)
static void emitWarning(LintContext& context, LintWarning::Code code, const Location& location, const char* format, ...)
{
if (!context.warningEnabled(code))
return;
va_list args;
va_start(args, format);
std::string message = vformat(format, args);
va_end(args);
LintWarning warning = {code, location, message};
context.result.push_back(warning);
}
static bool similar(AstExpr* lhs, AstExpr* rhs)
{
if (lhs->classIndex != rhs->classIndex)
return false;
#define CASE(T) else if (T* le = lhs->as<T>(), *re = rhs->as<T>(); le && re)
if (false)
return false;
CASE(AstExprGroup) return similar(le->expr, re->expr);
CASE(AstExprConstantNil) return true;
CASE(AstExprConstantBool) return le->value == re->value;
CASE(AstExprConstantNumber) return le->value == re->value;
CASE(AstExprConstantString) return le->value.size == re->value.size && memcmp(le->value.data, re->value.data, le->value.size) == 0;
CASE(AstExprLocal) return le->local == re->local;
CASE(AstExprGlobal) return le->name == re->name;
CASE(AstExprVarargs) return true;
CASE(AstExprIndexName) return le->index == re->index && similar(le->expr, re->expr);
CASE(AstExprIndexExpr) return similar(le->expr, re->expr) && similar(le->index, re->index);
CASE(AstExprFunction) return false; // rarely meaningful in context of this pass, avoids having to process statement nodes
CASE(AstExprUnary) return le->op == re->op && similar(le->expr, re->expr);
CASE(AstExprBinary) return le->op == re->op && similar(le->left, re->left) && similar(le->right, re->right);
CASE(AstExprTypeAssertion) return le->expr == re->expr; // the type doesn't affect execution semantics, avoids having to process type nodes
CASE(AstExprError) return false;
CASE(AstExprCall)
{
if (le->args.size != re->args.size || le->self != re->self)
return false;
if (!similar(le->func, re->func))
return false;
for (size_t i = 0; i < le->args.size; ++i)
if (!similar(le->args.data[i], re->args.data[i]))
return false;
return true;
}
CASE(AstExprTable)
{
if (le->items.size != re->items.size)
return false;
for (size_t i = 0; i < le->items.size; ++i)
{
const AstExprTable::Item& li = le->items.data[i];
const AstExprTable::Item& ri = re->items.data[i];
if (li.kind != ri.kind)
return false;
if (bool(li.key) != bool(ri.key))
return false;
else if (li.key && !similar(li.key, ri.key))
return false;
if (!similar(li.value, ri.value))
return false;
}
return true;
}
2022-02-24 23:53:37 +00:00
CASE(AstExprIfElse) return similar(le->condition, re->condition) && similar(le->trueExpr, re->trueExpr) && similar(le->falseExpr, re->falseExpr);
CASE(AstExprInterpString)
{
if (le->strings.size != re->strings.size)
return false;
if (le->expressions.size != re->expressions.size)
return false;
for (size_t i = 0; i < le->strings.size; ++i)
if (le->strings.data[i].size != re->strings.data[i].size ||
memcmp(le->strings.data[i].data, re->strings.data[i].data, le->strings.data[i].size) != 0)
return false;
for (size_t i = 0; i < le->expressions.size; ++i)
if (!similar(le->expressions.data[i], re->expressions.data[i]))
return false;
return true;
}
else
{
LUAU_ASSERT(!"Unknown expression type");
return false;
}
#undef CASE
}
class LintGlobalLocal : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintGlobalLocal pass;
pass.context = &context;
for (auto& global : context.builtinGlobals)
{
Global& g = pass.globals[global.first];
g.builtin = true;
g.deprecated = global.second.deprecated;
}
context.root->visit(&pass);
pass.report();
}
private:
2022-03-04 16:36:33 +00:00
struct FunctionInfo
{
explicit FunctionInfo(AstExprFunction* ast)
: ast(ast)
, dominatedGlobals({})
, conditionalExecution(false)
{
}
AstExprFunction* ast;
DenseHashSet<AstName> dominatedGlobals;
bool conditionalExecution;
};
struct Global
{
AstExprGlobal* firstRef = nullptr;
std::vector<AstExprFunction*> functionRef;
bool assigned = false;
bool builtin = false;
2022-03-04 16:36:33 +00:00
bool definedInModuleScope = false;
bool definedAsFunction = false;
bool readBeforeWritten = false;
std::optional<const char*> deprecated;
};
LintContext* context;
DenseHashMap<AstName, Global> globals;
std::vector<AstExprGlobal*> globalRefs;
2022-03-04 16:36:33 +00:00
std::vector<FunctionInfo> functionStack;
LintGlobalLocal()
: globals(AstName())
{
}
void report()
{
for (size_t i = 0; i < globalRefs.size(); ++i)
{
AstExprGlobal* gv = globalRefs[i];
Global* g = globals.find(gv->name);
if (!g || (!g->assigned && !g->builtin))
emitWarning(*context, LintWarning::Code_UnknownGlobal, gv->location, "Unknown global '%s'", gv->name.value);
else if (g->deprecated)
{
if (const char* replacement = *g->deprecated; replacement && strlen(replacement))
emitWarning(*context, LintWarning::Code_DeprecatedGlobal, gv->location, "Global '%s' is deprecated, use '%s' instead",
gv->name.value, replacement);
else
emitWarning(*context, LintWarning::Code_DeprecatedGlobal, gv->location, "Global '%s' is deprecated", gv->name.value);
}
}
for (auto& global : globals)
{
const Global& g = global.second;
if (g.functionRef.size() && g.assigned && g.firstRef->name != context->placeholder)
{
AstExprFunction* top = g.functionRef.back();
if (top->debugname.value)
emitWarning(*context, LintWarning::Code_GlobalUsedAsLocal, g.firstRef->location,
"Global '%s' is only used in the enclosing function '%s'; consider changing it to local", g.firstRef->name.value,
top->debugname.value);
else
emitWarning(*context, LintWarning::Code_GlobalUsedAsLocal, g.firstRef->location,
"Global '%s' is only used in the enclosing function defined at line %d; consider changing it to local",
g.firstRef->name.value, top->location.begin.line + 1);
}
else if (g.assigned && !g.readBeforeWritten && !g.definedInModuleScope && g.firstRef->name != context->placeholder)
2022-03-04 16:36:33 +00:00
{
emitWarning(*context, LintWarning::Code_GlobalUsedAsLocal, g.firstRef->location,
"Global '%s' is never read before being written. Consider changing it to local", g.firstRef->name.value);
}
}
}
bool visit(AstExprFunction* node) override
{
2022-03-04 16:36:33 +00:00
functionStack.emplace_back(node);
node->body->visit(this);
functionStack.pop_back();
return false;
}
bool visit(AstExprGlobal* node) override
{
if (!functionStack.empty() && !functionStack.back().dominatedGlobals.contains(node->name))
2022-03-04 16:36:33 +00:00
{
Global& g = globals[node->name];
g.readBeforeWritten = true;
}
trackGlobalRef(node);
if (node->name == context->placeholder)
emitWarning(
*context, LintWarning::Code_PlaceholderRead, node->location, "Placeholder value '_' is read here; consider using a named variable");
return true;
}
bool visit(AstExprLocal* node) override
{
if (node->local->name == context->placeholder)
emitWarning(
*context, LintWarning::Code_PlaceholderRead, node->location, "Placeholder value '_' is read here; consider using a named variable");
return true;
}
bool visit(AstStatAssign* node) override
{
for (size_t i = 0; i < node->vars.size; ++i)
{
AstExpr* var = node->vars.data[i];
if (AstExprGlobal* gv = var->as<AstExprGlobal>())
{
Global& g = globals[gv->name];
if (functionStack.empty())
2022-03-04 16:36:33 +00:00
{
g.definedInModuleScope = true;
}
else
{
if (!functionStack.back().conditionalExecution)
2022-03-04 16:36:33 +00:00
{
functionStack.back().dominatedGlobals.insert(gv->name);
2022-03-04 16:36:33 +00:00
}
}
if (g.builtin)
emitWarning(*context, LintWarning::Code_BuiltinGlobalWrite, gv->location,
"Built-in global '%s' is overwritten here; consider using a local or changing the name", gv->name.value);
else
g.assigned = true;
trackGlobalRef(gv);
}
else if (var->is<AstExprLocal>())
{
// We don't visit locals here because it's a local *write*, and visit(AstExprLocal*) assumes it's a local *read*
}
else
{
var->visit(this);
}
}
for (size_t i = 0; i < node->values.size; ++i)
node->values.data[i]->visit(this);
return false;
}
bool visit(AstStatFunction* node) override
{
if (AstExprGlobal* gv = node->name->as<AstExprGlobal>())
{
Global& g = globals[gv->name];
if (g.builtin)
emitWarning(*context, LintWarning::Code_BuiltinGlobalWrite, gv->location,
"Built-in global '%s' is overwritten here; consider using a local or changing the name", gv->name.value);
else
2022-03-04 16:36:33 +00:00
{
g.assigned = true;
g.definedAsFunction = true;
g.definedInModuleScope = functionStack.empty();
2022-03-04 16:36:33 +00:00
}
trackGlobalRef(gv);
}
return true;
}
2022-03-04 16:36:33 +00:00
class HoldConditionalExecution
{
public:
HoldConditionalExecution(LintGlobalLocal& p)
: p(p)
{
if (!p.functionStack.empty() && !p.functionStack.back().conditionalExecution)
{
resetToFalse = true;
p.functionStack.back().conditionalExecution = true;
}
}
~HoldConditionalExecution()
{
if (resetToFalse)
p.functionStack.back().conditionalExecution = false;
}
private:
bool resetToFalse = false;
LintGlobalLocal& p;
};
bool visit(AstStatIf* node) override
{
HoldConditionalExecution ce(*this);
node->condition->visit(this);
node->thenbody->visit(this);
if (node->elsebody)
node->elsebody->visit(this);
return false;
}
bool visit(AstStatWhile* node) override
{
HoldConditionalExecution ce(*this);
node->condition->visit(this);
node->body->visit(this);
return false;
}
bool visit(AstStatRepeat* node) override
{
HoldConditionalExecution ce(*this);
node->condition->visit(this);
node->body->visit(this);
return false;
}
bool visit(AstStatFor* node) override
{
HoldConditionalExecution ce(*this);
node->from->visit(this);
node->to->visit(this);
if (node->step)
node->step->visit(this);
node->body->visit(this);
return false;
}
bool visit(AstStatForIn* node) override
{
HoldConditionalExecution ce(*this);
for (AstExpr* expr : node->values)
expr->visit(this);
node->body->visit(this);
return false;
}
void trackGlobalRef(AstExprGlobal* node)
{
Global& g = globals[node->name];
globalRefs.push_back(node);
if (!g.firstRef)
{
g.firstRef = node;
// to reduce the cost of tracking we only track this for user globals
if (!g.builtin)
{
2022-03-04 16:36:33 +00:00
g.functionRef.clear();
g.functionRef.reserve(functionStack.size());
for (const FunctionInfo& entry : functionStack)
{
g.functionRef.push_back(entry.ast);
}
}
}
else
{
// to reduce the cost of tracking we only track this for user globals
if (!g.builtin)
{
// we need to find a common prefix between all uses of a global
size_t prefix = 0;
2022-03-04 16:36:33 +00:00
while (prefix < g.functionRef.size() && prefix < functionStack.size() && g.functionRef[prefix] == functionStack[prefix].ast)
prefix++;
g.functionRef.resize(prefix);
}
}
}
};
class LintSameLineStatement : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintSameLineStatement pass;
pass.context = &context;
pass.lastLine = ~0u;
context.root->visit(&pass);
}
private:
LintContext* context;
unsigned int lastLine;
bool visit(AstStatBlock* node) override
{
for (size_t i = 1; i < node->body.size; ++i)
{
const Location& last = node->body.data[i - 1]->location;
const Location& location = node->body.data[i]->location;
if (location.begin.line != last.end.line)
continue;
// We warn once per line with multiple statements
if (location.begin.line == lastLine)
continue;
// 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
if (node->body.data[i - 1]->is<AstStatLocal>() && node->body.data[i]->is<AstStatBlock>())
continue;
// Another common pattern is using multiple statements on the same line with semi-colons on each of them. White-list this pattern too.
if (node->body.data[i - 1]->hasSemicolon)
continue;
emitWarning(*context, LintWarning::Code_SameLineStatement, location,
"A new statement is on the same line; add semi-colon on previous statement to silence");
lastLine = location.begin.line;
}
return true;
}
};
class LintMultiLineStatement : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintMultiLineStatement pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
struct Statement
{
Location start;
unsigned int lastLine;
bool flagged;
};
std::vector<Statement> stack;
bool visit(AstExpr* node) override
{
Statement& top = stack.back();
if (!top.flagged)
{
Location location = node->location;
if (location.begin.line > top.lastLine)
{
top.lastLine = location.begin.line;
if (location.begin.column <= top.start.begin.column)
{
emitWarning(
*context, LintWarning::Code_MultiLineStatement, location, "Statement spans multiple lines; use indentation to silence");
top.flagged = true;
}
}
}
return true;
}
bool visit(AstExprTable* node) override
{
(void)node;
return false;
}
bool visit(AstStatRepeat* node) override
{
node->body->visit(this);
return false;
}
bool visit(AstStatBlock* node) override
{
for (size_t i = 0; i < node->body.size; ++i)
{
AstStat* stmt = node->body.data[i];
Statement s = {stmt->location, stmt->location.begin.line, false};
stack.push_back(s);
stmt->visit(this);
stack.pop_back();
}
return false;
}
};
class LintLocalHygiene : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintLocalHygiene pass;
pass.context = &context;
for (auto& global : context.builtinGlobals)
pass.globals[global.first].builtin = true;
context.root->visit(&pass);
pass.report();
}
private:
LintContext* context;
struct Local
{
AstNode* defined = nullptr;
bool function;
bool import;
bool used;
bool arg;
};
struct Global
{
bool used;
bool builtin;
AstExprGlobal* firstRef;
};
DenseHashMap<AstLocal*, Local> locals;
DenseHashMap<AstName, AstLocal*> imports;
DenseHashMap<AstName, Global> globals;
LintLocalHygiene()
: locals(NULL)
, imports(AstName())
, globals(AstName())
{
}
void report()
{
for (auto& l : locals)
{
if (l.second.used)
reportUsedLocal(l.first, l.second);
else if (l.second.defined)
reportUnusedLocal(l.first, l.second);
}
}
void reportUsedLocal(AstLocal* local, const Local& info)
{
if (AstLocal* shadow = local->shadow)
{
// LintDuplicateFunctions will catch this.
Local* shadowLocal = locals.find(shadow);
if (context->options.isEnabled(LintWarning::Code_DuplicateFunction) && info.function && shadowLocal && shadowLocal->function)
return;
// LintDuplicateLocal will catch this.
if (context->options.isEnabled(LintWarning::Code_DuplicateLocal) && shadowLocal && shadowLocal->defined == info.defined)
return;
// don't warn on inter-function shadowing since it is much more fragile wrt refactoring
if (shadow->functionDepth == local->functionDepth)
emitWarning(*context, LintWarning::Code_LocalShadow, local->location, "Variable '%s' shadows previous declaration at line %d",
local->name.value, shadow->location.begin.line + 1);
}
else if (Global* global = globals.find(local->name))
{
if (global->builtin)
; // there are many builtins with common names like 'table'; some of them are deprecated as well
else if (global->firstRef)
{
emitWarning(*context, LintWarning::Code_LocalShadow, local->location, "Variable '%s' shadows a global variable used at line %d",
local->name.value, global->firstRef->location.begin.line + 1);
}
else
{
emitWarning(*context, LintWarning::Code_LocalShadow, local->location, "Variable '%s' shadows a global variable", local->name.value);
}
}
}
void reportUnusedLocal(AstLocal* local, const Local& info)
{
if (local->name.value[0] == '_')
return;
if (info.function)
emitWarning(*context, LintWarning::Code_FunctionUnused, local->location, "Function '%s' is never used; prefix with '_' to silence",
local->name.value);
else if (info.import)
emitWarning(*context, LintWarning::Code_ImportUnused, local->location, "Import '%s' is never used; prefix with '_' to silence",
local->name.value);
else
emitWarning(*context, LintWarning::Code_LocalUnused, local->location, "Variable '%s' is never used; prefix with '_' to silence",
local->name.value);
}
bool isRequireCall(AstExpr* expr)
{
AstExprCall* call = expr->as<AstExprCall>();
if (!call)
return false;
AstExprGlobal* glob = call->func->as<AstExprGlobal>();
if (!glob)
return false;
return glob->name == "require";
}
bool visit(AstStatAssign* node) override
{
for (AstExpr* var : node->vars)
{
// We don't visit locals here because it's a local *write*, and visit(AstExprLocal*) assumes it's a local *read*
if (!var->is<AstExprLocal>())
var->visit(this);
}
for (AstExpr* value : node->values)
value->visit(this);
return false;
}
bool visit(AstStatLocal* node) override
{
if (node->vars.size == 1 && node->values.size == 1)
{
Local& l = locals[node->vars.data[0]];
l.defined = node;
l.import = isRequireCall(node->values.data[0]);
if (l.import)
imports[node->vars.data[0]->name] = node->vars.data[0];
}
else
{
for (size_t i = 0; i < node->vars.size; ++i)
{
Local& l = locals[node->vars.data[i]];
l.defined = node;
}
}
return true;
}
bool visit(AstStatLocalFunction* node) override
{
Local& l = locals[node->name];
l.defined = node;
l.function = true;
return true;
}
bool visit(AstExprLocal* node) override
{
Local& l = locals[node->local];
l.used = true;
return true;
}
bool visit(AstExprGlobal* node) override
{
Global& global = globals[node->name];
global.used = true;
if (!global.firstRef)
global.firstRef = node;
return true;
}
bool visit(AstType* node) override
{
return true;
}
bool visit(AstTypeReference* node) override
{
2022-02-18 01:18:01 +00:00
if (!node->prefix)
return true;
2022-02-18 01:18:01 +00:00
if (!imports.contains(*node->prefix))
return true;
2022-02-18 01:18:01 +00:00
AstLocal* astLocal = imports[*node->prefix];
Local& local = locals[astLocal];
LUAU_ASSERT(local.import);
local.used = true;
return true;
}
bool visit(AstExprFunction* node) override
{
if (node->self)
locals[node->self].arg = true;
for (size_t i = 0; i < node->args.size; ++i)
locals[node->args.data[i]].arg = true;
return true;
}
};
class LintUnusedFunction : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintUnusedFunction pass;
pass.context = &context;
context.root->visit(&pass);
pass.report();
}
private:
LintContext* context;
struct Global
{
Location location;
bool function;
bool used;
};
DenseHashMap<AstName, Global> globals;
LintUnusedFunction()
: globals(AstName())
{
}
void report()
{
for (auto& g : globals)
{
if (g.second.function && !g.second.used && g.first.value[0] != '_')
emitWarning(*context, LintWarning::Code_FunctionUnused, g.second.location, "Function '%s' is never used; prefix with '_' to silence",
g.first.value);
}
}
bool visit(AstStatFunction* node) override
{
if (AstExprGlobal* expr = node->name->as<AstExprGlobal>())
{
Global& g = globals[expr->name];
g.function = true;
g.location = expr->location;
node->func->visit(this);
return false;
}
return true;
}
bool visit(AstExprGlobal* node) override
{
Global& g = globals[node->name];
g.used = true;
return true;
}
};
class LintUnreachableCode : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintUnreachableCode pass;
pass.context = &context;
pass.analyze(context.root);
context.root->visit(&pass);
}
private:
LintContext* context;
// Note: this enum is order-sensitive!
// The order is in the "severity" of the termination and affects merging of status codes from different branches
// For example, if one branch breaks and one returns, the merged result is "break"
enum Status
{
Unknown,
Continue,
Break,
Return,
Error,
};
const char* getReason(Status status)
{
switch (status)
{
case Continue:
return "continue";
case Break:
return "break";
case Return:
return "return";
case Error:
return "error";
default:
return "unknown";
}
}
Status analyze(AstStat* node)
{
if (AstStatBlock* stat = node->as<AstStatBlock>())
{
for (size_t i = 0; i < stat->body.size; ++i)
{
AstStat* si = stat->body.data[i];
Status step = analyze(si);
if (step != Unknown)
{
if (i + 1 == stat->body.size)
return step;
AstStat* next = stat->body.data[i + 1];
// silence the warning for common pattern of Error (coming from error()) + Return
if (step == Error && si->is<AstStatExpr>() && next->is<AstStatReturn>() && i + 2 == stat->body.size)
return Error;
emitWarning(*context, LintWarning::Code_UnreachableCode, next->location, "Unreachable code (previous statement always %ss)",
getReason(step));
return step;
}
}
return Unknown;
}
else if (AstStatIf* stat = node->as<AstStatIf>())
{
Status ifs = analyze(stat->thenbody);
Status elses = stat->elsebody ? analyze(stat->elsebody) : Unknown;
return std::min(ifs, elses);
}
else if (AstStatWhile* stat = node->as<AstStatWhile>())
{
analyze(stat->body);
return Unknown;
}
else if (AstStatRepeat* stat = node->as<AstStatRepeat>())
{
analyze(stat->body);
return Unknown;
}
else if (node->is<AstStatBreak>())
{
return Break;
}
else if (node->is<AstStatContinue>())
{
return Continue;
}
else if (node->is<AstStatReturn>())
{
return Return;
}
else if (AstStatExpr* stat = node->as<AstStatExpr>())
{
if (AstExprCall* call = stat->expr->as<AstExprCall>())
if (doesCallError(call))
return Error;
return Unknown;
}
else if (AstStatFor* stat = node->as<AstStatFor>())
{
analyze(stat->body);
return Unknown;
}
else if (AstStatForIn* stat = node->as<AstStatForIn>())
{
analyze(stat->body);
return Unknown;
}
else
{
return Unknown;
}
}
bool visit(AstExprFunction* node) override
{
analyze(node->body);
return true;
}
};
class LintUnknownType : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintUnknownType pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
enum TypeKind
{
2022-03-24 22:04:14 +00:00
Kind_Unknown,
Kind_Primitive, // primitive type supported by VM - boolean/userdata/etc. No differentiation between types of userdata.
2022-03-24 22:04:14 +00:00
Kind_Vector, // 'vector' but only used when type is used
Kind_Userdata, // custom userdata type
};
TypeKind getTypeKind(const std::string& name)
{
if (name == "nil" || name == "boolean" || name == "userdata" || name == "number" || name == "string" || name == "table" ||
name == "function" || name == "thread" || name == "buffer")
return Kind_Primitive;
if (name == "vector")
return Kind_Vector;
2022-04-15 00:57:43 +01:00
if (std::optional<TypeFun> maybeTy = context->scope->lookupType(name))
return Kind_Userdata;
2022-04-15 00:57:43 +01:00
return Kind_Unknown;
}
void validateType(AstExprConstantString* expr, std::initializer_list<TypeKind> expected, const char* expectedString)
{
std::string name(expr->value.data, expr->value.size);
TypeKind kind = getTypeKind(name);
2022-03-24 22:04:14 +00:00
if (kind == Kind_Unknown)
{
emitWarning(*context, LintWarning::Code_UnknownType, expr->location, "Unknown type '%s'", name.c_str());
return;
}
for (TypeKind ek : expected)
{
if (kind == ek)
return;
}
emitWarning(*context, LintWarning::Code_UnknownType, expr->location, "Unknown type '%s' (expected %s)", name.c_str(), expectedString);
}
bool visit(AstExprBinary* node) override
{
if (node->op == AstExprBinary::CompareNe || node->op == AstExprBinary::CompareEq)
{
AstExpr* lhs = node->left;
AstExpr* rhs = node->right;
if (!rhs->is<AstExprConstantString>())
std::swap(lhs, rhs);
AstExprCall* call = lhs->as<AstExprCall>();
AstExprConstantString* arg = rhs->as<AstExprConstantString>();
if (call && arg)
{
AstExprGlobal* g = call->func->as<AstExprGlobal>();
if (g && g->name == "type")
{
validateType(arg, {Kind_Primitive, Kind_Vector}, "primitive type");
}
else if (g && g->name == "typeof")
{
validateType(arg, {Kind_Primitive, Kind_Userdata}, "primitive or userdata type");
}
}
}
return true;
}
};
class LintForRange : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintForRange pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
double getLoopEnd(double from, double to)
{
return from + floor(to - from);
}
bool visit(AstStatFor* node) override
{
// note: we silence all warnings below if *any* step is specified, assuming that the user knows best
if (!node->step)
{
AstExprConstantNumber* fc = node->from->as<AstExprConstantNumber>();
AstExprUnary* fu = node->from->as<AstExprUnary>();
AstExprConstantNumber* tc = node->to->as<AstExprConstantNumber>();
AstExprUnary* tu = node->to->as<AstExprUnary>();
Location rangeLocation(node->from->location, node->to->location);
// for i=#t,1 do
if (fu && fu->op == AstExprUnary::Len && tc && tc->value == 1.0)
emitWarning(
*context, LintWarning::Code_ForRange, rangeLocation, "For loop should iterate backwards; did you forget to specify -1 as step?");
// for i=8,1 do
else if (fc && tc && fc->value > tc->value)
emitWarning(
*context, LintWarning::Code_ForRange, rangeLocation, "For loop should iterate backwards; did you forget to specify -1 as step?");
// for i=1,8.75 do
else if (fc && tc && getLoopEnd(fc->value, tc->value) != tc->value)
emitWarning(*context, LintWarning::Code_ForRange, rangeLocation, "For loop ends at %g instead of %g; did you forget to specify step?",
getLoopEnd(fc->value, tc->value), tc->value);
// for i=0,#t do
else if (fc && tu && fc->value == 0.0 && tu->op == AstExprUnary::Len)
emitWarning(*context, LintWarning::Code_ForRange, rangeLocation, "For loop starts at 0, but arrays start at 1");
// for i=#t,0 do
else if (fu && fu->op == AstExprUnary::Len && tc && tc->value == 0.0)
emitWarning(*context, LintWarning::Code_ForRange, rangeLocation,
"For loop should iterate backwards; did you forget to specify -1 as step? Also consider changing 0 to 1 since arrays start at 1");
}
return true;
}
};
class LintUnbalancedAssignment : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintUnbalancedAssignment pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
void assign(size_t vars, const AstArray<AstExpr*>& values, const Location& location)
{
if (vars != values.size && values.size > 0)
{
AstExpr* last = values.data[values.size - 1];
if (vars < values.size)
emitWarning(*context, LintWarning::Code_UnbalancedAssignment, location,
"Assigning %d values to %d variables leaves some values unused", int(values.size), int(vars));
else if (last->is<AstExprCall>() || last->is<AstExprVarargs>())
; // we don't know how many values the last expression returns
else if (last->is<AstExprConstantNil>())
; // last expression is nil which explicitly silences the nil-init warning
else
emitWarning(*context, LintWarning::Code_UnbalancedAssignment, location,
"Assigning %d values to %d variables initializes extra variables with nil; add 'nil' to value list to silence", int(values.size),
int(vars));
}
}
bool visit(AstStatLocal* node) override
{
assign(node->vars.size, node->values, node->location);
return true;
}
bool visit(AstStatAssign* node) override
{
assign(node->vars.size, node->values, node->location);
return true;
}
};
class LintImplicitReturn : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintImplicitReturn pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
Location getEndLocation(const AstStat* node)
{
Location loc = node->location;
if (node->is<AstStatExpr>() || node->is<AstStatAssign>() || node->is<AstStatLocal>())
return loc;
if (loc.begin.line == loc.end.line)
return loc;
// assume that we're in context of a statement that has an "end" block
return Location(Position(loc.end.line, std::max(0, int(loc.end.column) - 3)), loc.end);
}
AstStatReturn* getValueReturn(AstStat* node)
{
struct Visitor : AstVisitor
{
AstStatReturn* result = nullptr;
bool visit(AstExpr* node) override
{
(void)node;
return false;
}
bool visit(AstStatReturn* node) override
{
if (!result && node->list.size > 0)
result = node;
return false;
}
};
Visitor visitor;
node->visit(&visitor);
return visitor.result;
}
bool visit(AstExprFunction* node) override
{
const AstStat* bodyf = getFallthrough(node->body);
AstStat* vret = getValueReturn(node->body);
if (bodyf && vret)
{
Location location = getEndLocation(bodyf);
if (node->debugname.value)
emitWarning(*context, LintWarning::Code_ImplicitReturn, location,
"Function '%s' can implicitly return no values even though there's an explicit return at line %d; add explicit return to silence",
node->debugname.value, vret->location.begin.line + 1);
else
emitWarning(*context, LintWarning::Code_ImplicitReturn, location,
"Function can implicitly return no values even though there's an explicit return at line %d; add explicit return to silence",
vret->location.begin.line + 1);
}
return true;
}
};
class LintFormatString : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintFormatString pass;
pass.context = &context;
context.root->visit(&pass);
}
static void fuzz(const char* data, size_t size)
{
LintContext context;
LintFormatString pass;
pass.context = &context;
pass.checkStringFormat(data, size);
pass.checkStringPack(data, size, false);
pass.checkStringMatch(data, size);
pass.checkStringReplace(data, size, -1);
pass.checkDateFormat(data, size);
}
private:
LintContext* context;
static inline bool isAlpha(char ch)
{
// use or trick to convert to lower case and unsigned comparison to do range check
return unsigned((ch | ' ') - 'a') < 26;
}
static inline bool isDigit(char ch)
{
// use unsigned comparison to do range check for performance
return unsigned(ch - '0') < 10;
}
const char* checkStringFormat(const char* data, size_t size)
{
const char* flags = "-+ #0";
const char* options = "cdiouxXeEfgGqs*";
for (size_t i = 0; i < size; ++i)
{
if (data[i] == '%')
{
i++;
// escaped % doesn't allow for flags/etc.
if (i < size && data[i] == '%')
continue;
// skip flags
while (i < size && strchr(flags, data[i]))
i++;
// skip width (up to two digits)
if (i < size && isDigit(data[i]))
i++;
if (i < size && isDigit(data[i]))
i++;
// skip precision
if (i < size && data[i] == '.')
{
i++;
// up to two digits
if (i < size && isDigit(data[i]))
i++;
if (i < size && isDigit(data[i]))
i++;
}
if (i == size)
return "unfinished format specifier";
if (!strchr(options, data[i]))
return "invalid format specifier: must be a string format specifier or %";
}
}
return nullptr;
}
const char* checkStringPack(const char* data, size_t size, bool fixed)
{
const char* options = "<>=!bBhHlLjJTiIfdnczsxX ";
const char* unsized = "<>=!zX ";
for (size_t i = 0; i < size; ++i)
{
if (!strchr(options, data[i]))
return "unexpected character; must be a pack specifier or space";
if (data[i] == 'c' && (i + 1 == size || !isDigit(data[i + 1])))
return "fixed-sized string format must specify the size";
if (data[i] == 'X' && (i + 1 == size || strchr(unsized, data[i + 1])))
return "X must be followed by a size specifier";
if (fixed && (data[i] == 'z' || data[i] == 's'))
return "pack specifier must be fixed-size";
if ((data[i] == '!' || data[i] == 'i' || data[i] == 'I' || data[i] == 'c' || data[i] == 's') && i + 1 < size && isDigit(data[i + 1]))
{
bool isc = data[i] == 'c';
unsigned int v = 0;
while (i + 1 < size && isDigit(data[i + 1]) && v <= (INT_MAX - 9) / 10)
{
v = v * 10 + (data[i + 1] - '0');
i++;
}
if (i + 1 < size && isDigit(data[i + 1]))
return "size specifier is too large";
if (!isc && (v == 0 || v > 16))
return "integer size must be in range [1,16]";
}
}
return nullptr;
}
const char* checkStringMatchSet(const char* data, size_t size, const char* magic, const char* classes)
{
for (size_t i = 0; i < size; ++i)
{
if (data[i] == '%')
{
i++;
if (i == size)
return "unfinished character class";
if (isDigit(data[i]))
{
return "sets can not contain capture references";
}
else if (isAlpha(data[i]))
{
// lower case lookup - upper case for every character class is defined as its inverse
if (!strchr(classes, data[i] | ' '))
return "invalid character class, must refer to a defined class or its inverse";
}
else
{
// technically % can escape any non-alphanumeric character but this is error-prone
if (!strchr(magic, data[i]))
return "expected a magic character after %";
}
if (i + 1 < size && data[i + 1] == '-')
return "character range can't include character sets";
}
else if (data[i] == '-')
{
if (i + 1 < size && data[i + 1] == '%')
return "character range can't include character sets";
}
}
return nullptr;
}
const char* checkStringMatch(const char* data, size_t size, int* outCaptures = nullptr)
{
const char* magic = "^$()%.[]*+-?)";
const char* classes = "acdglpsuwxz";
std::vector<int> openCaptures;
int totalCaptures = 0;
for (size_t i = 0; i < size; ++i)
{
if (data[i] == '%')
{
i++;
if (i == size)
return "unfinished character class";
if (isDigit(data[i]))
{
if (data[i] == '0')
return "invalid capture reference, must be 1-9";
int captureIndex = data[i] - '0';
if (captureIndex > totalCaptures)
return "invalid capture reference, must refer to a valid capture";
for (int open : openCaptures)
if (open == captureIndex)
return "invalid capture reference, must refer to a closed capture";
}
else if (isAlpha(data[i]))
{
if (data[i] == 'b')
{
if (i + 2 >= size)
return "missing brace characters for balanced match";
i += 2;
}
else if (data[i] == 'f')
{
if (i + 1 >= size || data[i + 1] != '[')
return "missing set after a frontier pattern";
// we can parse the set with the regular logic
}
else
{
// lower case lookup - upper case for every character class is defined as its inverse
if (!strchr(classes, data[i] | ' '))
return "invalid character class, must refer to a defined class or its inverse";
}
}
else
{
// technically % can escape any non-alphanumeric character but this is error-prone
if (!strchr(magic, data[i]))
return "expected a magic character after %";
}
}
else if (data[i] == '[')
{
size_t j = i + 1;
// empty patterns don't exist as per grammar rules, so we skip leading ^ and ]
if (j < size && data[j] == '^')
j++;
if (j < size && data[j] == ']')
j++;
// scan for the end of the pattern
while (j < size && data[j] != ']')
{
// % escapes the next character
if (j + 1 < size && data[j] == '%')
j++;
j++;
}
if (j == size)
return "expected ] at the end of the string to close a set";
if (const char* error = checkStringMatchSet(data + i + 1, j - i - 1, magic, classes))
return error;
LUAU_ASSERT(data[j] == ']');
i = j;
}
else if (data[i] == '(')
{
totalCaptures++;
openCaptures.push_back(totalCaptures);
}
else if (data[i] == ')')
{
if (openCaptures.empty())
return "unexpected ) without a matching (";
openCaptures.pop_back();
}
}
if (!openCaptures.empty())
return "expected ) at the end of the string to close a capture";
if (outCaptures)
*outCaptures = totalCaptures;
return nullptr;
}
const char* checkStringReplace(const char* data, size_t size, int captures)
{
for (size_t i = 0; i < size; ++i)
{
if (data[i] == '%')
{
i++;
if (i == size)
return "unfinished replacement";
if (data[i] != '%' && !isDigit(data[i]))
return "unexpected replacement character; must be a digit or %";
if (isDigit(data[i]) && captures >= 0 && data[i] - '0' > captures)
return "invalid capture index, must refer to pattern capture";
}
}
return nullptr;
}
const char* checkDateFormat(const char* data, size_t size)
{
const char* options = "aAbBcdHIjmMpSUwWxXyYzZ";
for (size_t i = 0; i < size; ++i)
{
if (data[i] == '%')
{
i++;
if (i == size)
return "unfinished replacement";
if (data[i] != '%' && !strchr(options, data[i]))
return "unexpected replacement character; must be a date format specifier or %";
}
if (data[i] == 0)
return "date format can not contain null characters";
}
return nullptr;
}
void matchStringCall(AstName name, AstExpr* self, AstArray<AstExpr*> args)
{
if (name == "format")
{
if (AstExprConstantString* fmt = self->as<AstExprConstantString>())
if (const char* error = checkStringFormat(fmt->value.data, fmt->value.size))
emitWarning(*context, LintWarning::Code_FormatString, fmt->location, "Invalid format string: %s", error);
}
else if (name == "pack" || name == "packsize" || name == "unpack")
{
if (AstExprConstantString* fmt = self->as<AstExprConstantString>())
if (const char* error = checkStringPack(fmt->value.data, fmt->value.size, name == "packsize"))
emitWarning(*context, LintWarning::Code_FormatString, fmt->location, "Invalid pack format: %s", error);
}
else if ((name == "match" || name == "gmatch") && args.size > 0)
{
if (AstExprConstantString* pat = args.data[0]->as<AstExprConstantString>())
if (const char* error = checkStringMatch(pat->value.data, pat->value.size))
emitWarning(*context, LintWarning::Code_FormatString, pat->location, "Invalid match pattern: %s", error);
}
else if (name == "find" && args.size > 0 && args.size <= 2)
{
if (AstExprConstantString* pat = args.data[0]->as<AstExprConstantString>())
if (const char* error = checkStringMatch(pat->value.data, pat->value.size))
emitWarning(*context, LintWarning::Code_FormatString, pat->location, "Invalid match pattern: %s", error);
}
else if (name == "find" && args.size >= 3)
{
AstExprConstantBool* mode = args.data[2]->as<AstExprConstantBool>();
// find(_, _, _, true) is a raw string find, not a pattern match
if (mode && !mode->value)
if (AstExprConstantString* pat = args.data[0]->as<AstExprConstantString>())
if (const char* error = checkStringMatch(pat->value.data, pat->value.size))
emitWarning(*context, LintWarning::Code_FormatString, pat->location, "Invalid match pattern: %s", error);
}
else if (name == "gsub" && args.size > 1)
{
int captures = -1;
if (AstExprConstantString* pat = args.data[0]->as<AstExprConstantString>())
if (const char* error = checkStringMatch(pat->value.data, pat->value.size, &captures))
emitWarning(*context, LintWarning::Code_FormatString, pat->location, "Invalid match pattern: %s", error);
if (AstExprConstantString* rep = args.data[1]->as<AstExprConstantString>())
if (const char* error = checkStringReplace(rep->value.data, rep->value.size, captures))
emitWarning(*context, LintWarning::Code_FormatString, rep->location, "Invalid match replacement: %s", error);
}
}
void matchCall(AstExprCall* node)
{
AstExprIndexName* func = node->func->as<AstExprIndexName>();
if (!func)
return;
if (node->self)
{
AstExprGroup* group = func->expr->as<AstExprGroup>();
AstExpr* self = group ? group->expr : func->expr;
if (self->is<AstExprConstantString>())
matchStringCall(func->index, self, node->args);
else if (std::optional<TypeId> type = context->getType(self))
if (isString(*type))
matchStringCall(func->index, self, node->args);
return;
}
AstExprGlobal* lib = func->expr->as<AstExprGlobal>();
if (!lib)
return;
if (lib->name == "string")
{
if (node->args.size > 0)
{
AstArray<AstExpr*> rest = {node->args.data + 1, node->args.size - 1};
matchStringCall(func->index, node->args.data[0], rest);
}
}
else if (lib->name == "os")
{
if (func->index == "date" && node->args.size > 0)
{
if (AstExprConstantString* fmt = node->args.data[0]->as<AstExprConstantString>())
if (const char* error = checkDateFormat(fmt->value.data, fmt->value.size))
emitWarning(*context, LintWarning::Code_FormatString, fmt->location, "Invalid date format: %s", error);
}
}
}
bool visit(AstExprCall* node) override
{
matchCall(node);
return true;
}
};
class LintTableLiteral : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintTableLiteral pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
bool visit(AstExprTable* node) override
{
int count = 0;
for (const AstExprTable::Item& item : node->items)
if (item.kind == AstExprTable::Item::List)
count++;
DenseHashMap<AstArray<char>*, int, AstArrayPredicate, AstArrayPredicate> names(nullptr);
DenseHashMap<int, int> indices(-1);
for (const AstExprTable::Item& item : node->items)
{
if (!item.key)
continue;
if (AstExprConstantString* expr = item.key->as<AstExprConstantString>())
{
int& line = names[&expr->value];
if (line)
emitWarning(*context, LintWarning::Code_TableLiteral, expr->location,
"Table field '%.*s' is a duplicate; previously defined at line %d", int(expr->value.size), expr->value.data, line);
else
line = expr->location.begin.line + 1;
}
else if (AstExprConstantNumber* expr = item.key->as<AstExprConstantNumber>())
{
if (expr->value >= 1 && expr->value <= double(count) && double(int(expr->value)) == expr->value)
emitWarning(*context, LintWarning::Code_TableLiteral, expr->location,
"Table index %d is a duplicate; previously defined as a list entry", int(expr->value));
else if (expr->value >= 0 && expr->value <= double(INT_MAX) && double(int(expr->value)) == expr->value)
{
int& line = indices[int(expr->value)];
if (line)
emitWarning(*context, LintWarning::Code_TableLiteral, expr->location,
"Table index %d is a duplicate; previously defined at line %d", int(expr->value), line);
else
line = expr->location.begin.line + 1;
}
}
}
return true;
}
bool visit(AstType* node) override
{
return true;
}
bool visit(AstTypeTable* node) override
{
Sync to upstream/release/614 (#1173) # What's changed? Add program argument passing to scripts run using the Luau REPL! You can now pass `--program-args` (or shorthand `-a`) to the REPL which will treat all remaining arguments as arguments to pass to executed scripts. These values can be accessed through variadic argument expansion. You can read these values like so: ``` local args = {...} -- gets you an array of all the arguments ``` For example if we run the following script like `luau test.lua -a test1 test2 test3`: ``` -- test.lua print(...) ``` you should get the output: ``` test1 test2 test3 ``` ### Native Code Generation * Improve A64 lowering for vector operations by using vector instructions * Fix lowering issue in IR value location tracking! - A developer reported a divergence between code run in the VM and Native Code Generation which we have now fixed ### New Type Solver * Apply substitution to type families, and emit new constraints to reduce those further * More progress on reducing comparison (`lt/le`)type families * Resolve two major sources of cyclic types in the new solver ### Miscellaneous * Turned internal compiler errors (ICE's) into warnings and errors ------- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> --------- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: David Cope <dcope@roblox.com> Co-authored-by: Lily Brown <lbrown@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
2024-02-23 20:08:34 +00:00
if (FFlag::DebugLuauDeferredConstraintResolution)
{
struct Rec
{
AstTableAccess access;
Location location;
};
DenseHashMap<AstName, Rec> names(AstName{});
for (const AstTableProp& item : node->props)
{
Rec* rec = names.find(item.name);
if (!rec)
{
names[item.name] = Rec{item.access, item.location};
continue;
}
if (int(rec->access) & int(item.access))
{
if (rec->access == item.access)
emitWarning(*context, LintWarning::Code_TableLiteral, item.location,
"Table type field '%s' is a duplicate; previously defined at line %d", item.name.value, rec->location.begin.line + 1);
else if (rec->access == AstTableAccess::ReadWrite)
emitWarning(*context, LintWarning::Code_TableLiteral, item.location,
"Table type field '%s' is already read-write; previously defined at line %d", item.name.value,
rec->location.begin.line + 1);
else if (rec->access == AstTableAccess::Read)
emitWarning(*context, LintWarning::Code_TableLiteral, rec->location,
"Table type field '%s' already has a read type defined at line %d", item.name.value, rec->location.begin.line + 1);
else if (rec->access == AstTableAccess::Write)
emitWarning(*context, LintWarning::Code_TableLiteral, rec->location,
"Table type field '%s' already has a write type defined at line %d", item.name.value, rec->location.begin.line + 1);
else
LUAU_ASSERT(!"Unreachable");
}
else
rec->access = AstTableAccess(int(rec->access) | int(item.access));
}
return true;
}
DenseHashMap<AstName, int> names(AstName{});
for (const AstTableProp& item : node->props)
{
int& line = names[item.name];
if (line)
emitWarning(*context, LintWarning::Code_TableLiteral, item.location,
"Table type field '%s' is a duplicate; previously defined at line %d", item.name.value, line);
else
line = item.location.begin.line + 1;
}
return true;
}
struct AstArrayPredicate
{
size_t operator()(const AstArray<char>* value) const
{
return hashRange(value->data, value->size);
}
bool operator()(const AstArray<char>* lhs, const AstArray<char>* rhs) const
{
return (lhs && rhs) ? lhs->size == rhs->size && memcmp(lhs->data, rhs->data, lhs->size) == 0 : lhs == rhs;
}
};
};
class LintUninitializedLocal : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintUninitializedLocal pass;
pass.context = &context;
context.root->visit(&pass);
pass.report();
}
private:
struct Local
{
bool defined;
bool initialized;
bool assigned;
AstExprLocal* firstUse;
};
LintContext* context;
DenseHashMap<AstLocal*, Local> locals;
LintUninitializedLocal()
: locals(NULL)
{
}
void report()
{
for (auto& lp : locals)
{
AstLocal* local = lp.first;
const Local& l = lp.second;
if (l.defined && !l.initialized && !l.assigned && l.firstUse)
{
emitWarning(*context, LintWarning::Code_UninitializedLocal, l.firstUse->location,
"Variable '%s' defined at line %d is never initialized or assigned; initialize with 'nil' to silence", local->name.value,
local->location.begin.line + 1);
}
}
}
bool visit(AstStatLocal* node) override
{
AstExpr* last = node->values.size ? node->values.data[node->values.size - 1] : nullptr;
bool vararg = last && (last->is<AstExprVarargs>() || last->is<AstExprCall>());
for (size_t i = 0; i < node->vars.size; ++i)
{
Local& l = locals[node->vars.data[i]];
l.defined = true;
l.initialized = vararg || i < node->values.size;
}
return true;
}
bool visit(AstStatAssign* node) override
{
for (size_t i = 0; i < node->vars.size; ++i)
visitAssign(node->vars.data[i]);
for (size_t i = 0; i < node->values.size; ++i)
node->values.data[i]->visit(this);
return false;
}
bool visit(AstStatFunction* node) override
{
visitAssign(node->name);
node->func->visit(this);
return false;
}
bool visit(AstExprLocal* node) override
{
Local& l = locals[node->local];
if (!l.firstUse)
l.firstUse = node;
return false;
}
void visitAssign(AstExpr* var)
{
if (AstExprLocal* lv = var->as<AstExprLocal>())
{
Local& l = locals[lv->local];
l.assigned = true;
}
else
{
var->visit(this);
}
}
};
class LintDuplicateFunction : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintDuplicateFunction pass{&context};
context.root->visit(&pass);
}
private:
LintContext* context;
DenseHashMap<std::string, Location> defns;
LintDuplicateFunction(LintContext* context)
: context(context)
, defns("")
{
}
bool visit(AstStatBlock* block) override
{
defns.clear();
for (AstStat* stat : block->body)
{
if (AstStatFunction* func = stat->as<AstStatFunction>())
trackFunction(func->name->location, buildName(func->name));
else if (AstStatLocalFunction* func = stat->as<AstStatLocalFunction>())
trackFunction(func->name->location, func->name->name.value);
}
return true;
}
void trackFunction(Location location, const std::string& name)
{
if (name.empty())
return;
Location& defn = defns[name];
if (defn.end.line == 0 && defn.end.column == 0)
defn = location;
else
report(name, location, defn);
}
std::string buildName(AstExpr* expr)
{
if (AstExprLocal* local = expr->as<AstExprLocal>())
return local->local->name.value;
else if (AstExprGlobal* global = expr->as<AstExprGlobal>())
return global->name.value;
else if (AstExprIndexName* indexName = expr->as<AstExprIndexName>())
{
std::string lhs = buildName(indexName->expr);
if (lhs.empty())
return lhs;
lhs += '.';
lhs += indexName->index.value;
return lhs;
}
else
return std::string();
}
void report(const std::string& name, Location location, Location otherLocation)
{
emitWarning(*context, LintWarning::Code_DuplicateFunction, location, "Duplicate function definition: '%s' also defined on line %d",
name.c_str(), otherLocation.begin.line + 1);
}
};
class LintDeprecatedApi : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintDeprecatedApi pass{&context};
context.root->visit(&pass);
}
private:
LintContext* context;
LintDeprecatedApi(LintContext* context)
: context(context)
{
}
bool visit(AstExprIndexName* node) override
{
Sync to upstream/release/566 (#853) * Fixed incorrect lexeme generated for string parts in the middle of an interpolated string (Fixes https://github.com/Roblox/luau/issues/744) * DeprecatedApi lint can report some issues without type inference information * Fixed performance of autocomplete requests when suggestions have large intersection types (Solves https://github.com/Roblox/luau/discussions/847) * Marked `table.getn`/`foreach`/`foreachi` as deprecated ([RFC: Deprecate table.getn/foreach/foreachi](https://github.com/Roblox/luau/blob/master/rfcs/deprecate-table-getn-foreach.md)) * With -O2 optimization level, we now optimize builtin calls based on known argument/return count. Note that this change can be observable if `getfenv/setfenv` is used to substitute a builtin, especially if arity is different. Fastcall heavy tests show a 1-2% improvement. * Luau can now be built with clang-cl (Fixes https://github.com/Roblox/luau/issues/736) We also made many improvements to our experimental components. For our new type solver: * Overhauled data flow analysis system, fixed issues with 'repeat' loops, global variables and type annotations * Type refinements now work on generic table indexing with a string literal * Type refinements will properly track potentially 'nil' values (like t[x] for a missing key) and their further refinements * Internal top table type is now isomorphic to `{}` which fixes issues when `typeof(v) == 'table'` type refinement is handled * References to non-existent types in type annotations no longer resolve to 'error' type like in old solver * Improved handling of class unions in property access expressions * Fixed default type packs * Unsealed tables can now have metatables * Restored expected types for function arguments And for native code generation: * Added min and max IR instructions mapping to vminsd/vmaxsd on x64 * We now speculatively extract direct execution fast-paths based on expected types of expressions which provides better optimization opportunities inside a single basic block * Translated existing math fastcalls to IR form to improve tag guard removal and constant propagation
2023-03-03 20:21:14 +00:00
if (std::optional<TypeId> ty = context->getType(node->expr))
check(node, follow(*ty));
else if (AstExprGlobal* global = node->expr->as<AstExprGlobal>())
check(node->location, global->name, node->index);
Sync to upstream/release/566 (#853) * Fixed incorrect lexeme generated for string parts in the middle of an interpolated string (Fixes https://github.com/Roblox/luau/issues/744) * DeprecatedApi lint can report some issues without type inference information * Fixed performance of autocomplete requests when suggestions have large intersection types (Solves https://github.com/Roblox/luau/discussions/847) * Marked `table.getn`/`foreach`/`foreachi` as deprecated ([RFC: Deprecate table.getn/foreach/foreachi](https://github.com/Roblox/luau/blob/master/rfcs/deprecate-table-getn-foreach.md)) * With -O2 optimization level, we now optimize builtin calls based on known argument/return count. Note that this change can be observable if `getfenv/setfenv` is used to substitute a builtin, especially if arity is different. Fastcall heavy tests show a 1-2% improvement. * Luau can now be built with clang-cl (Fixes https://github.com/Roblox/luau/issues/736) We also made many improvements to our experimental components. For our new type solver: * Overhauled data flow analysis system, fixed issues with 'repeat' loops, global variables and type annotations * Type refinements now work on generic table indexing with a string literal * Type refinements will properly track potentially 'nil' values (like t[x] for a missing key) and their further refinements * Internal top table type is now isomorphic to `{}` which fixes issues when `typeof(v) == 'table'` type refinement is handled * References to non-existent types in type annotations no longer resolve to 'error' type like in old solver * Improved handling of class unions in property access expressions * Fixed default type packs * Unsealed tables can now have metatables * Restored expected types for function arguments And for native code generation: * Added min and max IR instructions mapping to vminsd/vmaxsd on x64 * We now speculatively extract direct execution fast-paths based on expected types of expressions which provides better optimization opportunities inside a single basic block * Translated existing math fastcalls to IR form to improve tag guard removal and constant propagation
2023-03-03 20:21:14 +00:00
return true;
}
bool visit(AstExprCall* node) override
{
// getfenv/setfenv are deprecated, however they are still used in some test frameworks and don't have a great general replacement
// for now we warn about the deprecation only when they are used with a numeric first argument; this produces fewer warnings and makes use
// of getfenv/setfenv a little more localized
if (!node->self && node->args.size >= 1)
{
if (AstExprGlobal* fenv = node->func->as<AstExprGlobal>(); fenv && (fenv->name == "getfenv" || fenv->name == "setfenv"))
{
AstExpr* level = node->args.data[0];
std::optional<TypeId> ty = context->getType(level);
if ((ty && isNumber(*ty)) || level->is<AstExprConstantNumber>())
{
// some common uses of getfenv(n) can be replaced by debug.info if the goal is to get the caller's identity
const char* suggestion = (fenv->name == "getfenv") ? "; consider using 'debug.info' instead" : "";
emitWarning(
*context, LintWarning::Code_DeprecatedApi, node->location, "Function '%s' is deprecated%s", fenv->name.value, suggestion);
}
}
}
return true;
}
Sync to upstream/release/566 (#853) * Fixed incorrect lexeme generated for string parts in the middle of an interpolated string (Fixes https://github.com/Roblox/luau/issues/744) * DeprecatedApi lint can report some issues without type inference information * Fixed performance of autocomplete requests when suggestions have large intersection types (Solves https://github.com/Roblox/luau/discussions/847) * Marked `table.getn`/`foreach`/`foreachi` as deprecated ([RFC: Deprecate table.getn/foreach/foreachi](https://github.com/Roblox/luau/blob/master/rfcs/deprecate-table-getn-foreach.md)) * With -O2 optimization level, we now optimize builtin calls based on known argument/return count. Note that this change can be observable if `getfenv/setfenv` is used to substitute a builtin, especially if arity is different. Fastcall heavy tests show a 1-2% improvement. * Luau can now be built with clang-cl (Fixes https://github.com/Roblox/luau/issues/736) We also made many improvements to our experimental components. For our new type solver: * Overhauled data flow analysis system, fixed issues with 'repeat' loops, global variables and type annotations * Type refinements now work on generic table indexing with a string literal * Type refinements will properly track potentially 'nil' values (like t[x] for a missing key) and their further refinements * Internal top table type is now isomorphic to `{}` which fixes issues when `typeof(v) == 'table'` type refinement is handled * References to non-existent types in type annotations no longer resolve to 'error' type like in old solver * Improved handling of class unions in property access expressions * Fixed default type packs * Unsealed tables can now have metatables * Restored expected types for function arguments And for native code generation: * Added min and max IR instructions mapping to vminsd/vmaxsd on x64 * We now speculatively extract direct execution fast-paths based on expected types of expressions which provides better optimization opportunities inside a single basic block * Translated existing math fastcalls to IR form to improve tag guard removal and constant propagation
2023-03-03 20:21:14 +00:00
void check(AstExprIndexName* node, TypeId ty)
{
if (const ClassType* cty = get<ClassType>(ty))
{
const Property* prop = lookupClassProp(cty, node->index.value);
if (prop && prop->deprecated)
2022-02-11 19:02:09 +00:00
report(node->location, *prop, cty->name.c_str(), node->index.value);
}
Sync to upstream/release/566 (#853) * Fixed incorrect lexeme generated for string parts in the middle of an interpolated string (Fixes https://github.com/Roblox/luau/issues/744) * DeprecatedApi lint can report some issues without type inference information * Fixed performance of autocomplete requests when suggestions have large intersection types (Solves https://github.com/Roblox/luau/discussions/847) * Marked `table.getn`/`foreach`/`foreachi` as deprecated ([RFC: Deprecate table.getn/foreach/foreachi](https://github.com/Roblox/luau/blob/master/rfcs/deprecate-table-getn-foreach.md)) * With -O2 optimization level, we now optimize builtin calls based on known argument/return count. Note that this change can be observable if `getfenv/setfenv` is used to substitute a builtin, especially if arity is different. Fastcall heavy tests show a 1-2% improvement. * Luau can now be built with clang-cl (Fixes https://github.com/Roblox/luau/issues/736) We also made many improvements to our experimental components. For our new type solver: * Overhauled data flow analysis system, fixed issues with 'repeat' loops, global variables and type annotations * Type refinements now work on generic table indexing with a string literal * Type refinements will properly track potentially 'nil' values (like t[x] for a missing key) and their further refinements * Internal top table type is now isomorphic to `{}` which fixes issues when `typeof(v) == 'table'` type refinement is handled * References to non-existent types in type annotations no longer resolve to 'error' type like in old solver * Improved handling of class unions in property access expressions * Fixed default type packs * Unsealed tables can now have metatables * Restored expected types for function arguments And for native code generation: * Added min and max IR instructions mapping to vminsd/vmaxsd on x64 * We now speculatively extract direct execution fast-paths based on expected types of expressions which provides better optimization opportunities inside a single basic block * Translated existing math fastcalls to IR form to improve tag guard removal and constant propagation
2023-03-03 20:21:14 +00:00
else if (const TableType* tty = get<TableType>(ty))
2022-02-11 19:02:09 +00:00
{
auto prop = tty->props.find(node->index.value);
if (prop != tty->props.end() && prop->second.deprecated)
Sync to upstream/release/566 (#853) * Fixed incorrect lexeme generated for string parts in the middle of an interpolated string (Fixes https://github.com/Roblox/luau/issues/744) * DeprecatedApi lint can report some issues without type inference information * Fixed performance of autocomplete requests when suggestions have large intersection types (Solves https://github.com/Roblox/luau/discussions/847) * Marked `table.getn`/`foreach`/`foreachi` as deprecated ([RFC: Deprecate table.getn/foreach/foreachi](https://github.com/Roblox/luau/blob/master/rfcs/deprecate-table-getn-foreach.md)) * With -O2 optimization level, we now optimize builtin calls based on known argument/return count. Note that this change can be observable if `getfenv/setfenv` is used to substitute a builtin, especially if arity is different. Fastcall heavy tests show a 1-2% improvement. * Luau can now be built with clang-cl (Fixes https://github.com/Roblox/luau/issues/736) We also made many improvements to our experimental components. For our new type solver: * Overhauled data flow analysis system, fixed issues with 'repeat' loops, global variables and type annotations * Type refinements now work on generic table indexing with a string literal * Type refinements will properly track potentially 'nil' values (like t[x] for a missing key) and their further refinements * Internal top table type is now isomorphic to `{}` which fixes issues when `typeof(v) == 'table'` type refinement is handled * References to non-existent types in type annotations no longer resolve to 'error' type like in old solver * Improved handling of class unions in property access expressions * Fixed default type packs * Unsealed tables can now have metatables * Restored expected types for function arguments And for native code generation: * Added min and max IR instructions mapping to vminsd/vmaxsd on x64 * We now speculatively extract direct execution fast-paths based on expected types of expressions which provides better optimization opportunities inside a single basic block * Translated existing math fastcalls to IR form to improve tag guard removal and constant propagation
2023-03-03 20:21:14 +00:00
{
// strip synthetic typeof() for builtin tables
if (tty->name && tty->name->compare(0, 7, "typeof(") == 0 && tty->name->back() == ')')
Sync to upstream/release/566 (#853) * Fixed incorrect lexeme generated for string parts in the middle of an interpolated string (Fixes https://github.com/Roblox/luau/issues/744) * DeprecatedApi lint can report some issues without type inference information * Fixed performance of autocomplete requests when suggestions have large intersection types (Solves https://github.com/Roblox/luau/discussions/847) * Marked `table.getn`/`foreach`/`foreachi` as deprecated ([RFC: Deprecate table.getn/foreach/foreachi](https://github.com/Roblox/luau/blob/master/rfcs/deprecate-table-getn-foreach.md)) * With -O2 optimization level, we now optimize builtin calls based on known argument/return count. Note that this change can be observable if `getfenv/setfenv` is used to substitute a builtin, especially if arity is different. Fastcall heavy tests show a 1-2% improvement. * Luau can now be built with clang-cl (Fixes https://github.com/Roblox/luau/issues/736) We also made many improvements to our experimental components. For our new type solver: * Overhauled data flow analysis system, fixed issues with 'repeat' loops, global variables and type annotations * Type refinements now work on generic table indexing with a string literal * Type refinements will properly track potentially 'nil' values (like t[x] for a missing key) and their further refinements * Internal top table type is now isomorphic to `{}` which fixes issues when `typeof(v) == 'table'` type refinement is handled * References to non-existent types in type annotations no longer resolve to 'error' type like in old solver * Improved handling of class unions in property access expressions * Fixed default type packs * Unsealed tables can now have metatables * Restored expected types for function arguments And for native code generation: * Added min and max IR instructions mapping to vminsd/vmaxsd on x64 * We now speculatively extract direct execution fast-paths based on expected types of expressions which provides better optimization opportunities inside a single basic block * Translated existing math fastcalls to IR form to improve tag guard removal and constant propagation
2023-03-03 20:21:14 +00:00
report(node->location, prop->second, tty->name->substr(7, tty->name->length() - 8).c_str(), node->index.value);
else
report(node->location, prop->second, tty->name ? tty->name->c_str() : nullptr, node->index.value);
}
}
Sync to upstream/release/566 (#853) * Fixed incorrect lexeme generated for string parts in the middle of an interpolated string (Fixes https://github.com/Roblox/luau/issues/744) * DeprecatedApi lint can report some issues without type inference information * Fixed performance of autocomplete requests when suggestions have large intersection types (Solves https://github.com/Roblox/luau/discussions/847) * Marked `table.getn`/`foreach`/`foreachi` as deprecated ([RFC: Deprecate table.getn/foreach/foreachi](https://github.com/Roblox/luau/blob/master/rfcs/deprecate-table-getn-foreach.md)) * With -O2 optimization level, we now optimize builtin calls based on known argument/return count. Note that this change can be observable if `getfenv/setfenv` is used to substitute a builtin, especially if arity is different. Fastcall heavy tests show a 1-2% improvement. * Luau can now be built with clang-cl (Fixes https://github.com/Roblox/luau/issues/736) We also made many improvements to our experimental components. For our new type solver: * Overhauled data flow analysis system, fixed issues with 'repeat' loops, global variables and type annotations * Type refinements now work on generic table indexing with a string literal * Type refinements will properly track potentially 'nil' values (like t[x] for a missing key) and their further refinements * Internal top table type is now isomorphic to `{}` which fixes issues when `typeof(v) == 'table'` type refinement is handled * References to non-existent types in type annotations no longer resolve to 'error' type like in old solver * Improved handling of class unions in property access expressions * Fixed default type packs * Unsealed tables can now have metatables * Restored expected types for function arguments And for native code generation: * Added min and max IR instructions mapping to vminsd/vmaxsd on x64 * We now speculatively extract direct execution fast-paths based on expected types of expressions which provides better optimization opportunities inside a single basic block * Translated existing math fastcalls to IR form to improve tag guard removal and constant propagation
2023-03-03 20:21:14 +00:00
}
Sync to upstream/release/566 (#853) * Fixed incorrect lexeme generated for string parts in the middle of an interpolated string (Fixes https://github.com/Roblox/luau/issues/744) * DeprecatedApi lint can report some issues without type inference information * Fixed performance of autocomplete requests when suggestions have large intersection types (Solves https://github.com/Roblox/luau/discussions/847) * Marked `table.getn`/`foreach`/`foreachi` as deprecated ([RFC: Deprecate table.getn/foreach/foreachi](https://github.com/Roblox/luau/blob/master/rfcs/deprecate-table-getn-foreach.md)) * With -O2 optimization level, we now optimize builtin calls based on known argument/return count. Note that this change can be observable if `getfenv/setfenv` is used to substitute a builtin, especially if arity is different. Fastcall heavy tests show a 1-2% improvement. * Luau can now be built with clang-cl (Fixes https://github.com/Roblox/luau/issues/736) We also made many improvements to our experimental components. For our new type solver: * Overhauled data flow analysis system, fixed issues with 'repeat' loops, global variables and type annotations * Type refinements now work on generic table indexing with a string literal * Type refinements will properly track potentially 'nil' values (like t[x] for a missing key) and their further refinements * Internal top table type is now isomorphic to `{}` which fixes issues when `typeof(v) == 'table'` type refinement is handled * References to non-existent types in type annotations no longer resolve to 'error' type like in old solver * Improved handling of class unions in property access expressions * Fixed default type packs * Unsealed tables can now have metatables * Restored expected types for function arguments And for native code generation: * Added min and max IR instructions mapping to vminsd/vmaxsd on x64 * We now speculatively extract direct execution fast-paths based on expected types of expressions which provides better optimization opportunities inside a single basic block * Translated existing math fastcalls to IR form to improve tag guard removal and constant propagation
2023-03-03 20:21:14 +00:00
void check(const Location& location, AstName global, AstName index)
{
if (const LintContext::Global* gv = context->builtinGlobals.find(global))
{
if (const TableType* tty = get<TableType>(gv->type))
{
auto prop = tty->props.find(index.value);
if (prop != tty->props.end() && prop->second.deprecated)
report(location, prop->second, global.value, index.value);
}
}
}
2022-02-11 19:02:09 +00:00
void report(const Location& location, const Property& prop, const char* container, const char* field)
{
std::string suggestion = prop.deprecatedSuggestion.empty() ? "" : format(", use '%s' instead", prop.deprecatedSuggestion.c_str());
if (container)
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s.%s' is deprecated%s", container, field, suggestion.c_str());
else
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s' is deprecated%s", field, suggestion.c_str());
}
};
class LintTableOperations : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
if (!context.module)
return;
LintTableOperations pass{&context};
context.root->visit(&pass);
}
private:
LintContext* context;
LintTableOperations(LintContext* context)
: context(context)
{
}
bool visit(AstExprUnary* node) override
{
if (node->op == AstExprUnary::Len)
checkIndexer(node, node->expr, "#");
return true;
}
bool visit(AstExprCall* node) override
{
if (AstExprGlobal* func = node->func->as<AstExprGlobal>())
{
if (func->name == "ipairs" && node->args.size == 1)
checkIndexer(node, node->args.data[0], "ipairs");
}
else if (AstExprIndexName* func = node->func->as<AstExprIndexName>())
{
if (AstExprGlobal* tablib = func->expr->as<AstExprGlobal>(); tablib && tablib->name == "table")
checkTableCall(node, func);
}
return true;
}
void checkIndexer(AstExpr* node, AstExpr* expr, const char* op)
{
std::optional<Luau::TypeId> ty = context->getType(expr);
if (!ty)
return;
const TableType* tty = get<TableType>(follow(*ty));
if (!tty)
return;
if (!tty->indexer && !tty->props.empty() && tty->state != TableState::Generic)
Sync to upstream/release/603 (#1097) # What's changed? - Record the location of properties for table types (closes #802) - Implement stricter UTF-8 validations as per the RFC (https://github.com/luau-lang/rfcs/pull/1) - Implement `buffer` as a new type in both the old and new solvers. - Changed errors produced by some `buffer` builtins to be a bit more generic to avoid platform-dependent error messages. - Fixed a bug where `Unifier` would copy some persistent types, tripping some internal assertions. - Type checking rules on relational operators is now a little bit more lax. - Improve dead code elimination for some `if` statements with complex always-false conditions ## New type solver - Dataflow analysis now generates phi nodes on exit of branches. - Dataflow analysis avoids producing a new definition for locals or properties that are not owned by that loop. - If a function parameter has been constrained to `never`, report errors at all uses of that parameter within that function. - Switch to using the new `Luau::Set` to replace `std::unordered_set` to alleviate some poor allocation characteristics which was negatively affecting overall performance. - Subtyping can now report many failing reasons instead of just the first one that we happened to find during the test. - Subtyping now also report reasons for type pack mismatches. - When visiting `if` statements or expressions, the resulting context are the common terms in both branches. ## Native codegen - Implement support for `buffer` builtins to its IR for x64 and A64. - Optimized `table.insert` by not inserting a table barrier if it is fastcalled with a constant. ## Internal Contributors Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Arseny Kapoulkine <arseny@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: Lily Brown <lbrown@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
2023-11-10 21:10:07 +00:00
emitWarning(
*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table without an array part is likely a bug", op);
else if (tty->indexer && isString(tty->indexer->indexType)) // note: to avoid complexity of subtype tests we just check if the key is a string
emitWarning(*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table with string keys is likely a bug", op);
}
void checkTableCall(AstExprCall* node, AstExprIndexName* func)
{
AstExpr** args = node->args.data;
if (func->index == "insert" && node->args.size == 2)
{
if (AstExprCall* tail = args[1]->as<AstExprCall>())
{
if (std::optional<TypeId> funty = context->getType(tail->func))
{
size_t ret = getReturnCount(follow(*funty));
if (ret > 1)
emitWarning(*context, LintWarning::Code_TableOperations, tail->location,
"table.insert may change behavior if the call returns more than one result; consider adding parentheses around second "
"argument");
}
}
}
if (func->index == "insert" && node->args.size >= 3)
{
// table.insert(t, 0, ?)
if (isConstant(args[1], 0.0))
emitWarning(*context, LintWarning::Code_TableOperations, args[1]->location,
"table.insert uses index 0 but arrays are 1-based; did you mean 1 instead?");
// table.insert(t, #t, ?)
if (isLength(args[1], args[0]))
emitWarning(*context, LintWarning::Code_TableOperations, args[1]->location,
"table.insert will insert the value before the last element, which is likely a bug; consider removing the second argument or "
"wrap it in parentheses to silence");
// table.insert(t, #t+1, ?)
if (AstExprBinary* add = args[1]->as<AstExprBinary>();
add && add->op == AstExprBinary::Add && isLength(add->left, args[0]) && isConstant(add->right, 1.0))
emitWarning(*context, LintWarning::Code_TableOperations, args[1]->location,
"table.insert will append the value to the table; consider removing the second argument for efficiency");
}
if (func->index == "remove" && node->args.size >= 2)
{
// table.remove(t, 0)
if (isConstant(args[1], 0.0))
emitWarning(*context, LintWarning::Code_TableOperations, args[1]->location,
"table.remove uses index 0 but arrays are 1-based; did you mean 1 instead?");
// note: it's tempting to check for table.remove(t, #t), which is equivalent to table.remove(t), but it's correct, occurs frequently,
// and also reads better.
// table.remove(t, #t-1)
if (AstExprBinary* sub = args[1]->as<AstExprBinary>();
sub && sub->op == AstExprBinary::Sub && isLength(sub->left, args[0]) && isConstant(sub->right, 1.0))
emitWarning(*context, LintWarning::Code_TableOperations, args[1]->location,
"table.remove will remove the value before the last element, which is likely a bug; consider removing the second argument or "
"wrap it in parentheses to silence");
}
if (func->index == "move" && node->args.size >= 4)
{
// table.move(t, 0, _, _)
if (isConstant(args[1], 0.0))
emitWarning(*context, LintWarning::Code_TableOperations, args[1]->location,
"table.move uses index 0 but arrays are 1-based; did you mean 1 instead?");
// table.move(t, _, _, 0)
else if (isConstant(args[3], 0.0))
emitWarning(*context, LintWarning::Code_TableOperations, args[3]->location,
"table.move uses index 0 but arrays are 1-based; did you mean 1 instead?");
}
2022-02-04 16:45:57 +00:00
if (func->index == "create" && node->args.size == 2)
{
// table.create(n, {...})
if (args[1]->is<AstExprTable>())
emitWarning(*context, LintWarning::Code_TableOperations, args[1]->location,
"table.create with a table literal will reuse the same object for all elements; consider using a for loop instead");
// table.create(n, {...} :: ?)
if (AstExprTypeAssertion* as = args[1]->as<AstExprTypeAssertion>(); as && as->expr->is<AstExprTable>())
emitWarning(*context, LintWarning::Code_TableOperations, as->expr->location,
"table.create with a table literal will reuse the same object for all elements; consider using a for loop instead");
}
}
bool isConstant(AstExpr* expr, double value)
{
AstExprConstantNumber* n = expr->as<AstExprConstantNumber>();
return n && n->value == value;
}
bool isLength(AstExpr* expr, AstExpr* table)
{
AstExprUnary* n = expr->as<AstExprUnary>();
return n && n->op == AstExprUnary::Len && similar(n->expr, table);
}
size_t getReturnCount(TypeId ty)
{
if (auto ftv = get<FunctionType>(ty))
2022-06-17 02:05:14 +01:00
return size(ftv->retTypes);
if (auto itv = get<IntersectionType>(ty))
{
// We don't process the type recursively to avoid having to deal with self-recursive intersection types
size_t result = 0;
for (TypeId part : itv->parts)
if (auto ftv = get<FunctionType>(follow(part)))
2022-06-17 02:05:14 +01:00
result = std::max(result, size(ftv->retTypes));
return result;
}
return 0;
}
};
class LintDuplicateCondition : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintDuplicateCondition pass{&context};
context.root->visit(&pass);
}
private:
LintContext* context;
LintDuplicateCondition(LintContext* context)
: context(context)
{
}
bool visit(AstStatIf* stat) override
{
if (!stat->elsebody)
return true;
if (!stat->elsebody->is<AstStatIf>())
return true;
// if..elseif chain detected, we need to unroll it
std::vector<AstExpr*> conditions;
conditions.reserve(2);
AstStatIf* head = stat;
while (head)
{
head->condition->visit(this);
head->thenbody->visit(this);
conditions.push_back(head->condition);
if (head->elsebody && head->elsebody->is<AstStatIf>())
{
head = head->elsebody->as<AstStatIf>();
continue;
}
if (head->elsebody)
head->elsebody->visit(this);
break;
}
detectDuplicates(conditions);
// block recursive visits so that we only analyze each chain once
return false;
}
2022-02-11 19:02:09 +00:00
bool visit(AstExprIfElse* expr) override
{
if (!expr->falseExpr->is<AstExprIfElse>())
return true;
// if..elseif chain detected, we need to unroll it
std::vector<AstExpr*> conditions;
conditions.reserve(2);
AstExprIfElse* head = expr;
while (head)
{
head->condition->visit(this);
head->trueExpr->visit(this);
conditions.push_back(head->condition);
if (head->falseExpr->is<AstExprIfElse>())
{
head = head->falseExpr->as<AstExprIfElse>();
continue;
}
head->falseExpr->visit(this);
break;
}
detectDuplicates(conditions);
// block recursive visits so that we only analyze each chain once
return false;
}
bool visit(AstExprBinary* expr) override
{
if (expr->op != AstExprBinary::And && expr->op != AstExprBinary::Or)
return true;
// for And expressions, it's idiomatic to use "a and a or b" as a ternary replacement, so we detect this pattern
if (expr->op == AstExprBinary::Or)
{
AstExprBinary* la = expr->left->as<AstExprBinary>();
if (la && la->op == AstExprBinary::And)
{
AstExprBinary* lb = la->left->as<AstExprBinary>();
AstExprBinary* rb = la->right->as<AstExprBinary>();
// check that the length of and-chain is exactly 2
if (!(lb && lb->op == AstExprBinary::And) && !(rb && rb->op == AstExprBinary::And))
{
la->left->visit(this);
la->right->visit(this);
expr->right->visit(this);
return false;
}
}
}
// unroll condition chain
std::vector<AstExpr*> conditions;
conditions.reserve(2);
extractOpChain(conditions, expr, expr->op);
detectDuplicates(conditions);
// block recursive visits so that we only analyze each chain once
return false;
}
void extractOpChain(std::vector<AstExpr*>& conditions, AstExpr* expr, AstExprBinary::Op op)
{
if (AstExprBinary* bin = expr->as<AstExprBinary>(); bin && bin->op == op)
{
extractOpChain(conditions, bin->left, op);
extractOpChain(conditions, bin->right, op);
}
else if (AstExprGroup* group = expr->as<AstExprGroup>())
{
extractOpChain(conditions, group->expr, op);
}
else
{
conditions.push_back(expr);
}
}
void detectDuplicates(const std::vector<AstExpr*>& conditions)
{
// Limit the distance at which we consider duplicates to reduce N^2 complexity to KN
const size_t kMaxDistance = 5;
for (size_t i = 0; i < conditions.size(); ++i)
{
for (size_t j = std::max(i, kMaxDistance) - kMaxDistance; j < i; ++j)
{
if (similar(conditions[j], conditions[i]))
{
if (conditions[i]->location.begin.line == conditions[j]->location.begin.line)
emitWarning(*context, LintWarning::Code_DuplicateCondition, conditions[i]->location,
"Condition has already been checked on column %d", conditions[j]->location.begin.column + 1);
else
emitWarning(*context, LintWarning::Code_DuplicateCondition, conditions[i]->location,
"Condition has already been checked on line %d", conditions[j]->location.begin.line + 1);
break;
}
}
}
}
};
class LintDuplicateLocal : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintDuplicateLocal pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
DenseHashMap<AstLocal*, AstNode*> locals;
LintDuplicateLocal()
: locals(nullptr)
{
}
bool visit(AstStatLocal* node) override
{
// early out for performance
if (node->vars.size == 1)
return true;
for (size_t i = 0; i < node->vars.size; ++i)
locals[node->vars.data[i]] = node;
for (size_t i = 0; i < node->vars.size; ++i)
{
AstLocal* local = node->vars.data[i];
if (local->shadow && locals[local->shadow] == node && !ignoreDuplicate(local))
{
if (local->shadow->location.begin.line == local->location.begin.line)
emitWarning(*context, LintWarning::Code_DuplicateLocal, local->location, "Variable '%s' already defined on column %d",
local->name.value, local->shadow->location.begin.column + 1);
else
emitWarning(*context, LintWarning::Code_DuplicateLocal, local->location, "Variable '%s' already defined on line %d",
local->name.value, local->shadow->location.begin.line + 1);
}
}
return true;
}
bool visit(AstExprFunction* node) override
{
if (node->self)
locals[node->self] = node;
for (size_t i = 0; i < node->args.size; ++i)
locals[node->args.data[i]] = node;
for (size_t i = 0; i < node->args.size; ++i)
{
AstLocal* local = node->args.data[i];
if (local->shadow && locals[local->shadow] == node && !ignoreDuplicate(local))
{
if (local->shadow == node->self)
emitWarning(*context, LintWarning::Code_DuplicateLocal, local->location, "Function parameter 'self' already defined implicitly");
else if (local->shadow->location.begin.line == local->location.begin.line)
emitWarning(*context, LintWarning::Code_DuplicateLocal, local->location, "Function parameter '%s' already defined on column %d",
local->name.value, local->shadow->location.begin.column + 1);
else
emitWarning(*context, LintWarning::Code_DuplicateLocal, local->location, "Function parameter '%s' already defined on line %d",
local->name.value, local->shadow->location.begin.line + 1);
}
}
return true;
}
bool ignoreDuplicate(AstLocal* local)
{
return local->name == "_";
}
};
2022-02-11 19:02:09 +00:00
class LintMisleadingAndOr : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintMisleadingAndOr pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
bool visit(AstExprBinary* node) override
{
if (node->op != AstExprBinary::Or)
return true;
AstExprBinary* and_ = node->left->as<AstExprBinary>();
if (!and_ || and_->op != AstExprBinary::And)
return true;
const char* alt = nullptr;
if (and_->right->is<AstExprConstantNil>())
alt = "nil";
else if (AstExprConstantBool* c = and_->right->as<AstExprConstantBool>(); c && c->value == false)
alt = "false";
if (alt)
emitWarning(*context, LintWarning::Code_MisleadingAndOr, node->location,
"The and-or expression always evaluates to the second alternative because the first alternative is %s; consider using if-then-else "
"expression instead",
alt);
return true;
}
};
2022-08-04 23:35:33 +01:00
class LintIntegerParsing : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintIntegerParsing pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
bool visit(AstExprConstantNumber* node) override
{
switch (node->parseResult)
{
case ConstantNumberParseResult::Ok:
case ConstantNumberParseResult::Malformed:
break;
case ConstantNumberParseResult::Imprecise:
emitWarning(*context, LintWarning::Code_IntegerParsing, node->location,
"Number literal exceeded available precision and was truncated to closest representable number");
break;
2022-08-04 23:35:33 +01:00
case ConstantNumberParseResult::BinOverflow:
emitWarning(*context, LintWarning::Code_IntegerParsing, node->location,
"Binary number literal exceeded available precision and was truncated to 2^64");
2022-08-04 23:35:33 +01:00
break;
case ConstantNumberParseResult::HexOverflow:
emitWarning(*context, LintWarning::Code_IntegerParsing, node->location,
"Hexadecimal number literal exceeded available precision and was truncated to 2^64");
2022-08-04 23:35:33 +01:00
break;
}
return true;
}
};
class LintComparisonPrecedence : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintComparisonPrecedence pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
static bool isEquality(AstExprBinary::Op op)
{
return op == AstExprBinary::CompareNe || op == AstExprBinary::CompareEq;
}
static bool isComparison(AstExprBinary::Op op)
{
return op == AstExprBinary::CompareNe || op == AstExprBinary::CompareEq || op == AstExprBinary::CompareLt || op == AstExprBinary::CompareLe ||
op == AstExprBinary::CompareGt || op == AstExprBinary::CompareGe;
}
static bool isNot(AstExpr* node)
{
AstExprUnary* expr = node->as<AstExprUnary>();
return expr && expr->op == AstExprUnary::Not;
}
bool visit(AstExprBinary* node) override
{
if (!isComparison(node->op))
return true;
// not X == Y; we silence this for not X == not Y as it's likely an intentional boolean comparison
if (isNot(node->left) && !isNot(node->right))
{
std::string op = toString(node->op);
if (isEquality(node->op))
emitWarning(*context, LintWarning::Code_ComparisonPrecedence, node->location,
"not X %s Y is equivalent to (not X) %s Y; consider using X %s Y, or add parentheses to silence", op.c_str(), op.c_str(),
node->op == AstExprBinary::CompareEq ? "~=" : "==");
else
emitWarning(*context, LintWarning::Code_ComparisonPrecedence, node->location,
"not X %s Y is equivalent to (not X) %s Y; add parentheses to silence", op.c_str(), op.c_str());
}
else if (AstExprBinary* left = node->left->as<AstExprBinary>(); left && isComparison(left->op))
{
std::string lop = toString(left->op);
std::string rop = toString(node->op);
if (isEquality(left->op) || isEquality(node->op))
emitWarning(*context, LintWarning::Code_ComparisonPrecedence, node->location,
"X %s Y %s Z is equivalent to (X %s Y) %s Z; add parentheses to silence", lop.c_str(), rop.c_str(), lop.c_str(), rop.c_str());
else
emitWarning(*context, LintWarning::Code_ComparisonPrecedence, node->location,
"X %s Y %s Z is equivalent to (X %s Y) %s Z; did you mean X %s Y and Y %s Z?", lop.c_str(), rop.c_str(), lop.c_str(), rop.c_str(),
lop.c_str(), rop.c_str());
}
return true;
}
};
static void fillBuiltinGlobals(LintContext& context, const AstNameTable& names, const ScopePtr& env)
{
ScopePtr current = env;
while (true)
{
for (auto& [global, binding] : current->bindings)
{
AstName name = names.get(global.c_str());
if (name.value)
{
auto& g = context.builtinGlobals[name];
g.type = binding.typeId;
if (binding.deprecated)
g.deprecated = binding.deprecatedSuggestion.c_str();
}
}
if (current->parent)
current = current->parent;
else
break;
}
}
2022-02-18 01:18:01 +00:00
static const char* fuzzyMatch(std::string_view str, const char** array, size_t size)
{
if (FInt::LuauSuggestionDistance == 0)
return nullptr;
size_t bestDistance = FInt::LuauSuggestionDistance;
size_t bestMatch = size;
for (size_t i = 0; i < size; ++i)
{
size_t ed = editDistance(str, array[i]);
if (ed <= bestDistance)
{
bestDistance = ed;
bestMatch = i;
}
}
return bestMatch < size ? array[bestMatch] : nullptr;
}
static void lintComments(LintContext& context, const std::vector<HotComment>& hotcomments)
{
bool seenMode = false;
for (const HotComment& hc : hotcomments)
{
// We reserve --!<space> for various informational (non-directive) comments
if (hc.content.empty() || hc.content[0] == ' ' || hc.content[0] == '\t')
continue;
if (!hc.header)
{
emitWarning(context, LintWarning::Code_CommentDirective, hc.location,
"Comment directive is ignored because it is placed after the first non-comment token");
}
else
{
2022-04-29 02:24:24 +01:00
size_t space = hc.content.find_first_of(" \t");
2022-02-18 01:18:01 +00:00
std::string_view first = std::string_view(hc.content).substr(0, space);
if (first == "nolint")
{
2022-04-29 02:24:24 +01:00
size_t notspace = hc.content.find_first_not_of(" \t", space);
2022-02-18 01:18:01 +00:00
if (space == std::string::npos || notspace == std::string::npos)
{
// disables all lints
}
else if (LintWarning::parseName(hc.content.c_str() + notspace) == LintWarning::Code_Unknown)
{
const char* rule = hc.content.c_str() + notspace;
// skip Unknown
if (const char* suggestion = fuzzyMatch(rule, kWarningNames + 1, LintWarning::Code__Count - 1))
emitWarning(context, LintWarning::Code_CommentDirective, hc.location,
"nolint directive refers to unknown lint rule '%s'; did you mean '%s'?", rule, suggestion);
else
emitWarning(
context, LintWarning::Code_CommentDirective, hc.location, "nolint directive refers to unknown lint rule '%s'", rule);
}
}
else if (first == "nocheck" || first == "nonstrict" || first == "strict")
{
if (space != std::string::npos)
emitWarning(context, LintWarning::Code_CommentDirective, hc.location,
"Comment directive with the type checking mode has extra symbols at the end of the line");
else if (seenMode)
emitWarning(context, LintWarning::Code_CommentDirective, hc.location,
"Comment directive with the type checking mode has already been used");
else
seenMode = true;
}
2022-07-14 23:52:26 +01:00
else if (first == "optimize")
{
size_t notspace = hc.content.find_first_not_of(" \t", space);
if (space == std::string::npos || notspace == std::string::npos)
emitWarning(context, LintWarning::Code_CommentDirective, hc.location, "optimize directive requires an optimization level");
else
{
const char* level = hc.content.c_str() + notspace;
if (strcmp(level, "0") && strcmp(level, "1") && strcmp(level, "2"))
emitWarning(context, LintWarning::Code_CommentDirective, hc.location,
"optimize directive uses unknown optimization level '%s', 0..2 expected", level);
}
}
else if (first == "native")
Sync to upstream/release/588 (#992) Type checker/autocomplete: * `Luau::autocomplete` no longer performs typechecking internally, make sure to run `Frontend::check` before performing autocomplete requests * Autocomplete string suggestions without "" are now only suggested inside the "" * Autocomplete suggestions now include `function (anonymous autofilled)` key with a full suggestion for the function expression (with arguments included) stored in `AutocompleteEntry::insertText` * `AutocompleteEntry::indexedWithSelf` is provided for function call suggestions made with `:` * Cyclic modules now see each other type exports as `any` to prevent memory use-after-free (similar to module return type) Runtime: * Updated inline/loop unroll cost model to better handle assignments (Fixes https://github.com/Roblox/luau/issues/978) * `math.noise` speed was improved by ~30% * `table.concat` speed was improved by ~5-7% * `tonumber` and `tostring` now have fastcall paths that execute ~1.5x and ~2.5x faster respectively (fixes #777) * Fixed crash in `luaL_typename` when index refers to a non-existing value * Fixed potential out of memory scenario when using `string.sub` or `string.char` in a loop * Fixed behavior of some fastcall builtins when called without arguments under -O2 to match original functions * Support for native code execution in VM is now enabled by default (note: native code still has to be generated explicitly) * `Codegen::compile` now accepts `CodeGen_OnlyNativeModules` flag. When set, only modules that have a `--!native` hot-comment at the top will be compiled to native code In our new typechecker: * Generic type packs are no longer considered to be variadic during unification * Timeout and cancellation now works in new solver * Fixed false positive errors around 'table' and 'function' type refinements * Table literals now use covariant unification rules. This is sound since literal has no type specified and has no aliases * Fixed issues with blocked types escaping the constraint solver * Fixed more places where error messages that should've been suppressed were still reported * Fixed errors when iterating over a top table type In our native code generation (jit): * 'DebugLuauAbortingChecks' flag is now supported on A64 * LOP_NEWCLOSURE has been translated to IR
2023-07-28 16:13:53 +01:00
{
if (space != std::string::npos)
emitWarning(
context, LintWarning::Code_CommentDirective, hc.location, "native directive has extra symbols at the end of the line");
Sync to upstream/release/588 (#992) Type checker/autocomplete: * `Luau::autocomplete` no longer performs typechecking internally, make sure to run `Frontend::check` before performing autocomplete requests * Autocomplete string suggestions without "" are now only suggested inside the "" * Autocomplete suggestions now include `function (anonymous autofilled)` key with a full suggestion for the function expression (with arguments included) stored in `AutocompleteEntry::insertText` * `AutocompleteEntry::indexedWithSelf` is provided for function call suggestions made with `:` * Cyclic modules now see each other type exports as `any` to prevent memory use-after-free (similar to module return type) Runtime: * Updated inline/loop unroll cost model to better handle assignments (Fixes https://github.com/Roblox/luau/issues/978) * `math.noise` speed was improved by ~30% * `table.concat` speed was improved by ~5-7% * `tonumber` and `tostring` now have fastcall paths that execute ~1.5x and ~2.5x faster respectively (fixes #777) * Fixed crash in `luaL_typename` when index refers to a non-existing value * Fixed potential out of memory scenario when using `string.sub` or `string.char` in a loop * Fixed behavior of some fastcall builtins when called without arguments under -O2 to match original functions * Support for native code execution in VM is now enabled by default (note: native code still has to be generated explicitly) * `Codegen::compile` now accepts `CodeGen_OnlyNativeModules` flag. When set, only modules that have a `--!native` hot-comment at the top will be compiled to native code In our new typechecker: * Generic type packs are no longer considered to be variadic during unification * Timeout and cancellation now works in new solver * Fixed false positive errors around 'table' and 'function' type refinements * Table literals now use covariant unification rules. This is sound since literal has no type specified and has no aliases * Fixed issues with blocked types escaping the constraint solver * Fixed more places where error messages that should've been suppressed were still reported * Fixed errors when iterating over a top table type In our native code generation (jit): * 'DebugLuauAbortingChecks' flag is now supported on A64 * LOP_NEWCLOSURE has been translated to IR
2023-07-28 16:13:53 +01:00
}
2022-02-18 01:18:01 +00:00
else
{
static const char* kHotComments[] = {
"nolint",
"nocheck",
"nonstrict",
"strict",
2022-07-14 23:52:26 +01:00
"optimize",
Sync to upstream/release/588 (#992) Type checker/autocomplete: * `Luau::autocomplete` no longer performs typechecking internally, make sure to run `Frontend::check` before performing autocomplete requests * Autocomplete string suggestions without "" are now only suggested inside the "" * Autocomplete suggestions now include `function (anonymous autofilled)` key with a full suggestion for the function expression (with arguments included) stored in `AutocompleteEntry::insertText` * `AutocompleteEntry::indexedWithSelf` is provided for function call suggestions made with `:` * Cyclic modules now see each other type exports as `any` to prevent memory use-after-free (similar to module return type) Runtime: * Updated inline/loop unroll cost model to better handle assignments (Fixes https://github.com/Roblox/luau/issues/978) * `math.noise` speed was improved by ~30% * `table.concat` speed was improved by ~5-7% * `tonumber` and `tostring` now have fastcall paths that execute ~1.5x and ~2.5x faster respectively (fixes #777) * Fixed crash in `luaL_typename` when index refers to a non-existing value * Fixed potential out of memory scenario when using `string.sub` or `string.char` in a loop * Fixed behavior of some fastcall builtins when called without arguments under -O2 to match original functions * Support for native code execution in VM is now enabled by default (note: native code still has to be generated explicitly) * `Codegen::compile` now accepts `CodeGen_OnlyNativeModules` flag. When set, only modules that have a `--!native` hot-comment at the top will be compiled to native code In our new typechecker: * Generic type packs are no longer considered to be variadic during unification * Timeout and cancellation now works in new solver * Fixed false positive errors around 'table' and 'function' type refinements * Table literals now use covariant unification rules. This is sound since literal has no type specified and has no aliases * Fixed issues with blocked types escaping the constraint solver * Fixed more places where error messages that should've been suppressed were still reported * Fixed errors when iterating over a top table type In our native code generation (jit): * 'DebugLuauAbortingChecks' flag is now supported on A64 * LOP_NEWCLOSURE has been translated to IR
2023-07-28 16:13:53 +01:00
"native",
2022-02-18 01:18:01 +00:00
};
if (const char* suggestion = fuzzyMatch(first, kHotComments, std::size(kHotComments)))
emitWarning(context, LintWarning::Code_CommentDirective, hc.location, "Unknown comment directive '%.*s'; did you mean '%s'?",
int(first.size()), first.data(), suggestion);
else
emitWarning(context, LintWarning::Code_CommentDirective, hc.location, "Unknown comment directive '%.*s'", int(first.size()),
first.data());
}
}
}
}
static bool hasNativeCommentDirective(const std::vector<HotComment>& hotcomments)
{
LUAU_ASSERT(FFlag::LuauNativeAttribute);
LUAU_ASSERT(FFlag::LintRedundantNativeAttribute);
for (const HotComment& hc : hotcomments)
{
if (hc.content.empty() || hc.content[0] == ' ' || hc.content[0] == '\t')
continue;
if (hc.header)
{
size_t space = hc.content.find_first_of(" \t");
std::string_view first = std::string_view(hc.content).substr(0, space);
if (first == "native")
return true;
}
}
return false;
}
struct LintRedundantNativeAttribute : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LUAU_ASSERT(FFlag::LuauNativeAttribute);
LUAU_ASSERT(FFlag::LintRedundantNativeAttribute);
LintRedundantNativeAttribute pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
bool visit(AstExprFunction* node) override
{
node->body->visit(this);
for (const auto attribute : node->attributes)
{
if (attribute->type == AstAttr::Type::Native)
{
emitWarning(*context, LintWarning::Code_RedundantNativeAttribute, attribute->location,
"native attribute on a function is redundant in a native module; consider removing it");
}
}
return false;
}
};
2022-02-18 01:18:01 +00:00
std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const ScopePtr& env, const Module* module,
const std::vector<HotComment>& hotcomments, const LintOptions& options)
{
LintContext context;
context.options = options;
context.root = root;
context.placeholder = names.get("_");
context.scope = env;
context.module = module;
fillBuiltinGlobals(context, names, env);
if (context.warningEnabled(LintWarning::Code_UnknownGlobal) || context.warningEnabled(LintWarning::Code_DeprecatedGlobal) ||
context.warningEnabled(LintWarning::Code_GlobalUsedAsLocal) || context.warningEnabled(LintWarning::Code_PlaceholderRead) ||
context.warningEnabled(LintWarning::Code_BuiltinGlobalWrite))
{
LintGlobalLocal::process(context);
}
if (context.warningEnabled(LintWarning::Code_MultiLineStatement))
LintMultiLineStatement::process(context);
if (context.warningEnabled(LintWarning::Code_SameLineStatement))
LintSameLineStatement::process(context);
if (context.warningEnabled(LintWarning::Code_LocalShadow) || context.warningEnabled(LintWarning::Code_FunctionUnused) ||
context.warningEnabled(LintWarning::Code_ImportUnused) || context.warningEnabled(LintWarning::Code_LocalUnused))
{
LintLocalHygiene::process(context);
}
if (context.warningEnabled(LintWarning::Code_FunctionUnused))
LintUnusedFunction::process(context);
if (context.warningEnabled(LintWarning::Code_UnreachableCode))
LintUnreachableCode::process(context);
if (context.warningEnabled(LintWarning::Code_UnknownType))
LintUnknownType::process(context);
if (context.warningEnabled(LintWarning::Code_ForRange))
LintForRange::process(context);
if (context.warningEnabled(LintWarning::Code_UnbalancedAssignment))
LintUnbalancedAssignment::process(context);
if (context.warningEnabled(LintWarning::Code_ImplicitReturn))
LintImplicitReturn::process(context);
if (context.warningEnabled(LintWarning::Code_FormatString))
LintFormatString::process(context);
if (context.warningEnabled(LintWarning::Code_TableLiteral))
LintTableLiteral::process(context);
if (context.warningEnabled(LintWarning::Code_UninitializedLocal))
LintUninitializedLocal::process(context);
if (context.warningEnabled(LintWarning::Code_DuplicateFunction))
LintDuplicateFunction::process(context);
if (context.warningEnabled(LintWarning::Code_DeprecatedApi))
LintDeprecatedApi::process(context);
if (context.warningEnabled(LintWarning::Code_TableOperations))
LintTableOperations::process(context);
if (context.warningEnabled(LintWarning::Code_DuplicateCondition))
LintDuplicateCondition::process(context);
if (context.warningEnabled(LintWarning::Code_DuplicateLocal))
LintDuplicateLocal::process(context);
2022-02-11 19:02:09 +00:00
if (context.warningEnabled(LintWarning::Code_MisleadingAndOr))
LintMisleadingAndOr::process(context);
2022-02-18 01:18:01 +00:00
if (context.warningEnabled(LintWarning::Code_CommentDirective))
lintComments(context, hotcomments);
2022-08-04 23:35:33 +01:00
if (context.warningEnabled(LintWarning::Code_IntegerParsing))
LintIntegerParsing::process(context);
if (context.warningEnabled(LintWarning::Code_ComparisonPrecedence))
LintComparisonPrecedence::process(context);
if (FFlag::LuauNativeAttribute && FFlag::LintRedundantNativeAttribute &&
context.warningEnabled(LintWarning::Code_RedundantNativeAttribute))
{
if (hasNativeCommentDirective(hotcomments))
LintRedundantNativeAttribute::process(context);
}
std::sort(context.result.begin(), context.result.end(), WarningComparator());
return context.result;
}
std::vector<AstName> getDeprecatedGlobals(const AstNameTable& names)
{
LintContext context;
std::vector<AstName> result;
result.reserve(context.builtinGlobals.size());
for (auto& p : context.builtinGlobals)
if (p.second.deprecated)
result.push_back(p.first);
return result;
}
void fuzzFormatString(const char* data, size_t size)
{
LintFormatString::fuzz(data, size);
}
} // namespace Luau