#!/home/nop/lua-5.0/bin/lua --[[ Usage: lualint [-r|-s] filename.lua [ [-r|-s] [filename.lua] ...] lualint performs static analysis of Lua source code's usage of global variables.. It uses luac's bytecode listing. It reports all accesses to undeclared global variables, which catches many typing errors in variable names. For example: local really_aborting local function abort() os.exit(1) end if not os.getenv("HOME") then realy_aborting = true abortt() end reports: /tmp/example.lua:4: *** global SET of realy_aborting /tmp/example.lua:5: global get of abortt It is primarily designed for use on LTN7-style modules, where each source file only exports one global symbol. (A module contained in the file "foobar.lua" should only export the symbol "foobar".) A "relaxed" mode is available for source not in LTN7 style. It only detects reads from globals that were never set. The switch "-r" puts lualint into relaxed mode for the following files; "-s" switches back to strict. Required packages are tracked, although not recursively. If you call "myext.process()" you should require "myext", and not depend on other dependencies to load it. LUA_PATH is followed as usual to find requirements. Some (not strictly LTN7) modules may wish to export other variables into the global environment. To do so, use the declare function: declare "xpairs" function xpairs(node) [...] Similarly, to quiet warnings about reading global variables you are aware may be unavailable: lint_ignore "lua_fltk_version" if lua_fltk_version then print("fltk loaded") end One way of defining these is in a module "declare.lua": function declare(s) end declare "lint_ignore" function lint_ignore(s) end (Setting declare is OK, because it's in the "declare" module.) These functions don't have to do anything, or in fact actually exist! They can be in dead code: if false then declare "xpairs" end This is because lualint only performs a rather primitive and cursory scan of the bytecode. Perhaps declarations should only be allowed in the main chunk. TODO: The errors don't come out in any particular order. Should switch to Rici's parser, which should do a much better job of this, and allow detection of some other common situations. CREDITS: Jay Carlson (nop@nop.com) This is all Ben Jackson's (ben@ben.com) fault, who did some similar tricks in MOO. ]] local function Set(l) local t = {} for _,v in ipairs(l) do t[v] = true end return t end local ignoreget = Set{ "LUA_PATH", "_G", "_LOADED", "_TRACEBACK", "_VERSION", "__pow", "arg", "assert", "collectgarbage", "coroutine", "debug", "dofile", "error", "gcinfo", "getfenv", "getmetatable", "io", "ipairs", "loadfile", "loadlib", "loadstring", "math", "newproxy", "next", "os", "pairs", "pcall", "print", "rawequal", "rawget", "rawset", "require", "setfenv", "setmetatable", "string", "table", "tonumber", "tostring", "type", "unpack", "xpcall", } local function fileexists(fname) local f = io.open(fname) if f then f:close() return true else return false end end -- borrowed from LTN11 local function locate(name) local path = LUA_PATH if type(path) ~= "string" then path = os.getenv "LUA_PATH" or "./?.lua" end for path in string.gfind(path, "[^;]+") do path = string.gsub(path, "?", name) if fileexists(path) then return path end end return nil end local function scanfile(filename) local modules = {} local declared = {} local lint_ignored = {} local refs = {} local saw_last = nil local context, curfunc if not fileexists(filename) then return nil, "file "..filename.." does not exist" end -- Run once to see if it parses correctly if not os.execute("luac -o lualint.tmp "..filename) then return nil, "file "..filename.." did not successfully parse" end if not fileexists("lualint.tmp") then return nil, "file "..filename.." did not successfully parse" end assert(os.remove("lualint.tmp")) local bc = assert(io.popen("luac -l -p "..filename)) for line in bc:lines() do -- main (64 instructions, 256 bytes at 0x805c1a0) -- function (6 instructions, 24 bytes at 0x805c438) local found, _, type, fname = string.find(line, "(%w+) <([^>]+)>") if found then if context == "main" then fname="*MAIN*" end curfunc = fname end -- print("sawlast", saw_last) -- 2 [1] LOADK 1 1 ; "lazytree" local found, _, constname = string.find(line, '%sLOADK .-;%s"(.-)"') if saw_last and found then if saw_last == "require" then -- print("require", constname) table.insert(modules, constname) elseif saw_last == "declare" then -- print("declare", constname) table.insert(declared, constname) elseif saw_last == "lint_ignore" then lint_ignored[constname] = true end end -- 4 [2] GETGLOBAL 0 0 ; require local found, _, lineno, instr, gname = string.find(line, "%[(%d+)%]%s+([SG]ETGLOBAL).-; (.+)") if found then local t = refs[curfunc] or {SETGLOBAL={n=0}, GETGLOBAL={n=0}} local err = {name=gname, lineno=lineno} table.insert(t[instr], err) refs[curfunc] = t saw_last = gname else saw_last = nil end end bc:close() return modules, declared, lint_ignored, refs end local found_sets = false local found_gets = false local parse_failed = false local import_failed = true -- print("args", arg[1]) local function lint(filename, relaxed) local modules, declared, lint_ignored, refs = scanfile(filename) if not modules then print(string.format("%s:%d: *** could not parse: %s ", filename, 1, declared)) parse_failed = true return end local imported_declare_set = {} for i,module in ipairs(modules) do local path = locate(module) if not path then print(string.format("%s:%d: could not find imported module %s ", filename, 1, module)) import_failed = true else local success, imported_declare, _, _ = scanfile(path) if not success then print(string.format("%s:%d: could not parse import: %s ", path, 1, imported_declare)) import_failed = true else for i,declared in ipairs(imported_declare) do imported_declare_set[declared] = true end end end end local moduleset = Set(modules) local declaredset = Set(declared) local self_name = nil do local _ if string.find(filename, "/") then _, _, self_name = string.find(filename, ".-/(%w+)%.lua") else _,_,self_name = string.find(filename, "(%w+)%.lua") end end -- print("selfname", self_name) local was_set = {} local function will_warn_for(name) if relaxed and was_set[name] then return false end if name == self_name or lint_ignored[name] or ignoreget[name] or moduleset[name] or declaredset[name] or imported_declare_set[name] then return false end return true end for f,t in pairs(refs) do for _,r in ipairs(t.SETGLOBAL) do if r.name ~= self_name and not declaredset[r.name] then was_set[r.name] = true if not relaxed then print(string.format("%s:%d: *** global SET of %s", filename, r.lineno, r.name)) found_sets = true end end end end for f,t in pairs(refs) do for _,r in ipairs(t.GETGLOBAL) do if will_warn_for(r.name) then print(string.format("%s:%d: global get of %s", filename, r.lineno, r.name)) found_gets = true end end end end if arg.n == 0 then print("usage: lualint filename.lua [filename.lua ...]") os.exit(1) end local relaxed_mode = false for i,v in ipairs(arg) do if v == "-r" then relaxed_mode = true elseif v == "-s" then relaxed_mode = false else lint(v, relaxed_mode) end end if parse_failed then os.exit(3) elseif import_failed then os.exit(1) elseif found_sets then os.exit(2) elseif found_gets then os.exit(1) else os.exit(0) end