Skip to content

Instantly share code, notes, and snippets.

@stevedonovan
Last active December 11, 2015 12:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stevedonovan/4602636 to your computer and use it in GitHub Desktop.
Save stevedonovan/4602636 to your computer and use it in GitHub Desktop.
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