Wolfram 语言中的静态分析工具
寻找错误并修复它们不仅仅是我的一种激情,更是一种强迫症。几年前,作为一名QA(质量检测)开发人员,我为 Wolfram 语言创建了 MUnit 单元测试框架,这是一个用于编写和运行语言单元测试的框架。从那时起,我创造了更多的工具来帮助开发人员编写更好的 Wolfram 语言代码,同时在这个过程中检查出错误。
编写好的测试需要大量的知识和大量的时间。由于我们需要能够尽快测试和解决问题,以便按期发布新功能,我们转向静态分析,以便能够做到这一点。
静态分析是在运行源代码之前对其进行检查的过程,以试图预测其行为并发现问题。作为一种测试方法,它是非常有用的。在代码运行时发现问题并不总是可行的。运行代码的成本也很高--如果代码失败了,那就更是如此。
考虑到构成 Wolfram 语言的大量代码(有120万行的内核启动 Wolfram 语言代码,横跨1900个文件,还有85万行的程序包 Wolfram 语言代码,横跨3700个文件),必须要有一个策略来测试所有这些代码的错误。Wolfram 公司对 Wolfram 语言的每一个角落都有专门的测试(其中有些是我写的!)
CodeInspector paclet 是那些重要的静态分析工具之一,它使开发人员能够完成更好的工作。CodeInspector 包含在最近发布的 Mathematica 12.2中,它可以扫描 Wolfram 语言代码并报告问题,而不需要用户手动运行 paclet。CodeInspector 与 CodeParser 和 CodeFormatter 一起构成 CodeTools 套件,供内部和外部用户使用,以提高其 Wolfram Language 代码的质量。
一般来说,静态分析不能发现程序中所有可能的 bug (这是通过 Rice 定理对停止问题的不可控性所产生的结果)。但是,静态分析仍然可以提供大量的重要信息
例如,很容易看出这里的测试中不需要 &&True。
这可能是遗留的调试代码,或者仅仅是逻辑上的一个错误。静态分析工具可能会警告说,&& True 不需要,可以去掉或改成别的东西。虽然静态分析工具不能辨别作者的意图,但它们可以找到值得调查的 "可能的问题 "的类别。
创建一个静态分析工具来测试 Wolfram 语言中的错误,有一系列非常具体的挑战。作为一种编码语言,Wolfram 语言具有难以置信的动态和灵活性。虽然这通常被认为是对开发人员的一种奖励,但它确实使抽象建模非常困难。函数可以在运行时被重新定义,而且在 Wolfram语言中精确定义一个值的概念也很复杂。
鉴于语言本身的局限性,CodeInspector 基于语法树的模式匹配进行轻量级静态分析。这类似于其他语言的 "提示工具"。事实上,CodeInspector paclet 的原名是 Lint! 但很快就发现,它所做的工作不仅仅是检查,所以它被改名为 CodeInspector)。)
CodeInspector 目前有大约两百条内置规则,可以应用于被检查的代码。这些规则从常见的语法问题(如缺少逗号)到更隐蔽的问题(如在符号求解器中使用 Q 函数)。许多规则包括修复代码的建议。
CodeInspector 包含在 Mathematica 12.2 中。如果您使用的是旧版本的 Mathematica,您可以通过运行以下内容获得 CodeInspector:
为了以编程方式获得以下代码片断中所有问题的列表:
...您可以运行这个测试:
要获得测试中发现的所有问题的可视化摘要,请使用 CodeInspectSummarize(包含在 CodeInspector paclet 中):
您甚至可以在命令行上使用 CodeInspectSummarize:
有多种方法可以控制 CodeInspectSummarize 的输出。为了做到这一点,我们需要对问题进行分类,这本身就是一个有趣的问题!这是因为我们需要在以可查询的方式公开问题的许多属性与建立一个易于人类使用的系统之间取得适当的平衡。这是因为我们需要在以可查询的方式暴露问题的许多属性与拥有一个易于人类消费和理解的系统之间取得适当的平衡。
我使用两个维度,至少现在是这样:严重程度和信心等级。如果输出显示有问题,严重性表示每个问题有多严重。这个问题会不会影响到用户?它是否会意外地发射核弹头?知识就是力量,特别是当您需要了解手头问题的影响时。
ConfidenceLevel表示该问题实际上是一个问题而不是一个假阳性的置信度。ConfidenceLevel 是一个介于 0.0 和 1.0 之间的真实值。ConfidenceLevel →0.0 意味着对所报告的问题完全没有信心,而 ConfidenceLevel →1.0 意味着眼前肯定有问题,比如函数中不匹配的括号。ConfidenceLevel 为 0.5 意味着大约有一半的时间出现这种问题,是一个假阳性。在括号不匹配的情况下,ConfidenceLevel 是1.0。CodeInspector 中更多的实验性规则会有更低的 ConfidenceLevel,当我添加启发式方法来消除假阳性时,我会增加问题的 ConfidenceLevel。为我的目的重新使用 ConfidenceLevel 符号可能是对符号的滥用,但它很方便。
因为 Wolfram 语言是如此的动态,很难判断一个所谓的 bug 实际上是一个错误。即使在前面的示例中,If 语句也可能是故意编写的。仅语法错误,例如:
......可以百分百确定地被标记出来。请注意,即使是 "明显的 "问题,如:
...不一定有 ConfidenceLevel → 1.0。因此,CodeInspector 报告的每个问题都有一个相关的 ConfidenceLevel,表明置信该问题实际上是一个问题。
默认情况下,CodeInspectSummarize 会报告 95% 或更高置信度的问题。
还有四种与问题相关的不同严重程度:
Remark 是代码风格的问题,而不是其他问题。
Warning 是一个可能不会产生不正确结果的问题,但仍然是不正确的。
Error 是一个将执行不正确的代码并给出不正确的结果的问题。
Fatal 是一个无法恢复的错误,如语法错误。
这些严重程度应该与 ConfidenceLevel 同时诠释。只有当问题不是假阳性时,严重程度才有意义。
这说明了一个常见的错误:忘记了 &。
从#的位置开始,我们沿着树寻找一个匹配的 &:
没有发现&,所以报告了一个问题。注意这个规则的置信度较低,我需要指定ConfidenceLevel → 0.8才能看到它。
您可以根据您关心的语法从不同的规则中选择。例如,如果您想用一条规则来查找实数加到整数上的情况,那么您就不关心 1.2+3 与 Plus [1.2, 3] 的具体语法。
语法有三个不同的层次:
具体的语法:空白很重要。
聚合语法:琐事已被删除,您关心的是实际使用的运算符。
抽象语法:更抽象的问题,如未使用的变量、坏符号、坏函数调用等。
示例1:
在这个例子中,我忘了在行末加一个分号,所以整个表达式被当作 a=1*a+b 处理。这是不正确的,会导致代码运行时的无限递归:
示例2:
在这个例子中,我忘了给 PatternTest 插入一个问号。
CodeInspector 会捕捉到 Q 函数被当作头的情况,并建议插入一个问号:
示例3:
在这个例子中,我试图用 ImageDimensions 的输出来指定 ImageSize,但这两个函数的单位并不相同。ImageSize 选项期望的是点,但 ImageDimensions 则返回像素:
CodeInspector 会定期在 Wolfram Research 的开发人员编写的内部代码上运行。以下是最近遇到的两个问题,被 CodeInspector 发现并修复。这些问题很微妙,通过编写测试很难发现。
问题1:
需要用括号来包住整个右边的内容。原来的代码相当于:
这当然不是作者的本意。
问题2:
inc后面的额外下划线_意味着{__}被当作inc的可选值。但我们的目的是让 inc 匹配模式 {__}。CodeInspector 能够发现这些问题,并在发布代码前将其修复。
CodeInspectSummarize 报告指定文件的问题的方式与报告指定字符串的问题的方式完全相同。
由于 Wolfram 语言代码是解释的,因此没有编译步骤,可能不清楚何时是扫描问题的最佳时机。在实践中,我发现在构建小程序的时候是扫描的好时机。
我已经为 CMake 编写了脚本,在构建 paclet 之前扫描每个 Wolfram Language 文件。下面是当我的代码中有错别字时,我试图构建 CodeInspector paclet 本身时的情况:
因此,我可以看到我的代码中的错别字,并立即在源代码中修复它。否则,我就会用糟糕的代码构建 paclet,并在试图运行代码时遇到奇怪的错误。这凸显了尽快发现并修复问题的众多原因之一--通过测试 CodeInspector 本身来证明 CodeInspector 的意义。