diff --git a/files/docs/strictness/CHANGELOG.md b/files/docs/strictness/CHANGELOG.md new file mode 100644 index 0000000..49d4365 --- /dev/null +++ b/files/docs/strictness/CHANGELOG.md @@ -0,0 +1,20 @@ +CHANGELOG +========= + +###0.2.0 (06/11/14) +* `strictness` no longer create globals. It returns a local table of library functions. +* `strictness` can now create (or convert) strict/unstrict tables (or environnements). +* Strict/unstrict rules are now applied per table (or environnement). +* Strict mode enforces variable declarations and complain on undefined fields access/assignment. +* Tables (or environnements) already existing metatables are preserved, including `__index` and `__newindex` fields. +* Added `strictness.strict` to convert a normal table (or environnement) to a strict one. +* Added `strictness.unstrict` to convert a strict table (or environnement) to a normal one. +* Added `strictness.is_strict` to check if a table (or environnement) is strict. +* Added `strictness.strictf` to wrap a normal function into a non strict function. +* Added `strictness.unstrictf` to wrap a normal function into a strict function. +* Added `strictness.run_strict` to run a normal function in strict mode. +* Added `strictness.run_unstrict` to run a normal function in non strict mode. +* Made compliant with Lua 5.2 new _ENV lexical scoping (although strictly speaking there are no globals in Lua 5.2). + +###0.1.0 (10/05/13) +* Initial release \ No newline at end of file diff --git a/files/docs/strictness/LICENSE b/files/docs/strictness/LICENSE new file mode 100644 index 0000000..8ba7b05 --- /dev/null +++ b/files/docs/strictness/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013-2014 Roland Y. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/files/docs/strictness/README.md b/files/docs/strictness/README.md new file mode 100644 index 0000000..1f7ed5f --- /dev/null +++ b/files/docs/strictness/README.md @@ -0,0 +1,63 @@ +strictness +=========== + +[![Build Status](https://travis-ci.org/Yonaba/strictness.png)](https://travis-ci.org/Yonaba/strictness) +[![Coverage Status](https://coveralls.io/repos/Yonaba/strictness/badge.png?branch=master)](https://coveralls.io/r/Yonaba/strictness?branch=master) +[![License](http://img.shields.io/badge/Licence-MIT-brightgreen.svg)](LICENSE) + +With the __Lua__ programming language, undeclared variables are not detected until runtime, as Lua will not complain when loading code. +This is releated to the convention that Lua uses : [global by default](http://www.lua.org/pil/1.2.html). In other words, when a variable is not recognized as *local*, it will be +interpreted as a *global* one, and will involve a lookup in the global environment `_G` (for Lua 5.1). Note that this behaviour has been addressed +in Lua 5.2, which strictly speaking has no globals, because of its [lexical scoping](http://www.luafaq.org/#T8.2.1). + +*strictness* is a module to track *access and assignment* to undefined variables in your code. It *enforces* to declare globals and modules variables before +assigning them values. As such, it helps having a better control on the scope of variables across the code. + +*strictness* is mostly meant to work with Lua [5.1](http://www.lua.org/versions.html#5.1), but it is compatible with Lua [5.2](http://www.lua.org/versions.html#5.2). + +##Installation + +####Git + + git clone git://github.com/Yonaba/strictness + +####Download + +* See [releases](https://github.com/Yonaba/strictness/releases) + +####LuaRocks + + luarocks install strictness + +####MoonRocks + + moonrocks install strictness + +or + + luarocks install strictness --server=http://rocks.moonscript.org strictness + + +## Documentation + +See [tutorial.md](doc/tutorial.md). + +##Tests + +This project has specification tests. To run these tests, execute the following command from the project root folder: + + lua spec/tests.lua + +##Similar projects + +Feel free to check those alternate implementations, from with *strictness* takes some inspiration: + +* [strict.lua](http://rtfc.googlecode.com/svn-history/r2/trunk/lua-5.1/etc/strict.lua) which is included in the official Lua 5.1 distribution, +* [pl.strict](https://github.com/stevedonovan/Penlight/blob/master/lua/pl/strict.lua) which is part of [Penlight](https://github.com/stevedonovan/Penlight), + +##License +This work is under [MIT-LICENSE](http://www.opensource.org/licenses/mit-license.php)
+*Copyright (c) 2013-2014 Roland Yonaba*. +See [LICENSE](LICENSE). + +[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/Yonaba/strictness/trend.png)](https://bitdeli.com/free "Bitdeli Badge") \ No newline at end of file diff --git a/files/docs/strictness/html/index.html b/files/docs/strictness/html/index.html new file mode 100644 index 0000000..fe1c75a --- /dev/null +++ b/files/docs/strictness/html/index.html @@ -0,0 +1,306 @@ + + + + + strictness documentation + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module strictness

+

strictness, a "strict" mode for Lua.

+

Source on Github

+

Info:

+
    +
  • Copyright: 2013-2014
  • +
  • License: MIT
  • +
  • Author: Roland Yonaba
  • +
+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
strict ([t[, ...]])Makes a given table strict.
is_strict (t)Checks if a given table is strict.
unstrict (t)Makes a given table non-strict.
strictf (f)Creates a strict function.
unstrictf (f)Creates a non-strict function.
run_strict (f[, ...])Returns the result of a function call in strict mode.
run_unstrict (f[, ...])Returns the result of a function call in non-strict mode.
+ +
+
+ + +

Functions

+ +
+
+ + strict ([t[, ...]]) +
+
+ 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. + + +

Parameters:

+
    +
  • t + a table +
  • +
  • ... + a vararg list of allowed fields in the table. +
  • +
+ +

Returns:

+
    + + 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')
    +
+ +
+
+ + is_strict (t) +
+
+ Checks if a given table is strict. + + +

Parameters:

+
    +
  • t + a table +
  • +
+ +

Returns:

+
    + + true if the table is strict, false otherwise. +
+ + + +

Usage:

+
    +
    local is_strict = strictness.is_strict(a_table)
    +
+ +
+
+ + unstrict (t) +
+
+ Makes a given table non-strict. It mutates the passed-in table and + returns it. The returned table is non-strict. + + +

Parameters:

+
    +
  • t + a table +
  • +
+ + + + +

Usage:

+
    +
    local unstrict_table = strictness.unstrict(trict_table)
    +
+ +
+
+ + strictf (f) +
+
+ 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. + + +

Parameters:

+
    +
  • f + a function, or a callable value. +
  • +
+ + + + +

Usage:

+
    +
    + local strict_f = strictness.strictf(a_function)
    + local result = strict_f(...)
    +
+ +
+
+ + unstrictf (f) +
+
+ 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. + + +

Parameters:

+
    +
  • f + a function, or a callable value. +
  • +
+ + + + +

Usage:

+
    +
    + local unstrict_f = strictness.unstrictf(a_function)
    + local result = unstrict_f(...)
    +
+ +
+
+ + run_strict (f[, ...]) +
+
+ Returns the result of a function call in strict mode. + + +

Parameters:

+
    +
  • f + a function, or a callable value. +
  • +
  • ... + a vararg list of arguments to function f. +
  • +
+ + + + +

Usage:

+
    +
    local result = strictness.run_strict(a_function, arg1, arg2)
    +
+ +
+
+ + run_unstrict (f[, ...]) +
+
+ Returns the result of a function call in non-strict mode. + + +

Parameters:

+
    +
  • f + a function, or a callable value. +
  • +
  • ... + a vararg list of arguments to function f. +
  • +
+ + + + +

Usage:

+
    +
    local result = strictness.run_unstrict(a_function, arg1, arg2)
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.2 +
+
+ + diff --git a/files/docs/strictness/html/ldoc.css b/files/docs/strictness/html/ldoc.css new file mode 100644 index 0000000..765c710 --- /dev/null +++ b/files/docs/strictness/html/ldoc.css @@ -0,0 +1,302 @@ +/* BEGIN RESET + +Copyright (c) 2010, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.8.2r1 +*/ +html { + color: #000; + background: #FFF; +} +body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td { + margin: 0; + padding: 0; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +fieldset,img { + border: 0; +} +address,caption,cite,code,dfn,em,strong,th,var,optgroup { + font-style: inherit; + font-weight: inherit; +} +del,ins { + text-decoration: none; +} +li { + list-style: disc; + margin-left: 20px; +} +caption,th { + text-align: left; +} +h1,h2,h3,h4,h5,h6 { + font-size: 100%; + font-weight: bold; +} +q:before,q:after { + content: ''; +} +abbr,acronym { + border: 0; + font-variant: normal; +} +sup { + vertical-align: baseline; +} +sub { + vertical-align: baseline; +} +legend { + color: #000; +} +input,button,textarea,select,optgroup,option { + font-family: inherit; + font-size: inherit; + font-style: inherit; + font-weight: inherit; +} +input,button,textarea,select {*font-size:100%; +} +/* END RESET */ + +body { + margin-left: 1em; + margin-right: 1em; + font-family: arial, helvetica, geneva, sans-serif; + background-color: #ffffff; margin: 0px; +} + +code, tt { font-family: monospace; } +span.parameter { font-family:monospace; } +span.parameter:after { content:":"; } +span.types:before { content:"("; } +span.types:after { content:")"; } +.type { font-weight: bold; font-style:italic } + +body, p, td, th { font-size: .95em; line-height: 1.2em;} + +p, ul { margin: 10px 0 0 0px;} + +strong { font-weight: bold;} + +em { font-style: italic;} + +h1 { + font-size: 1.5em; + margin: 0 0 20px 0; +} +h2, h3, h4 { margin: 15px 0 10px 0; } +h2 { font-size: 1.25em; } +h3 { font-size: 1.15em; } +h4 { font-size: 1.06em; } + +a:link { font-weight: bold; color: #004080; text-decoration: none; } +a:visited { font-weight: bold; color: #006699; text-decoration: none; } +a:link:hover { text-decoration: underline; } + +hr { + color:#cccccc; + background: #00007f; + height: 1px; +} + +blockquote { margin-left: 3em; } + +ul { list-style-type: disc; } + +p.name { + font-family: "Andale Mono", monospace; + padding-top: 1em; +} + +pre.example { + background-color: rgb(245, 245, 245); + border: 1px solid silver; + padding: 10px; + margin: 10px 0 10px 0; + font-family: "Andale Mono", monospace; + font-size: .85em; +} + +pre { + background-color: rgb(245, 245, 245); + border: 1px solid silver; + padding: 10px; + margin: 10px 0 10px 0; + overflow: auto; + font-family: "Andale Mono", monospace; +} + + +table.index { border: 1px #00007f; } +table.index td { text-align: left; vertical-align: top; } + +#container { + margin-left: 1em; + margin-right: 1em; + background-color: #f0f0f0; +} + +#product { + text-align: center; + border-bottom: 1px solid #cccccc; + background-color: #ffffff; +} + +#product big { + font-size: 2em; +} + +#main { + background-color: #f0f0f0; + border-left: 2px solid #cccccc; +} + +#navigation { + float: left; + width: 18em; + vertical-align: top; + background-color: #f0f0f0; + overflow: visible; +} + +#navigation h2 { + background-color:#e7e7e7; + font-size:1.1em; + color:#000000; + text-align: left; + padding:0.2em; + border-top:1px solid #dddddd; + border-bottom:1px solid #dddddd; +} + +#navigation ul +{ + font-size:1em; + list-style-type: none; + margin: 1px 1px 10px 1px; +} + +#navigation li { + text-indent: -1em; + display: block; + margin: 3px 0px 0px 22px; +} + +#navigation li li a { + margin: 0px 3px 0px -1em; +} + +#content { + margin-left: 18em; + padding: 1em; + width: 700px; + border-left: 2px solid #cccccc; + border-right: 2px solid #cccccc; + background-color: #ffffff; +} + +#about { + clear: both; + padding: 5px; + border-top: 2px solid #cccccc; + background-color: #ffffff; +} + +@media print { + body { + font: 12pt "Times New Roman", "TimeNR", Times, serif; + } + a { font-weight: bold; color: #004080; text-decoration: underline; } + + #main { + background-color: #ffffff; + border-left: 0px; + } + + #container { + margin-left: 2%; + margin-right: 2%; + background-color: #ffffff; + } + + #content { + padding: 1em; + background-color: #ffffff; + } + + #navigation { + display: none; + } + pre.example { + font-family: "Andale Mono", monospace; + font-size: 10pt; + page-break-inside: avoid; + } +} + +table.module_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.module_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.module_list td.name { background-color: #f0f0f0; min-width: 200px; } +table.module_list td.summary { width: 100%; } + + +table.function_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.function_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.function_list td.name { background-color: #f0f0f0; min-width: 200px; } +table.function_list td.summary { width: 100%; } + +ul.nowrap { + overflow:auto; + white-space:nowrap; +} + +dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} +dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} +dl.table h3, dl.function h3 {font-size: .95em;} + +/* stop sublists from having initial vertical space */ +ul ul { margin-top: 0px; } +ol ul { margin-top: 0px; } +ol ol { margin-top: 0px; } +ul ol { margin-top: 0px; } + +/* styles for prettification of source */ +pre .comment { color: #558817; } +pre .constant { color: #a8660d; } +pre .escape { color: #844631; } +pre .keyword { color: #2239a8; font-weight: bold; } +pre .library { color: #0e7c6b; } +pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } +pre .string { color: #a8660d; } +pre .number { color: #f8660d; } +pre .operator { color: #2239a8; font-weight: bold; } +pre .preprocessor, pre .prepro { color: #a33243; } +pre .global { color: #800080; } +pre .prompt { color: #558817; } +pre .url { color: #272fc2; text-decoration: underline; } diff --git a/files/docs/strictness/tutorial.md b/files/docs/strictness/tutorial.md new file mode 100644 index 0000000..b80eaeb --- /dev/null +++ b/files/docs/strictness/tutorial.md @@ -0,0 +1,277 @@ +strictness tutorial +=================== + +# Table of Contents + +* [What is *strictness* ?](#what) +* [Tutorial](#tuto) + * [Adding *strictness* to your project](#adding) + * [The *strictness* module](#module) + * [Strict tables](#stricttables) + * [Non-strict tables](#unstricttables) + * [Checking strictness](#checking) + * [Strict functions](#strictf) + * [Non-strict functions](#unstrictf) + * [Combo functions](#combo) +* [License](#license) + +# What is *strictness* ? + +*strictness* is a Lua module for tracking accesses and assignements to indefined variables in Lua code. It is actually known that [undefined variables](http://lua-users.org/wiki/DetectingUndefinedVariables) and global variables as well can be very problematic especially when working on large projects and maintaining code that spans across several files. + +*strictness* aims to address this problem by providing a solution similar to [strict structs](http://lua-users.org/wiki/StrictStructs), so that accessing undefined fields will always throw an error. + +**[[⬆]](#TOC)** + +# Tutorial + +## Adding *strictness* to your project + +Place the file [strictness.lua](strictness.lua) in your Lua project and call it with [require](http://pgl.yoyo.org/luai/i/require). *strictness* does not write anything in the global (or the current) environnement. It rather returns a local module of functions. + +```lua +local strictness = require "strictness" +```` + +**[[⬆]](#TOC)** + +## The *strictness* module + +### Strict tables + +*strictness* provides the function `strictness.strict` that patches a given table, so that we can no longer access to undefined keys in this table. +Let us apply appy this on the global environnement: + +```lua +strictness.strict(_G) +print(x) --> this line produces an error +```` + +The statement `print(x)`produces the following error: + +```` +...\test.lua:2: Attempt to access undeclared variable "x" in . +``` + +To avoid this, we now have to __declare explitely__ our globals. Assigning `nil` will do: + +```lua +strictness.strict(_G) +x = nil +print(x) --> nil +x = 3 +print(x) --> 3 +```` + +A table can be made strict with allowed varnames. + +```lua +strictness.strict(_G, 'x', 'y', 'z') -- "varnames x, y and z are allowed" +print(x, y, z) --> nil, nil, nil +x, y, z = 1, 2, 3 +print(x, y, z) --> 1, 2, 3 +```` + +Also, in case nothing is passed to `strictness.strict`, it will return a new table: + +```lua +local t = strictness.strict() +print(t.k) --> produces an error +t.k = nil --> declare a field "k" +print(t.k) --> nil +```` + +`strictness.strict` preserves the metatable of the passed-in table. + +```lua +local t = setmetatable({}, { + __call = function() return 'call' end, + __tostring = function() return t.name end +}) +t.name = 'table t' +strictness.strict(t) +print(t()) --> "call" +print(t) --> "table t" +```` + +In case a table was already made strict, passing it again to `strictness.strict` will raise an error: + +```lua +local t = {} +strictness.strict(t) +strictness.strict(t) --> this will produce an error +```` + +```` +...\test.lua:3: was already made strict. +```` + +**[[⬆]](#TOC)** + +### Non-Strict (or normal) tables + +A strict table can be converted back to a normal one via `strictness.unstrict`: + +```lua +local t = strictness.strict() +strictness.unstrict(t) +t.k = 5 +print(t.k) --> 5 +```` + +**[[⬆]](#TOC)** + +### Checking strictness + +`strictness.is_strict` checks if a given table was patched via `strictness.strict`: + +```lua +local strict_table = strictness.strict() +local normal_table = {} + +print(strictness.is_strict(strict_table)) --> true +print(strictness.is_strict(normal_table)) --> false +```` + +**[[⬆]](#TOC)** + +### Strict functions + +`strictness.strictf` returns a wrapper function that runs the original function in strict mode. The returned function is not allowed to write or access undefined fields in its environment. Let us draw an example: + +```lua +local env = {} -- a blank environment for our functions + +-- A function that writes a varname and assigns it a value +local function normal_f(varname, value) + env[varname] = value +end +-- Convert the original function to a strict one +local strict_f = strictness.strictf(normal_f) + +-- set environments for functions +setfenv(normal_f, env) +setfenv(strict_f, env) + +-- Call the normal function, no error +normal_f("var1", "hello") +print(env.var1) --> "hello" + + +strict_f("var2", "hello") --> produces an error +```` + +```` +...\test.lua:5: Attempt to assign value to an undeclared variable "var2" in . +```` + +Notice that here, the strict function always run in strict mode whether its environment is strict or not. + +**[[⬆]](#TOC)** + +### Non-strict functions + +Similarly, `strictness.unstrictf` creates a wrapper function that runs in non-strict mode in its environment. In other terms, the returned function is allowed to access and assign values in its environments, whether or not this environment is strict. + +```lua +local env = strictness.strict() -- a blank and strict environment for our functions + +-- A function that assigns a value to a variable named "some_var" +local function normal_f(value) + some_var = value +end + +-- Converts the original function to a non-strict one +local unstrict_f = strictness.unstrictf(normal_f) + +-- set environments for functions +setfenv(normal_f, env) +setfenv(unstrict_f, env) + +-- Call the normal function, it should err because its env is strict +normal_f("hello") --> produces an error + +-- Call the non-strict function, no error +unstrict_f("hello") +print(env.some_var) --> "hello +```` + +Here is an example with Lua 5.2: + +```lua +local new_env = {print = print} -- a new env +do + local _ENV = strictness.strict(new_env) -- sets a new strict env for the do..end scope + local function normal_f(value) some_var = value end -- our normal function + normal_f(5) --> produces an error, since normal_f cannot write in the strict _ENV +end +```` + +```lua +local new_env = {print = print} -- a new env +do + local _ENV = strictness.strict(new_env) -- sets a new strict env for the do..end scope + local function normal_f(value) some_var = value end -- our normal function + local unstrict_f = strictness.unstrictf(normal_f) -- the non-strict version of our normal function + unstrict_f(5) -- no longer produces error + print(some_var) --> 5 +end +```` + +**[[⬆]](#TOC)** + +### Combo functions + +*strictness* also provides two combo functions, `strictness.run_strict` and `strictness.run_unstrict`. Those functions takes a function `f` plus an optional vararg `...` and return the result of the call `f(...)` in strict and non-strict mode respectively. +Syntactically speaking, `strictnes.run_strict` is the equivalent to this: + +```lua +local strict_f = strictness.strictf(f) +strict_f(...) +```` + +While `strictness.run_unstrict` is a short for: + +```lua +local unstrict_f = strictness.unstrictf(f) +unstrict_f(...) +```` + +Here is an example for `strictness.run_strict`: + +```lua +local strictness = require 'strictness' + +local env = {} -- an environment + +-- A function that assigns a value to a variable named "some_var" +local function normal_f(value) some_var = value end + +setfenv(normal_f, env) -- defines an env for normal_f +strictness.run_strict(normal_f, 3) --> produces an error +```` + +And another example with `strictness.run_unstrict``: + +```lua +local strictness = require 'strictness' + +local env = strictness.strict() -- a strict environment + +-- A function that assigns a value to a variable named "some_var" +local function normal_f(value) some_var = value end + +setfenv(normal_f, env) -- defines an env for normal_f +strictness.run_unstrict`(normal_f, 3) -- no error! +print(env.some_var, some_var) --> 3, nil +```` + +**[[⬆]](#TOC)** + +# LICENSE + +This work is under [MIT-LICENSE](http://www.opensource.org/licenses/mit-license.php)
+*Copyright (c) 2013-2014 Roland Yonaba*.
+See [LICENSE](http://github.com/Yonaba/strictness/blob/master/LICENSE). + +**[[⬆]](#TOC)** \ No newline at end of file diff --git a/files/lua/strictness.lua b/files/lua/strictness.lua new file mode 100644 index 0000000..a1912a9 --- /dev/null +++ b/files/lua/strictness.lua @@ -0,0 +1,259 @@ +#!/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 ', + _DESCRIPTION = 'Tracking accesses and assignments to undefined variables in Lua code' +} \ No newline at end of file