检测未定义变量

lua-users home
wiki

问题

如何捕获对[未定义变量] (未声明的变量) 的访问? 这是一个经常被问到的问题。

下面介绍了各种方法。这些方法在何时以及如何检测未定义全局变量的访问方面有所不同。首先,让我们考虑一下问题的性质……

问题

在 Lua 程序中,变量名中的拼写错误可能很难发现,因为 Lua 通常不会抱怨变量未定义。例如,考虑这个定义了两个函数的程序

function f(x) print(X) end
function g(x) print(X + 1) end

Lua 在加载此代码时不会报错。前两行可能错了(例如,“x”拼写错误为“X”),也可能没错(也许 X 是另一个全局变量)。事实上,Lua 无法知道代码是否错误。原因是,如果 Lua 未将一个变量识别为局部变量(例如,通过使用“local”关键字进行静态声明或函数参数定义),该变量将被解释为全局变量(“X”就是这种情况)。现在,一个全局变量是否已定义并不容易确定或描述。X 的值是 t['X'],其中 t = getfenv() 是当前运行函数的“环境表”。X 始终有一个值,尽管如果 X 是拼写错误,它很可能是 nil。我们可以将 Xnil 解释为 X 未定义,但 X 是否为 nil 只能在运行时确定。例如

        -- X is "undefined"
f(X)    -- print nil
X = 2   -- X is defined
f(X)    -- prints 2
X = nil -- X is "undefined" again
f(X)    -- prints nil

即使是上面的代码也能无错误地运行。当 Xnil 时,print(X) 变为 print(nil),打印 nil 值是有效的。但是,请考虑调用函数 g

g(X)

这会因错误 "attempt to perform arithmetic on global 'X' (a nil value)" 而失败。原因在于 print(X + 1) 变为 print(nil + 1),而 nil 无法与数字相加。但是,直到 nil + 1 代码实际执行时才会观察到此错误。

显然,我们希望更主动地检测未定义全局变量,例如在编译时或至少在生产发布之前(例如,在测试套件中)检测它们。已开发出以下方法。

方法 #1:运行时检查

对未定义全局变量的读写可以在运行时发生时检测到。这些方法通过覆盖当前运行函数的环境表中的 __index__newindex 元方法来工作。Lua 将对未定义全局变量的读写发送到这些元方法,这些元方法可以被编程为引发运行时错误。

