原文链接https://storage.googleapis.com/pub-tools-public-publication-data/pdf/3198e114c4b70702b27e6d88de2c92734c9ac4c0.pdf

软件bug耗费开发者和软件公司大量的时间和金钱。 以2014年为例,被广泛使用的SSL协议实现中的一个(“goto fail”)bug导致可接受无效的SSL证书,另外一个与日期格式化相关的bug导致Twitter的大范围服务中断。此类错误通常可以被静态分析检测,其实事实上在阅读代码或文档阶段均可以快速识别,可以最终现实是情况仍然在生产环境实施发生。

之前的工作已经完善报道将bug检测工具应用于软件开发的经验。但虽然开发人员使用静态分析工具方面有如此多的成功案例,仍有以下原因导致工程师并不总情愿使用静态分析工具或主动忽略工具产生的告警信息:

  • 未合理整合。工具未集成到开发人员的工作流程中或者是程序运行时间太长;

  • 无效的告警。告警信息可行性性差;

  • 不值得信赖。用户因为误报而不再信任结果;

  • 缺陷实际利用场景不清晰。报告的bug在理论上是可行的,但缺陷在实际利用场景下并不清晰;

  • 修复成本过高。修复已检测到的代码缺陷的成本太高或有其他方面的风险;

  • 告警不易理解。使用者并不了解告警信息的的具体信息和原理。

接下来本文描述了我们如何吸取Google在先前使用FindBug进行java语言分析方面以及学术文献中的得到的经验和教训,最终在Google公司成功构建了软件工程师日常使用的静态分析基础设施架构。借助吸收于工程师的意见建议,Google的工具可以在有问题的代码被合入到公司级别的代码仓库之前检测到每天工程师所修复的数千个问题。

在工具作用范围方面,我们专注于将静态分析融入为Google核心开发流程的一部分,并服务于大部分Google开发人员。许多静态代码分析工具在部署于google的20亿行代码级别下将相形见绌,因此大规模场景下运行复杂分析的技术的优先级并不高。

当然必须要考量到Google外部开发人员在专业领域(例如航空航天和医学设备领域)工作的可能会使用特定的静态分析工具和工作流程。同样开发项目涉及特定类型(例如内核代码和设备驱动程序)的开发人员可能需要进行特定的分析方法。静态分析方面已经有很多卓越的工作成果,我们并不认为我们所反馈的经验和心得是独一无二的,但我们坚信整理和分享我们在提高google的代码质量和改善开发体验方面的工作是有所裨益的。

术语定义。我们使用以下术语定义:分析工具对源代码运行一个或多种“检查器”,并识别出可能呈现出软件故障的“缺陷”。如果开发人员在看到问题后没有采取积极行动,如果开发人员遇到识别出的缺陷并未采取适当的修复方式,我们将其视为“实际误报”。如果静态分析并未准确的识别报告到某项缺陷,但开发人员主动采取措施修改此处代码以提高可读性和可维护性,那么这就不算是有效的“误报”。如果一个分析报告了实际的代码错误,但是开发人员因未理解这处代码问题因而没有采取任何行动,就视为这是一个“实际误报”。我们使用这种概念区别强调了研发角度的重要性。开发人员而不是工具作者对感知并直接影响到工具的误报率。

谷歌如何编译构建软件

下文我们将概述Google软件开发流程的关键点。在Google,几乎所有开发工具(开发环境除外)都是集中化和标准化的。该基础架构的许多部分都是由内部团队拥有的Scratch构建的,保留了具备实验性质的灵活性。

源代码控制和代码所有权。Google已开发并使用单源的代码管理系统。并试验单分支存储(几乎)所有的Google专有代码。开发人员使用限制分支的“基于trunk”的开发方式,通常以版本发布而不是特性进行划分。任何工程师经代码所有者的批准都可以更改任何代码。代码所有权基于路径划分;目录的所有者对子目录拥有同样权限。

