实现了一个lualint

在使用动态语言的过程中,由于其运行时检查特性,很多手误并不能在刚运行时暴露出来。虽然并不会导致程序个程序crash掉,但总归是很麻烦的。

网上找了一个lualint试了试,效果还不错。这个工具的原理就是基于在写lua代码时并不会使用全局变量。在检测到全局变量时直接报警即可。

仔细研究了一下这个lualint的原理。

lua自带的工具luac是可以把编译后的OPCODE打印出来的,更关键的是自带注释。

利用OPCODE和注释就可以分析出在哪一行哪些变量是使用了什么全局变量。只要我们在代码中不使用全局变量这一事实成立,那么所有使用了全局变量的地方都可能是手误打错了。

但对于我来说这个lualint有2个缺陷。

1. 但这这个lualint是并不支持lua5.3版本。因此在使用如’//’这类lua5.3新支持的特性时,这个lualint工具就不能很好的工作了。
2. 如果我require了一个表,并且使用了这个表中不存在的字段,那么他并不会报警,这对我来说很不习惯。因为大部分模块调用都是使用通过使用module.member的方式来调用的,如果member的名字打错(如A.l1打成了A.ll), 这个工具并不会报警出来。

基于以上原因,我基于lua53重新实现了一个lua分析工具修复了上述缺陷。虽然依然叫lualint, 但为了简化实现复杂度,这个lua检查工具在部分地方使用了动态分析。

在lua53中,对全局变量的作法做了些改动。

所有全局变量都是放在_ENV表中存放的,而一个代码块的所有代码是共享一个upvalue的。而访问全局变量是通过类似 ‘GETTABUP 0 0 -1 ; _ENV “print”‘的OPCODE来完成的。因此检测访问全局变量的OPCODE要改为监测使用GETTABUP/SETTABUP 对于_ENV表中的字段进行操作的代码。

考虑如下代码:

local b = require "testb"
--code1
print(b.CONST_A)

function hello()
        --code2
        print(b.CONST_A)
end

对于b.CONAT_A中的访问检测则稍微有些麻烦。

在code1处对模块testb的成员CONST_A的访问OPCODE和在code2处的OPCODE并不相同,因为code1属于main chunk而code2属于function chunk, 变量b与code1是平级的,即相当于code1的局部变量,而对于code2来说b变量为函数hello的upvalue, 因此在main chunk中访问模块testb的成员CONST_A使用的是GETTABLE而在function chunk使用的是GETTABUP. 顺便插一句,lua之所以能够实现的如此简洁,应该与其这种设计的统一性不无关系。

如果通过分析OPCODE来检测模块B是否有CONST_A变量虽然也可以做到,但是会很麻烦。因此这里取了个巧,采用了动态加载的方式。模拟require的行为,然后将模块testb的返回值接收下来并保存,当访问testb的成员时直接查看testb返回的表中是否有这个成员即可。

当然事情不可能这么简单,在函数中访问testb中CONST_A时会生成的OPCODE如’GETTABUP 1 1 -3 ; b “CONST_B”‘。 这里存在两个麻烦:一个是如果b是某个模块的别名,那么怎么确认他是哪个模块的别名,,因为生成require的OPCODE并不会有别名b的信息。一个是怎么找到这个b到底是模块testb的别名还是此函数的另外一个上值。

对于第一个问题,其实在分析require的过程中,我会重新再解析一遍当前代码文本,require中的那一行找到相关模块’testb’的别名即为b,做一个反向映射,如果GETTABUP指令访问b就把b当做别名找出真正的模块名’testb’,从而找出模块’testb’的表引用。由于需要分析require,因此顺带着就把整个project的依赖关系给解析了,在使用时也就不需要使用如find . -name ‘*.lua’ | xargs grep ./lualint 之类的命令来使用了。设置完ENTRY之后,直接使用./lua lualint.lua就可以自动分析完整个project是否警告信息。

对于第二个问题出于对实现的简化,采用了鸵鸟政策,碰到此类情况均当做是对模块’xxx’的变量的访问。当然做了一点小小的优化,如果从别名找不到真正的模块名,就认为访问的不是模块的成员变量,直接忽略。

ps.由于急用,实现的有些丑陋,因此附一篇文章大致说一下思路:D



发表评论