Lua 发行版中的“strict”模块(etc/strict.lua (下载 [Lua 5.1][Lua 5.2]) 采用了这种方法。或者,请参阅 [LuaStrict] (ThomasLauer 编写),它是 strict 方法的扩展。

以下是此方法的优缺点

优点

缺点

以下内容已从“强制局部声明”移至此处

Niklas Frykholm 编写的代码在 Lua 邮件列表中找到。我认为将此类宝石记录在 wiki 上会很有价值,因为它们很容易在数百封邮件中丢失或被遗忘。强制局部变量声明的概念是阻止您使用未声明的变量。这实际上也阻止了您意外使用本应是局部作用域但被视为全局变量的未声明变量,这在调试时可能会带来麻烦。

SR - 您能否解释一下此解决方案提供了什么而 DetectingUndefinedVariables 没有?您是否知道 etc/strict.lua,但认为这种方法更好?

有许多有效的解决方案可以强制变量声明,但我个人发现 Niklas Frykholm 的解决方案最优雅且不显眼(对性能的影响也很小,因为程序中声明的大多数变量都是局部作用域,只有在声明全局变量时才会触发代码)。

基本上,任何时候您调用 GLOBAL_lock(_G)(请注意 _G 是全局变量表)在您的代码中的某个地方,从那一刻起,任何时候您尝试使用未明确声明为“local”的变量,Lua 都会返回一个错误。

我对代码进行了一些修改,以便用户可以通过在变量前添加双下划线(例如 __name, __global_count)来显式允许全局声明,但这可能不方便。不过,您可以选择更改代码以使用另一种命名方法来满足您的喜好(例如 G_name, G_global_count)。 (读者提问:以“__”为前缀的全局变量的这种即时声明难道不会再次导致拼写错误吗?即,接受 __valueX__valueX 作为合法,这在某种程度上(很大程度上)违背了最初的想法?)

--===================================================
--=  Niklas Frykholm 
-- basically if user tries to create global variable
-- the system will not let them!!
-- call GLOBAL_lock(_G)
--
--===================================================
function GLOBAL_lock(t)
  local mt = getmetatable(t) or {}
  mt.__newindex = lock_new_index
  setmetatable(t, mt)
end

--===================================================
-- call GLOBAL_unlock(_G)
-- to change things back to normal.
--===================================================
function GLOBAL_unlock(t)
  local mt = getmetatable(t) or {}
  mt.__newindex = unlock_new_index
  setmetatable(t, mt)
end

function lock_new_index(t, k, v)
  if (k~="_" and string.sub(k,1,2) ~= "__") then
    GLOBAL_unlock(_G)
    error("GLOBALS are locked -- " .. k ..
          " must be declared local or prefix with '__' for globals.", 2)
  else
    rawset(t, k, v)
  end
end

function unlock_new_index(t, k, v)
  rawset(t, k, v)
end

--SamLie?

方法 #2:静态分析(编译时检查)

另一种方法是在编译时检测未定义的全局变量。当然,Lua 可以作为解释型语言使用,而无需显式的编译步骤(尽管它内部会编译为字节码)。然而,我们的意思是在代码正常执行之前检测到未定义的全局变量。这可以在不实际执行所有代码而仅解析代码的情况下完成。这有时被称为源代码的“静态分析”。

要在此编译时进行检测,您可以使用 Lua 5.1 编译器(luac)执行以下命令行技巧(在 *nix 类操作系统上)

luac -p -l myprogram.lua | grep ETGLOBAL

5.1 的全自动化命令 [analyzelua.sh]

for f in *.lua; do luac-5.1 -p -l "$f" | grep ETGLOBAL | cut -d ';' -f 2 | sort | uniq | sed -E 's/^ (.+)$/local \1 = \1;/' > "_globals.${f}.txt"; done

对于 Lua 5.2/5.3

luac -p -l myprogram.lua | grep 'ETTABUP.*_ENV'

5.2/5.3 的全自动化命令 [analyzelua.sh]

for f in *.lua; do luac-5.3 -p -l "$f" | grep 'ETTABUP.*_ENV' | cut -d ';' -f 2 | cut -d ' ' -f 1-3 | sort | uniq | sed -E 's/^ _ENV "(.+)"\s*$/local \1 = \1;/' > "_globals.${f}.txt"; done

这会列出所有对全局变量(包括已定义和未定义变量)的获取和设置。您可能会发现某些获取/设置被解释为全局变量,而您实际上希望它们是局部变量(缺少“local”语句或拼写错误)。如果您遵循“像瘟疫一样避免全局变量”的编码风格(即,尽可能使用局部变量(词法作用域)),则上述方法效果很好。

此方法的一个扩展在 Lua 5.1.2 发行版的 tests/globals.lua 中,它实现了 *nix 管道“ | grep ETGLOBAL”,但比其更有效,因为它会过滤掉预定义的全局变量(例如 print, math, string 等)。另请参阅 LuaList:2006-05/msg00306.html,以及 LuaLint。另请参阅 Egil Hjelmeland 的 [globals]。globals.lua 的一个更高级版本是 [globalsplus.lua] (DavidManura),它也会查找全局表中的字段。更高级的字节码分析在 [lglob] [3] (SteveDonovan) 中完成。

外部“linter”工具或语义感知文本编辑器(如 [Lua for IntelliJ IDEA], LuaInspect, 旧的 LuaFish, 或下面的 Metalua 代码)可以解析和静态分析 Lua 代码,从而达到类似的效果,并能检测其他类别的编码错误或可疑的编码实践。例如,LuaFish(它相当实验性)甚至可以检测到 string:length()math.cos("hello") 是无效的。

[Lua Checker] (5.1) 是其中一个工具,它像 C 的“lint”程序一样分析 Lua 源代码中的常见编程错误。它包含一个 Lua 5.1 bison 解析器。

love-studio [OptionalTypeSystem] 允许在普通 Lua 注释中使用类型注解

-- this is a description
-- @param(a : number) some parameter 
-- @ret(number) first return value
-- @ret(string) second return value
function Thing:Method(a)
        return 3,"blarg"
end

--@var(number) The x coordinate
--@var(number) The y coordinate
local x,y = 0,0

它被描述为“可选类型系统(如 Gilad Bracha 在其论文 Pluggable Type Systems 中定义的)是一个类型系统,它 a) 对编程语言的运行时语义没有影响,以及 b) 不强制要求在语法中进行类型注解。”