系统构建。 Google代码库中的所有代码都经一个编译环节独立的Bazel版本进行编译,也就是说,所有输入都必须在源代码控制中显式声明和存储以便构建环节的易于分发性和并行化处理。在Google的构建系统中Java规则依赖于JDK和经源码管理的Java编译器,并且可以通过快捷引入新版本来为所有使用者更新这些二进制文件。构建通常来自源代码(通过head),几乎很少有二进制组件签入分支。因为所有开发人员都使用相同的构建系统,所以任何代码都可以编译成功而不报错。

分析工具。 Google使用的静态分析工具通常并不复杂。Google基础架构不支持在这样级别的系统上运行过程间或基于程序的完整性分析,也没有大规模使用高级静态分析技术(例如separation logic技术)。即便是简单的检查器也需要分析基础架构来支持对工作流程进行集成。已部署作为一般开发流程的分析器类型包括:

  • 样式检查器(例如Checkstyle,Pylint,和Golint);

  • 可扩展bug查找编译器(例如Error Prone,ClangTidy, Clang Thread SafetyAnalysis, Govet,和CheckerFramework),包括但不限于抽象语法树模式匹配工具,基于类型的检查器和检测未调用变量的分析器。

  • 调用生产服务的分析器(例如检查代码注释中提到的员工是否仍然在Google上工作) );

  • 检查构建输出的属性(例如产出二进制文件的大小)。

Google的C++ linter可以捕获“goto fail”漏洞,其检查if语句是否后面有括号。基于模式匹配的检查器会识别出日期格式化错误,那么导致Twitter宕机的代码就不会在Google编译通过。谷歌开发人员也使用动态分析工具(如AddressSanitizer)来寻找缓冲区漏洞、使用ThreadSanitizerto查找数据竞争问题。这些工具在测试环节,有时甚至是有生产流量的环境中运行。

集成开发环境(IDE)。静态分析问题在开发过程早期切入点是集成在IDE中。但是Google开发人员使用各种各样的编辑器,因此在调用构建工具之前很难一致地检测所有开发人员的错误。虽然谷歌确实使用与流行的内部IDE集成的分析,但要求具有可分析的特定IDE前途漫漫,路险而艰。

测试。几乎所有Google代码都包含相应的测试环节,从单元测试到大规模集成测试。测试活动是系统构建中首先需融合的理念,就如编译环节一样都是独立和分布式的。对于大多数项目,开发人员编写并维护代码的测试用例;项目通常没有单独的测试或QA组。

Google的持续构建和测试系统会在每次代码提交时运行测试,会及时反馈由于开发人员的代码更改导致构建失败或测试用例不通过。它还支持在提交之前测试变更处以避免破坏项目依赖关系。

Code review。每次提交到Google的代码都会首先通过code review。虽然任何开发人员都可以对Google任何代码处进行更改,但代码的所有者必须在提交合入之前review才批准更改。此外,即使是代码所有者也必须在提交变更之前review其代码。代码审查通过一个与其他开发基础设施紧密集成的集中式、基于Web的工具进行。静态分析结果可在代码审查中展现。

代码发布。谷歌团队频繁发版,大部分的发布验证和部署过程都是通过“push on green”的方法自动完成的,这意味着难以依靠进行辛劳的手动发布验证过程。如果Google工程师在生产环境中发现错误,同必须中断服务相比而言可以以相对较低的成本回退新版本并将其部署到生产服务器。

从FindBugs学习到的

从2008年到2010年期间早期摸索研究阶段,谷歌的静态分析技术专注于使用FindBug进行Java分析:由马里兰大学的William Pugh和宾夕法尼亚州约克学院的DavidHovemeyer创建的独立工具。其原理为分析已编译的Java类文件并提取可以导致bug的代码结构模型。截至2018年1月,FindBugs在google仅作为极少数工程师使用的命令行工具。一个名为“BugBot”的小型Google团队与原作者Pugh合作,有三次大的尝试试图将FindBugs集成到Google开发流程中。

我们通过尝试学到了以下几点:

尝试1:Bug dashboard。最初在2006年,FindBugs被整合为一个集中性的工具来每晚扫描整个谷歌代码库,将生成的结果入库方便工程师通过dashboard进行检视。尽管FindBugs在谷歌的Java代码库中发现了数百个错误,但是该dashboard效果微乎其微,因为错误信息dashboard脱离于日常开发流程,而且同现有的其他静态分析结果不能有机结合。

尝试2:集中改进bug。

接下来BugBot团队开始手动分类每晚查找到的新问题,识别处理相对重要的bug报告。 2009年5月,数百名Google工程师参加了全公司范围的“Fix it”周活动,聚焦解决FindBugs的告警问题.总共审查了3954个告警信息(占总数9473的42%),但实际上只修复了16%(640个)。实际44%的报告结果(1746个)已经提交了bug反馈跟踪。 尽管Fixit活动证实FindBugs发现的许多问题都是现实存在的代码缺陷,但很大一部分并没有重要到需要实际采取修复措施的地步。人工手动分类问题、提交bug报告在大规模环境下是难以持续进行的。

尝试3:集成于代码审查。接下来BugBot团队集成实现了这样的系统:当review人员得到待review通知时,FindBugs将会自动运行并将扫描结果作为代码审查的注释展示出来,以上代码review团队已经针对编码规范/风格问题已经实现完成。谷歌开发人员可以忽略误报,并针对FindBugs的结果可信度进行筛选。该工具进一步尝试仅显示新的FindBugs告警,但有时会因为错误的分类将其视为新问题。随着代码审查工具在2011年被替换,这种集成随着寿终正寝,原因有两个:实际工作中的高误报率导致开发人员对工具失去信心,开发人员自由定制进行过滤导致各方对分析结果观点不一致。

纳入编译流程

在FindBugs的实验同时期,Google的C++开发流程通过向Clang编译器添加新的检查规则而得到不断进步。Clang团队实现了的新的编译器检查器,包括修复建议信息,使用ClangMR以分布式方法在整个Google代码库上运行经过更新的编译器来优化检查,并编码实现修复了代码库中的存量bug问题。一旦代码库已标记被修复存量问题,Clang团队就会应用新检查器将新问题标记为编译器错误(而不是告警,Clang团队发现谷歌开发人员会忽略告警)来中止构建,对此必须加以解决才能通过。Clang团队通过这一策略非常成功地改进了代码库质量。

我们遵循这个思路并在javac编译器基础之上构建了一个简单易用的基于模式分析的Java静态分析工具,名为Error Prone。推出的第一个检查规则名为PreconditionsCheckNotNull,用于检测程序运行之初是否方法检测入参为空,比如checkNotNull(“uid为null”,uid)而不是checkNotNull(uid,“uid was null”)。

为了在不破坏任何连续构建的情况下启动PreconditionsCheckNotNull这样的检查器,Error Prone团队使用它对基于javac的MapReduce程序对整个代码库运行此类检查,类比ClangMR,使用FlumeJava构建称之为JavacFlume。JavacFlume会生成一系列的修复建议,比对其中的不同,然后应用这些到整个代码库进行修复。Error Prone团队使用内部工具Rosie,将大规模代码变更拆分为小的变更处,每个改变只会影响到单个项目并测试这些变更处,并将它们发送给相应的团队进行代码审查。团队仅审查适用于其代码的修复方案,并且仅在批准通过它们进行合入,Rosie才会提交实际更改。最终所以存量问题的修复和变更都得到通过,已有缺陷均得到处理。团队正式开启了编译器错误的方式。

当我们对收到这些补丁的开发人员进行调查反馈时,其中57%收到合入代码的fix方案的人的乐于得到此类信息,,41%的人持中立态度。只有2%的人反应较为消极会说:“这样仅仅会加重我的工作量”

编译器检查的价值

