local parser = require "luacheck.parser"
|
local utils = require "luacheck.utils"
|
|
local stage = {}
|
|
local function redefined_warning(message_format)
|
return {
|
message_format = message_format,
|
fields = {"name", "prev_line", "prev_column", "prev_end_column", "self"}
|
}
|
end
|
|
stage.warnings = {
|
["411"] = redefined_warning("variable {name!} was previously defined on line {prev_line}"),
|
["412"] = redefined_warning("variable {name!} was previously defined as an argument on line {prev_line}"),
|
["413"] = redefined_warning("variable {name!} was previously defined as a loop variable on line {prev_line}"),
|
["421"] = redefined_warning("shadowing definition of variable {name!} on line {prev_line}"),
|
["422"] = redefined_warning("shadowing definition of argument {name!} on line {prev_line}"),
|
["423"] = redefined_warning("shadowing definition of loop variable {name!} on line {prev_line}"),
|
["431"] = redefined_warning("shadowing upvalue {name!} on line {prev_line}"),
|
["432"] = redefined_warning("shadowing upvalue argument {name!} on line {prev_line}"),
|
["433"] = redefined_warning("shadowing upvalue loop variable {name!} on line {prev_line}"),
|
["521"] = {message_format = "unused label {label!}", fields = {"label"}}
|
}
|
|
local type_codes = {
|
var = "1",
|
func = "1",
|
arg = "2",
|
loop = "3",
|
loopi = "3"
|
}
|
|
local function warn_redefined(chstate, var, prev_var, is_same_scope)
|
local code = "4" .. (is_same_scope and "1" or var.line == prev_var.line and "2" or "3") .. type_codes[prev_var.type]
|
|
chstate:warn_var(code, var, {
|
self = var.self and prev_var.self,
|
prev_line = prev_var.node.line,
|
prev_column = chstate:offset_to_column(prev_var.node.line, prev_var.node.offset),
|
prev_end_column = chstate:offset_to_column(prev_var.node.line, prev_var.node.end_offset)
|
})
|
end
|
|
local function warn_unused_label(chstate, label)
|
chstate:warn_range("521", label.range, {
|
label = label.name
|
})
|
end
|
|
local pseudo_labels = utils.array_to_set({"do", "else", "break", "end", "return"})
|
|
local Line = utils.class()
|
|
function Line:__init(node, parent, value)
|
-- Maps variables to arrays of accessing items.
|
self.accessed_upvalues = {}
|
-- Maps variables to arrays of mutating items.
|
self.mutated_upvalues = {}
|
-- Maps variables to arays of setting items.
|
self.set_upvalues = {}
|
self.lines = {}
|
self.node = node
|
self.parent = parent
|
self.value = value
|
self.items = utils.Stack()
|
end
|
|
-- Calls callback with line, index, item, ... for each item reachable from starting item.
|
-- `visited` is a set of already visited indexes.
|
-- Callback can return true to stop walking from current item.
|
function Line:walk(visited, index, callback, ...)
|
if visited[index] then
|
return
|
end
|
|
visited[index] = true
|
|
local item = self.items[index]
|
|
if callback(self, index, item, ...) then
|
return
|
end
|
|
if not item then
|
return
|
elseif item.tag == "Jump" then
|
return self:walk(visited, item.to, callback, ...)
|
elseif item.tag == "Cjump" then
|
self:walk(visited, item.to, callback, ...)
|
end
|
|
return self:walk(visited, index + 1, callback, ...)
|
end
|
|
local function new_scope(line)
|
return {
|
vars = {},
|
labels = {},
|
gotos = {},
|
line = line
|
}
|
end
|
|
local function new_var(line, node, type_)
|
return {
|
name = node[1],
|
node = node,
|
type = type_,
|
self = node.implicit,
|
line = line,
|
scope_start = line.items.size + 1,
|
values = {}
|
}
|
end
|
|
local function new_value(var_node, value_node, item, is_init)
|
local value = {
|
var = var_node.var,
|
var_node = var_node,
|
type = is_init and var_node.var.type or "var",
|
node = value_node,
|
using_lines = {},
|
empty = is_init and not value_node and (var_node.var.type == "var"),
|
item = item
|
}
|
|
if value_node and value_node.tag == "Function" then
|
value.type = "func"
|
value_node.value = value
|
end
|
|
return value
|
end
|
|
local function new_label(line, name, range)
|
return {
|
name = name,
|
range = range,
|
index = line.items.size + 1
|
}
|
end
|
|
local function new_goto(name, jump, range)
|
return {
|
name = name,
|
jump = jump,
|
range = range
|
}
|
end
|
|
local function new_jump_item(is_conditional)
|
return {
|
tag = is_conditional and "Cjump" or "Jump"
|
}
|
end
|
|
local function new_eval_item(node)
|
return {
|
tag = "Eval",
|
node = node,
|
accesses = {},
|
used_values = {},
|
lines = {}
|
}
|
end
|
|
local function new_noop_item(node, loop_end)
|
return {
|
tag = "Noop",
|
node = node,
|
loop_end = loop_end
|
}
|
end
|
|
local function new_local_item(node)
|
return {
|
tag = "Local",
|
node = node,
|
lhs = node[1],
|
rhs = node[2],
|
accesses = node[2] and {},
|
used_values = node[2] and {},
|
lines = node[2] and {}
|
}
|
end
|
|
local function new_set_item(node)
|
return {
|
tag = "Set",
|
node = node,
|
lhs = node[1],
|
rhs = node[2],
|
accesses = {},
|
mutations = {},
|
used_values = {},
|
lines = {}
|
}
|
end
|
|
local function is_unpacking(node)
|
return node.tag == "Dots" or node.tag == "Call" or node.tag == "Invoke"
|
end
|
|
local LinState = utils.class()
|
|
function LinState:__init(chstate)
|
self.chstate = chstate
|
self.lines = utils.Stack()
|
self.scopes = utils.Stack()
|
end
|
|
function LinState:enter_scope()
|
self.scopes:push(new_scope(self.lines.top))
|
end
|
|
function LinState:leave_scope()
|
local left_scope = self.scopes:pop()
|
local prev_scope = self.scopes.top
|
|
for _, goto_ in ipairs(left_scope.gotos) do
|
local label = left_scope.labels[goto_.name]
|
|
if label then
|
goto_.jump.to = label.index
|
label.used = true
|
else
|
if not prev_scope or prev_scope.line ~= self.lines.top then
|
if goto_.name == "break" then
|
parser.syntax_error("'break' is not inside a loop", goto_.range)
|
else
|
parser.syntax_error(("no visible label '%s'"):format(goto_.name), goto_.range)
|
end
|
end
|
|
table.insert(prev_scope.gotos, goto_)
|
end
|
end
|
|
for name, label in pairs(left_scope.labels) do
|
if not label.used and not pseudo_labels[name] then
|
warn_unused_label(self.chstate, label)
|
end
|
end
|
|
for _, var in pairs(left_scope.vars) do
|
var.scope_end = self.lines.top.items.size
|
end
|
end
|
|
function LinState:register_var(node, type_)
|
local var = new_var(self.lines.top, node, type_)
|
local prev_var = self:resolve_var(var.name)
|
|
if prev_var then
|
local is_same_scope = self.scopes.top.vars[var.name]
|
|
if var.name ~= "..." then
|
warn_redefined(self.chstate, var, prev_var, is_same_scope)
|
end
|
|
if is_same_scope then
|
prev_var.scope_end = self.lines.top.items.size
|
end
|
end
|
|
self.scopes.top.vars[var.name] = var
|
node.var = var
|
return var
|
end
|
|
function LinState:register_vars(nodes, type_)
|
for _, node in ipairs(nodes) do
|
self:register_var(node, type_)
|
end
|
end
|
|
function LinState:resolve_var(name)
|
for _, scope in utils.ripairs(self.scopes) do
|
local var = scope.vars[name]
|
|
if var then
|
return var
|
end
|
end
|
end
|
|
function LinState:check_var(node)
|
if not node.var then
|
node.var = self:resolve_var(node[1])
|
end
|
|
return node.var
|
end
|
|
function LinState:register_label(name, range)
|
local prev_label = self.scopes.top.labels[name]
|
|
if prev_label then
|
assert(not pseudo_labels[name])
|
parser.syntax_error(("label '%s' already defined on line %d"):format(
|
name, prev_label.range.line), range, prev_label.range)
|
end
|
|
self.scopes.top.labels[name] = new_label(self.lines.top, name, range)
|
end
|
|
function LinState:emit(item)
|
self.lines.top.items:push(item)
|
end
|
|
function LinState:emit_goto(name, is_conditional, range)
|
local jump = new_jump_item(is_conditional)
|
self:emit(jump)
|
table.insert(self.scopes.top.gotos, new_goto(name, jump, range))
|
end
|
|
local tag_to_boolean = {
|
Nil = false, False = false,
|
True = true, Number = true, String = true, Table = true, Function = true
|
}
|
|
-- Emits goto that jumps to ::name:: if bool(cond_node) == false.
|
function LinState:emit_cond_goto(name, cond_node)
|
local cond_bool = tag_to_boolean[cond_node.tag]
|
|
if cond_bool ~= true then
|
self:emit_goto(name, cond_bool ~= false)
|
end
|
end
|
|
function LinState:emit_noop(node, loop_end)
|
self:emit(new_noop_item(node, loop_end))
|
end
|
|
function LinState:emit_stmt(stmt)
|
self["emit_stmt_" .. stmt.tag](self, stmt)
|
end
|
|
function LinState:emit_stmts(stmts)
|
for _, stmt in ipairs(stmts) do
|
self:emit_stmt(stmt)
|
end
|
end
|
|
function LinState:emit_block(block)
|
self:enter_scope()
|
self:emit_stmts(block)
|
self:leave_scope()
|
end
|
|
function LinState:emit_stmt_Do(node)
|
self:emit_noop(node)
|
self:emit_block(node)
|
end
|
|
function LinState:emit_stmt_While(node)
|
self:emit_noop(node)
|
self:enter_scope()
|
self:register_label("do")
|
self:emit_expr(node[1])
|
self:emit_cond_goto("break", node[1])
|
self:emit_block(node[2])
|
self:emit_noop(node, true)
|
self:emit_goto("do")
|
self:register_label("break")
|
self:leave_scope()
|
end
|
|
function LinState:emit_stmt_Repeat(node)
|
self:emit_noop(node)
|
self:enter_scope()
|
self:register_label("do")
|
self:enter_scope()
|
self:emit_stmts(node[1])
|
self:emit_expr(node[2])
|
self:leave_scope()
|
self:emit_cond_goto("do", node[2])
|
self:register_label("break")
|
self:leave_scope()
|
end
|
|
function LinState:emit_stmt_Fornum(node)
|
self:emit_noop(node)
|
self:emit_expr(node[2])
|
self:emit_expr(node[3])
|
|
if node[5] then
|
self:emit_expr(node[4])
|
end
|
|
self:enter_scope()
|
self:register_label("do")
|
self:emit_goto("break", true)
|
self:enter_scope()
|
self:emit(new_local_item({{node[1]}}))
|
self:register_var(node[1], "loopi")
|
self:emit_stmts(node[5] or node[4])
|
self:leave_scope()
|
self:emit_noop(node, true)
|
self:emit_goto("do")
|
self:register_label("break")
|
self:leave_scope()
|
end
|
|
function LinState:emit_stmt_Forin(node)
|
self:emit_noop(node)
|
self:emit_exprs(node[2])
|
self:enter_scope()
|
self:register_label("do")
|
self:emit_goto("break", true)
|
self:enter_scope()
|
self:emit(new_local_item({node[1]}))
|
self:register_vars(node[1], "loop")
|
self:emit_stmts(node[3])
|
self:leave_scope()
|
self:emit_noop(node, true)
|
self:emit_goto("do")
|
self:register_label("break")
|
self:leave_scope()
|
end
|
|
function LinState:emit_stmt_If(node)
|
self:emit_noop(node)
|
self:enter_scope()
|
|
for i = 1, #node - 1, 2 do
|
self:enter_scope()
|
self:emit_expr(node[i])
|
self:emit_cond_goto("else", node[i])
|
self:emit_block(node[i + 1])
|
self:emit_goto("end")
|
self:register_label("else")
|
self:leave_scope()
|
end
|
|
if #node % 2 == 1 then
|
self:emit_block(node[#node])
|
end
|
|
self:register_label("end")
|
self:leave_scope()
|
end
|
|
function LinState:emit_stmt_Label(node)
|
self:register_label(node[1], node)
|
end
|
|
function LinState:emit_stmt_Goto(node)
|
self:emit_noop(node)
|
self:emit_goto(node[1], false, node)
|
end
|
|
function LinState:emit_stmt_Break(node)
|
self:emit_goto("break", false, node)
|
end
|
|
function LinState:emit_stmt_Return(node)
|
self:emit_noop(node)
|
self:emit_exprs(node)
|
self:emit_goto("return")
|
end
|
|
function LinState:emit_expr(node)
|
local item = new_eval_item(node)
|
self:scan_expr(item, node)
|
self:emit(item)
|
end
|
|
function LinState:emit_exprs(exprs)
|
for _, expr in ipairs(exprs) do
|
self:emit_expr(expr)
|
end
|
end
|
|
LinState.emit_stmt_Call = LinState.emit_expr
|
LinState.emit_stmt_Invoke = LinState.emit_expr
|
|
function LinState:emit_stmt_Local(node)
|
local item = new_local_item(node)
|
self:emit(item)
|
|
if node[2] then
|
self:scan_exprs(item, node[2])
|
end
|
|
self:register_vars(node[1], "var")
|
end
|
|
function LinState:emit_stmt_Localrec(node)
|
local item = new_local_item(node)
|
self:register_var(node[1][1], "var")
|
self:emit(item)
|
self:scan_expr(item, node[2][1])
|
end
|
|
function LinState:emit_stmt_Set(node)
|
local item = new_set_item(node)
|
self:scan_exprs(item, node[2])
|
|
for _, expr in ipairs(node[1]) do
|
if expr.tag == "Id" then
|
local var = self:check_var(expr)
|
|
if var then
|
self:register_upvalue_action(item, var, "set_upvalues")
|
end
|
else
|
assert(expr.tag == "Index")
|
self:scan_lhs_index(item, expr)
|
end
|
end
|
|
self:emit(item)
|
end
|
|
|
function LinState:scan_expr(item, node)
|
local scanner = self["scan_expr_" .. node.tag]
|
|
if scanner then
|
scanner(self, item, node)
|
end
|
end
|
|
function LinState:scan_exprs(item, nodes)
|
for _, node in ipairs(nodes) do
|
self:scan_expr(item, node)
|
end
|
end
|
|
function LinState:register_upvalue_action(item, var, key)
|
for _, line in utils.ripairs(self.lines) do
|
if line == var.line then
|
break
|
end
|
|
if not line[key][var] then
|
line[key][var] = {}
|
end
|
|
table.insert(line[key][var], item)
|
end
|
end
|
|
function LinState:mark_access(item, node)
|
node.var.accessed = true
|
|
if not item.accesses[node.var] then
|
item.accesses[node.var] = {}
|
end
|
|
table.insert(item.accesses[node.var], node)
|
self:register_upvalue_action(item, node.var, "accessed_upvalues")
|
end
|
|
function LinState:mark_mutation(item, node)
|
node.var.mutated = true
|
|
if not item.mutations[node.var] then
|
item.mutations[node.var] = {}
|
end
|
|
table.insert(item.mutations[node.var], node)
|
self:register_upvalue_action(item, node.var, "mutated_upvalues")
|
end
|
|
function LinState:scan_expr_Id(item, node)
|
if self:check_var(node) then
|
self:mark_access(item, node)
|
end
|
end
|
|
function LinState:scan_expr_Dots(item, node)
|
local dots = self:check_var(node)
|
|
if not dots or dots.line ~= self.lines.top then
|
parser.syntax_error("cannot use '...' outside a vararg function", node)
|
end
|
|
self:mark_access(item, node)
|
end
|
|
function LinState:scan_lhs_index(item, node)
|
if node[1].tag == "Id" then
|
if self:check_var(node[1]) then
|
self:mark_mutation(item, node[1])
|
end
|
elseif node[1].tag == "Index" then
|
self:scan_lhs_index(item, node[1])
|
else
|
self:scan_expr(item, node[1])
|
end
|
|
self:scan_expr(item, node[2])
|
end
|
|
LinState.scan_expr_Index = LinState.scan_exprs
|
LinState.scan_expr_Call = LinState.scan_exprs
|
LinState.scan_expr_Invoke = LinState.scan_exprs
|
LinState.scan_expr_Paren = LinState.scan_exprs
|
LinState.scan_expr_Table = LinState.scan_exprs
|
LinState.scan_expr_Pair = LinState.scan_exprs
|
|
function LinState:scan_expr_Op(item, node)
|
self:scan_expr(item, node[2])
|
|
if node[3] then
|
self:scan_expr(item, node[3])
|
end
|
end
|
|
-- Puts tables {var = value{} into field `set_variables` of items in line which set values.
|
-- Registers set values in field `values` of variables.
|
function LinState:register_set_variables()
|
local line = self.lines.top
|
|
for _, item in ipairs(line.items) do
|
if item.tag == "Local" or item.tag == "Set" then
|
item.set_variables = {}
|
|
local is_init = item.tag == "Local"
|
local unpacking_item -- Rightmost item of rhs which may unpack into several lhs items.
|
|
if item.rhs then
|
local last_rhs_item = item.rhs[#item.rhs]
|
|
if is_unpacking(last_rhs_item) then
|
unpacking_item = last_rhs_item
|
end
|
end
|
|
local secondaries -- Array of values unpacked from rightmost rhs item.
|
|
if unpacking_item and (#item.lhs > #item.rhs) then
|
secondaries = {}
|
end
|
|
for i, node in ipairs(item.lhs) do
|
local value
|
|
if node.var then
|
value = new_value(node, item.rhs and item.rhs[i] or unpacking_item, item, is_init)
|
item.set_variables[node.var] = value
|
table.insert(node.var.values, value)
|
end
|
|
if secondaries and (i >= #item.rhs) then
|
if value then
|
value.secondaries = secondaries
|
table.insert(secondaries, value)
|
else
|
-- If one of secondary values is assigned to a global or index,
|
-- it is considered used.
|
secondaries.used = true
|
end
|
end
|
end
|
end
|
end
|
end
|
|
function LinState:build_line(node)
|
self.lines:push(Line(node, self.lines.top))
|
self:enter_scope()
|
self:emit(new_local_item({node[1]}))
|
self:enter_scope()
|
self:register_vars(node[1], "arg")
|
self:emit_stmts(node[2])
|
self:leave_scope()
|
self:register_label("return")
|
self:leave_scope()
|
self:register_set_variables()
|
local line = self.lines:pop()
|
|
for _, prev_line in ipairs(self.lines) do
|
table.insert(prev_line.lines, line)
|
end
|
|
return line
|
end
|
|
function LinState:scan_expr_Function(item, node)
|
local line = self:build_line(node)
|
table.insert(item.lines, line)
|
|
for _, nested_line in ipairs(line.lines) do
|
table.insert(item.lines, nested_line)
|
end
|
end
|
|
-- Builds linear representation (line) of AST and assigns it as `chstate.top_line`.
|
-- Assings an array of all lines as `chstate.lines`.
|
-- Adds warnings for redefined/shadowed locals and unused labels.
|
function stage.run(chstate)
|
local linstate = LinState(chstate)
|
chstate.top_line = linstate:build_line({{{tag = "Dots", "..."}}, chstate.ast})
|
assert(linstate.lines.size == 0)
|
assert(linstate.scopes.size == 0)
|
|
chstate.lines = {chstate.top_line}
|
|
for _, nested_line in ipairs(chstate.top_line.lines) do
|
table.insert(chstate.lines, nested_line)
|
end
|
end
|
|
return stage
|