另一种方法是修补 Lua 解析器本身。请参阅 LuaList:2006-10/msg00206.html 获取此类示例。

注意:修改 lparser.c:singlevar 以获得更正确的错误处理:--DavidManura
/* based on 5.1.4 */
static void singlevar (LexState *ls, expdesc *var) {
  TString *varname;
  FuncState *fs;
  check(ls, TK_NAME);
  varname = ls->t.seminfo.ts;
  fs = ls->fs;
  singlevaraux(fs, varname, var, 1);
  luaX_next(ls);
  /* luaX_next should occur after any luaX_syntaxerror */
}

以下是此方法的优缺点

优点

缺点

一个 Lua Lint 工具

以下实用程序将对 Lua 源代码进行 lint 检查,检测未定义变量(并且可以进行扩展以执行其他有趣的操作)。

-- lint.lua - A lua linter.
--
-- Warning: In a work in progress.  Not currently well tested.
--
-- This relies on Metalua 0.2 ( http://metalua.luaforge.net/ )
-- libraries (but doesn't need to run under Metalua).
-- The metalua parsing is a bit slow, but does the job well.
--
-- Usage:
--   lua lint.lua myfile.lua
--
-- Features:
--   - Outputs list of undefined variables used.
--     (note: this works well for locals, but globals requires
--      some guessing)
--   - TODO: add other lint stuff.
--
-- David Manura, 2007-03
-- Licensed under the same terms as Lua itself.

-- Capture default list of globals.
local globals = {}; for k,v in pairs(_G) do globals[k] = "global" end

-- Metalua imports
require "mlp_stat"
require "mstd"  --debug
require "disp"  --debug

local filename = assert(arg[1])

-- Load source.
local fh = assert(io.open(filename))
local source = fh:read("*a")
fh:close()

-- Convert source to AST (syntax tree).
local c = mlp.block(mll.new(source))

--Display AST.
--print(tostringv(c))
--print(disp.ast(c))
--print("---")
--for k,v in pairs(c) do print(k,disp.ast(v)) end

-- Helper function: Parse current node in AST recursively.
function traverse(ast, scope, level)
  level = level or 1
  scope = scope or {}

  local blockrecurse

  if ast.tag == "Local" or ast.tag == "Localrec" then
    local vnames, vvalues = ast[1], ast[2]
    for i,v in ipairs(vnames) do
      assert(v.tag == "Id")
      local vname = v[1]
      --print(level, "deflocal",v[1])
      local parentscope = getmetatable(scope).__index
      parentscope[vname] = "local"
    end
    blockrecurse = 1
  elseif ast.tag == "Id" then
    local vname = ast[1]
    --print(level, "ref", vname, scope[vname])
    if not scope[vname] then
      print(string.format("undefined %s at line %d", vname, ast.line))
    end
  elseif ast.tag == "Function" then
    local params = ast[1]
    local body = ast[2]
    for i,v in ipairs(params) do
      local vname = v[1]
      assert(v.tag == "Id" or v.tag == "Dots")
      if v.tag == "Id" then
        scope[vname] = "local"
      end
    end
    blockrecurse = 1
  elseif ast.tag == "Let" then
    local vnames, vvalues = ast[1], ast[2]
    for i,v in ipairs(vnames) do
      local vname = v[1]
      local parentscope = getmetatable(scope).__index
      parentscope[vname] = "global" -- note: imperfect
    end
    blockrecurse = 1
  elseif ast.tag == "Fornum" then
    local vname = ast[1][1]
    scope[vname] = "local"
    blockrecurse = 1
  elseif ast.tag == "Forin" then
    local vnames = ast[1]
    for i,v in ipairs(vnames) do
      local vname = v[1]
      scope[vname] = "local"
    end
    blockrecurse = 1
  end

  -- recurse (depth-first search through AST)
  for i,v in ipairs(ast) do
    if i ~= blockrecurse and type(v) == "table" then
      local scope = setmetatable({}, {__index = scope})
      traverse(v, scope, level+1)
    end
  end
end

-- Default list of defined variables.
local scope = setmetatable({}, {__index = globals})

traverse(c, scope) -- Start check.

Example

-- test1.lua
local y = 5
local function test(x)
  print("123",x,y,z)
end

local factorial
function factorial(n)
  return n == 1 and 1 or n * factorial(n-1)
end

g = function(w) return w*2 end

for k=1,2 do print(k) end

for k,v in pairs{1,2} do print(v) end

test(2)
print(g(2))

输出

$ lua lint.lua test1.lua
undefined z at line 4

一个更广泛的版本在 LuaInspect 中。Fabien 提供的另一个更 Metalua 风格(可能更好)的 Metalua 实现位于 [1],下面还有一个更简单的。另请参阅 MetaLua 信息。

可以使用其他 Lua 解析器(请参阅 LuaGrammar,尤其是 LpegRecipes)来完成类似的事情,例如 Leg [2]

另一个 Metalua 解决方案

这段 Metalua 代码使用标准的 walker 库来打印程序中使用的所有全局变量的列表,插入到程序中。

-{ block:
   require 'walk.id' -- Load scope-aware walker library
   -- This function lists all the free variables used in `ast'
   function list_globals (ast)
      -- Free variable names will be accumulated as keys in table `globals'
      local walk_cfg, globals = { id = { } }, { }
      function walk_cfg.id.free(v) globals[v[1]] = true end
      walk_id.block(walk_cfg, ast)
            -- accumulate global var names in the table "globals"
      print "Global vars used in this chunk:"
      for v in keys(globals) do print(" - "..v) end
   end
   -- Hook the globals lister after the generation of a chunk's AST:
   mlp.chunk.transformers:add(list_globals) }

--FabienFleutot

另一个 Metalua 解决方案:Metalint

“Metalint [4] 是一个检查 Lua 和 Metalua 源代码中全局变量使用的实用程序。除了检查顶层全局变量外,它还检查模块中的字段:例如,它会捕获拼写错误,如 taable.insert(),以及 table.iinsert()。Metalint 使用声明文件,这些文件列出了已声明的全局变量以及可以对它们做什么……”。[4]

方法 #3:混合运行时/编译时方法

混合方法是可能的。请注意,检测全局变量访问(至少是直接访问,而不是通过 _Ggetfenv())最好在编译时完成,而确定这些全局变量是否已定义可能最好在运行时完成(或者可能,在“加载时”,在 loadfile 完成时)。因此,折衷的方法是将这两个问题分开,并在最合适的时候进行。一种这样的混合方法由 ["checkglobals" 模块+补丁] 采用,它提供了一个 checkglobals(f, env) 函数(完全用 Lua 实现)。简而言之,checkglobals 验证函数 f(默认情况下假定为调用函数)仅使用 env 表中定义的全局变量(默认情况下假定为 f 的环境)。checkglobals 需要一个小的补丁,向 debug 库的 debug.getinfo / lua_getinfo 函数添加一个额外的 'g' 选项,以在函数 f 中词法列出全局变量访问。

语义感知编辑器

请参阅 ProgramAnalysis 下的编辑器/IDE,了解突出显示未定义变量的编辑器。这可以通过静态分析和/或调用 Lua 解释器来实现。这种方式很方便,因为任何错误都会立即在屏幕上以上下文显示,而无需调用任何外部构建工具和浏览其输出。

Lua 语法扩展

已提出一些语法扩展,以便由 Lua 编译器更自动地处理未定义变量。

历史:Lua 4 旧笔记

这是一个快速粗略的解决方案,用于防止在 Lua 4.0 中向未定义的全局变量赋值。

function undefed_global(varname, newvalue)
  error("assignment to undefined global " .. varname)
end

function guard_globals()
  settagmethod(tag(nil), "setglobal", undefed_global)
end

一旦调用了 guard_globals(),任何对值为 nil 的全局变量的赋值都会产生错误。因此,通常会在加载脚本后,运行它们之前调用 guard_globals()。例如

SomeVariable = 0

function ClearVariable()
  SomeVariabl = 1      -- typo here
end

-- now demonstrate that we catch the typo
guard_globals()

ClearVariable()        -- generates an error at the typo line

“getglobal”标签方法也可以用来捕获对未定义全局变量的读取。此外,通过更多的代码,可以使用一个单独的表来区分“已定义”的全局变量(碰巧值为 nil)和“未定义”的全局变量(从未被访问过)。

另请参阅


RecentChanges · preferences
编辑 · 历史
最后编辑于 2019 年 3 月 17 日晚上 9:21 GMT (差异)