Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
An extended undefined variable checker for Lua 5.1 using bytecode analysis based on David Manura's globalplus.lua http://lua-users.org/wiki/DetectingUndefinedVariables
-- globalsplus.lua
-- Like globals.lua in Lua 5.1.4 -- globalsplus.lua
-- Like globals.lua in Lua 5.1.4 but records fields in global tables too.
-- Probably works but not well tested. Could be extended even further.
--
-- usage: lua globalsplus.lua example.lua
--
-- D.Manura, 2010-07, public domain
--
-- See http://lua-users.org/wiki/DetectingUndefinedVariables
-- extended by Steve Donovan, 2013 with tolerant mode and explicit extra whitelist
local append = table.insert
local lua52 = _VERSION:match '5%.2$'
local load,luac = load
if not lua52 then
function load(str,src,mode,env)
local chunk,err = loadstring(str,src)
if err then return nil,err end
setfenv(chunk, env)
return chunk
end
luac = 'luac'
else
luac = 'luac52'
end
local function exists (file)
local f = io.open(file)
if not f then
return nil
else
f:close()
return file
end
end
local function contents (file)
local f = io.open(file)
local res = f:read '*a'
f:close()
return res
end
local function parse(line)
local idx,linenum,opname,arga,argb,extra =
line:match('^%s+(%d+)%s+%[(%d+)%]%s+(%w+)%s+([-%d]+)%s+([-%d]+)%s*(.*)')
if idx then
idx = tonumber(idx)
linenum = tonumber(linenum)
arga = tonumber(arga)
argb = tonumber(argb)
end
local argc, const
if extra then
local extra2
argc, extra2 = extra:match('^([-%d]+)%s*(.*)')
if argc then argc = tonumber(argc); extra = extra2 end
end
if extra then
const = extra:match('^; (.+)')
end
return {idx=idx,linenum=linenum,opname=opname,arga=arga,argb=argb,argc=argc,const=const}
end
local function stripq (const)
return const:match('"(.*)"')
end
local function getname (const)
if lua52 then
if const:match '^_ENV ' then
return stripq(const)
end
else
return const
end
end
local function getglobals(fh,line)
local globals, requires = {},{}
local last
while line do
local data = parse(line)
local opname = data.opname
if opname == 'GETGLOBAL' or opname == 'GETTABUP' then
local name = getname(data.const)
if name then
data.gname = name
last = data
append(globals, {linenum=last.linenum, name=name, isset=false})
end
elseif opname == 'SETGLOBAL' or opname == 'SETTABUP' then
local name = getname(data.const)
if name then
append(globals, {linenum=data.linenum, name=name, isset=true})
end
elseif (opname == 'GETTABLE' or opname == 'SETTABLE') and last and data.const
and last.gname and (data.idx - last.idx <= 2) and last.arga == data.arga
then
local name = stripq(data.const)
if name then
data.gname = last.gname .. '.' .. name
append(globals, {linenum=last.linenum, name=data.gname, isset=data.opname=='SETTABLE'})
last = nil
end
elseif data.opname == 'LOADK' then
if last and last.const == 'require' then
append(requires,{linenum=last.linenum,name=stripq(data.const)})
else
last = nil
end
elseif next(data) == nil then -- end of function disassembly --
last = nil
end
line = fh:read()
end
return globals, requires
end
local function rindex(t, name)
local top,last_t,ok = t
for part in name:gmatch('[%w_]+') do
last_t = t
ok,t = pcall(function() return t[part] end)
if not ok or t == nil then return nil, last_t ~= top end
end
return t
end
local function load_whitelist (file)
local res = {}
local chunk,err = load(contents(file),'tmp','t',res)
if err then
print("whitelist compilation error",err)
return nil
end
local ok,err = pcall(chunk)
if not ok then
print("whitelist runtime error",err)
return nil
end
return res
end
local whitelist = _G
if #arg == 0 then
print [[
usage: [-t] [-w whitelist] <script>
where t means "tolerant"; required modules are loaded, defined globals are ok
-w loads a whitelist, which is a file containing symbol={entries..} lines
If globals.whitelist exists, use that implicitly.
Unless tolerant, warn about altering known globals.
]]
return
end
local tolerant, extra_whitelist = false, nil
local idx = 1
if arg[idx] == '-t' then
tolerant = true
idx = idx + 1
end
if arg[idx] == '-w' then
extra_whitelist = load_whitelist(arg[idx+1])
idx = idx + 2
end
local file = arg[idx]
if not file then
print 'no file provided!'
return
end
if exists('global.whitelist') then
extra_whitelist = load_whitelist 'global.whitelist'
end
local inf = io.popen(luac..' -p -l '..file)
local line = inf:read()
if not line then -- we hit
return
end
local globals, requires = getglobals(inf,line)
inf:close()
if tolerant and requires then
for _,item in ipairs(requires) do
if not pcall(require,item.name) then
io.write('warning: could not require "',item.name,'"\n')
end
end
end
if extra_whitelist then
for k,v in pairs(extra_whitelist) do
whitelist[k] = v
end
end
if tolerant then
for k,v in pairs(globals) do
if v.isset then
whitelist[v.name] = {}
end
end
end
table.sort(globals, function(a,b) return a.linenum < b.linenum end)
for i,v in ipairs(globals) do
if v.name == nil then print(v.linenum) end
local found, found_root = rindex(whitelist, v.name)
if not tolerant and (found or found_root) and v.isset then
io.write('globals: ',file,':',v.linenum,': redefining global ',v.name,'\n')
elseif not found then
io.write('globals: ',file,':',v.linenum,': undefined ',v.isset and 'set' or 'get',' ',v.name,'\n')
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.