--[[ 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 -- optivo 20080903 added MySQL Proxy specific ignores 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", "proxy", "package", "module" } local ignoreset = Set{ "read_auth", "connect_server", "read_handshake", "read_auth_result", "disconnect_client", "read_query", "read_query_result" } local function fileexists(fname) local f = io.open(fname) if f then f:close() return true else return false end end -- borrowed from LTN11 -- optivo 20080910 - search LUA_CPATH too local function locate(name) local paths = {"LUA_PATH", "LUA_CPATH"} for _, path in ipairs(paths) do path = os.getenv(path) or "./?.lua" -- optivo 20080903 - need to replace dots in imports with slash name = string.gsub(name, "%.", "/") for path in string.gfind(path, "[^;]+") do path = string.gsub(path, "?", name) if fileexists(path) then return path end 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 = false -- 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 = {} -- optivo 20080910 - handling of LUA_CPATH modules 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 if string.match(path, "%.lua$") then 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 else -- optivo 20080910 - add all parts of the module name to the list of declared modules local mod = module repeat imported_declare_set[mod] = true _, _, mod = string.find(mod, "(.*)%.") until (mod == nil) 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 -- optivo 20080903 add ignore set if (r.name ~= self_name and not declaredset[r.name]) and not ignoreset[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 io.stdout:write("Processing file '" .. v .. "' ... ") lint(v, relaxed_mode) end end if parse_failed then print("PARSE FAILED") os.exit(3) end if import_failed then print("MODULE NOT FOUND - this may be due to the limitations of lualint finding libs and luarocks modules.") end if found_sets then print("FOUND SETS") os.exit(2) end if found_gets then print("FOUND GETS") os.exit(1) end print("OK") os.exit(0)