LuaCheck校验原理解析

LuaCheck介绍

LuaCheck是一款开源Lua代码检查工具,其内置检查项如下:

==== =============================================================================
Code Description
==== =============================================================================
011  A syntax error.
021  An invalid inline option.
022  An unpaired inline push directive.
023  An unpaired inline pop directive.
111  Setting an undefined global variable.
112  Mutating an undefined global variable.
113  Accessing an undefined global variable.
121  Setting a read-only global variable.
122  Setting a read-only field of a global variable.
131  Unused implicitly defined global variable.
142  Setting an undefined field of a global variable.
143  Accessing an undefined field of a global variable.
211  Unused local variable.
212  Unused argument.
213  Unused loop variable.
221  Local variable is accessed but never set.
231  Local variable is set but never accessed.
232  An argument is set but never accessed.
233  Loop variable is set but never accessed.
241  Local variable is mutated but never accessed.
311  Value assigned to a local variable is unused.
312  Value of an argument is unused.
313  Value of a loop variable is unused.
314  Value of a field in a table literal is unused.
321  Accessing uninitialized local variable.
331  Value assigned to a local variable is mutated but never accessed.
341  Mutating uninitialized local variable.
411  Redefining a local variable.
412  Redefining an argument.
413  Redefining a loop variable.
421  Shadowing a local variable.
422  Shadowing an argument.
423  Shadowing a loop variable.
431  Shadowing an upvalue.
432  Shadowing an upvalue argument.
433  Shadowing an upvalue loop variable.
511  Unreachable code.
512  Loop can be executed at most once.
521  Unused label.
531  Left-hand side of an assignment is too short.
532  Left-hand side of an assignment is too long.
541  An empty ``do`` ``end`` block.
542  An empty ``if`` branch.
551  An empty statement.
561  Cyclomatic complexity of a function is too high.
571  A numeric for loop goes from #(expr) down to 1 or less without negative step.
611  A line consists of nothing but whitespace.
612  A line contains trailing whitespace.
613  Trailing whitespace in a string.
614  Trailing whitespace in a comment.
621  Inconsistent indentation (``SPACE`` followed by ``TAB``).
631  Line is too long.
==== =============================================================================

开源地址:https://github.com/mpeterv/luacheck

项目有需要对Lua代码进行检查,上述检查规则不能满足需求,需要增加自定义规则,因此对此工具源码进行学习,并开源基于此工具开发自定义检查规则。

安装Lua开发环境

LuaCheck是通过Lua代码开发,运行需要Lua环境,此处以ubuntu 20.04镜像为例;

此处需要注意,LuaCheck相关安装文档中只提到需要安装apt-get install lua5.3 luarocks ,实际还需要安装liblua5.3-dev 开发库,否则通过luarocks安装luacheck会编译失败。完整安装命令:

apt-get install lua5.3 luarocks liblua5.3-dev

安装完成后,如果目的是运行luacheck,不需要做二次开发,那么直接执行 luarocks install luacheck 即可,安装完成验证安装成功:

root@979bc2a8a939:/# luacheck --version
Luacheck: 1.2.0
Lua: PUC-Rio Lua 5.3
Argparse: 0.7.1
LuaFileSystem: 1.8.0
LuaLanes: Not found

我这里需要做另外开发,因此改为拉取代码后从源码运行:

git clone https://github.com/mpeterv/luacheck.git

执行命令(在luacheck代码包目录下,其他目录需要更换对应路径):

lua -e 'package.path="./src/?.lua;./src/?/init.lua;"..package.path' bin/luacheck.lua ...

源码分析

LuaCheck基于Lua脚本,通过对指定目录/文件代码文本逐行分析,并根据匹配规则打印告警信息;

其执行步骤如下:

bin/luacheck.lua -> src/luacheck/main.lua -> src/luacheck/runner.lua -> src/luacheck/check.lua -> src/luacheck/stages.lua -> src/luacheck/stages/*.lua(此处即为定义的规则模块)

与其校验规则相关的有以下因素:

  1. 命令行传参:参考 docsrc/cli.rst,允许通过命令行参数启用/禁用某些全局配置,这些全局配置相关的变量可以在校验脚本(src/luacheck/stages/*.lua)中取用,并根据此设置对应行为(常见如std为mix时不检查某项);
  2. 配置文件传参:与命令行传参一致,只不过把对应参数放到配置文件中(默认为.luacheckrc),执行校验时,若当前目录下存在.luacheckrc文件,则会加载其中的对应配置,可以通过 --no-config 命令行参数忽略配置文件;
  3. 校验规则脚本:执行校验流程的脚本为 src/luacheck/stages.lua ,这里定义了执行校验的过程;

这里来看stages.lua执行校验过程:

for _, stage_module in ipairs(stages.modules) do
   if stage_module.warnings then
      register_warnings(stage_module.warnings)
   end
end

function stages.run(chstate)
   for _, stage_module in ipairs(stages.modules) do
      stage_module.run(chstate)
   end
end

这里是依次执行前面定义的stage模块,而stage模块列表如下:

stages.names = {
   "parse",
   "unwrap_parens",
   "linearize",
   "parse_inline_options",
   "name_functions",
   "resolve_locals",
   "detect_bad_whitespace",
   "detect_cyclomatic_complexity",
   "detect_empty_blocks",
   "detect_empty_statements",
   "detect_globals",
   "detect_reversed_fornum_loops",
   "detect_unbalanced_assignments",
   "detect_uninit_accesses",
   "detect_unreachable_code",
   "detect_unused_fields",
   "detect_unused_locals"
}

可见从parse到resolve_locals为执行校验的前置步骤,可以认为是框架的部分,而之后开始是校验规则,也就是执行校验的lua脚本的步骤;

前置步骤分别为:

  1. parse.lua:读取文本内容;
  2. unwrap_parens.lua:递归解析ast语法树,对部分父节点做展开;
  3. linearize.lua:将ast语法树构建为线性表示,并将所有行挂载为chstate.lines对象(这里还包括了4xx、521的校验规则,在此处执行检查);
  4. parse_inline_options:当迭代器取用行对象时(步骤3构建的),将行的关联选项推入chstate.inline_options中,迭代到下个对象时将其移除(这里做了021、022、023的校验);
  5. name_functions:递归将ast语法树中的函数对象命名替换,如函数被赋值给变量的,将其命名为"foo",赋值给字段的,将字段中的值修改,如"foo.bar.baz";
  6. resolve_locals:遍历步骤3构建的行,解析其中的local 函数;

上述内容均在代码及注释中体现,可以自行查看对应stages下文件;

之后进入规则模块,此处以detect_empty_blocks.lua中内容为例:

local core_utils = require "luacheck.core_utils"

local stage = {}

stage.warnings = {
   ["541"] = {message_format = "empty do..end block", fields = {}},
   ["542"] = {message_format = "empty if branch", fields = {}}
}

local function check_block(chstate, block, code)
   if #block == 0 then
      chstate:warn_range(code, block)
   end
end


local function check_node(chstate, node)
   if node.tag == "Do" then
      check_block(chstate, node, "541")
      return
   end

   for index = 2, #node, 2 do
      check_block(chstate, node[index], "542")
   end

   if #node % 2 == 1 then
      check_block(chstate, node[#node], "542")
   end
end

function stage.run(chstate)
   core_utils.each_statement(chstate, {"Do", "If"}, check_node)
end

return stage

此处代码中,stage.run为该模块的入口函数,所有校验代码应当在此处执行,传入的 chstate 为代码行的上下文对象,可以从中取得代码行、行参数(对应步骤3、4获取的信息),此处调用了来自core_utils的each_statement函数,代码如下:

-- Calls `callback(chstate, node, ...)` for each statement node within AST with tag in given array.
function core_utils.each_statement(chstate, tags_array, callback, ...)
   local tags = utils.array_to_set(tags_array)

   for _, line in ipairs(chstate.lines) do
      scan_for_statements(chstate, line.node[2], tags, callback, ...)
   end
end

可见此函数用于从chstate对象中取出指定tag的行,并使用传入函数进行解析,上述代码中的传入函数为check_node,这个函数定义了如下检查规则:

  1. 如果传入代码块为Do … end,则检查代码块包裹代码是否为空,若为空则打印541告警;
  2. 否则遍历代码块中的元素,每隔一个元素检查其是否为空,若为空则打印542告警;
  3. 若代码块对2取余为1(奇数判定),则再检查最后一个元素是否为空,若为空则打印542告警;

由此,就实现了规则 541542 检定;

自定义校验规则: 参考我的下一篇博客 自定义luacheck校验规则

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值