编译错误是在开发流程的早期显示并已集成到开发人员工作流程中。我们发现扩展编译检查器有效地提高了Google的代码质量。因为Error Prone中的检查是内部针对javac的抽象语法树而不是字节码(与FindBugs不同)编写的,所以团队外部的开发者可以相对容易地进行检查。利用这些外部贡献对于提高Error Prone的整体影响力至关重要。截至2018年1月,共有162名作者提供了733项检查器。

越早报告问题越好

Google的集中构建系统会记录所有构建过程和构建结果,因此我们确保所有的用户在指定时间窗口可看到其中的错误消息。我们向最近遇到编译器错误的开发人员和已收到针对同一问题修复建议进行修复的开发人员发送了一项调查反馈。谷歌开发者认为在编译时标记出来的问题(与合入代码时期的补丁相对)会捕获更重要的错误;例如,调查参与者认为74%的问题在编译时被标记为“切实问题”,而在合入代码中发现的问题只有21%。此外,调查参与者认为在编译时发现的问题中有6%(在合入代码阶段中为0%)是“重要级别”。这个结果可以通过“幸存者偏差效应”来解释; 也就是说在代码提交时错误很可能是由更高昂的手段(如测试和代码审查)捕获的。将尽可能多的检查前置到编译器中是一种避免这些成本花销的可靠办法。

编译器检查的标准

为了规模推广我们的工作,因为中断编译将是一个较大的动作,所以我们已经定义了在编译器中启用检查的标准,设置为严格高标注模式。Google上的编译器检查应当简单易读、可操作的且易于修复(尽量实现的错误消息提示应该包括可通用实现的修复建议); 没有产生有效误报(分析动作不应中断构建实际上正确的无误代码); 并仅报告反馈真实的bug而非风格或编码规范方面的问题。 衡量满足这些标准的分析器的主要目标不仅仅是简单检测问题,而是在整个代码库中自动修复这些编译器错误。 但是这些标准也限制了Error Prone团队在编译代码时启用的检查范围; 许多问题不能被准确检测或通用的问题修复环节仍然是摆在我们面前的问题。

在代码review阶段展示告警信息

一旦Error Prone团队构建实现了在编译时检测问题所需的基础设施架构,业已证明该方法切实有效,我们希望查找出更多有高影响因子的bug,bug不局限于我们做的编译器错误检查和为Java和C++以外的语言提供分析结果。静态分析结果的第二个集成切入点是Google的代码审查工具-Critique;静态分析结果通过使用Tricorder在Google的程序分析平台的Critique中显示。截至2018年1月,Google的C ++和Java版本的编译器错误均清零,所有分析结果都显示在编译器错误或在代码审查阶段。

代码审查检查的标准

与编译时检查不同,代码审查期间显示的分析结果允许囊括达到10%的有效误报率。在代码审查期间所期望的反馈并不总是完美无缺的,并且开发者在实际采用之前需评估相应的修复建议。 Google在代码审计阶段的检查器应符合以下几个标准:

易于理解。对于工程师来说是明白无误易于理解的;

方案可行,易于修复。修复程序可能需要比编译器检查阶段花费更多的时间、思考和工夫,检查结果应包括有关如何界定问题的指导内容;

不到10%的有效误报率。开发人员应该感受到检查器在至少90%的情况下均找到实际bug缺陷;

对代码质量产生重大影响。发现的问题可能不会影响程序正确运行,但开发人员应该认真对待它们并选择修复它们。

有些问题严重到足以在编译器中标记出来,致力于但降低或开发自动修复方案并非可行。例如有的修复问题可能需要对代码进行重构。将这些检查结果作为编译器错误启用将需要手动清理现有的实现,这在Google这样大规模的代码库上是不可行的。分析工具在代码审查中显示这些检查可避免引入新问题,允许开发人员决定是否采取措施进行恰当的修复。代码审查也是报告相对不太重要的问题(如规范问题或简化优化代码)的良好时机。根据我们的经验,在编译阶段出报告对开发人员总是难以欣然接受的,并且使得快速迭代和调试变得更加困难;举例来说,一处针对代码路径不可达的检测器可能会阻碍一处用于调试的代码块。但在代码审查时,开发人员正在细心准备完成他们的代码;他们正处在虚心接受的心态,更容易接受可读性和风格细节的问题。

