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(此处即为定义的规则模块)
与其校验规则相关的有以下因素:
- 命令行传参:参考 docsrc/cli.rst,允许通过命令行参数启用/禁用某些全局配置,这些全局配置相关的变量可以在校验脚本(src/luacheck/stages/*.lua)中取用,并根据此设置对应行为(常见如std为mix时不检查某项);
- 配置文件传参:与命令行传参一致,只不过把对应参数放到配置文件中(默认为.luacheckrc),执行校验时,若当前目录下存在.luacheckrc文件,则会加载其中的对应配置,可以通过
--no-config
命令行参数忽略配置文件; - 校验规则脚本:执行校验流程的脚本为
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脚本的步骤;
前置步骤分别为:
- parse.lua:读取文本内容;
- unwrap_parens.lua:递归解析ast语法树,对部分父节点做展开;
- linearize.lua:将ast语法树构建为线性表示,并将所有行挂载为chstate.lines对象(这里还包括了4xx、521的校验规则,在此处执行检查);
- parse_inline_options:当迭代器取用行对象时(步骤3构建的),将行的关联选项推入chstate.inline_options中,迭代到下个对象时将其移除(这里做了021、022、023的校验);
- name_functions:递归将ast语法树中的函数对象命名替换,如函数被赋值给变量的,将其命名为"foo",赋值给字段的,将字段中的值修改,如"foo.bar.baz";
- 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,这个函数定义了如下检查规则:
- 如果传入代码块为Do … end,则检查代码块包裹代码是否为空,若为空则打印541告警;
- 否则遍历代码块中的元素,每隔一个元素检查其是否为空,若为空则打印542告警;
- 若代码块对2取余为1(奇数判定),则再检查最后一个元素是否为空,若为空则打印542告警;
由此,就实现了规则 541
、542
检定;
自定义校验规则: 参考我的下一篇博客 自定义luacheck校验规则