fy36
2025-07-01 350eb5ec9163d3ea21416b1525bb80191e958071
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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
-- 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 "")
    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)