-- luainspect.init - core LuaInspect source analysis. -- -- This module is a bit more high level than luainspect.ast. It deals more with -- interpretation/inference of semantics of an AST. It also uses luainspect.globals, -- which does the basic semantic interpretation of globals/locals. -- -- (c) 2010 David Manura, MIT License. local M = {} -- This is the API version. It is an ISO8601 date expressed as a fraction. M.APIVERSION = 0.20100805 local LA = require "luainspect.ast" local LD = require "luainspect.dump" local LG = require "luainspect.globals" local LS = require "luainspect.signatures" local T = require "luainspect.types" local COMPAT = require "luainspect.compat_env" --! require 'luainspect.typecheck' (context) local ENABLE_RETURN_ANALYSIS = true local DETECT_DEADCODE = false -- may require more validation (false positives) -- Functional forms of Lua operators. -- Note: variable names like _1 are intentional. These affect debug info and -- will display in any error messages. local ops = {} ops['add'] = function(_1,_2) return _1+_2 end ops['sub'] = function(_1,_2) return _1-_2 end ops['mul'] = function(_1,_2) return _1*_2 end ops['div'] = function(_1,_2) return _1/_2 end ops['mod'] = function(_1,_2) return _1%_2 end ops['pow'] = function(_1,_2) return _1^_2 end ops['concat'] = function(_1,_2) return _1.._2 end ops['eq'] = function(_1,_2) return _1==_2 end ops['lt'] = function(_1,_2) return _1<_2 end ops['le'] = function(_1,_2) return _1<=_2 end ops['and'] = function(_1,_2) return _1 and _2 end ops['or'] = function(_1,_2) return _1 or _2 end ops['not'] = function(_1) return not _1 end ops['len'] = function(_1) return #_1 end ops['unm'] = function(_1) return -_1 end -- Performs binary operation. Supports types. local function dobinop(opid, a, b) if (a == T.number or b == T.number) and (a == T.number or type(a) == 'number' ) and (b == T.number or type(b) == 'number' ) then if opid == 'eq' or opid == 'lt' or opid == 'le' then return T.boolean elseif opid == 'concat' then return T.string else return T.number end elseif (a == T.string or b == T.string) and (a == T.string or type(a) == 'string' ) and (b == T.string or type(b) == 'string' ) then if opid == 'concat' or opid == 'and' or opid == 'or' then return T.string elseif opid == 'eq' or opid == 'lt' or opid == 'le' then return T.boolean else return T.number end elseif (a == T.boolean or b == T.boolean) and (a == T.boolean or type(a) == 'boolean' ) and (b == T.boolean or type(b) == 'boolean' ) then if opid == 'eq' or opid == 'and' or opid == 'or' then return T.boolean else error('invalid operation on booleans: ' .. opid, 0) end elseif T.istype[a] or T.istype[b] then return T.universal else return ops[opid](a, b) end end -- Performs unary operation. Supports types. local function dounop(opid, a) if opid == 'not' then if T.istype[a] then return T.boolean else return ops[opid](a) end elseif a == T.number then if opid == 'unm' then return T.number else -- 'len' error('invalid operation on number: ' .. opid, 0) end elseif a == T.string then return T.number elseif a == T.boolean then error('invalid operation on boolean: ' .. opid, 0) elseif T.istype[a] then return nil, 'unknown' else return ops[opid](a) end end -- Like info in debug.getinfo but inferred by static analysis. -- object -> {fpos=fpos, source="@" .. source, fast=ast, tokenlist=tokenlist} -- Careful: value may reference key (affects pre-5.2 which lacks emphemerons). -- See also ast.nocollect. M.debuginfo = setmetatable({}, {__mode='v'}) -- Modules loaded via require_inspect. -- module name string -> {return value, AST node} -- note: AST node is maintained to prevent nocollect fields in ast being collected. -- note: not a weak table. M.package_loaded = {} -- Stringifies interpreted value for debugging. -- CATEGORY: debug local function debugvalue(ast) local s if ast then s = ast.value ~= T.universal and 'known:' .. tostring(ast.value) or 'unknown' else s = '?' end return s end -- Reads contents of text file in path, in binary mode. -- On error, returns nil and error message. local function readfile(path) local fh, err = io.open(path, 'rb') if fh then local data; data, err = fh:read'*a' if data then return data end end return nil, err end -- Similar to string.gsub but with plain replacement (similar to option in string.match) -- http://lua-users.org/lists/lua-l/2002-04/msg00118.html -- CATEGORY: utility/string local function plain_gsub(s, pattern, repl) repl = repl:gsub('(%%)', '%%%%') return s:gsub(pattern, repl) end -- Infer name of variable or literal that AST node represents. -- This is for debugging messages. local function infer_name(ast) if ast == nil then return nil elseif ast.tag == 'Id' then return "'"..ast[1].."'" elseif ast.tag == 'Number' then return 'number' elseif ast.tag == 'String' then return 'string' elseif ast.tag == 'True' then return 'true' elseif ast.tag == 'False' then return 'false' elseif ast.tag == 'Nil' then return 'nil' else return nil end end --[[ This is like `pcall` but any error string returned does not contain the "chunknamem:currentline: " prefix (based on luaL_where) if the error occurred in the current file. This avoids error messages in user code (f) being reported as being inside this module if this module calls user code. Also, local variable names _1, _2, etc. in error message are replaced with names inferred (if any) from corresponding AST nodes in list `asts` (note: nil's in asts skip replacement). --]] local _prefix local _clean local function pzcall(f, asts, ...) _prefix = _prefix or select(2, pcall(function() error'' end)):gsub(':%d+: *$', '') -- note: specific to current file. _clean = _clean or function(asts, ok, ...) if ok then return true, ... else local err = ... if type(err) == 'string' then if err:sub(1,#_prefix) == _prefix then local more = err:match('^:%d+: *(.*)', #_prefix+1) if more then err = more err = err:gsub([[local '_(%d+)']], function(name) return infer_name(asts[tonumber(name)]) end) end end end return ok, err end end return _clean(asts, pcall(f, ...)) end -- Loads source code of given module name. -- Returns code followed by path. -- note: will also search in the directory `spath` and its parents. -- This should preferrably be an absolute path or it might not work correctly. -- It must be slash terminated. -- CATEGORY: utility/package local function load_module_source(name, spath) -- Append parent directories to list of paths to search. local package_path = package.path local ppath = spath repeat package_path = package_path .. ';' .. ppath .. '?.lua;' .. ppath .. '?/init.lua' local nsub ppath, nsub = ppath:gsub('[^\\/]+[\\/]$', '') until nsub == 0 for spec in package_path:gmatch'[^;]+' do local testpath = plain_gsub(spec, '%?', (name:gsub('%.', '/'))) local src, err_ = readfile(testpath) if src then return src, testpath end end return nil end -- Clears global state. -- This includes cached inspected modules. function M.clear_cache() for k,v in pairs(M.package_loaded) do M.package_loaded[k] = nil end end -- Gets all keywords related to AST `ast`, where `top_ast` is the root of `ast` -- and `src` is source code of `top_ast` -- Related keywords are defined as all keywords directly associated with block containing node -- `ast`. Furthermore, break statements are related to containing loop statements, -- and return statements are related to containing function statement (if any). -- function declaration syntactic sugar is handled specially too to ensure the 'function' keyword -- is highlighted even though it may be outside of the `Function AST. -- -- Returns token list or nil if not applicable. Returned `ast` is AST containing related keywords. -- CATEGORY: keyword comprehension local iskeystat = {Do=true, While=true, Repeat=true, If=true, Fornum=true, Forin=true, Local=true, Localrec=true, Return=true, Break=true, Function=true, Set=true -- note: Set for `function name` } local isloop = {While=true, Repeat=true, Fornum=true, Forin=true} local isblock = {Do=true, While=true, Repeat=true, If=true, Fornum=true, Forin=true, Function=true} function M.related_keywords(ast, top_ast, tokenlist, src) -- Expand or contract AST for certain contained statements. local more if ast.tag == 'Return' then -- if `return` selected, that consider containing function selected (if any) if not ast.parent then LA.mark_parents(top_ast) end local ancestor_ast = ast.parent while ancestor_ast ~= nil and ancestor_ast.tag ~= 'Function' do ancestor_ast = ancestor_ast.parent end if ancestor_ast then ast = ancestor_ast end -- but only change if exists elseif ast.tag == 'Break' then -- if `break` selected, that consider containing loop selected if not ast.parent then LA.mark_parents(top_ast) end local ancestor_ast = ast.parent while ancestor_ast ~= nil and not isloop[ancestor_ast.tag] do ancestor_ast = ancestor_ast.parent end ast = ancestor_ast elseif ast.tag == 'Set' then local val1_ast = ast[2][1] if val1_ast.tag == 'Function' then local token = tokenlist[LA.ast_idx_range_in_tokenlist(tokenlist, ast)] if token.tag == 'Keyword' and token[1] == 'function' then -- function with syntactic sugar `function f` ast = ast[2][1] -- select `Function node else more = true end else more = true end elseif ast.tag == 'Localrec' and ast[2][1].tag == 'Function' then -- if `local function f` selected, which becomes a `Localrec, consider `Function node. ast = ast[2][1] --IMPROVE: only contract ast if `function` part of `local function` is selected. else more = true end if more then -- not yet handled -- Consider containing block. if not ast.parent then LA.mark_parents(top_ast) end local ancestor_ast = ast while ancestor_ast ~= top_ast and not isblock[ancestor_ast.tag] do ancestor_ast = ancestor_ast.parent end ast = ancestor_ast end -- keywords in statement/block. if iskeystat[ast.tag] then local keywords = {} for i=1,#tokenlist do local token = tokenlist[i] if token.ast == ast and token.tag == 'Keyword' then keywords[#keywords+1] = token end end -- Expand keywords for certaining statements. if ast.tag == 'Function' then -- if `Function, also select 'function' and 'return' keywords local function f(ast) for _,cast in ipairs(ast) do if type(cast) == 'table' then if cast.tag == 'Return' then local token = tokenlist[LA.ast_idx_range_in_tokenlist(tokenlist, cast)] keywords[#keywords+1] = token elseif cast.tag ~= 'Function' then f(cast) end end end end f(ast) if not ast.parent then LA.mark_parents(top_ast) end local grand_ast = ast.parent.parent if grand_ast.tag == 'Set' then local token = tokenlist[LA.ast_idx_range_in_tokenlist(tokenlist, grand_ast)] if token.tag == 'Keyword' and token[1] == 'function' then keywords[#keywords+1] = token end elseif grand_ast.tag == 'Localrec' then local tidx = LA.ast_idx_range_in_tokenlist(tokenlist, grand_ast) repeat tidx = tidx + 1 until not tokenlist[tidx] or (tokenlist[tidx].tag == 'Keyword' and tokenlist[tidx][1] == 'function') local token = tokenlist[tidx] keywords[#keywords+1] = token end elseif isloop[ast.tag] then -- if loop, also select 'break' keywords local function f(ast) for _,cast in ipairs(ast) do if type(cast) == 'table' then if cast.tag == 'Break' then local tidx = LA.ast_idx_range_in_tokenlist(tokenlist, cast) keywords[#keywords+1] = tokenlist[tidx] elseif not isloop[cast.tag] then f(cast) end end end end f(ast) end return keywords, ast end return nil, ast end -- Mark tokenlist (top_ast/tokenlist/src) with keywordid AST attributes. -- All keywords related to each other have the same keyword ID integer. -- NOTE: This is not done/undone by inspect/uninspect. -- CATEGORY: keyword comprehension function M.mark_related_keywords(top_ast, tokenlist, src) local id = 0 local idof = {} for _, token in ipairs(tokenlist) do if token.tag == 'Keyword' and not idof[token] then id = id + 1 local match_ast = LA.smallest_ast_containing_range(top_ast, tokenlist, token.fpos, token.lpos) local ktokenlist = M.related_keywords(match_ast, top_ast, tokenlist, src) if ktokenlist then for _, ktoken in ipairs(ktokenlist) do ktoken.keywordid = id idof[ktoken] = true end end -- note: related_keywords may return a keyword set not containing given keyword. end end end -- function for t[k] local function tindex(_1, _2) return _1[_2] end local unescape = {['d'] = '.'} -- Sets known value on ast to v if ast not pegged. -- CATEGORY: utility function for infer_values. local function set_value(ast, v) if not ast.isvaluepegged then ast.value = v end end local function known(o) return not T.istype[o] end local function unknown(o) return T.istype[o] end -- CATEGORY: utility function for infer_values. local function tastnewindex(t_ast, k_ast, v_ast) if known(t_ast.value) and known(k_ast.value) and known(v_ast.value) then local _1, _2, _3 = t_ast.value, k_ast.value, v_ast.value if _1[_2] ~= nil and _3 ~= _1[_2] then -- multiple values return T.universal else _1[_2] = _3 return _3 end else return T.universal end end -- Gets expected number of parameters for function (min, max) values. -- In case of vararg, max is unknown and set to nil. local function function_param_range(ast) local names_ast = ast[1] if #names_ast >= 1 and names_ast[#names_ast].tag == 'Dots' then return #names_ast-1, nil else return #names_ast, #names_ast end end -- Gets number of arguments to function call: (min, max) range. -- In case of trailing vararg or function call, max is unknown and set to nil. local function call_arg_range(ast) if ast.tag == 'Invoke' then if #ast >= 3 and (ast[#ast].tag == 'Dots' or ast[#ast].tag == 'Call' or ast[#ast].tag == 'Invoke') then return #ast-2, nil else return #ast-1, #ast-1 end else if #ast >= 2 and (ast[#ast].tag == 'Dots' or ast[#ast].tag == 'Call' or ast[#ast].tag == 'Invoke') then return #ast-2, nil else return #ast-1, #ast-1 end end end -- Reports warning. List of strings. local function warn(report, ...) report('warning: ' .. table.concat({...}, ' ')) end -- Reports status messages. List of strings. local function status(report, ...) report('status: ' .. table.concat({...}, ' ')) end -- unique value used to detect require loops (A require B require A) local REQUIRE_SENTINEL = function() end -- Gets single return value of chunk ast. Assumes ast is inspected. local function chunk_return_value(ast) local vinfo if ENABLE_RETURN_ANALYSIS then local info = M.debuginfo[ast.value] local retvals = info and info.retvals if retvals then vinfo = retvals[1] else vinfo = T.universal end else if ast[#ast] and ast[#ast].tag == 'Return' and ast[#ast][1] then vinfo = ast[#ast][1] else vinfo = T.universal end end return vinfo end -- Version of require that does source analysis (inspect) on module. function M.require_inspect(name, report, spath) local plinfo = M.package_loaded[name] if plinfo == REQUIRE_SENTINEL then warn(report, "loop in require when loading " .. name) return nil end if plinfo then return plinfo[1] end status(report, 'loading:' .. name) M.package_loaded[name] = REQUIRE_SENTINEL -- avoid recursion on require loops local msrc, mpath = load_module_source(name, spath) local vinfo, mast if msrc then local err; mast, err = LA.ast_from_string(msrc, mpath) if mast then local mtokenlist = LA.ast_to_tokenlist(mast, msrc) M.inspect(mast, mtokenlist, msrc, report) vinfo = chunk_return_value(mast) else vinfo = T.error(err) warn(report, err, " ", mpath) --Q:error printing good? end else warn(report, 'module not found: ' .. name) vinfo = T.error'module not found' --IMPROVE: include search paths? end M.package_loaded[name] = {vinfo, mast} return vinfo, mast end -- Marks AST node and all children as dead (ast.isdead). local function mark_dead(ast) LA.walk(ast, function(bast) bast.isdead = true end) end -- Gets list of `Return statement ASTs in `Function (or chunk) f_ast, not including -- return's in nested functions. Also returns boolean `has_implicit` indicating -- whether function may return by exiting the function without a return statement. -- Returns that are never exected are omitted (e.g. last return is omitted in -- `function f() if x then return 1 else return 2 end return 3 end`). -- Also marks AST nodes with ast.isdead (dead-code). local function get_func_returns(f_ast) local isalwaysreturn = {} local returns = {} local function f(ast, isdead) for _,cast in ipairs(ast) do if type(cast) == 'table' then if isdead then mark_dead(cast) end -- even if DETECT_DEADCODE disabled if cast.tag ~= 'Function' and not isdead then -- skip nested functions f(cast, isdead) -- depth-first traverse end if ast.tag ~= 'If' and isalwaysreturn[cast] then isdead = true end -- subsequent statements in block never executed end end -- Code on walking up AST: propagate children to parents if ast.tag == 'Return' then returns[#returns+1] = ast isalwaysreturn[ast] = true elseif ast.tag == 'If' then if #ast%2 ~= 0 then -- has 'else' block local isreturn = true for i=2,#ast do if (i%2==0 or i==#ast) and not isalwaysreturn[ast[i]] then isreturn = nil; break end end isalwaysreturn[ast] = isreturn end else -- note: iterates not just blocks, but should be ok for i=1,#ast do if isalwaysreturn[ast[i]] then isalwaysreturn[ast] = true; break end end end end f(f_ast, false) local block_ast = f_ast.tag == 'Function' and f_ast[2] or f_ast local has_implicit = not isalwaysreturn[block_ast] return returns, has_implicit end -- temporary hack? local function valnode_normalize(valnode) if valnode then return valnode.value else return T.none end end -- Gets return value at given return argument index, given list of `Return statements. -- Return value is a superset of corresponding types in list of statements. -- Example: {`Return{1,2,3}, `Return{1,3,'z'}} would return -- 1, T.number, and T.universal for retidx 1, 2 and 3 respectively. local function get_return_value(returns, retidx) if #returns == 0 then return T.none elseif #returns == 1 then return valnode_normalize(returns[1][retidx]) else local combined_value = valnode_normalize(returns[1][retidx]) for i=2,#returns do local cur_value = valnode_normalize(returns[i][retidx]) combined_value = T.superset_types(combined_value, cur_value) if combined_value == T.universal then -- can't expand set further return combined_value end end return combined_value --TODO: handle values with possibly any number of return values, like f() end end -- Gets return values (or types) on `Function (or chunk) represented by given AST. local function get_func_return_values(f_ast) local returns, has_implicit = get_func_returns(f_ast) if has_implicit then returns[#returns+1] = {tag='Return'} end local returnvals = {n=0} for retidx=1,math.huge do local value = get_return_value(returns, retidx) if value == T.none then break end returnvals[#returnvals+1] = value returnvals.n = returnvals.n + 1 end return returnvals end -- Example: AST of `function(x) if x then return 1,2,3 else return 1,3,"z" end end` -- returns {1, T.number, T.universal}. -- Given list of values, return the first nvalues values plus the rest of the values -- as a tuple. Useful for things like -- local ok, values = valuesandtuple(1, pcall(f)) -- CATEGORY: utility function (list) local function valuesandtuple(nvalues, ...) if nvalues >= 1 then return (...), valuesandtuple(nvalues-1, select(2, ...)) else return {n=select('#', ...), ...} end end -- Infers values of variables. Also marks dead code (ast.isdead). --FIX/WARNING - this probably needs more work -- Sets top_ast.valueglobals, ast.value, ast.valueself -- CATEGORY: code interpretation function M.infer_values(top_ast, tokenlist, src, report) if not top_ast.valueglobals then top_ast.valueglobals = {} end -- infer values LA.walk(top_ast, function(ast) -- walk down if ast.tag == 'Function' then local paramlist_ast = ast[1] for i=1,#paramlist_ast do local param_ast = paramlist_ast[i] if param_ast.value == nil then param_ast.value = T.universal end end end end, function(ast) -- walk up -- process `require` statements. if ast.tag == 'Local' or ast.tag == 'Localrec' then local vars_ast, values_ast = ast[1], ast[2] local valuelist = #values_ast > 0 and values_ast[#values_ast].valuelist for i=1,#vars_ast do local var_ast, value_ast = vars_ast[i], values_ast[i] local value if value_ast then value = value_ast.value elseif valuelist then local vlidx = i - #values_ast + 1 value = valuelist.sizeunknown and vlidx > valuelist.n and T.universal or valuelist[vlidx] end set_value(var_ast, value) end elseif ast.tag == 'Set' then -- note: implementation similar to 'Local' local vars_ast, values_ast = ast[1], ast[2] local valuelist = #values_ast > 0 and values_ast[#values_ast].valuelist for i=1,#vars_ast do local var_ast, value_ast = vars_ast[i], values_ast[i] local value if value_ast then value = value_ast.value elseif valuelist then local vlidx = i - #values_ast + 1 value = valuelist.sizeunknown and vlidx > valuelist.n and T.universal or valuelist[vlidx] end if var_ast.tag == 'Index' then local t_ast, k_ast = var_ast[1], var_ast[2] if not T.istype[t_ast.value] then -- note: don't mutate types local v_ast = {value=value} local ok; ok, var_ast.value = pzcall(tastnewindex, {t_ast, k_ast, v_ast}, t_ast, k_ast, v_ast) if not ok then var_ast.value = T.error(var_ast.value) end --FIX: propagate to localdefinition? end else assert(var_ast.tag == 'Id', var_ast.tag) if var_ast.localdefinition then set_value(var_ast, value) else -- global local name = var_ast[1] top_ast.valueglobals[name] = value end end --FIX: propagate to definition or localdefinition? end elseif ast.tag == 'Fornum' then local var_ast = ast[1] set_value(var_ast, T.number) elseif ast.tag == 'Forin' then local varlist_ast, iter_ast = ast[1], ast[2] if #iter_ast == 1 and iter_ast[1].tag == 'Call' and iter_ast[1][1].value == ipairs then for i, var_ast in ipairs(varlist_ast) do if i == 1 then set_value(var_ast, T.number) -- handle the type of the value as the type of the first element -- in the table that is a parameter for ipairs elseif i == 2 then local t_ast = iter_ast[1][2] local value = T.universal if (known(t_ast.value) or T.istabletype[t_ast.value]) then local ok; ok, value = pzcall(tindex, {t_ast, {tag='Number', 1}}, t_ast.value, 1) if not ok then value = T.error(t_ast.value) end end set_value(var_ast, value) else set_value(var_ast, nil) end end elseif #iter_ast == 1 and iter_ast[1].tag == 'Call' and iter_ast[1][1].value == pairs then local t_ast = iter_ast[1][2] local value = T.universal local key if t_ast.value and (known(t_ast.value) or T.istabletype[t_ast.value]) then key = next(t_ast.value) local ok; ok, value = pzcall(tindex, {t_ast, {tag='String', key}}, t_ast.value, key) if not ok then value = T.error(t_ast.value) end end for i, var_ast in ipairs(varlist_ast) do if i == 1 then set_value(var_ast, type(key)) elseif i == 2 then set_value(var_ast, value) else set_value(var_ast, nil) end end else -- general case, unknown iterator for _, var_ast in ipairs(varlist_ast) do set_value(var_ast, T.universal) end end elseif ast.tag == 'Id' then if ast.localdefinition then local localdefinition = ast.localdefinition if not localdefinition.isset then -- IMPROVE: support non-const (isset false) too set_value(ast, localdefinition.value) end else -- global local name = ast[1] local v = top_ast.valueglobals[name] if v ~= nil then ast.value = v else local ok; ok, ast.value = pzcall(tindex, {{tag='Id', '_G'}, {tag='String', name}}, _G, name) if not ok then ast.value = T.error(ast.value) end end end elseif ast.tag == 'Index' then local t_ast, k_ast = ast[1], ast[2] if (known(t_ast.value) or T.istabletype[t_ast.value]) and known(k_ast.value) then local ok; ok, ast.value = pzcall(tindex, {t_ast, k_ast}, t_ast.value, k_ast.value) if not ok then ast.value = T.error(ast.value) end end elseif ast.tag == 'Call' or ast.tag == 'Invoke' then -- Determine function to call (infer via index if method call). local isinvoke = ast.tag == 'Invoke' if isinvoke then local t, k = ast[1].value, ast[2].value if known(t) and known(k) then local ok; ok, ast.valueself = pzcall(tindex, {ast[1], ast[2]}, t, k) if not ok then ast.valueself = T.error(ast.valueself) end end end local func; if isinvoke then func = ast.valueself else func = ast[1].value end -- Handle function call. local argvalues_concrete = true; do -- true iff all arguments known precisely. if #ast >= 2 then local firstargvalue; if isinvoke then firstargvalue = ast.valueself else firstargvalue = ast[2].value end if unknown(firstargvalue) then argvalues_concrete = false else -- test remaining args for i=3,#ast do if unknown(ast[i].value) then argvalues_concrete = false; break end end end end end local found if known(func) and argvalues_concrete then -- attempt call with concrete args -- Get list of values of arguments. local argvalues; do argvalues = {n=#ast-1}; for i=1,argvalues.n do argvalues[i] = ast[i+1].value end if isinvoke then argvalues[1] = ast.valueself end -- `self` end -- Any call to require is handled specially (source analysis). if func == require and type(argvalues[1]) == 'string' then local spath = tostring(ast.lineinfo.first):gsub(' (nparammax or math.huge) then ast.note = "Too many arguments; " iswarn = true end if iswarn then ast.note = ast.note .. "expected " .. nparammin .. (nparammax == nparammin and "" or " to " .. (nparammax or "infinity")) .. " but got " .. nargmin .. (nargmax == nargmin and "" or " to " .. (nargmax or "infinity")) .. "." end end end end) end -- Resolves identifier to value [*] function M.resolve_id(id, scope, valueglobals, _G) local val if scope[id] then val = scope[id].value elseif valueglobals[id] ~= nil then val = valueglobals[id] else val = _G[id] -- assumes not raise end return val end -- Resolves prefix chain expression to value. [*] -- On error returns nil and error object function M.resolve_prefixexp(ids, scope, valueglobals, _G) local _1 = M.resolve_id(ids[1], scope, valueglobals, _G) local ok, err = pzcall(function() for i=2,#ids do _1 = _1[ids[i]] end end, {}) if err then return nil, err or '?' end return _1 end -- Gets local scope at given 1-indexed char position function M.get_scope(pos1, ast, tokenlist) local mast, isafter = LA.current_statementblock(ast, tokenlist, pos1) local scope = LG.variables_in_scope(mast, isafter) return scope end -- Gets names in prefix expression ids (as returned by resolve_prefixexp). [*] function M.names_in_prefixexp(ids, pos, ast, tokenlist) local scope = M.get_scope(pos, ast, tokenlist) --FIX: above does not handle `for x=1,2 do| print(x) end` where '|' is cursor position. local names = {} if #ids == 0 then -- global for name in pairs(scope) do names[#names+1] = name end for name in pairs(ast.valueglobals) do names[#names+1] = name end for name in pairs(_G) do names[#names+1] = name end else -- field local t, err_ = M.resolve_prefixexp(ids, scope, ast.valueglobals, _G) if type(t) == 'table' then -- note: err_ implies false here for name in pairs(t) do names[#names+1] = name end end end return names end -- Gets signature (function argument string or helpinfo string) on value. -- Returns nil on not found. function M.get_signature_of_value(value) local info = M.debuginfo[value] -- first try this if info and info.fast then local fidx, lidx = LA.ast_idx_range_in_tokenlist(info.tokenlist, info.fast[1]) local ts = {} if fidx then for i=fidx,lidx do local token = info.tokenlist[i] ts[#ts+1] = token.tag == 'Dots' and '...' or token[1] end end local sig = 'function(' .. table.concat(ts, ' ') .. ')' if info.retvals then local vals = info.retvals local ts = {} if vals.n == 0 then sig = sig .. " no returns" else for i=1,vals.n do local val = vals[i] ts[#ts+1] = T.istype[val] and tostring(val) or LD.dumpstring(val) --Q:dumpstring too verbose? end sig = sig .. " returns " .. table.concat(ts, ", ") end end return sig end local sig = LS.value_signatures[value] -- else try this return sig end -- Gets signature (function argument string or helpinfo string) on variable ast. -- Returns nil on not found. function M.get_signature(ast) if known(ast.value) then return M.get_signature_of_value(ast.value) end end -- Gets 1-indexed character (or line) position and filename of -- definition associated with AST node (if any). function M.ast_to_definition_position(ast, tokenlist) local local_ast = ast.localdefinition local fpos, fline, path if local_ast then local tidx = LA.ast_idx_range_in_tokenlist(tokenlist, local_ast) if tidx then local spath = tostring(ast.lineinfo.first):gsub('