summaryrefslogtreecommitdiff
path: root/love2dToAPK/tools/tools/zbstudio-win/src/editor/inspect.lua
blob: a9eef01edea2a9b92d4b5c8833909500f7c744f6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
-- Copyright 2012-15 Paul Kulchenko, ZeroBrane LLC
-- Integration with LuaInspect
---------------------------------------------------------

local M, LA, LI, T = {}

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 = {}
  checks = function() end

  LA = require "luainspect.ast"
  LI = require "luainspect.init"
  T = require "luainspect.types"
end

function M.pos2line(pos)
  return pos and 1 + select(2, M.src:sub(1,pos):gsub(".-\n[^\n]*", ""))
end

function M.warnings_from_string(src, file)
  init()

  local ast, err, linenum, colnum = LA.ast_from_string(src, file)
  if not ast and err then return nil, 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 = GetSpec(wx.wxFileName(file):GetExt())
  for k in pairs(spec and GetApi(spec.apitype or "none").ac.childs or {}) do
    globinit[k] = true
  end

  M.src, M.file = src, file
  return M.show_warnings(ast, globinit)
end

local function cleanError(err)
  return err and err:gsub(".-:%d+: file%s+",""):gsub(", line (%d+), char %d+", ":%1")
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

  local warn, err, line, pos = M.warnings_from_string(src, file)
  return warn, cleanError(err), line, pos
end

function AnalyzeString(src, file)
  local warn, err, line, pos = M.warnings_from_string(src, file or "<string>")
  return warn, cleanError(err), line, pos
end

function M.show_warnings(top_ast, globinit)
  local warnings = {}
  local function warn(msg, linenum, path)
    warnings[#warnings+1] = (path or M.file or "?") .. ":" .. (linenum or M.pos2line(M.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)
    M.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 M.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
      if not fieldseen[name] then
        fieldseen[name] = true
        local var = index(ast.parent)
        local parent = ast.parent and var
          and (" in '"..var:gsub("%."..name.."$","").."'")
          or ""

        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 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
  DisplayOutput("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 = M.warnings_from_string(editorText, filePath)
  if err then -- report compilation error
    DisplayOutputLn((": not completed.\n%s"):format(cleanError(err)))
    return false
  end

  DisplayOutputLn((": %s warning%s.")
    :format(#warn > 0 and #warn or 'no', #warn == 1 and '' or 's'))
  DisplayOutputNoMarker(table.concat(warn, "\n") .. (#warn > 0 and "\n" or ""))

  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, { reportstats = false, keepoutput = true })
    end
  end)
frame:Connect(ID_ANALYZE, wx.wxEVT_UPDATE_UI,
  function (event) event:Enable(GetEditor() ~= nil) end)