alunizaje/android/tools/zbstudio.app/Contents/ZeroBraneStudio/lualibs/luainspect/init.lua

1455 lines
49 KiB
Lua
Raw Normal View History

2016-11-03 00:05:36 +01:00
-- 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('<C|','<'):match('<([^|]+)') -- a HACK? relies on AST lineinfo
local val, mast = M.require_inspect(argvalues[1], report, spath:gsub('[^\\/]+$', ''))
if known(val) and val ~= nil then
ast.value = val
found = true
end -- note: on nil value, assumes analysis failed (not found). This is a heuristic only.
if mast and mast.valueglobals then ast.valueglobals = mast.valueglobals end
end
-- Attempt call if safe.
if not found and (LS.safe_function[func] or func == pcall and LS.safe_function[argvalues[1]]) then
local ok; ok, ast.valuelist = valuesandtuple(1, pcall(func, unpack(argvalues,1,argvalues.n)))
ast.value = ast.valuelist[1]; if not ok then ast.value = T.error(ast.value) end
found = true
end
end
if not found then
-- Attempt mock function. Note: supports nonconcrete args too.
local mf = LS.mock_functions[func]
if mf then
ast.valuelist = mf.outputs; ast.value = ast.valuelist[1]
else
-- Attempt infer from return statements in function source.
local info = M.debuginfo[func]
if not info then -- try match from dynamic debug info
local dinfo = type(func) == 'function' and debug.getinfo(func)
if dinfo then
local source, linedefined = dinfo.source, dinfo.linedefined
if source and linedefined then
local sourceline = source .. ':' .. linedefined
info = M.debuginfo[sourceline]
end
end
end
local retvals = info and info.retvals
if retvals then
ast.valuelist = retvals; ast.value = ast.valuelist[1]
else
-- Could not infer.
ast.valuelist = {n=0, sizeunknown=true}; ast.value = T.universal
end
end
end
elseif ast.tag == 'String' or ast.tag == 'Number' then
ast.value = ast[1]
elseif ast.tag == 'True' or ast.tag == 'False' then
ast.value = (ast.tag == 'True')
elseif ast.tag == 'Function' or ast == top_ast then -- includes chunk
if ast.value == nil then -- avoid redefinition
local x
local val = function() x=nil end
local fpos = LA.ast_pos_range(ast, tokenlist)
local source, linenum = tostring(ast.lineinfo.first):gsub('<C|','<'):match('<([^|]+)|L(%d+)') -- a HACK? relies on AST lineinfo
local retvals
if ENABLE_RETURN_ANALYSIS then
retvals = get_func_return_values(ast) --Q:move outside of containing conditional?
end
local info = {fpos=fpos, source="@" .. source, fast=ast, tokenlist=tokenlist, retvals=retvals, top_ast = top_ast}
M.debuginfo[val] = info
local sourceline = '@' .. source .. ':' .. linenum
local oldinfo = M.debuginfo[sourceline]
if oldinfo then
if oldinfo.fast ~= ast then
-- Two functions on the same source line cannot necessarily be disambiguated.
-- Unfortuntely, Lua debuginfo lacks exact character position.
-- http://lua-users.org/lists/lua-l/2010-08/msg00273.html
-- So, just disable info if ambiguous. Note: a slight improvement is to use the lastlinedefined.
M.debuginfo[sourceline] = false
end
else
if oldinfo == nil then
M.debuginfo[sourceline] = info -- store by sourceline too for quick lookup from dynamic debug info
end -- else false (do nothing)
end
ast.value = val
ast.nocollect = info -- prevents garbage collection while ast exists
end
elseif ast.tag == 'Table' then
if ast.value == nil then -- avoid redefinition
local value = {}
local n = 1
for _,east in ipairs(ast) do
if east.tag == 'Pair' then
local kast, vast = east[1], east[2]
if known(kast.value) and known(vast.value) then
if kast.value == nil then
-- IMPROVE? warn in some way?
else
value[kast.value] = vast.value
end
end
else
if known(east.value) then
value[n] = east.value
end
n = n + 1
end
end
--table.foreach(value, print)
ast.value = value
end
elseif ast.tag == 'Paren' then
ast.value = ast[1].value
elseif ast.tag == 'Op' then
local opid, aast, bast = ast[1], ast[2], ast[3]
local ok
if bast then
ok, ast.value = pzcall(dobinop, {aast, bast}, opid, aast.value, bast.value)
else
ok, ast.value = pzcall(dounop, {aast}, opid, aast.value)
end
if not ok then ast.value = T.error(ast.value) end
elseif ast.tag == 'If' then
-- detect dead-code
if DETECT_DEADCODE then
for i=2,#ast,2 do local valnode = ast[i-1]
local bval = T.boolean_cast(valnode.value)
if bval == false then -- certainly false
mark_dead(ast[i])
elseif bval == true then -- certainly true
for ii=i+1,#ast do if ii%2 == 0 or ii==#ast then -- following blocks are dead
mark_dead(ast[ii])
end end
break
end
end
end
-- IMPROVE? `if true return end; f()` - f could be marked as deadcode
elseif ast.tag == 'While' then
-- detect dead-code
if DETECT_DEADCODE then
local expr_ast, body_ast = ast[1], ast[2]
if T.boolean_cast(expr_ast.value) == false then
mark_dead(body_ast)
end
end
end
end)
end
-- Labels variables with unique identifiers.
-- Sets ast.id, ast.resolvedname
-- CATEGORY: code interpretation
function M.mark_identifiers(ast)
local id = 0
local seen_globals = {}
LA.walk(ast, function(ast)
if ast.tag == 'Id' or ast.isfield then
if ast.localdefinition then
if ast.localdefinition == ast then -- lexical definition
id = id + 1
ast.id = id
else
ast.id = ast.localdefinition.id
end
elseif ast.isfield then
local previousid = ast.previous.id
if not previousid then -- note: ("abc"):upper() has no previous ID
id = id + 1
previousid = id
end
local name = previousid .. '.' .. ast[1]:gsub('%%', '%%'):gsub('%.', '%d')
if not seen_globals[name] then
id = id + 1
seen_globals[name] = id
end
ast.id = seen_globals[name]
-- also resolve name
local previousresolvedname = ast.previous.resolvedname
if previousresolvedname then
ast.resolvedname = previousresolvedname .. '.' .. ast[1]:gsub('%%', '%%'):gsub('%.', '%d')
end
else -- global
local name = ast[1]
if not seen_globals[name] then
id = id + 1
seen_globals[name] = id
end
ast.id = seen_globals[name]
-- also resolve name
ast.resolvedname = ast[1]
end
end
end)
end
-- Environment in which to execute special comments (see below).
local env = setmetatable({}, {__index=_G})
env.context = env
env.number = T.number
env.string = T.string
env.boolean = T.boolean
env.error = T.error
-- Applies value to all identifiers with name matching pattern.
-- This command is callable inside special comments.
-- CATEGORY: code interpretation / special comment command
function env.apply_value(pattern, val)
local function f(ast)
if ast.tag == 'Id' and ast[1]:match(pattern) then
ast.value = val; ast.isvaluepegged = true
end
for _,bast in ipairs(ast) do
if type(bast) == 'table' then
f(bast)
end
end
end
f(env.ast) -- ast from environment
--UNUSED:
-- for i=env.asti, #env.ast do
-- local bast = env.ast[i]
-- if type(bast) == 'table' then f(bast) end
--end
end
-- Evaluates all special comments (i.e. comments prefixed by '!') in code.
-- This is similar to luaanalyze.
-- CATEGORY: code interpretation / special comments
function M.eval_comments(ast, tokenlist, report)
local function eval(command, ast)
--DEBUG('!', command:gsub('%s+$', ''), ast.tag)
local f, err = COMPAT.load(command, nil, 't', env)
if f then
env.ast = ast
local ok, err = pcall(f, ast)
if not ok then warn(report, err, ': ', command) end
env.ast = nil
else
warn(report, err, ': ', command)
end
end
for idx=1,#tokenlist do
local token = tokenlist[idx]
if token.tag == 'Comment' then
local command = token[1]:match'^!(.*)'
if command then
local mast = LA.smallest_ast_containing_range(ast, tokenlist, token.fpos, token.lpos)
eval(command, mast)
end
end
end
end
--IMPROVE: in `do f() --[[!g()]] h()` only apply g to h.
-- Partially undoes effects of inspect().
-- Note: does not undo mark_tag2 and mark_parents (see replace_statements).
-- CATEGORY: code interpretation
function M.uninspect(top_ast)
-- remove ast from M.debuginfo
for k, info in pairs(M.debuginfo) do
if info and info.top_ast == top_ast then
M.debuginfo[k] = nil
end
end
-- Clean ast.
LA.walk(top_ast, function(ast)
-- undo inspect_globals.globals
ast.localdefinition = nil
ast.functionlevel = nil
ast.isparam = nil
ast.isset = nil
ast.isused = nil
ast.isignore = nil
ast.isfield = nil
ast.previous = nil
ast.localmasked = nil
ast.localmasking = nil
-- undo mark_identifiers
ast.id = nil
ast.resolvedname = nil
-- undo infer_values
ast.value = nil
ast.valueself = nil
ast.valuelist = nil
ast.isdead = nil -- via get_func_returns
ast.isvaluepegged = nil
-- undo walk setting ast.seevalue
ast.seevalue = nil
-- undo walk setting ast.definedglobal
ast.definedglobal = nil
-- undo notes
ast.note = nil
ast.nocollect = nil
-- undo infer_values
ast.valueglobals = nil
end)
end
-- Main inspection routine. Inspects top_ast/tokenlist.
-- Error/status messages are sent to function `report`.
-- CATEGORY: code interpretation
function M.inspect(top_ast, tokenlist, src, report)
--DEBUG: local t0 = os.clock()
if not report then -- compat for older version of lua-inspect
assert('inspect signature changed; please upgrade your code')
end
report = report or function() end
local globals = LG.globals(top_ast)
M.mark_identifiers(top_ast)
M.eval_comments(top_ast, tokenlist, report)
M.infer_values(top_ast, tokenlist, src, report)
M.infer_values(top_ast, tokenlist, src, report) -- two passes to handle forward declarations of globals (IMPROVE: more passes?)
-- Make some nodes as having values related to its parent.
-- This allows clicking on `bar` in `foo.bar` to display
-- the value of `foo.bar` rather than just "bar".
LA.walk(top_ast, function(ast)
if ast.tag == 'Index' then
ast[2].seevalue = ast
elseif ast.tag == 'Invoke' then
ast[2].seevalue = {value=ast.valueself, parent=ast}
end
end)
local function eval_name_helper(name)
local var = _G
for part in (name .. '.'):gmatch("([^.]*)%.") do
part = part:gsub('%%(.)', unescape)
if type(var) ~= 'table' and type(var) ~= 'userdata' then return nil end --TODO:improve?
var = var[part]
if var == nil then return nil end
end
return var
end
local function eval_name(name)
local ok, o = pzcall(eval_name_helper, {}, name)
if ok then return o else return nil end
end
LA.walk(top_ast, function(ast)
if top_ast ~= ast and ast.valueglobals then
for k in pairs(ast.valueglobals) do globals[k] = {set = ast} end
ast.valueglobals = nil
end
if ast.tag == 'Id' or ast.isfield then
local vname = ast[1]
--TODO: rename definedglobal to definedfield for clarity
local atype = ast.localdefinition and 'local' or ast.isfield and 'field' or 'global'
local definedglobal = ast.resolvedname and eval_name(ast.resolvedname) ~= nil or
atype == 'global' and (globals[vname] and globals[vname].set) or nil
ast.definedglobal = definedglobal
-- FIX: _G includes modules imported by inspect.lua, which is not desired
elseif ast.tag == 'Call' or ast.tag == 'Invoke' then
-- Argument count check.
local value = ast.valueself or ast[1].value
local info = M.debuginfo[value]
local fast = info and info.fast
if fast or LS.argument_counts[value] then
local nparammin, nparammax
if fast then
nparammin, nparammax = function_param_range(info.fast)
else
nparammin, nparammax = unpack(LS.argument_counts[value])
end
local nargmin, nargmax = call_arg_range(ast)
--print('DEBUG:', nparammin, nparammax, nargmin, nargmax)
local iswarn
local target_ast = ast.tag == 'Call' and ast[1] or ast[2]
if (nargmax or math.huge) < nparammin then
ast.note = "Too few arguments; "
iswarn = true
elseif nargmin > (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('<C|','<'):match('<([^|]+)') -- a HACK? using lineinfo
fpos = tokenlist[tidx].fpos; path = spath
end
end
if not fpos then
local valueast = ast.seevalue or ast
local val = valueast and valueast.value
local info = M.debuginfo[val] or type(val) == 'function' and debug.getinfo(val)
if info then
if info.source:match'^@' then
path = info.source:match'@(.*)'
if info.linedefined then
fline = info.linedefined
else
fpos = info.fpos
end
end
end
end
return fpos, fline, path
end
-- Returns true iff value in ast node is known in some way.
function M.is_known_value(ast)
local vast = ast.seevalue or ast
return vast.definedglobal or known(vast.value) and vast.value ~= nil
end
-- Gets list of variable attributes for AST node.
function M.get_var_attributes(ast)
local vast = ast.seevalue or ast
local attributes = {}
if ast.localdefinition then
attributes[#attributes+1] = "local"
if ast.localdefinition.functionlevel < ast.functionlevel then
attributes[#attributes+1] = 'upvalue'
end
if ast.localdefinition.isparam then
attributes[#attributes+1] = "param"
end
if not ast.localdefinition.isused then attributes[#attributes+1] = 'unused' end
if ast.isignore then attributes[#attributes+1] = 'ignore' end
if ast.localdefinition.isset then attributes[#attributes+1] = 'mutatebind'
else attributes[#attributes+1] = 'constbind' end
if ast.localmasking then
attributes[#attributes+1] = "masking"
end
if ast.localmasked then
attributes[#attributes+1] = "masked"
end
elseif ast.tag == 'Id' then -- global
attributes[#attributes+1] = (M.is_known_value(vast) and "known" or "unknown")
attributes[#attributes+1] = "global"
elseif ast.isfield then
attributes[#attributes+1] = (M.is_known_value(vast) and "known" or "unknown")
attributes[#attributes+1] = "field"
else
attributes[#attributes+1] = "FIX" -- shouldn't happen?
end
if vast.parent and (vast.parent.tag == 'Call' or vast.parent.tag == 'Invoke')
and vast.parent.note
then
attributes[#attributes+1] = 'warn'
end
return attributes
end
-- Gets detailed information about value in AST node, as string.
function M.get_value_details(ast, tokenlist, src)
local lines = {}
if not ast then return '?' end
local vast = ast.seevalue or ast
lines[#lines+1] = "attributes: " .. table.concat(M.get_var_attributes(ast), " ")
lines[#lines+1] = "value: " .. tostring(vast.value)
local sig = M.get_signature(vast)
if sig then
local kind = sig:find '%w%s*%b()$' and 'signature' or 'description'
lines[#lines+1] = kind .. ": " .. sig
end
local fpos, fline, path = M.ast_to_definition_position(ast, tokenlist)
if fpos or fline then
local fcol
if fpos then
fline, fcol = LA.pos_to_linecol(fpos, src)
end
local location = path .. ":" .. (fline) .. (fcol and ":" .. fcol or "")
lines[#lines+1] = "location defined: " .. location
end
if ast.localdefinition and ast.localmasking then
local fpos = LA.ast_pos_range(ast.localmasking, tokenlist)
if fpos then
local linenum = LA.pos_to_linecol(fpos, src)
lines[#lines+1] = "masking definition at line: " .. linenum
end
end
-- Render warning notes attached to calls/invokes.
local note = vast.parent and (vast.parent.tag == 'Call' or vast.parent.tag == 'Invoke')
and vast.parent.note
if note then
lines[#lines+1] = "WARNING: " .. note
end
return table.concat(lines, "\n")
end
-- Gets list of all warnings, as strings.
-- In HTML Tidy format (which supports column numbers in SciTE, although is
-- slightly verbose and lacks filename).
function M.list_warnings(tokenlist, src)
local warnings = {}
local ttoken
local function warn(msg)
local linenum, colnum = LA.pos_to_linecol(ttoken.fpos, src)
warnings[#warnings+1] = "line " .. linenum .. " column " .. colnum .. " - " .. msg
end
local isseen = {}
for i,token in ipairs(tokenlist) do ttoken = token
if token.ast then
local ast = token.ast
if ast.localmasking then
local pos = LA.ast_pos_range(ast.localmasking, tokenlist)
local linenum = pos and LA.pos_to_linecol(pos, src)
warn("local " .. ast[1] .. " masks another local" .. (pos and " on line " .. linenum or ""))
end
if ast.localdefinition == ast and not ast.isused and not ast.isignore then
warn("unused local " .. ast[1])
end
if ast.isfield and not(known(ast.seevalue.value) and ast.seevalue.value ~= nil) then
warn("unknown field " .. ast[1])
elseif ast.tag == 'Id' and not ast.localdefinition and not ast.definedglobal then
warn("unknown global " .. ast[1])
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
local esrc = LA.ast_to_text(vast.parent, tokenlist, src)
-- IMPROVE: large items like `f(function() ... end)` may be shortened.
warn(note .. (esrc and "for " .. esrc or ""))
end
end
end
return warnings
end
return M