luaforwindows/files/lua/strictness.lua

259 lines
8.8 KiB
Lua

#!/usr/bin/env lua
------------------
-- *strictness, a "strict" mode for Lua*.
-- Source on [Github](http://github.com/Yonaba/strictness)
-- @author Roland Yonaba
-- @copyright 2013-2014
-- @license MIT
local _LUA52 = _VERSION:match('Lua 5.2')
local setmetatable, getmetatable = setmetatable, getmetatable
local pairs, ipairs = pairs, ipairs
local rawget, rawget = rawget, rawget
local unpack = _LUA52 and table.unpack or unpack
local tostring, select, error = tostring, select, error
local getfenv = getfenv
local _MODULEVERSION = '0.2.0'
----------------------------- Private definitions -----------------------------
if _LUA52 then
-- Provide a replacement for getfenv in Lua 5.2, using the debug library
-- Taken from: http://lua-users.org/lists/lua-l/2010-06/msg00313.html
-- Slightly modified to handle f being nil and return _ENV if f is global.
getfenv = function(f)
f = (type(f) == 'function' and f or debug.getinfo((f or 0) + 1, 'f').func)
local name, val
local up = 0
repeat
up = up + 1
name, val = debug.getupvalue(f, up)
until name == '_ENV' or name == nil
return val~=nil and val or _ENV
end
end
-- Lua reserved keywords
local is_reserved_keyword = {
['and'] = true, ['break'] = true, ['do'] = true, ['else'] = true,
['elseif'] = true, ['end'] = true, ['false'] = true, ['for'] = true,
['function'] = true, ['if'] = true, ['in'] = true, ['local'] = true,
['nil'] = true, ['not'] = true, ['or'] = true, ['repeat'] = true,
['return'] = true, ['then'] = true, ['true'] = true, ['until'] = true,
['while'] = true,
}; if _LUA52 then is_reserved_keyword['goto'] = true end
-- Throws an error if cond
local function complain_if(cond, msg, level)
return cond and error(msg, level or 3)
end
-- Checks if iden match an valid Lua identifier syntax
local function is_identifier(iden)
return tostring(iden):match('^[%a_]+[%w_]*$') and
not is_reserved_keyword[iden]
end
-- Checks if all elements of vararg are valid Lua identifiers
local function validate_identifiers(...)
local arg, varnames= {...}, {}
for i, iden in ipairs(arg) do
complain_if(not is_identifier(iden),
('varname #%d "<%s>" is not a valid Lua identifier.')
:format(i, tostring(iden)),4)
varnames[iden] = true
end
return varnames
end
-- add true keys in register all keys in t
local function add_allowed_keys(t,register)
for key in pairs(t) do
if is_identifier(key) then register[key] = true end
end
return register
end
-- Checks if the given arg is callable
local function callable(f)
return type(f) == 'function' or (getmetatable(f) and getmetatable(f).__call)
end
------------------------------- Module functions ------------------------------
--- Makes a given table strict. It mutates the passed-in table (or creates a
-- new table) and returns it. The returned table is strict, indexing or
-- assigning undefined fields will raise an error.
-- @function strictness.strict
-- @param[opt] t a table
-- @param[opt] ... a vararg list of allowed fields in the table.
-- @return the passed-in table `t` or a new table, patched to be strict.
-- @usage
-- local t = strictness.strict()
-- local t2 = strictness.strict({})
-- local t3 = strictness.strict({}, 'field1', 'field2')
local function make_table_strict(t, ...)
t = t or {}
local has_mt = getmetatable(t)
complain_if(type(t) ~= 'table',
('Argument #1 should be a table, not %s.'):format(type(t)),3)
local mt = getmetatable(t) or {}
complain_if(mt.__strict,
('<%s> was already made strict.'):format(tostring(t)),3)
local varnames = v
mt.__allowed = add_allowed_keys(t, validate_identifiers(...))
mt.__predefined_index = mt.__index
mt.__predefined_newindex = mt.__newindex
mt.__index = function(tbl, key)
if not mt.__allowed[key] then
if mt.__predefined_index then
local expected_result = mt.__predefined_index(tbl, key)
if expected_result then return expected_result end
end
complain_if(true,
('Attempt to access undeclared variable "%s" in <%s>.')
:format(key, tostring(tbl)),3)
end
return rawget(tbl, key)
end
mt.__newindex = function(tbl, key, val)
if mt.__predefined_newindex then
mt.__predefined_newindex(tbl, key, val)
if rawget(tbl, key) ~= nil then return end
end
if not mt.__allowed[key] then
if val == nil then
mt.__allowed[key] = true
return
end
complain_if(not mt.__allowed[key],
('Attempt to assign value to an undeclared variable "%s" in <%s>.')
:format(key,tostring(tbl)),3)
mt.__allowed[key] = true
end
rawset(tbl, key, val)
end
mt.__strict = true
mt.__has_mt = has_mt
return setmetatable(t, mt)
end
--- Checks if a given table is strict.
-- @function strictness.is_strict
-- @param t a table
-- @return `true` if the table is strict, `false` otherwise.
-- @usage
-- local is_strict = strictness.is_strict(a_table)
local function is_table_strict(t)
complain_if(type(t) ~= 'table',
('Argument #1 should be a table, not %s.'):format(type(t)),3)
return not not (getmetatable(t) and getmetatable(t).__strict)
end
--- Makes a given table non-strict. It mutates the passed-in table and
-- returns it. The returned table is non-strict.
-- @function strictness.unstrict
-- @param t a table
-- @usage
-- local unstrict_table = strictness.unstrict(trict_table)
local function make_table_unstrict(t)
complain_if(type(t) ~= 'table',
('Argument #1 should be a table, not %s.'):format(type(t)),3)
if is_table_strict(t) then
local mt = getmetatable(t)
if not mt.__has_mt then
setmetatable(t, nil)
else
mt.__index, mt.__newindex = mt.__predefined_index, mt.__predefined_newindex
mt.__strict, mt.__allowed, mt.__has_mt = nil, nil, nil
mt.__predefined_index, mt.__predefined_newindex = nil, nil
end
end
return t
end
--- Creates a strict function. Wraps the given function and returns the wrapper.
-- The new function will always run in strict mode in its environment, whether
-- or not this environment is strict.
-- @function strictness.strictf
-- @param f a function, or a callable value.
-- @usage
-- local strict_f = strictness.strictf(a_function)
-- local result = strict_f(...)
local function make_function_strict(f)
complain_if(not callable(f),
('Argument #1 should be a callable, not %s.'):format(type(f)),3)
return function(...)
local ENV = getfenv(f)
local was_strict = is_table_strict(ENV)
if not was_strict then make_table_strict(ENV) end
local results = {f(...)}
if not was_strict then make_table_unstrict(ENV) end
return unpack(results)
end
end
--- Creates a non-strict function. Wraps the given function and returns the wrapper.
-- The new function will always run in non-strict mode in its environment, whether
-- or not this environment is strict.
-- @function strictness.unstrictf
-- @param f a function, or a callable value.
-- @usage
-- local unstrict_f = strictness.unstrictf(a_function)
-- local result = unstrict_f(...)
local function make_function_unstrict(f)
complain_if(not callable(f),
('Argument #1 should be a callable, not %s.'):format(type(f)),3)
return function(...)
local ENV = getfenv(f)
local was_strict = is_table_strict(ENV)
make_table_unstrict(ENV)
local results = {f(...)}
if was_strict then make_table_strict(ENV) end
return unpack(results)
end
end
--- Returns the result of a function call in strict mode.
-- @function strictness.run_strict
-- @param f a function, or a callable value.
-- @param[opt] ... a vararg list of arguments to function `f`.
-- @usage
-- local result = strictness.run_strict(a_function, arg1, arg2)
local function run_strict(f,...)
complain_if(not callable(f),
('Argument #1 should be a callable, not %s.'):format(type(f)),3)
return make_function_strict(f)(...)
end
--- Returns the result of a function call in non-strict mode.
-- @function strictness.run_unstrict
-- @param f a function, or a callable value.
-- @param[opt] ... a vararg list of arguments to function `f`.
-- @usage
-- local result = strictness.run_unstrict(a_function, arg1, arg2)
local function run_unstrict(f,...)
complain_if(not callable(f),
('Argument #1 should be a callable, not %s.'):format(type(f)),3)
return make_function_unstrict(f)(...)
end
return {
strict = make_table_strict,
unstrict = make_table_unstrict,
is_strict = is_table_strict,
strictf = make_function_strict,
unstrictf = make_function_unstrict,
run_strict = run_strict,
run_unstrict = run_unstrict,
_VERSION = 'strictness v'.._MODULEVERSION,
_URL = 'http://github.com/Yonaba/strictness',
_LICENSE = 'MIT <http://raw.githubusercontent.com/Yonaba/strictness/master/LICENSE>',
_DESCRIPTION = 'Tracking accesses and assignments to undefined variables in Lua code'
}