自动编写代码检查器:一种基于LLM的逻辑引导API检索与逐案迭代方法
Automatically Write Code Checker: An LLM-based Approach with Logic-guided API Retrieval and Case by Case Iteration
![![[Pasted image 20250324153147.png]]](https://i-blog.csdnimg.cn/direct/3d0eb357979c447babd670c09a36a48f.png)
摘要
随着对代码质量保证需求的不断增长,开发人员不仅在使用现有的静态代码检查器,还在寻求定制化的检查器以满足其特定需求。如今,各种代码检查框架提供了广泛的检查器定制接口来满足这一需求。然而,抽象的检查逻辑以及大规模框架中复杂的API使用使得这项任务充满挑战。为此,自动化代码检查器生成有望减轻检查器开发的负担。在本文中,我们探讨了自动化检查器生成的可行性,并提出了AutoChecker,这是一种创新的基于大语言模型(LLM)的方法,能够仅根据规则描述和测试套件自动生成代码检查器。AutoChecker并非一次性生成检查器,而是通过每次结合规则和单个测试用例逐步更新检查器,即逐案迭代生成检查器。在每次迭代中,AutoChecker首先将整体逻辑分解为一系列子操作,然后使用逻辑引导的API上下文检索策略从框架的所有API中搜索相关API上下文。为了评估AutoChecker的有效性,我们应用AutoChecker和两种基于LLM的基线方法,针对20条内置的PMD规则(包括简单规则和复杂规则)自动生成检查器。实验结果表明,AutoChecker在所有有效性指标上显著优于基线方法,其平均测试通过率提升了4.2倍以上。此外,由AutoChecker生成的检查器已成功应用于实际项目中,性能与官方检查器相当。
附加关键词和短语:代码检查器,静态分析,LLM,API检索
1 引言
静态代码检查工具通过根据一组预定义规则自动生成安全报告,在确保代码质量方面发挥着至关重要的作用。理想情况下,用户应该能够利用任何工具来满足其特定的检查需求 [31]。为了实现这一目标,许多代码检查工具致力于开发可扩展框架,允许用户使用自定义检查器扫描代码。例如,PMD [10] 和 SonarQube [14] 经过精心设计以支持二次开发,用户可以基于其检查规则用 Java 编写自定义检查器;CodeQL [3] 和其他基于 DSL 的工具 [67] 也鼓励用户定制化操作,要求用户“使用强大的查询语言编写查询以发现漏洞,并分享查询以帮助他人完成相同任务 [13]”。然而,由于现有检查框架的高度复杂性 [21](例如,PMD 框架中排除其预设检查器的部分已超过 30 KLOC)、庞大的框架特定 API 知识和不完整的 API 文档,以及需要实现的非平凡检查逻辑,检查器定制仍然是一个耗时且具有挑战性的任务。这对不熟悉特定代码检查框架但有紧急定制需求的用户来说尤为困扰。
如今,大型语言模型(LLMs)的兴起推动了自动代码生成的发展。受此启发,我们旨在探索基于 LLMs 的检查器代码自动生成方法,帮助用户更轻松、灵活地分析软件代码。根据我们的调研,现有的代码生成任务可分为以下两类:
- 非项目特定代码生成,例如普通代码补全 [35, 49]、普通补丁生成 [45, 65]、算法问题的普通代码生成 [32, 74] 等。
- 项目特定代码生成,例如项目特定代码补全 [18, 25, 64, 69]、项目演化过程中的代码修复 [47, 54] 等。
与非项目特定代码生成相比,项目特定代码生成是一项升级任务,其中目标代码可以访问项目中定义的特定类和方法。因此,它需要项目特定知识(项目中定义的类和方法)才能正确生成代码。否则,在代码生成过程中可能会出现意外的幻觉。一般来说,检查器生成属于项目特定代码生成。每个检查器都定义并运行在代码检查框架(即项目)之上。然而,这项任务比以往研究的任务更加困难。图 1 展示了手动检查器开发的整体流程。当项目经理有检查器定制需求时,他们必须向开发人员提供规则描述,这通常不够清晰且未正式化。为澄清规则,他们还应提供测试套件以覆盖所有可能的场景(包括极端情况)。基于这些信息,开发人员将总结从规则描述和测试套件中得出的正确且全面的检查逻辑。然后,他们会根据对框架的知识,使用框架 API 实现逻辑以编写检查器。与之前具有明确指导的代码生成任务不同,复杂的检查逻辑和大量可用的框架 API 使得正确生成预期检查器变得更加困难。
根据手动检查器开发的流程,我们必须首先应对开发人员在编写新检查器时可能遇到的以下两个主要挑战:
C1:复杂的检查逻辑。 总结全面的检查逻辑对于检查器生成至关重要。规则描述(用于粗略目标)和测试套件(覆盖所有场景)都应被视为输入。然而,复杂的检查逻辑可能导致过多的输入信息,包括众多测试用例。一方面,这会超出 LLM 生成涵盖所有场景的完整逻辑的能力;另一方面,可能会达到 LLM 的 token 限制。因此,复杂的检查逻辑很难一次性总结出来。
C2:不清晰的框架 API 知识。 在定制代码检查器时,开发人员需要对其选择的检查框架中可用的 API 有深入理解。由于一个代码检查框架可能包含数千个 API,但只有一个检查器需要其中少数几个,自动检查器生成可能缺乏哪些 API 可调用以及如何使用这些 API 的知识,这会导致生成过程中的幻觉。
为应对上述挑战,我们提出了一种新颖的方法,基于迭代测试驱动的检查器生成与逻辑引导的 API 上下文检索,自动编写静态检查器,并将其实现为 AutoChecker。具体而言,我们采取以下措施:
-
为获取涵盖复杂场景的全面检查逻辑,我们迭代生成和优化检查器,每次迭代由给定规则和测试套件中单个选定测试用例(初始失败测试用例)指导(C1)。 这一方法受到图 1 所示的手动检查器开发流程的启发,其中开发人员会在测试套件上验证检查器,并参考失败测试用例进行迭代更新,直到通过所有测试。在此演变过程中,AutoChecker 能够逐案例覆盖完整测试套件,并生成全面的检查逻辑。
![![[Pasted image 20250324153539.png]]](https://i-blog.csdnimg.cn/direct/182042bb26064b7a85f7a1274e41126a.png)
-
在每次检查器生成迭代中,AutoChecker 采用逻辑引导的 API 上下文检索,提取任务导向的框架知识(框架 API 的定义和用法)作为参考(C2)。 具体来说,AutoChecker 将检查逻辑分解为离散的子操作,随后为每个子操作检索相应的 API 上下文,这些操作在我们构建的 API 上下文数据库 Meta-Op DB(半自动化)和 Custom-API DB(自动化)中执行。通过这种逻辑引导的检索,AutoChecker 能够为检查器生成提取精确的 API 上下文。
为评估 AutoChecker 的有效性,我们选择了 20 条 PMD 规则,其中 10 条简单规则和 10 条复杂规则均匀分布。实验结果表明,AutoChecker 在编译率和测试通过率上均优于基线方法。具体而言,AutoChecker 生成的所有规则的检查器均可成功编译,平均测试通过率达到 82.28%(简单规则为 84.70%,复杂规则为 79.86%),分别是 NoCaseLLM 和 AllCasesLLM 的 4.3 倍和 4.2 倍。此外,我们通过选择通过所有测试的 AutoChecker 生成检查器,并将其应用于五个大规模实际 Java 项目进行了实用性评估。结果表明,当提供足够测试用例时,AutoChecker 可生成与基准检查器性能相当的检查器。
我们在本文中做出以下贡献:
- 我们提出了一种自动测试驱动的检查器生成方法,采用迭代生成管道逐步处理复杂的检查逻辑。
- 我们开发了一种逻辑引导的 API 上下文检索策略,并设计了一个通用的 Meta-Op 集以改进 API 检索,其中包含 354 个原子检查操作。
- 我们将该方法实现为 AutoChecker,可以根据给定规则和测试套件自动编写自定义代码检查器。实验结果表明,AutoChecker 生成的检查器在所有有效性指标上均显著优于基线方法。与官方检查器相比,它们在大规模实际项目中也取得了预期效果。
AutoChecker 的代码和数据集可在以下链接获取:https://anonymous.4open.science/r/AutoChecker-EF16/。
2 背景与动机
2.1 静态代码检查器
静态代码检查器被设计用于静态分析工具中,以在不执行代码的情况下分析代码 [19, 26, 57]。现有的静态分析工具通常基于以下方法开发:抽象语法树(AST)遍历(如 PMD [10]、SonarQube [14]、PyLint [12] 等)、数据流分析(如 Soot [15]、Coverity [4] 等)、符号执行(如 Clang Static Analyzer [2] 等)、代码库查询(如 CodeQL [3] 等)。其中,基于 AST 遍历的工具通常轻量且用户友好,其检查器也易于复用和扩展,因此成为软件公司在质量保证中的首选。
静态代码检查器通常配置了一组规则,这些规则定义了需要在代码中查找的代码模式。这些规则可以是检查框架中预定义的,也可以由用户根据特定项目需求进行定制。具体而言,对于基于 AST 的检查器,在遍历 AST 的过程中,它将相应的规则应用于每个对应的 AST 节点,寻找可能指示潜在缺陷的匹配项。需要注意的是,大多数规则涉及多个节点以实现复杂的检查逻辑。在遍历过程中,当找到匹配项时,检查器会生成报告。
2.2 动机示例
PMD [10] 是一种流行的基于 AST 的静态检查工具,支持 18 种编程语言(主要关注 Java 和 Apex),并包含 400 多条内置规则。在此,我们以现有的一条 PMD 规则(AssignmentToNonFinalStaticRule)作为动机示例,其描述为“在构造函数中对非 final 静态字段的赋值是不安全的”。相应的规则检查器应报告所有符合该规则描述的不安全赋值。
为了为给定规则开发高质量的代码检查器,开发人员不仅需要理解检查逻辑,还需要熟悉目标检查框架中的内置框架 API,例如 PMD 框架。这对自动检查器生成同样适用。根据我们的调研,自动检查器生成的主要挑战在于两个方面:生成全面的检查逻辑(抽象层面)和调用正确的框架 API(实现层面)。作为首次尝试,我们直接使用最先进的 LLM(GPT-4 [7]),根据所选规则的规则描述和完整测试套件生成检查器。
![![[Pasted image 20250324153557.png]]](https://i-blog.csdnimg.cn/direct/24f1a0bfa2a043f7bd4219df770289c8.png)
在抽象层面,我们分别从 LLM 生成的检查器和基准检查器中手动总结检查子任务。如图 2 所示,基准检查器首先定位构造函数内的变量和字段访问,然后检查引用的符号(标识符)是否为静态且非 final。相比之下,LLM 生成的检查器从构造函数声明开始查找不安全字段。然而,它仅检查 ASTFieldDeclaration 中的字段,而重新赋值表达式中的不安全字段会被遗漏,这相比基准检查器缺乏完整性。因此,尽管我们已向 LLM 提供了足够的测试用例作为参考,但由于输入信息过多,它仍未能生成全面的检查逻辑。考虑到这一点,我们在 AutoChecker 中提出了一种迭代代码生成方法,该方法通过逐案例指导更新检查器的逻辑,即每轮生成由单个测试用例指导。如图 2 所示,AutoChecker 通过访问构造函数内的所有赋值表达式修复了完整性问题,从而从与基准不同的角度生成了正确的检查逻辑。
![![[Pasted image 20250324153619.png]]](https://i-blog.csdnimg.cn/direct/24914fcee959421ba116f9e2851ea264.png)
在实现层面,我们深入研究了自动生成检查器的代码实现。如图 3 所示,如果我们直接要求 LLM 根据规则和测试套件编写检查器,它会通过猜测某些框架 API 来生成代码。这些幻觉 API(例如方法 jjtGetNumChildren 和类 ASTName)在我们的实验 PMD 版本中并未定义,其使用会导致编译错误。我们在图 4 中收集了幻觉 API 的数量(重复的 API 仅计数一次),占使用框架 API 总数的 31.25%。为填补这一空白,AutoChecker 在每轮检查器生成中检索相关的 API 上下文,为检查器实现提供必要的 API 知识。如图 4 所示,AutoChecker 生成的最终检查器未包含幻觉 API,能够成功编译并通过所有测试。
3 方法论
![![[Pasted image 20250324153632.png]]](https://i-blog.csdnimg.cn/direct/2ee89e1761c348c594fbdcc3313f7810.png)
AutoChecker 的设计目标是基于大语言模型(LLMs),根据给定的规则描述及其对应的测试用例,自动生成正确的静态检查器。图 5 展示了 LLM 驱动的自动检查器开发流程的整体管道。有两种检查器生成器利用 LLM,通过不同提示词编写检查器,分别在初始检查器生成(3.3.1)和迭代检查器生成(3.3.2)中介绍。值得注意的是,这两种生成器都依赖于 API 上下文检索器(3.2)来检索相关框架 API 的签名和用法作为参考,这有助于减少生成过程中的幻觉问题。本节详细介绍了我们提出的 AutoChecker 的方法论。在定义该方法解决的任务(第 3.1 节)后,第 3.2 节和第 3.3 节分别介绍了 API 上下文检索和测试驱动的检查器生成。
3.1 任务定义
给定一个检查规则rrr和一组测试用例(也称为测试套件TTT),我们的任务是自动生成正确的代码检查器CfC_fCf(最终版本),以通过所有测试。该任务可以定义为:
Gen(r,T)=Cf. Gen(r, T) = C_f. Gen(r,T)=Cf.
AutoChecker 将整个生成过程分为两部分:初始检查器生成和迭代检查器生成。如图 5 所示,从测试套件中选择一个初始测试用例t0t_0t0来生成初始检查器C0C_0C0,表示为:
InitGen(r,t0)=C0. InitGen(r, t_0) = C_0. InitGen(r,t0)=C0.
然后,将初始检查器输入到迭代检查器生成器中,作为候选进行验证和更新。这一过程可以表示为:
IterGen(r,T,C0)=Cf, IterGen(r, T, C_0) = C_f, IterGen(r,T,C0)=Cf,
其中TTT是完整的测试套件,CfC_fCf是最终目标检查器。需要注意的是,每轮检查器生成都依赖于 API 上下文检索,以提供框架 API 的相关知识KapiK_{api}Kapi。给定原始规则rrr和特定测试用例ttt,API 上下文检索过程可以一般性地表示为:
Retrieve(r,t)=Kapi.(1) Retrieve(r, t) = K_{api}. \tag{1} Retrieve(r,t)=Kapi.(1)
3.2 API 上下文检索
考虑到 PMD 框架中内置 API 数量庞大,检索过程对于为检查器生成提供准确且充足的信息至关重要。为了精确检索可用于目标检查器的相关 API,我们设计了一种受链式思维 [42, 63]、MapReduce [28] 和组合 API 推荐 [50] 启发的逻辑引导 API 上下文检索方法。如图 5 所示,AutoChecker 首先利用 LLM 将检查规则分解为包含子操作的检查骨架。然后,每个子操作用于单独的 API 上下文(API 签名和用法)检索,最终组成完整的 API 上下文。
3.2.1 框架 API 类型
对于基于 AST 的代码检查框架(如 PMD),框架 API 被预定义为基本检查操作。根据我们的调研,PMD 7.0.0-rc4 中的所有框架 API 都定义在 AST 节点类(如 ASTMethodDeclaration)和工具类(如 JavaAstUtils)中。具体而言,我们收集了三种类型的框架 API,其中节点相关 API 和边相关 API 来自 AST 节点类,而工具相关 API 来自工具类。
从 AST 节点类收集 API。
首先,我们构建了一个从每个单一 AST 节点类(即 ANC)到其所有可用 API 的映射,其中既包括类中声明的方法,也包括从其父类继承的方法。因此,定义在 ANC 中的 API 可以分为两类:一类用于执行节点相关检查操作(节点相关 API),另一类用于通用 AST 遍历(边相关 API)。边相关 API(如查找最近的父 AST 节点)定义在抽象 ANC(JavaNode)中,因此可以被每个具体的 ANC 实例调用。因此,我们通过搜索 JavaNode 的定义体并提取返回值为另一个节点的 API,收集所有边相关 API。过滤掉所有边相关 API 后,剩余的 API 被收集为节点相关 API。它们对 AST 节点执行具体操作,例如获取方法名称、参数数量、类修饰符等。
从工具类收集 API。
与特定 AST 节点无关的框架 API 被封装为工具类中的工具 API。它们提供了可以在任何地方调用的通用功能,例如检查方法是否为抽象方法。具体而言,每个工具相关 API 是一个属于工具类的静态方法,带有 final 修饰符和私有构造函数。通过检索所有工具类,收集工具相关 API。
总体而言,我们从 PMD 框架(PMD 7.0.0-rc4)中收集了三类 API:节点相关 API、边相关 API 和工具相关 API,详见表 1。鉴于 API 数量庞大(超过 11K),精确检索十分必要。
![![[Pasted image 20250324153842.png]]](https://i-blog.csdnimg.cn/direct/cbbfc48162a548de9e19df9ff4be2205.png)
3.2.2 API 上下文数据库构建
通常,我们将一个 API 上下文定义为 API 的签名或指示 API 使用方式的代码片段。为进一步进行上下文检索,每个 API 上下文应绑定相应的描述文本以说明其功能。然而,框架 API 的封装粒度在不同框架甚至同一框架内存在差异。因此,我们不应简单地基于所有提取的内置 API 构建 API 上下文数据库,因为这会影响检索的精度。
元操作集准备。
为构建一个通用的 API 上下文数据库以实现精确检索,我们设计了一个框架无关的元操作集(Meta-Operation Set,简称 Meta-Op Set),其中包含常用的基本功能元操作(meta-ops)。为提供全面的 Meta-Op Set,我们邀请了三名具有超过一年 PMD 开发经验的 Java 开发者进行收集。第一名开发者根据其经验和 PMD 框架的结构,收集并整理了大部分元操作并将其分类,其余两名开发者通过头脑风暴补充完善。最终的 Meta-Op Set 包含 14 类共 354 个操作,如图 6 所示。
![![[Pasted image 20250324153748.png]]](https://i-blog.csdnimg.cn/direct/7c420917f7df48f38c8f99eeed78b683.png)
Meta-Op 数据库(Meta-Op DB)基于 Meta-Op Set 构建,其中每个元素是一对元操作及其对应 API 上下文(以 API 签名或使用形式表示)。对于 PMD 7.0.0-rc4,我们通过检索先前从框架中收集的 API,为每个元操作找到对应的 API 上下文。具体而言,给定一个元操作,我们尝试找到满足其功能的正确框架 API。为此,我们收集所有框架 API 的描述作为匹配参考。
API 描述收集。
为获取每个 API 的描述,我们利用 API 签名中包含的语义信息自动生成摘要文本作为其描述。首先,类名和方法名可以反映 API 的功能,通常遵循流行框架中的驼峰命名规则。通过根据命名规则拆分名称中的单词并移除一些不必要的词(如 AST),我们为每个方法或类生成一个基本短语。例如,类 ASTStringLiteral 的基本短语是“字符串字面量”,方法 isEmpty 的基本短语是“是否为空”。然后,我们根据返回值类型确定描述前缀,如下所示:
- 如果返回类型为布尔值,API 旨在进行特征检查。对于这种类型,“检查是否”应作为前缀添加。
- 如果返回类型为非布尔值(如字符串),API 旨在进行数据获取,方法名通常以动作词(如“get”)作为前缀,因此无需额外添加前缀。
![![[Pasted image 20250324154142.png]]](https://i-blog.csdnimg.cn/direct/3e8996e907b54b1c86ae79af10b1827e.png)
通过连接基本短语和前缀,生成 API 的描述。如表 2 所示,我们为不同类别的 API 设计了不同的策略来生成描述。详细的生成规则见表 2。对于节点相关或边相关 API,类名、方法名以及返回类型(是否为布尔值)都被考虑以形成新的描述。对于工具相关 API,其类名(如 JavaAstUtil)通常不必要,因此省略。以定义在类 ASTStringLiteral 中的签名 boolean isEmpty() 为例,生成的描述为“检查字符串字面量是否为空”。
此外,一些 API 带有注释,这些注释也揭示了其功能。因此,我们还提取 API 注释作为描述的补充,注释以“//”开头并附加到描述文本末尾。所有注释均经过过滤以去除与功能无关的内容(如异常条件、作者信息等)。总体而言,我们为所有框架 API 生成了描述文本。
3.2.2 Meta-Op 数据库构建
![![[Pasted image 20250324154203.png]]](https://i-blog.csdnimg.cn/direct/282af48261d642e8b33b1a7faf0e9006.png)
我们通过结合 Meta-Op 集和带有描述的已收集 API 构建了 Meta-Op 数据库(Meta-Op DB)。给定一个元操作(meta-op),我们基于元操作与 API 描述之间的语义相似性检索出前三个框架 API。所有检索到的 API 均经过人工检查,只有完全符合操作需求的 API 被设为该元操作的 API 上下文。对于未匹配到任何 API 的元操作,我们手动编写代码片段作为其 API 上下文。因此,Meta-Op DB 中的 API 上下文以操作-签名对或操作-代码片段对的形式存在。我们在图 7 中提供了两个示例。
Custom-API 数据库构建。
由于 Meta-Op DB 并未覆盖完整的框架 API,我们构建了 Custom-API 数据库(Custom-API DB)以补充框架 API 的检索能力。因此,Custom-API DB 中的每个元素表示一个描述-签名对,包含特定 API 的签名及其收集到的描述。
3.2.3 逻辑引导的 API 上下文检索
![![[Pasted image 20250324154246.png]]](https://i-blog.csdnimg.cn/direct/c10e28811253479eb8919d7844e8fa11.png)
如公式 (1) 所定义,AutoChecker 根据检查器规则和给定测试用例检索相关 API 上下文。这一过程如算法 1 所示。考虑到遍历 AST 节点是所有检查器的基本功能需求,我们提供 21 个边相关 API 的签名(见表 1)作为基础 API 上下文。这些以签名为形式的 API 上下文将直接添加到最终检索到的 API 上下文集合中,无需进一步检索。
对于逻辑引导的 API 上下文检索,AutoChecker 首先通过将检查器规则拆分为子操作生成检查骨架(第 3 行)。在给定检查器规则、测试用例和 Meta-Op 集作为输入的情况下,AutoChecker 利用 LLM 进行拆分。具体而言,Meta-Op 集作为子操作的参考,指导 LLM 在与元操作类似的粒度下生成子操作。然后,我们基于 Meta-Op 和 Custom-API 数据库为每个子操作检索 API 上下文(第 5-10 行)。在单次检索过程中,子操作被设置为查询条件,以检索语义相似度得分最高的 API 上下文。如果最高得分仍低于阈值,则检索失败并返回 None。对于完整的检索过程,AutoChecker 首先搜索 Meta-Op DB(第 5 行),如果未命中,则继续搜索 Custom-API DB。在搜索 Custom-API DB 之前,AutoChecker 通过忽略无关的节点相关 API 缩小数据库范围(第 7-8 行)。具体而言,定义在 AST 节点类中的框架 API,其对应的 AST 节点在测试用例的 AST 中不存在时,被认为是无关的。总体而言,包括基础和检索到的所有相关 API 上下文均被收集。
3.3 迭代测试驱动的检查器生成
为了自动生成正确的检查器,AutoChecker 采用迭代测试驱动的检查器生成方法。与一次性使用完整测试套件生成不同,AutoChecker 每次基于单个测试用例迭代更新检查器。如第 3.1 节所定义,整个过程具体分为初始检查器生成和迭代检查器生成两部分。
3.3.1 初始检查器生成
在此阶段,AutoChecker 从测试套件中选择第一个测试用例作为初始测试用例,其中测试用例通常按复杂性排序。如图 5 所示,AutoChecker 然后通过任务信息构建(TaskInfo Construction)和 API 上下文检索构建用于初始检查器生成的 LLM 提示词(InitPrompt)。
![![[Pasted image 20250324154303.png]]](https://i-blog.csdnimg.cn/direct/76fd5ca678b749baaf3062c008591ddc.png)
算法 2 详细描述了生成初始检查器的过程。AutoChecker 首先通过移除注释和重新格式化预处理初始测试用例(第 2 行)。在任务信息构建过程中,AutoChecker 根据检查器规则和清理后的测试用例准备 InitPrompt 的任务信息。由于代码检查操作是在目标代码(测试用例)的 AST 上执行的,AutoChecker 使用 PMD 内置解析器提取 PMD 风格的 AST(第 3 行)。同时,我们还从框架中手动总结了一个 PMD 检查器模板,如图 8 所示。
![[Pasted image 20250324154443.png]]
总体而言,规则描述、清理后的测试用例、测试用例 AST 和 PMD 检查器模板被准备为任务信息构建期间的任务信息。然后,AutoChecker 通过 API 上下文检索,根据检查器规则和清理后的测试用例检索相关 API 上下文(第 4 行)。基于任务信息构建生成的任务信息和 API 上下文检索收集的相关 API 上下文,初始检查器由 LLM 生成(第 5-6 行)。
3.3.2 迭代检查器生成
基于初始检查器作为候选检查器,AutoChecker 根据在整个测试套件上的执行结果迭代验证和更新检查器,如算法 3 所示。
![![[Pasted image 20250324154353.png]]](https://i-blog.csdnimg.cn/direct/cd7f3341ba1b4c6194b8623c29496202.png)
在每一轮中,AutoChecker 在整个测试套件上运行候选检查器并收集执行报告(第 5 行)。如果检查器未能通过所有测试,AutoChecker 将重新查询 LLM,基于失败的检查器和第一个失败的测试用例生成新的检查器(第 9-14 行)。AutoChecker 基本上遵循初始检查器生成中的提示词构建方式,通过任务信息构建和 API 上下文检索,基于检查器规则和失败的测试用例生成新提示词。需要注意的是,此阶段的 LLM 被要求根据输入更新候选检查器。因此,候选检查器也被包含在最终提示词(IterPrompt)中(第 13 行)。最后,当候选检查器通过所有测试用例或达到最大迭代次数时,AutoChecker 输出最终检查器及其记录的测试通过率(第 16 行)。
4 实现与实验设置
在本节中,我们首先介绍 AutoChecker 的实现细节,然后描述实验设置中的规则选择、基线方法和评估指标。
4.1 实现细节
AutoChecker 构建在一个默认的代码检查框架之上。通过调研,我们发现 PMD 是一种广泛使用的开源代码检查工具,在有效性和易用性方面表现优异 [46]。因此,我们选择了 PMD 7.0.0-rc4 [10](我们工作开始时 PMD 的最新版本)作为具体的代码检查框架,并选择其内置规则进行进一步评估。所有对应于这些规则的测试用例均从 PMD 的默认测试套件 [10] 中收集,并通常按难度和复杂性排序。默认情况下,AutoChecker 按照排列顺序选择测试用例用于迭代检查器生成。
AutoChecker 基于 LangChain [8] 实现,LangChain 是开发基于 LLM 应用程序最广泛使用的框架。在 LangChain 兼容的各种模型中,我们使用 OpenAI 开发的 GPT-4 [7] 作为生成的 LLM,并使用 BAAI [1] 发布的流行开源嵌入模型 bge-large-en-v1.5 [66] 进行检索。为实现第 3 节中引入的方法论,我们还应用了以下设置:
提示词增强(Prompt Augmentation)。
为了在每个提示词中更好地表示测试用例,我们对测试用例及其 AST 进行特定的增强策略。对于每个测试用例,我们手动收集其描述和预期违规数量作为补充信息,连同源代码一起提供。特别是对于 PMD 的内置规则,其测试用例已组织良好并包含上述额外信息,可以直接从 PMD 的官方文档 [10] 中获取。此外,AutoChecker 在 AST 解析过程中保留从标识符解析出的具体 AST 节点名称,自动增强测试用例 AST。例如,从方法名“length”解析出的 AST 节点 ASTClassDeclaration 将被增强为 ASTMethodDeclaration(length)。这种增强将 AST 与其对应的测试用例源代码连接起来。
输出后处理(Output Postprocessing)。
为了避免生成的检查器中出现导入错误,AutoChecker 将检查器的导入部分替换为默认导入,与图 8 所示的 PMD 模板中的导入一致。这确保所有必要的包都被正确导入。
![![[Pasted image 20250324154454.png]]](https://i-blog.csdnimg.cn/direct/30934d416dd94562b18a987d635ef0a8.png)
API 检索配置(Configuration for API Retrieval)。
在 API 上下文检索期间,我们根据经验设置了阈值,其中子操作与 Meta-Op DB 的语义相似度得分为 0.85,与 Custom-API DB 的阈值为 0.8。所有这些参数都可以由用户配置。
迭代生成配置(Configuration for Iterative Generation)。
对于每次基于单个测试用例和规则的检查器生成迭代,AutoChecker 查询 LLM 生成检查器。如果生成的检查器遇到编译错误,AutoChecker 将直接要求 LLM 最多两次纠正错误以获得该迭代的结果候选检查器。为减轻 LLM 在生成过程中的随机性,每次迭代最多重新执行五次。在重新执行过程中,如果生成的检查器成功编译并通过给定的测试用例,AutoChecker 输出检查器代码并跳出该迭代。如果当前迭代的测试用例即使经过五次尝试仍未通过,AutoChecker 将跳过该测试用例并使用另一个测试用例启动下一次迭代。
4.2 实验设置
4.2.1 PMD 规则选择
PMD 的开源存储库 [11] 中有 132 条内置规则,这些规则根据其功能分组。在所有这些规则中,我们过滤掉了四个在 PMD 官方文档中已弃用或缺失的规则。对于剩下的 128 条规则,我们根据检查器官方实现中主要检查的 AST 节点进一步重新分类。
![![[Pasted image 20250324154509.png]]](https://i-blog.csdnimg.cn/direct/0f63867133804a42956f1280c6a1ca9f.png)
图 9 列出了重新分类类别中规则的分布情况。
由于实现这些内置规则的检查器难度各异,为了清晰评估,我们根据相应内置检查器的实现复杂性将所有规则分为简单规则和复杂规则。统计上,我们通过计算内置检查器代码中的特定元素来评估规则的复杂性。当“检查器的代码行数、导入语句数、方法调用语句数和控制语句数均小于所有内置检查器的平均值,并且导入的语义类数小于 1”时,选择为简单规则。其他不满足此条件的规则标记为复杂规则。总体而言,我们在 10 个类别中收集了 128 条规则,包括 64 条简单规则和 64 条复杂规则。为了评估,我们随机选择 10 条简单规则,同时确保每条规则属于一个独特类别,并选择 10 条复杂规则。所选规则列于表 3 中。
![![[Pasted image 20250324154603.png]]](https://i-blog.csdnimg.cn/direct/57d9b62a8a8d4e36bef37667d56dd01c.png)
4.2.2 基线方法
根据我们的调研,目前没有先前的工作提出完全自动化的静态检查器生成方法。因此,我们基于 LLM 的能力开发了以下两种基线方法,以评估 AutoChecker 的有效性。第一种是 NoCaseLLM,它仅使用规则描述和 PMD 检查器模板(参见图 8)生成检查器,未提供测试用例。另一种是 AllCasesLLM,它使用规则描述、PMD 检查器模板和所有测试用例生成检查器。与 NoCaseLLM 相比,它增加了测试套件。由于 GPT-4 当前的最大输入 token 限制为 8,192,我们逐步向 AllCasesLLM 的提示词中添加测试用例,直到达到 token 限制或包含所有测试用例为止。在生成检查器时,这两种基线方法均一次性生成检查器,遵循第 4.1 节中 AutoChecker 的配置。对于每条规则,两种基线方法各有五次生成检查器的机会,并在每次生成中有两次修复编译错误的机会。一旦生成可编译的检查器,我们停止过程并输出最终检查器。
4.2.3 评估指标
我们设计了四种类型的指标以进一步评估方法。
◆ Rulepc:给定一条规则,如果一种方法生成的检查器可以通过编译,我们将该规则记录为该方法的 Rulepc。Rulepc 的数量记录为 #Rulepc。
◆ Rulepit:给定一条规则,如果一种方法生成的检查器可以通过测试套件中的第一个测试用例,我们将该规则记录为该方法的 Rulepit。对于基线方法,我们评估其最终检查器;对于 AutoChecker,我们评估由单个测试用例引导的初始检查器。Rulepit 的数量记录为 #Rulepit。
◆ TCpass:对于所有规则,我们运行由一种方法生成的最终检查器在测试用例上的结果。通过的测试用例总数记录为 #TCpass。
◆ TCpr:对于每条规则,我们记录生成的最终检查器的测试通过率(通过的测试用例数 / 总测试用例数)为 TCpr。一组规则的最终检查器的平均通过率记录为 avg_TCpr。
5 评估
为了评估 AutoChecker 的性能,我们回答以下研究问题:
• RQ1. 有效性:AutoChecker 是否能有效生成高质量的检查器?
• RQ2. 消融研究:检索模块和迭代模块分别有多大帮助?
• RQ3. 成本:AutoChecker 的时间和经济成本是多少?
• RQ4. 实用性:AutoChecker 的检查器在分析大规模项目时的表现如何?
5.1 RQ1:有效性评估
![![[Pasted image 20250324154628.png]]](https://i-blog.csdnimg.cn/direct/f845d306d7994f3eb8906cc2062f64ab.png)
首先,我们评估 AutoChecker 与两个基线方法(NoCaseLLM 和 AllCasesLLM)在选定的 20 条规则上的有效性。如表 4 所示,AutoChecker 可以为所有规则生成可通过编译的检查器并通过初始测试用例,这些结果通过指标 #Rulepc 和 #Rulepit 显示。相比之下,基线方法中只有少数规则(20%-25%)具有可通过编译的最终检查器,而能够通过初始测试用例的规则数量更少(15%)。对于这 20 条规则的 373 个内置测试用例,我们的方法通过了 278 个案例,是两种基线方法的 6 倍,如指标 #TCpass 所示。最后三列给出了所有规则、简单规则和复杂规则的平均测试通过率 avg_TCpr,其中 AutoChecker 在所有规则集上的 avg_TCpr 均优于基线方法。观察到简单规则的 avg_TCpr 高于复杂规则的现象表明,生成正确的检查器对于高难度规则更具挑战性。特别地,AllCasesLLM 的 avg_TCpr 从简单规则到复杂规则有显著下降,可能的原因是较长的提示词使 LLM 更难专注于具体内容。
![![[Pasted image 20250324154754.png]]](https://i-blog.csdnimg.cn/direct/c7840644c6504a05b3be5de21963112f.png)
此外,图 10 给出了 AutoChecker 下简单规则和复杂规则的更详细 TCpr。有六个生成的检查器通过了所有测试用例。除了一条复杂规则外,所有其他最终检查器的 TCpr 均超过 50%,这突显了我们方法在生成有用检查器方面的有效性。
![![[Pasted image 20250324154624.png]]](https://i-blog.csdnimg.cn/direct/7c6fe61dbcbe4a85b70c6a9cbbef6056.png)
根据表 4,我们可以看到基线方法无法为大多数规则生成可编译的检查器。事实上,尽管 AutoChecker 实现了最高的测试通过率(TCpr),但在整个迭代过程中,它也未能为一些复杂情况生成可编译的检查器。通过总结,我们确定了导致这些失败的三个主要原因,具体如下:
- 语法错误:NoCaseLLM 下大多数编译失败的检查器都遇到了语法错误,而 AllCasesLLM 则很少出现这种情况。前者可能的原因是由于输入不足导致需求不明确。
- 错误的类使用:由于我们预先导入了所有必需的包,因此这里遇到的类使用错误指的是使用了在所选 PMD 版本中不存在的类。如图 3 和图 4 所示,其中一些错误的类仅在其他 PMD 版本中可用,而另一些则完全是虚构的。基线方法面临严重的类使用错误问题:NoCaseLLM 下一半的生成失败规则(16 条中的 8 条)和 AllCasesLLM 下所有的生成失败规则(15 条中的 15 条)在其生成过程中表现出类使用错误。虽然 AutoChecker 在迭代过程中遇到了一些类错误,但最终检查器中没有此类错误。
- 错误的 API 调用:一方面,错误的类总是会导致错误的 API 调用;另一方面,即使使用了正确的类,仍可能存在调用不存在 API 的问题。例如,NoCaseLLM 下的一个检查器错误地尝试使用
ASTTryStatement的实例调用getNumCatchBlocks()。尽管ASTTryStatement是 PMD 中的有效类,但它既未声明也未继承 APIgetNumCatchBlocks(),从而导致另一种形式的编译错误。
在这些原因中,错误的类使用和错误的 API 调用均属于幻觉问题。由于幻觉对检查器生成过程有重大影响,我们的方法引入了 API 上下文检索模块,成功减少了幻觉现象。
☞ 回答 RQ1:AutoChecker 能够生成高质量的检查器,不仅能够成功通过编译,还能实现高测试用例通过率,显著优于基线方法。
5.2 RQ2:消融研究
为了评估 AutoChecker 中 API 上下文检索模块(简称 Retrieval)和迭代检查器生成模块(简称 Iteration)的有效性,我们通过禁用其中一个模块进行了消融实验。
AutoChecker 无 Retrieval 模块
![![[Pasted image 20250324154653.png]]](https://i-blog.csdnimg.cn/direct/fd45fd9ef2374e2b82dae88a645adfab.png)
第一种变体丢弃了 Retrieval 模块。表 5 给出了整个迭代过程中平均编译错误修复次数(#Repaircompilation)以及该变体的平均测试用例通过率(avg_TCpr)。可以看出,如果没有检索模块,修复尝试次数增加了一倍多(从 14 次增加到 31 次),这表明检索到的 API 对于检查器代码生成非常有用,并避免了许多幻觉问题。此外,没有检索模块的情况下,avg_TCpr 下降了 24.8%,反映了其对生成高质量检查器的影响。例如,AutoChecker 在 AvoidUsingOctalValuesRule 上实现了 100% 的 TCpr,但如果缺少检索模块提供的 getBase(),由于编译失败,TCpr 急剧下降至 0%。
为进一步探索 Meta-Op 集对改进检索的帮助程度,我们为每条规则收集了每次迭代中通过给定测试用例的检索到的 API 上下文。然后,我们计算最终检查器中使用的 API 上下文的比例(称为 Retrieval Recall)。所选 20 条规则的平均 Retrieval Recall 为 34.47%,表明超过三分之一的 API 上下文最终被利用。值得注意的是,这些有用的 API 上下文中,有 82% 是由 Meta-Op 数据库匹配的,突显了 Meta-Op 集在检索模块中的重要作用。
AutoChecker 无 Iteration 模块
![![[Pasted image 20250324154658.png]]](https://i-blog.csdnimg.cn/direct/c768695131fa4a98a311892ca5278c53.png)
另一种变体丢弃了 Iteration 模块,即所有测试用例、其 AST 和相关 API 上下文同时提供,无需迭代。如表 5 所示,与一次专注于单个测试用例逻辑的指导相比,丢弃 Iteration 模块导致性能大幅下降(70.64%)。这一显著下降强调了 Iteration 模块在提升 AutoChecker 有效性方面的重要作用。
此外,我们的观察表明,Iteration 模块对于具有更多测试用例的规则效果改善更为显著。对于测试用例少于 10 个的规则,在无 Iteration 模块的情况下,其 avg_TCpr 仅轻微下降至 69.02%。相比之下,对于剩余 13 条测试用例超过 10 个的规则,在相同方法下的 avg_TCpr 降至零。这一结果表明,过多的测试用例集中在单个提示词中会显著分散 LLM 的注意力。
☞ 回答 RQ2:Retrieval 和 Iteration 模块在生成高质量检查器方面均发挥了关键作用。缺少任何一个模块都会显著影响检查器的性能。
5.3 RQ3:成本评估
![![[Pasted image 20250324154814.png]]](https://i-blog.csdnimg.cn/direct/c4e5d20fbcf7448fbd5bb8f3bc5dcac6.png)
我们从时间和经济两个方面评估 AutoChecker 的成本。为了评估时间成本,我们收集了在 CPU 上运行的实验结果,并统计了简单规则、复杂规则以及所有规则组合的时间成本。图 11 展示了统计数据。对于所有规则,AutoChecker 平均需要 114 分钟生成一个有效的检查器。其中,生成简单规则的检查器仅需 70 分钟,而生成复杂规则的检查器则需要 156 分钟。与传统的手动开发自定义代码检查器的方法相比(通常需要数天时间并需要多个角色员工的协作,如开发人员、测试人员等),AutoChecker 能够显著降低开发成本。此外,它可以通过添加额外的测试轻松迭代和改进生成的检查器。
在经济成本方面,AutoChecker 平均使用 426k 输入 token 和 202k 输出 token 成功生成一个检查器。对于我们选择的 GPT-4 模型,平均成本约为 25 美元。对于需要大量定制化代码检查器以满足自身代码检查需求的企业来说,这比聘请专人开发检查器要经济得多。而且,随着 LLM 模型 API 价格的持续下降,这一成本可以进一步优化。
☞ 回答 RQ3:与传统的检查器开发方法相比,AutoChecker 的时间和经济成本更加可负担。
5.4 RQ4:实用性评估
为了评估我们生成的检查器在大规模项目中的适用性,我们从 GitHub 中选择了五个流行的 Java 项目,这些项目均拥有超过 50K 星标,代码规模从 50 KLOC 到 1,517 KLOC 不等。本实验基于六条生成检查器具有 100% TCpr 的规则进行。评估结果如表 6 所示。
![![[Pasted image 20250324154716.png]]](https://i-blog.csdnimg.cn/direct/f6cdcb6e50f64813b80255cea1dfdbaf.png)
首先,我们使用官方检查器作为基准分析这些项目。报告的违规数量显示在第二列中。然后,我们使用 AutoChecker 在原始测试套件的指导下生成检查器。第三列(原始测试套件)显示了 AutoChecker 检查器报告的违规数量及不一致的数量,其中成功报告了 94.15% 的基准违规。需要注意的是,只有当报告的位置能够匹配时,两条违规报告才被视为一致。然而,我们注意到六个检查器中有四个报告的违规数量低于基准值。通过分析,我们发现主要原因是一些常见的实现错误以及原始测试套件遗漏了一些极端情况。因此,我们可以通过基于 LLM 的检查器修复和手动增强测试套件来轻松提升生成检查器的能力。具体而言,我们采取以下步骤缩小这一差距:
-
修复实现错误:如果检查器导致意外崩溃,我们会获取崩溃报告。将报告和检查器代码提供给 LLM 可以帮助修复这些常见的实现错误。在我们的评估中,规则
AssignmentToNonFinalStatic的检查器因缺少空值检查操作而触发了一种崩溃。另一种崩溃由规则NullAssignment引发,原因是类型转换前未进行类型检查。 -
减少假阴性:针对假阴性,我们手动总结应报告但未被报告的代码片段,并将其添加到原始测试套件中。增强后,我们继续在其指导下进行迭代过程。对于存在假阴性的四条规则,第三列给出了额外增强的测试用例数量(指标为 #TCaug),其中内置测试用例的数量为 #TC。在增强后的测试套件上报告的违规数量显示在表 6 的最后一列中。可以看到,在添加测试用例后,所有假阴性都被消除了。
以表 6 中的规则 NullAssignment 为例,我们的检查器在项目中比官方检查器多报告了两次违规,这种差异是因为检查器设计不同,我们的检查器在同一位置生成了两次重复报告。由于它们是冗余的真实违规,我们并未将其视为假阳性。
☞ 回答 RQ4:在高质量测试套件的指导下,AutoChecker 能够生成实用的检查器,其结果与官方检查器相当。借助 AutoChecker,检查器实现这一艰巨任务正逐步转向适当的测试套件设计。
6 对有效性的影响因素
外部有效性:外部有效性的主要威胁来自用于实现的具体代码检查框架和所选的评估规则。首先,我们仅基于 PMD 框架实现了 AutoChecker 以自动生成静态代码检查器,这可能难以扩展到其他框架。为提高 AutoChecker 的可扩展性,我们将方法论设计为尽可能与框架无关,包括引入一个可用于跨框架 API 上下文检索的 Meta-Op 集。其次,所选的 20 条规则可能无法代表所有可能的真实世界检查器规则。为缓解这一问题,我们从 PMD 内置规则中选择规则,这些规则作为参考具有代表性。在根据难度对内置规则进行分类后,我们随机选择了等量分布的简单和复杂规则,以获得多样化的规则集。
内部有效性:主要的内部威胁是 Meta-Op 集的通用性。为缓解这一问题,我们邀请了三位资深开发者创建并补充该集合。此外,我们通过计算 Meta-Op 在 PMD 和 CodeQL 中与框架 API 对齐的比例,进一步评估了 Meta-Op 集的通用性。具体而言,我们在框架的内置 API 中搜索每个元操作的匹配项,结果显示两种工具中匹配的元操作比例相似(PMD 中为 129/354,CodeQL 中为 119/354),且有 74 个重叠。这表明许多元操作在不同框架中具有对应的 API,证明了 Meta-Op 集的通用性。
7 相关工作
7.1 代码检查器开发
静态代码检查工具提供了各种内置检查器,以帮助提高实际项目中的代码质量。现有研究 [43] [46] [72] [36] [58] [41] 从多个角度(包括有效性、一致性、性能等)对这些工具进行了全面评估。他们指出,不同工具之间的检查规则差异很大。Wang 等人 [61] 专注于识别工具之间检查器定义和实现的差异,发现即使对于语义相似的规则,其对应检查器的能力也因实现方式不同而有所差异。一些研究关注内置静态检查器的可靠性和完备性。例如,研究 [62] [44] [33] [40] 从用户的角度区分可操作警告(非误报)与海量警告。Cui 等人 [27] 通过收集相关历史问题,总结根本原因和缺陷模式,直接研究了误报和漏报问题。He 等人 [37] 使用 Csmith 生成随机程序,并定制了两种类型的 oracle 以检测静态分析器中的缺陷。鉴于这些工具中包含的规则不同,Nunes 等人 [52] 尝试结合多种工具,探索组合是否能够提升检查器性能。尽管这些研究关注现有检查器的质量,我们的工作则聚焦于自动开发新检查器以满足检查器扩展需求。有许多研究致力于设计新检查器以适应特定检查需求,但大多数研究 [20] [24] [39] [73] [70] 通过手动实现新检查器来根据发现的缺陷模式检测错误。近年来,关于自动检查器生成的工作引起了开发者的关注。例如,Wang 等人 [60] 通过比较缺陷修复前后下游代码的结构变化,自动生成了 CodeQL 检查器以检测开源软件中的类似缺陷。与此不同,AutoChecker 的目标是根据用户提供规则的基本文本描述及其对应的测试套件,生成更通用的新检查器。
7.2 基于 LLM 的特定存储库代码生成与上下文学习
近年来,基于 LLM 的特定存储库自动化代码生成蓬勃发展,旨在利用存储库中预定义的类和 API 生成代码。当前的存储库级代码生成方法主要采用检索增强生成(RAG)。Deng 等人 [29] 将这些方法分为基于排序和基于融合的方法。基于排序的方法 [69] [48] [56] [71] [68] [53] 从存储库中检索最相似的代码片段并将其纳入提示词,以指导 LLM 进行生成。Liu 等人 [48] 通过提供相关导入语句和相似代码片段,引导 LLM 生成存储库级代码。Zhang 等人 [69] 首先从存储库中检索最相似的代码,然后用它进行另一轮搜索以找到更合适的代码。这些工作基于 LLM 对相应存储库的知识,而 Zan 等人 [68] 探索了 RAG 在私有库中的应用,发现其同样适用。我们的工作使用这种基于排序的 RAG 方法,利用每个测试用例的子逻辑检索相应的 API 上下文。
基于融合的研究 [55] [18] [30] 侧重于组织存储库上下文,与 LLM 联合建模以选择最相关信息。Agrawal 等人 [18] 在后台查询静态分析工具,答案参与模型的解码阶段以影响代码生成。实际上,除了这两类方法外,据我们所知,还有一种 RAG 方法,即利用静态分析器收集与候选代码相关的上下文。Pan 等人 [53]、Liu 等人 [47] 和 Bui 等人 [23] 曾使用过这种方法。此外,一些研究 [22] [59] [34] [38] [51] 发现,上下文学习已被证明有助于未在模型上显式训练的下游任务,通过提供少量训练示例帮助 LLM 学习任务模式以回答问题 [22]。Ma 等人 [50] 将任务分解为子任务,通过提供相关示例检索组合 API。受此启发,我们的工作在分解复杂检查逻辑时利用上下文学习,以克服 LLM 对我们逻辑粒度需求了解不足的问题。
8 结论
我们提出了 AutoChecker,一种基于 LLM 的方法,能够根据规则描述和对应的测试套件自动生成静态代码检查器。AutoChecker 使用了一种新颖的测试驱动迭代检查器生成方法,逐步优化和更新检查器。在每轮检查器生成过程中,AutoChecker 采用逻辑引导的 API 上下文检索策略检索相关 API 上下文。实验结果表明,AutoChecker 在所有指标(包括平均测试通过率)上的有效性均优于基线方法。此外,在提供充足测试套件的情况下,AutoChecker 能够生成在实际项目中性能几乎与官方基准检查器相当的检查器。

被折叠的 条评论
为什么被折叠?



