Last active
December 11, 2015 12:48
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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