Files
OpenRA/src/editor/inspect.lua

194 lines
7.6 KiB
Lua

-- Integration with LuaInspect
-- (C) 2012 Paul Kulchenko
local M, LA, LI, T = {}
local FAST = true
local function init()
if LA then return end
require "metalua"
LA = require "luainspect.ast"
LI = require "luainspect.init"
T = require "luainspect.types"
if FAST then
LI.eval_comments = function () end
LI.infer_values = function () end
end
end
function M.warnings_from_string(src, file)
init()
local ast, err, linenum, colnum = LA.ast_from_string(src, file)
if err then return nil, err, linenum, colnum end
if FAST then
LI.inspect(ast, nil, src)
LA.ensure_parents_marked(ast)
else
local tokenlist = LA.ast_to_tokenlist(ast, src)
LI.inspect(ast, tokenlist, src)
LI.mark_related_keywords(ast, tokenlist, src)
end
return M.show_warnings(ast)
end
function M.show_warnings(top_ast)
local warnings = {}
local function warn(msg, linenum, path)
warnings[#warnings+1] = (path or "?") .. "(" .. (linenum or 0) .. "): " .. msg
end
local function known(o) return not T.istype[o] end
local function index(f) -- build abc.def.xyz name recursively
return (f[1].tag == 'Id' and f[1][1] or index(f[1])) .. '.' .. f[2][1] end
local isseen, globseen = {}, {}
LA.walk(top_ast, function(ast)
local line = ast.lineinfo and ast.lineinfo.first[1] or 0
local path = ast.lineinfo and ast.lineinfo.first[4] or '?'
local name = ast[1]
-- check if we're masking a variable in the same scope
if ast.localmasking and name ~= '_' and
ast.level == ast.localmasking.level then
local linenum = ast.localmasking.lineinfo.first[1]
local parent = ast.parent and ast.parent.parent
local func = parent and parent.tag == 'Localrec'
warn("local " .. (func and 'function' or 'variable') .. " '" ..
name .. "' masks earlier declaration " ..
(linenum and "on line " .. linenum or "in the same scope"),
line, path)
end
if ast.localdefinition == ast and not ast.isused and
not ast.isignore then
local parent = ast.parent and ast.parent.parent
local isparam = parent and parent.tag == 'Function'
if isparam then
if name ~= 'self' then
local func = parent.parent and parent.parent.parent
local assignment = not func.tag or func.tag == 'Set' or func.tag == 'Localrec'
-- anonymous functions can also be defined in expressions,
-- for example, 'Op' or 'Return' tags
local expression = not assignment and func.tag
local func1 = func[1][1]
local fname = assignment and func1 and type(func1[1]) == 'string'
and func1[1] or (func1 and func1.tag == 'Index' and index(func1))
-- "function foo(bar)" => func.tag == 'Set'
-- `Set{{`Id{"foo"}},{`Function{{`Id{"bar"}},{}}}}
-- "local function foo(bar)" => func.tag == 'Localrec'
-- "local _, foo = 1, function(bar)" => func.tag == 'Local'
-- "print(function(bar) end)" => func.tag == nil
-- "a = a or function(bar) end" => func.tag == nil
-- "return(function(bar) end)" => func.tag == 'Return'
-- "function tbl:foo(bar)" => func.tag == 'Set'
-- `Set{{`Index{`Id{"tbl"},`String{"foo"}}},{`Function{{`Id{"self"},`Id{"bar"}},{}}}}
-- "function tbl.abc:foo(bar)" => func.tag == 'Set'
-- `Set{{`Index{`Index{`Id{"tbl"},`String{"abc"}},`String{"foo"}}},{`Function{{`Id{"self"},`Id{"bar"}},{}}}},
warn("unused parameter '" .. name .. "'" ..
(func and (assignment or expression)
and (fname and func.tag
and (" in function '" .. fname .. "'")
or " in anonymous function")
or ""),
line, path)
end
else
if parent.tag == 'Localrec' then -- local function foo...
warn("unused local function '" .. name .. "'", line, path)
else
warn("unused local variable '" .. name .. "'; "..
"consider removing or replacing with '_'", line, path)
end
end
end
-- added check for FAST as ast.seevalue relies on value evaluation,
-- which is very slow even on simple and short scripts
if not FAST and ast.isfield and not(known(ast.seevalue.value) and ast.seevalue.value ~= nil) then
warn("unknown field " .. name, ast.lineinfo.first[1], path)
elseif ast.tag == 'Id' and not ast.localdefinition and not ast.definedglobal then
if not globseen[name] then
globseen[name] = true
local parent = ast.parent
-- if being called and not one of the parameters
if parent and parent.tag == 'Call' and parent[1] == ast then
warn("first use of unknown global function '" .. name .. "'", line, path)
else
warn("first use of unknown global variable '" .. name .. "'", line, path)
end
end
elseif ast.tag == 'Id' and not ast.localdefinition and ast.definedglobal then
local parent = ast.parent and ast.parent.parent
if parent and parent.tag == 'Set' and not globseen[name] -- report assignments to global
-- only report if it is on the left side of the assignment
-- this is a bit tricky as it can be assigned as part of a, b = c, d
-- `Set{ {lhs+} {expr+} } -- lhs1, lhs2... = e1, e2...
and parent[1] == ast.parent
and parent[2][1].tag ~= "Function" then -- but ignore global functions
warn("first assignment to global variable '" .. name .. "'", line, path)
globseen[name] = true
end
elseif (ast.tag == 'Set' or ast.tag == 'Local') and #(ast[2]) > #(ast[1]) then
warn(("value discarded in multiple assignment: %d values assigned to %d variable%s")
:format(#(ast[2]), #(ast[1]), #(ast[1]) > 1 and 's' or ''), line, path)
end
local vast = ast.seevalue or ast
local note = vast.parent
and (vast.parent.tag == 'Call' or vast.parent.tag == 'Invoke')
and vast.parent.note
if note and not isseen[vast.parent] then
isseen[vast.parent] = true
warn("function '" .. name .. "': " .. note, line, path)
end
end)
return warnings
end
local frame = ide.frame
local menu = frame.menuBar:GetMenu(frame.menuBar:FindMenu(TR("&Project")))
-- insert after "Compile" item
for item = 0, menu:GetMenuItemCount()-1 do
if menu:FindItemByPosition(item):GetId() == ID_COMPILE then
menu:Insert(item+1, ID_ANALYZE, TR("Analyze")..KSC(ID_ANALYZE), TR("Analyze the source code"))
break
end
end
local debugger = ide.debugger
local openDocuments = ide.openDocuments
local function analyzeProgram(editor)
local editorText = editor:GetText()
local id = editor:GetId()
local filePath = DebuggerMakeFileName(editor, openDocuments[id].filePath)
if frame.menuBar:IsChecked(ID_CLEAROUTPUT) then ClearOutput() end
DisplayOutput("Analyzing the source code")
frame:Update()
local warn, err = M.warnings_from_string(editorText, filePath)
if err then -- report compilation error
DisplayOutput(": not completed\n")
return false
end
DisplayOutput((": %s warning%s.\n")
:format(#warn > 0 and #warn or 'no', #warn == 1 and '' or 's'))
DisplayOutputNoMarker(table.concat(warn, "\n") .. "\n")
return true -- analyzed ok
end
frame:Connect(ID_ANALYZE, wx.wxEVT_COMMAND_MENU_SELECTED,
function ()
ActivateOutput()
local editor = GetEditor()
if not analyzeProgram(editor) then CompileProgram(editor) end
end)
frame:Connect(ID_ANALYZE, wx.wxEVT_UPDATE_UI,
function (event)
local editor = GetEditor()
event:Enable((debugger.server == nil and debugger.pid == nil) and (editor ~= nil))
end)