Tricorder

Tricorder设计理念旨在易于扩展,并支持包括静态和动态分析工具的众多不同类型的程序分析工具。我们在Tricorder中展示了一些无法作为编译器错误启用的Error Prone检查器。Error Prone还创造了一套新的C++分析组件,它与Tricorder集成称之为ClangTidy。Tricorder分析器的报告支持超过30种语言的结果,支持简单的语法分析如样式检查器,利用Java,JavaScript和C ++的编译器信息,并且可以直接与生产数据集成(例如关于当前正在运行的任务作业)。Tricorder持续在Google取得成功是因为它是支持分析器编写者的一个生态平台的插件模型,并在代码审查过程中高亮显示可行的修复方案,并提供反馈渠道以改进分析器并确保分析器开发人员对正向反馈采取措施。

使用户能够做出贡献。截至2018年1月,Tricorder包括146个分析器,其中125个来自Tricorder团队之外,7个插件系统用于数百个额外检查(例如ErrorProne和ClangTidy,它们是包括在七个分析插件系统中的两个)。

审阅者参与进来提供修复建议。

Tricorder检查器可以提供为代码审查工具提供代码review人员和开发者可见的合理修复建议。审阅者可以通过单击分析结果上的“请修复”按钮来要求开发者修复缺陷代码。直到他们的所有评论(手动和自动发现的)都得到解决之前,review者通常都不会批准合入代码变更。

迭代来自用户的反馈。除了“请修复”按钮,Tricorder还提供了一个“无用”按钮,评论者或提议者可以点击表示他们不认同分析发现的结果。点击动作会自动附带提交bug跟踪器中的错误,并将其指向分析器所属团队。 Tricorder团队跟进这些“无用”的点击标记,计算“请修复”与“无用”之间的点击比率。如果分析器的比例超过10%那么Tricorder团队会禁用该分析器直到作者对其进行改进。虽然Tricorder团队很少有永久性得禁用分析器,它已经禁用了一些分析器(在几个场景下),直到分析器作者删除和修改完成那些结果繁杂无用的检查器。

提交的错误通常能改进分析器效果,从而大大提高开发人员对这些分析器的满意度;例如,Error Prone团队在2014年开发了一个检查项,它会标记出来当Guava中传递太多参数传递给类似printf这样的的函数。类似printf的函数实际上并不接受所有printf说明符,只接受%S。大约每周一次Error Prone团队将收到一个“无用”bug,声称该分析不正确,实际上bug匹配代码中的格式统配符数量与实际传递的参数数量相匹配。而用户试图传递%s以外的通配占位符时,任何情况下分析器其实都是正确误区的。因此团队将代码检视说明文本更改为直接声明该函数仅接受%s占位符并停止获取有关该检查的错误。

Tricorder的使用规模。截至2018年1月,Tricorder已经分析了每天大约50000次代码审查变更。在高峰时段每秒进行三次分析。review者每天点击“请修复”超过5000次,作者每天应用自动修复方案约3000次。Tricorder分析器每天收到250次“无用”点击反馈。

代码审查分析的成功表明它在Google的开发人员工作流程中占据了“最佳位置”。在编译时显示的分析结果必须达到相对到的的质量和准确度,而依靠分析器不可能满足来继续识别严重问题。在review和代码合入之后,开发人员进行更改所面临的阻力会有所增加。因此,开发人员对已经测试和发布的代码进行修改时会比较纠结并且不太可能去解决低危和不太重要的问题。很多其他软件开发组织中的分析项目(例如针对Android / iOS应用程序的Facebook Infer分析)也强调代码审查是报告分析结果的关键切入点。

扩展分析器

随着Google开发者对Tricorder分析器的结果取得认可,他们继续要求深入扩展分析器。 Tricorder以两种方式解决这个问题:允许在项目级定制化并在开发流程的其他环节添加展示分析结果。在本节中,我们还讨论Google尚未利用更复杂的分析技术作为其核心开发流程的部分原因。

