259 lines
8.8 KiB
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'
|
||
|
}
|