Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
-- Copyright 2012-18 Paul Kulchenko, ZeroBrane LLC
-- Integration with LuaInspect or LuaCheck
---------------------------------------------------------
local function create_checker()
if ide.config.staticanalyzer.luacheck then
local config = type(ide.config.staticanalyzer.luacheck) == "table" and ide.config.staticanalyzer.luacheck or {}
local luacheck = require("luacheck")
-- globals only need to be generated once the API has changed.
-- maybe this can be a module instead?
local function build_env_from_api(tbl, out)
out = out or {}
for k, v in pairs(tbl) do
if v.type ~= "keyword" then
out[k] = {fields = v.childs and build_env_from_api(v.childs)}
end
end
return out
end
local function build_env()
local globals = {}
for _, api in pairs(ide:GetInterpreter():GetAPI() or {}) do
-- not sure if this is how you're supposed to get an api
local ok, tbl = pcall(require, "api/lua/" .. api)
if ok then
build_env_from_api(tbl, globals)
end
end
return globals
end
return function(src, file)
local api_globals = build_env()
if config.options then
-- add user config globals to api table
for k, v in pairs(config.options.globals or {}) do
api_globals[k] = v
end
config.options.globals = api_globals
end
local default_options = {
max_line_length = false,
globals = api_globals,
-- http://luacheck.readthedocs.io/en/stable/warnings.html
ignore = config.ignore or {
"6..", -- whitespace and style warnings
},
}
local data = luacheck.check_strings({src}, config.options or default_options)
-- I think luacheck can support showing multiple errors
-- but warnings_from_string is meant to only show one
if data.errors > 0 or data.fatals > 0 then
local report = data[1][1]
return nil, luacheck.get_message(report), report.line, report.column
end
local warnings = {}
for _, report in ipairs(data[1]) do
local str = luacheck.get_message(report)
if config.reportcode then
str = str .. "(" .. report.code .. ")"
end
table.insert(warnings, ("%s:%d:%d: %s"):format(
file,
report.line,
report.column, -- not standard when using luainspect
str
))
end
return warnings
end
else
local LA, LI, T
local current_ast
local current_src
local current_file
local function init()
if LA then return end
-- metalua is using 'checks', which noticeably slows the execution
-- stab it with out own
package.loaded.checks = {} -- make `require 'checks'` work even without `checks` module
rawset(_G, "checks", function() end) -- provide `checks` function
LA = require "luainspect.ast"
LI = require "luainspect.init"
T = require "luainspect.types"
end
local function pos2line(pos)
return pos and 1 + select(2, current_src:sub(1,pos):gsub(".-\n[^\n]*", ""))
end
local function show_warnings(top_ast, globinit)
local warnings = {}
local function warn(msg, linenum, path)
warnings[#warnings+1] = (path or current_file or "?") .. ":" .. (linenum or pos2line(current_ast.pos) or 0) .. ": " .. msg
end
local function known(o) return not T.istype[o] end
local function index(f) -- build abc.def.xyz name recursively
if not f or f.tag ~= 'Index' or not f[1] or not f[2] then return end
local main = f[1].tag == 'Id' and f[1][1] or index(f[1])
return main and type(f[2][1]) == "string" and (main .. '.' .. f[2][1]) or nil
end
local globseen, isseen, fieldseen = globinit or {}, {}, {}
LA.walk(top_ast, function(ast)
current_ast = ast
local path, line = tostring(ast.lineinfo):gsub('<C|','<'):match('<([^|]+)|L(%d+)')
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
and tostring(ast.localmasking.lineinfo.first):match('|L(%d+)')
or pos2line(ast.localmasking.pos)
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 and 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" mode as ast.seevalue relies on value evaluation,
-- which is very slow even on simple and short scripts
if ide.config.staticanalyzer.infervalue and ast.isfield
and not(known(ast.seevalue.value) and ast.seevalue.value ~= nil) then
local var = index(ast.parent)
local parent = ast.parent and var
and (" in '"..var:gsub("%."..name.."$","").."'")
or ""
if not fieldseen[name..parent] then
fieldseen[name..parent] = true
local tblref = ast.parent and ast.parent[1]
local localparam = (tblref and tblref.localdefinition
and tblref.localdefinition.isparam)
if not localparam then
warn("first use of unknown field '" .. name .."'"..parent,
ast.lineinfo and tostring(ast.lineinfo.first):match('|L(%d+)'), path)
end
end
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] and type(name) == "string" then
isseen[vast.parent] = true
warn("function '" .. name .. "': " .. note, line, path)
end
end)
return warnings
end
local function cleanError(err)
return err and err:gsub(".-:%d+: file%s+",""):gsub(", line (%d+), char %d+", ":%1")
end
init()
return function(src, file)
init()
local ast, err, linenum, colnum = LA.ast_from_string(src, file)
if not ast and err then return nil, cleanError(err), linenum, colnum end
LI.uninspect(ast)
if ide.config.staticanalyzer.infervalue then
local tokenlist = LA.ast_to_tokenlist(ast, src)
LI.clear_cache()
LI.inspect(ast, tokenlist, src)
LI.mark_related_keywords(ast, tokenlist, src)
else
-- stub out LI functions that depend on tokenlist,
-- which is not built in the "fast" mode
local ec, iv = LI.eval_comments, LI.infer_values
LI.eval_comments, LI.infer_values = function() end, function() end
LI.inspect(ast, nil, src)
LA.ensure_parents_marked(ast)
LI.eval_comments, LI.infer_values = ec, iv
end
local globinit = {arg = true} -- skip `arg` global variable
local spec = ide:FindSpec(wx.wxFileName(file):GetExt())
for k in pairs(spec and GetApi(spec.apitype or "none").ac.childs or {}) do
globinit[k] = true
end
current_src = src
current_file = file
return show_warnings(ast, globinit)
end
end
end
local checkers = {}
local function warnings_from_string(...)
local checktype = (ide.config.staticanalyzer.luacheck
-- luacheck globals depend on the interpreter, so create different checkers if needed
and "luacheck" .. (ide:GetInterpreter():GetFileName() or "") .. (ide:GetProject() or "")
or "luainspect")
if not checkers[checktype] then checkers[checktype] = create_checker() end
return checkers[checktype](...)
end
function AnalyzeFile(file)
local src, err = FileRead(file)
if not src and err then return nil, TR("Can't open file '%s': %s"):format(file, err) end
return warnings_from_string(src, file)
end
function AnalyzeString(src, file)
return warnings_from_string(src, file or "<string>")
end
local frame = ide.frame
-- insert after "Compile" item
local _, menu, compilepos = ide:FindMenuItem(ID.COMPILE)
if compilepos then
menu:Insert(compilepos+1, ID.ANALYZE, TR("Analyze")..KSC(ID.ANALYZE), TR("Analyze the source code"))
end
local function analyzeProgram(editor)
-- save all files (if requested) for "infervalue" analysis to keep the changes on disk
if ide.config.editor.saveallonrun and ide.config.staticanalyzer.infervalue then SaveAll(true) end
if ide:GetLaunchedProcess() == nil and not ide:GetDebugger():IsConnected() then ClearOutput() end
ide:GetOutput():Write("Analyzing the source code")
frame:Update()
local editorText = editor:GetTextDyn()
local doc = ide:GetDocument(editor)
local filePath = doc:GetFilePath() or doc:GetFileName()
local warn, err = warnings_from_string(editorText, filePath)
if err then -- report compilation error
ide:Print((": not completed.\n%s"):format(err))
return false
end
ide:Print((": %s warning%s.")
:format(#warn > 0 and #warn or 'no', #warn == 1 and '' or 's'))
ide:GetOutput():Write(table.concat(warn, "\n") .. (#warn > 0 and "\n" or ""))
return true -- analyzed ok
end
frame:Connect(ID.ANALYZE, wx.wxEVT_COMMAND_MENU_SELECTED,
function ()
ide:GetOutput():Activate()
local editor = ide:GetEditor()
if not analyzeProgram(editor) then
CompileProgram(editor, { reportstats = false, keepoutput = true })
end
end)
frame:Connect(ID.ANALYZE, wx.wxEVT_UPDATE_UI,
function (event) event:Enable(ide:GetEditor() ~= nil) end)