项目级定制

并非所有请求的分析器对于整个Google代码库中都具有同等价值;例如一些分析器有较高的误报率有关,因此具有相应的高误报率的检查器可能需要在特定的项目启用配置才有效。这些分析器仅对适合的团队才有用。

为了实现这些需求,我们的目标是使Tricorder达成可定制化。我们之前为FindBugs定制的经验实践效果较差;基于用户级别的定制化导致团队内部和团队之间出现差异化导致工具使用率下降。因为每个用户都可以看到不同的问题视图,所以没有办法确保每个从事同样项目工作的人都能看到特定的问题。如果开发人员从他们团队的代码中删除了所有未使用的导包,那么即使其他一个开发人员在删除未使用的导包方面不一致,该变更会很快被回退拒绝。

为了避免此类问题,Tricorder仅允许在项目级别进行配置,确保对特定项目进行更改的任何人都能看到与该项目相关的分析结果一致视图。维护结果视图的一致性使得几种类型的分析器能够执行以下动作:

产生二分结果。例如,Tricorder包括用于协议缓冲区定义的分析器,其识别不向后兼容的变化。开发人员团队使用它来确保序列化形式的协议缓冲区中的持久信息,但对于不以此形式存储数据的团队而言则很烦人。另一个例子是有分析器建议使用对于不能使用这些库或语言功能的项目,对Guava或Java代码实现没有意义;

需要特定的设置或代码内注释。例如,如果他们的代码被适当地注释,团队仅可使用Checker Framework的null ness去分析。另一项分析器是在合理配置后,将检查特定Android二进制文件的二进制大小和函数调用次数的增长,并警告开发人员是否是预期增长或者是否接近限制范围;

支持特定领域的语言(DSL)和特定于团队的编码指南。一些Google软件开发团队开发了一些小型DSL并且希望运行的相关检查器。其他团队已经实现在可读性和可维护性方面的最佳实践并希望继续执行这些检查;

同时是资源高度利用化的。按照包含动态分析的结果混合分析的案例。这样的分析为一些团队提供部分高价值,但对所有人来说成本太高或耗时太多。

截至2018年1月,Google内部大约有70个可选分析,其中2500个项目至少启用了其中一个。整个公司的数十个团队正在积极开发新的分析器,大多数隶属于都在开发工具组之外。

其他工作流程集成点

随着开发人员对这些工具的信任度增高,他们还要求进一步集成到工作流程中。Tricorder现在通过提供命令行工具,持续集成系统和代码审阅工具提供分析结果。

命令行支持。Tricorder团队为开发人员添加了命令行支持,这些开发人员实际上是代码管理员,经常浏览并清理团队代码库中的各种告警分析。这些开发人员也非常熟悉每个分析器将生成的修复类型,并且高度信任特定分析器。因此开发人员可以使用命令行工具自动应用给定分析中的所有修复并进行清理变更;

代码提交门槛。有些团队希望特定的分析器可以阻止代码提交而不是仅仅出现在代码审查工具中。通常要求阻止提交的能力是由具有高度定制检查器且保证没有误报的团队提出,通常用在自定义DSL或库。

代码展示结果。代码展示最适合显示大型项目(或整个代码库)中问题规模。例如,浏览有关已弃用API的代码时的分析结果可以显示迁移工作需要多少工作量;或者某些安全和隐私分析是全球性的,需要专业团队在确定是否存在问题之前审查结果。由于默认情况下不显示分析结果,因此代码浏览器允许特定团队启用分析视图,然后扫描整个代码库并审核结果,而这并不会干扰其他开发人员对这些分析器的注意力。如果分析结果具有关联修复,则开发人员只需单击代码浏览工具即可应用此修复。代码浏览器也非常适合显示生产数据利用的分析结果,因为在代码提交和运行之前这些数据均不可用。

复杂分析

