local utils = require "luacheck.utils"
|
|
local stage = {}
|
|
local function prefix_if_indirect(message)
|
return function(warning)
|
if warning.indirect then
|
return "indirectly " .. message
|
else
|
return message
|
end
|
end
|
end
|
|
local function setting_global_format_message(warning)
|
-- `module` field is set during filtering.
|
if warning.module then
|
return "setting non-module global variable {name!}"
|
else
|
return "setting non-standard global variable {name!}"
|
end
|
end
|
local global_warning_fields = {"name", "indexing", "previous_indexing_len", "top", "indirect"}
|
|
stage.warnings = {
|
["111"] = {message_format = setting_global_format_message, fields = global_warning_fields},
|
["112"] = {message_format = "mutating non-standard global variable {name!}", fields = global_warning_fields},
|
["113"] = {message_format = "accessing undefined variable {name!}", fields = global_warning_fields},
|
-- The following warnings are added during filtering.
|
["121"] = {message_format = "setting read-only global variable {name!}", fields = {}},
|
["122"] = {message_format = prefix_if_indirect("setting read-only field {field!} of global {name!}"), fields = {}},
|
["131"] = {message_format = "unused global variable {name!}", fields = {}},
|
["142"] = {message_format = prefix_if_indirect("setting undefined field {field!} of global {name!}"), fields = {}},
|
["143"] = {message_format = prefix_if_indirect("accessing undefined field {field!} of global {name!}"), fields = {}}
|
}
|
|
local action_codes = {
|
set = "1",
|
mutate = "2",
|
access = "3"
|
}
|
|
-- `index` describes an indexing, where `index[1]` is a global node
|
-- and other items describe keys: each one is a string node, "not_string",
|
-- or "unknown". `node` is literal base node that's indexed.
|
-- E.g. in `local a = table.a; a.b = "c"` `node` is `a` node of the second
|
-- statement and `index` describes `table.a.b`.
|
-- `index.previous_indexing_len` is optional length of prefix of `index` array representing last assignment
|
-- in the aliasing chain, e.g. `2` in the previous example (because last indexing is `table.a`).
|
local function warn_global(chstate, node, index, is_lhs, is_top_line)
|
local global = index[1]
|
local action = is_lhs and (#index == 1 and "set" or "mutate") or "access"
|
|
local indexing
|
|
if #index > 1 then
|
indexing = {}
|
|
for i, field in ipairs(index) do
|
if i > 1 then
|
if field == "unknown" then
|
indexing[i - 1] = true
|
elseif field == "not_string" then
|
indexing[i - 1] = false
|
else
|
indexing[i - 1] = field[1]
|
end
|
end
|
end
|
end
|
|
chstate:warn_range("11" .. action_codes[action], node, {
|
name = global[1],
|
indexing = indexing,
|
previous_indexing_len = index.previous_indexing_len,
|
top = is_top_line and action == "set" or nil,
|
indirect = node ~= global or nil
|
})
|
end
|
|
local function resolved_to_index(resolution)
|
return resolution ~= "unknown" and resolution ~= "not_string" and resolution.tag ~= "String"
|
end
|
|
local literal_tags = utils.array_to_set({"Nil", "True", "False", "Number", "String", "Table", "Function"})
|
|
local deep_resolve -- Forward declaration.
|
|
local function resolve_node(node, item)
|
if node.tag == "Id" or node.tag == "Index" then
|
deep_resolve(node, item)
|
return node.resolution
|
elseif literal_tags[node.tag] then
|
return node.tag == "String" and node or "not_string"
|
else
|
return "unknown"
|
end
|
end
|
|
-- Resolves value of an identifier or index node, tracking through simple
|
-- assignments like `local foo = bar.baz`.
|
-- Can be given an `Invoke` node to resolve the method field.
|
-- Sets `node.resolution` to "unknown", "not_string", `string node`, or
|
-- {previous_indexing_len = index, global_node, key...}.
|
-- Each key can be "unknown", "not_string" or `string_node`.
|
function deep_resolve(node, item)
|
if node.resolution then
|
return
|
end
|
|
-- Common case.
|
-- Also protects against infinite recursion, if it's even possible.
|
node.resolution = "unknown"
|
|
local base = node
|
local base_tag = node.tag == "Id" and "Id" or "Index"
|
local keys = {}
|
|
while base_tag == "Index" do
|
table.insert(keys, 1, base[2])
|
base = base[1]
|
base_tag = base.tag
|
end
|
|
if base_tag ~= "Id" then
|
return
|
end
|
|
local var = base.var
|
local base_resolution
|
local previous_indexing_len
|
|
if var then
|
if not item.used_values[var] or #item.used_values[var] ~= 1 then
|
-- Do not know where the value for the base local came from.
|
return
|
end
|
|
local value = item.used_values[var][1]
|
|
if not value.node then
|
return
|
end
|
|
base_resolution = resolve_node(value.node, value.item)
|
|
if resolved_to_index(base_resolution) then
|
previous_indexing_len = #base_resolution
|
end
|
else
|
base_resolution = {base}
|
end
|
|
if #keys == 0 then
|
node.resolution = base_resolution
|
elseif not resolved_to_index(base_resolution) then
|
-- Indexing something unknown or indexing a literal.
|
node.resolution = "unknown"
|
else
|
local resolution = utils.update({}, base_resolution)
|
resolution.previous_indexing_len = previous_indexing_len
|
|
for _, key in ipairs(keys) do
|
local key_resolution = resolve_node(key, item)
|
|
if resolved_to_index(key_resolution) then
|
key_resolution = "unknown"
|
end
|
|
table.insert(resolution, key_resolution)
|
end
|
|
-- Assign resolution only after all the recursive calls.
|
node.resolution = resolution
|
end
|
end
|
|
local function detect_in_node(chstate, item, node, is_top_line, is_lhs)
|
if node.tag == "Index" or node.tag == "Invoke" or node.tag == "Id" then
|
if node.tag == "Id" and node.var then
|
-- Do not warn about assignments to and accesses of local variables
|
-- that resolve to globals or their fields.
|
return
|
end
|
|
deep_resolve(node, item)
|
local resolution = node.resolution
|
|
-- Still need to recurse into base and key nodes.
|
-- E.g. don't miss a global in `(global1())[global2()].
|
|
if node.tag == "Invoke" then
|
for i = 3, #node do
|
detect_in_node(chstate, item, node[i], is_top_line)
|
end
|
end
|
|
if node.tag ~= "Id" then
|
repeat
|
detect_in_node(chstate, item, node[2], is_top_line)
|
node = node[1]
|
until node.tag ~= "Index"
|
|
if node.tag ~= "Id" then
|
detect_in_node(chstate, item, node, is_top_line)
|
end
|
end
|
|
if resolved_to_index(resolution) then
|
warn_global(chstate, node, resolution, is_lhs, is_top_line)
|
end
|
elseif node.tag ~= "Function" then
|
for _, nested_node in ipairs(node) do
|
if type(nested_node) == "table" then
|
detect_in_node(chstate, item, nested_node, is_top_line)
|
end
|
end
|
end
|
end
|
|
local function detect_in_nodes(chstate, item, nodes, is_top_line, is_lhs)
|
for _, node in ipairs(nodes) do
|
detect_in_node(chstate, item, node, is_top_line, is_lhs)
|
end
|
end
|
|
local function detect_globals_in_line(chstate, line)
|
local is_top_line = line == chstate.top_line
|
|
for _, item in ipairs(line.items) do
|
if item.tag == "Eval" then
|
detect_in_node(chstate, item, item.node, is_top_line)
|
elseif item.tag == "Local" then
|
if item.rhs then
|
detect_in_nodes(chstate, item, item.rhs, is_top_line)
|
end
|
elseif item.tag == "Set" then
|
detect_in_nodes(chstate, item, item.lhs, is_top_line, true)
|
detect_in_nodes(chstate, item, item.rhs, is_top_line)
|
end
|
end
|
end
|
|
-- Warns about assignments, field accesses, and mutations of global variables,
|
-- tracing through localizing assignments such as `local t = table`.
|
function stage.run(chstate)
|
for _, line in ipairs(chstate.lines) do
|
detect_globals_in_line(chstate, line)
|
end
|
end
|
|
return stage
|