在Google上广泛部署的所有静态分析都相对简单,尽管有些团队针对特定领域(例如Android应用程序)的项目特定分析框架进行过程间分析。 Google规模的过程分析在技术上是可行的。但是实施起来这样的分析非常具有挑战性。上面说到所有Google的代码都存贮在单独的整体的源码仓库中,因此从概念上讲代码仓库中的任何代码都可以是任意二进制文件的一部分。因此可以想象这样一种情况,其中特定代码审查的分析结果将需要分析整个代码仓库。尽管Facebook的Infer专注于过程间分析,将基于分离逻辑的分析器扩展到数百万行的代码库,但将这种分析器扩展到Google的数十亿行代码仓库仍然需要大量的工程化工作。截至2018年1月,实施一个更复杂的分析系统并不是Google的优先考虑因素:

大量投资。前期基础设施投资将是令人望而却步的;

需要努力降低误报率。分析团队必须开发技术,以显著降低许多分析器的误报率和/或严格限制该显示出来哪些错误信息,就如图infer做的一样;

还有更多要实施。分析团队仍然有更多“简易”的分析器需要去实现和集成;

高昂的前期成本。我们发现这种“简单”分析器的性价比很高,这是FindBugs的核心动机。相比之下,即使确定更复杂的检查器的成本ROI,前期成本也很高。

请注意,对于在专业领域(例如航空航天和医疗设备)或特定项目(例如设备驱动程序和手机应用程序)上工作的Google以外的开发人员,此ROI可能会有很大差异。

心得

我们尝试将静态分析融入到Google工作流程中的学费教会以下我们宝贵的经验教训:

发现bug缺陷很容易。当代码库足够庞大时,它几乎包含任何可以想象得到的代码模式。即使在具有完整测试覆盖率和严格的代码审查流程的成熟代码库中错误也在若隐若现。有时候问题在本地检查中并不明显,有时候由看似人畜无害的重构所引入错误。例如考虑以下代码片段使用类型为long的字段f,

result =
31 * result

  • (int) (f ^ (f >>> 32));

想象下如果开发人员将f的类型更改为int会发生什么。代码继续编译,但是向右偏移32变为no-op操作,字段与自身进行异或,变量的hash值变为常量0.结果是f不再影响hashCode方法生成的值。任何能够计算f类型的工具都可以正确地检测到向右偏移超过31的情况,我们在Google的代码库中修复了了31项出现该错误的代码,同时在Error Pone中将该检查纳入编译器错误。

由于发现错误很容易,Google使用简单的工具来检测错误类型。接下来分析编写者根据运行Google代码的结果进行微调。

大多数开发人员都不会如他们所想的那样使用静态分析工具。随着许多商业工具的发展,Google最初依赖FindBugs的实施,工程师选择访问集中的dashboard来查看他们项目中所发现的问题,但是其中很少有人真正去这样查看。查找已合入代码中的错误(可能已部署并在没有用户可感知到问题的情况下运行)为时已晚。为了确保大多数或所有工程师都能看到静态分析警告,必须将分析工具集成到工作流程中,并默认为每个人启用。Error Prone等项目不提供错误dashboard,而是通过额外的检查器扩展编译器,并且在代码审查时展示分析结果。

开发者的感受至关重要。根据我们的经验和材料积累,许多尝试将静态分析集成到软件开发组织的尝试都失败了。在Google管理层通常没有授权工程师使用静态分析工具。从事静态分析的工程师必须通过有效实际数据证明其影响力。要使静态分析项目取得成功开发者必须感知到他们从中受益并享受使用它的价值。

为了构建成功的分析平台,我们构建了可为开发人员提供高价值的工具。 Tricorder团队会仔细核阅已修复的问题,实际调研以了解开发人员的感受,使得通过分析工具提交bug更为便捷,并使用所有这些数据来持续改进。开发人员需要建立对分析工具的信任。如果一个工具浪费了开发人员时间的误报和反馈低级别问题,那么开发人员就会失去信心并忽视结果。

不局限于发现错误,修复它们。要推广静态分析工具,一种典型的方法是列举代码库中存在的大量问题。目的是通过指出纠正潜在错误或去在未来阻止bug发生来影响采取措施。但是如果开发人员受到不激励他们采取行动,那么这种潜在预期结果仍将无法实现。这是一个基本缺陷:分析工具通过它们识别的问题数来衡量它们的实用性,而流程集成会由于只有极少数的bug修复而失败。相反Google静态分析团队会同找bug一样也负责相应地修复工作,将其作为是否成功闭环的标准。专注于修复错误确保了工具提供可行的建议并最大限度地减少误报。在许多情况下,修复错误就像通过自动化工具找到它们一样容易。即使对于难以解决的问题,过去五年的研究也凸显了自动创建静态分析问题修复方面的新技术。

分析器开发需群策齐力。虽然特定的静态分析工具需要专家开发人员编写分析,但专家可能很少实际上并不知道哪些检查会产生较大的影响因子。此外分析器专家通常不是特定领域专家(例如那些使用API,语言和安全方面的专家)。通过FindBugs集成只有少数Google员工了解如何编写新检查器,因此小型BugBot团队必须自己完成所有工作。这限制了添加新检查的速度并事实上不能由其他人从他们的领域知识贡献而获益。像Tricorder这样的团队现在专注于降低开发人员提供的检查标准,不需要事先具备静态分析经验。例如Google工具Refaster允许开发人员通过在代码片段之前和之后指定示例来编写检查器。由于贡献者在自己调试错误代码之后经常有动力做出贡献,因此新的检查会逐步节省开发人员时间。

结论

我们的体会心得是重视集成于开发流程是静态分析工具实施的关键。虽然检查器工具作者可能认为开发人员应该面对他们编写的代码中存在缺陷列表感到高兴,但实际上我们并未发现这样的列表会激励开发人员去修复这些缺陷。作为分析工具开发人员,我们必须通过实际纠正的缺陷方面来定义衡量效果,而不是给开发人员提供数字。这意味着我们的责任远远超出了分析工具本身。

我们倡导一个专注于尽早推动工作流程集成的系统。尽可能将检查器作为编译器错误启用。为了避免中断构建工具编写者首先承担修复代码库中所有现有问题的任务,允许我们不断前行一步一步地提高Google代码库的质量。由于我们在编译器中呈现出错误告警,开发人员在编写代码后立即对其进行处理,这样他们仍然可以进行及时更改。为实现这一目标,我们开发了基础架构用于运行分析并在整个庞大的Google代码库中生成修复程序。我们还受益于代码审查和允许更改数百个文件的提交自动化,当然还有工程文化,其通常容许合入遗留代码的变更,因为改进代码胜过对修改风险的厌恶。

代码审查是提交代码之前显示分析警告的最佳切入点。为了确保开发人员能够接受分析结果Tricorder仅在开发人员在提交更改之前的修改代码阶段才会展示问题,并且Tricorder团队应用一系列标准来选择要显示的告警。Tricorder进一步在代码审查工具中收集统计数据,该工具用于检测分析器产生大量无效告警的根因。

为了克服告警被忽视,我们努力重新赢得得谷歌工程师的信任,发现谷歌开发人员有强烈的偏见去忽视静态分析,任何误报率不理想的报告都给他们不作为的理由。分析团队非常谨慎只有在根据描述客观标准对其进行审查后才能将检查结果作为错误或警告显示,因此开发人员很少被分析结果淹没,混淆或烦恼。调查和反馈渠道是这一过程的重要质量控制方法。现在开发人员已经对分析结果重新抱有信任感,Tricorder团队正在满足在Google开发人员工作流程中更多介入更多分析的需求。

我们在Google上构建了一个成功的静态分析基础架构,在编译时和代码审查期间可防止每天有数百个错误进入Google代码库。我们希望其他人可以从我们的经验中获益,将静态分析成功整合到他们自己的工作流程。