CSNIPPEX:问答网站的可编译代码段的自动合成

2020-2-28 13:47 1673 83

目前,众多代码片段汇集于主流的问答网站(如 StackOverflow)上。但是,这些代码片段中的大部分都具有类型信息不完整的问题,这使得它们常常无法编译,进而无法应用于各种软件工程任务。 基于此问题,本文提出了一种名为 CSnippEx 的技术。该技术通过解析外部依赖关系、生成导入声明以及修复语法错误,将无用代码片段自动地转化为可编译的 Java 源代码文件。在本文中,作者团队以 Eclipse 插件的形式对 CSnippEx 进行了实现,并利用 242,175 个包含代码片段的 StackOverflow 帖子对其性能进行了评估。CSnippEx 成功地将其中的 40,410 个代码片段转化成了可编译的 Java 文件。此外,CSnippEx 还能在几秒内有效地恢复每个帖子中代码片段的引用声明,其修复精度达到了 91.04%。

关键词

人工代码片段;开发折社交网络;程序合成;

1 引言

目前,越来越多的软件开发人员通过社交网络进行协作,或是分享工作经验。特别是问答类网站(Q&A),如 StackOverflow, CodeRanch 和 CodeProject,都是开发人员经常光顾的地方。在这些问答网站上,开发人员以问答的形式,提出问题或分享解决方案,并对问题和回答进行评级以达成更好的合作。随着时间的推移,这些问答网站积累了大量的群体知识。截至 2016 年 1 月,StackOverflow 已经索引了 1100 万个问题和 1800 万个答案。

这些群体知识对现代软件开发实践大有裨益。其原因在于:问答帖子经常会包含一些高质量的代码片段。这些代码片段,或是为编程任务提供解决方案;或是指导 bug 修复、提供 API 的使用范例。问答帖子通过不断被人们浏览、评价、讨论和更新不断优化,为代码重用和代码分析提供了宝贵的代码资源。为了提高工作生产的效率,开发人员经常会重用问答帖中的一些代码片段;当工作遭遇技术难题时,也常常会转向这些代码片段以汲取“编码灵感”。

然而,人们在这些 Q&A 代码片段进行重用和分析时,通常面临一个巨大的困难:这些代码片段无法直接编译。对于精确静态分析来说,大多数 Q&A 帖子(从 StackOverflow 收集的 491,906 篇帖子中的 91.59%)所包含的代码片段并不具有完整的语义,它们通常不可编译、不可执行,自然也就无法使用。出现这种情况的原因在于:回答者在编写代码片段时一般仅处于说明性目的,他们一般不会考虑这段代码是否能够通过编译。事实上,问答网站确实也很少对提交的代码片段进行语法检验。为了在高层次(设计或指导层面)上传达问题的解决方案,代码片段通常十分简洁,一般不会有具体的实现细节。尽管缺少的实现细节可以人工填补,但由于需要开发者对各种库都有相当程度的了解,这个填补过程一般非常繁琐。对于通常涉及众多代码片段的、基于群体的软件工程目标来说,这种人工填补显然是不符合需求的。

若要自动化地解决代码片段不可编译的问题,有两个技术挑战亟待解决:第一个难点是如何对代码片段的外部依赖关系进行解析。大多数代码片段使用简化名称引用库类。在完全类名(如 org.library1.sub.C)限制的情况下,命名(引用)二义性问题就很容易产生;第二个难点是如何将一个帖子中的多个代码片段自动地分割成适当数量的源文件。简单的启发式法则(如总是或从不将帖子中的代码片段合并到单个源文件中)无法适用于复杂的实际情况。

2 背景与动机

CSNIPPEX:问答网站的可编译代码段的自动合成

图 1:Q&A 代码片段合成的示例

2.1 问题表述

输入:CSnippEx 的输入是一个 Q&A 帖子(一个文本单元)。该帖子应包含一个或多个代码片段,也就是一个包含若干 Java 源代码行的集合。然而问答帖子也有可能含有一些用于交流的自然语言,这对代码片段的提取会产生一些干扰。一般来说,主流问答网站多使用专用的 HTML 标记对代码片段进行封装 (如 StackOverflow 使用 <pre><code>标签),因此提取清晰完整的代码片段并不是一件难事。

输出:CSnippEx 的输出是一个或多个编译单元。编译单元(C-Unit)是指一个合法的 Java 源代码文件,它可以被标准编译器接受(能够正常通过编译)。在编译单元中,若要进行 Java 类型的引用,该引用声明需要在源代码中对应体现 (参见图 1)。每个编译单元及其包含的所有声明类型都隶属于同一个命名空间(或包)。编译单元可以通过两种方式在代码中引用 Java 类型:简化名称 (如 List),完整名称(如 java.util.List)。隶属于同一个包的类型彼此之间可以通过简化名称相互引用。若要引用在其他包中定义的外部类型(即外部类型,如第三方库),则需要导入声明来保证 Java 编译器能够正确解析该引用。同时,Java 允许两种(非静态)导入声明:1) 单类型引入,通过指定完整名称的形式向编译单元中引入一个类。2) 按需类型引入,即将一个公共包下的所有类引入编译单元(如,import java.util.∗;)。此外,编译单元自身也需要有一个名称。按照 java 命名规则,该名称需要与编译单元声明的公共类或接口类的名称对应相同。作者将“融合一个问答帖子中所有编译单元”后得到编译单元集合称为编译组(Compilation Group)。每个编译组都需要与一个构建路径(Build Path)相关联。该构建路径包含编译单元引用的所有外部类型的声明(通常以 JAR 档案的形式存储)。对于 Java 这样的静态类型语言,所有构建路径必须完整正确,以保证每个引用类型的声明在编译时都正确可用的。

拟解决的问题:本文主要研究如何自动地将一个问答帖子转化为一个不会产生任何编译错误的编译组。

2.2 基于 StackOverflow 的问题分析

为了更好地理解要研究的问题,同时代码片段的不完整程度进行量化,作者调查分析了 StackOverflow 中的 Java 代码片段。该项调查是为了确定有多少问答帖子通过简单处理就可以转化为可用编译组。为了获得调查结果,作者首先构建了一项基线合成技术。调查和技术的相关描述如下:

数据集:作者团队对最新的 StackOverflow 数据进行了分析,收集了 907 226 个 Java 相关问题以及 1,660,039 个回答帖,并选取其中的高质量(被提问者采纳或得分至少为 1)的代码片段。调查范围最终集中于 1,103,464 个高质量回答。

代码包装:规范的编译单元不允许悬空(dangling)语句或是悬空方法声明,语句与方法必须包含在相应类或接口声明中。为了将帖子中的每个代码片段转换成规范的编译单元,基线合成技术将生成一个合成公共类,用于嵌入悬空方法声明和悬空语句。在嵌入语句时,悬空语句首先被嵌入到一个主方法(main 方法)中,之后再嵌入到合成类。

基线合成:作者将每篇帖子中的每个代码片段都视为独立的编译单元。基线合成技术根据在各个代码段中出现过的外部引用,为每个编译单元添加所有相应的导入声明。依照 Java 语言规范,基线合成技术首先使用代码段中存在的顶级公共类型(类或接口)名称来命名编译单元。如果不存在这样的顶级公共类型,基线合成技术将为编译单元随机命名。

所有编译单元最终会形成一个编译组。基线合成技术会将编译组中的所有编译单元放置在同一个包下,以保证它们之间可以通过简化名称相互引用。每个编译组的构建路径初始时包含 Java JDK 1.7 和 Android-20 SDK。如果编译单元包含未添加的导入声明,基线合成技术将自动查询 Maven 中央存储库,下载并在构建路径中配置必要的 jar 库。基于向后兼容特性,当 Maven 库中有多个包版本时,基线合成技术将选择下载最新的版本。

2.3 IDE 快速修复工具的局限性

现代集成开发环境(IDE)(例如 Eclipse,IntelliJ IDEA 等)提供了成熟、完善的快速修正(Quick Fix)工具。这些工具可向使用者推荐代码片段,协助开发者进行代码补全和代码更正。然而,这些快速修正工具并不能有效地恢复解决代码段中丢失类型信息的问题。 本文以图 1 为例,对快速修正工具存在的局限性进行说明。

图 1 中:Input 一栏展示了一个问答帖示例。该帖子中包含一个缺失了关于类的类型声明的片段(即,第一个代码片段中的 A,B 和 C),以及一个缺失了变量类型声明的片段(即,第二个代码片段中的 b);Output 一栏则罗列了定义 input 生成的可编译 Java 源代码。假设 A,B,C 三个相关类是由某些外部源定义的(如在库 JAR 中定义的),在缺失依赖的情况下,即是将这些代码片段包装在合成类中,并对方法声明进行补充,这些代码依旧会因为无法解析而无法编译,并且产生 cannot.resolve 错误。主要异常如下:

1)无法找到符号类 A,B 和 C:如果构建路径中没有配置适当的 JAR 库,快速修正工具就无法向用户推荐正确的导入声明。 图 2 展示了 Eclipse 快速修正工具是如何对图 1 中的代码片段提出修复建议的:快速修复工具提示使用无法解析的类名创建一个空类、接口或枚举类型。虽然这一举措可以解决编译错误,但本质上这只是在模拟声明完整性。这种修复方式最终会损害合成代码的实用性。此外,即使构建路径中包含正确的 JAR 库,由于类名的名称的二义性问题,面对同一简化名称,通常会有许多类的完整名称成为导入候选。快速修正工具根本无法帮助用户确定具体要生成哪一个类。

CSNIPPEX:问答网站的可编译代码段的自动合成

图 2:IDE(如 Eclipse)的快速修复建议。

2)标识符 b 不能解析为变量: 快速修正工具可以自动生成缺少的变量声明,如图 2 中的第 21 行。但由于不了解变量 b 的类型,工具最终自动创建了泛型类型 java.lang.Object。这显然是不满足用户想要的。

3)方法 m1(Object)未定义: 要修复这个错误,正确的做法是将两个代码段合并在同一个类中。然而快速修正工具并不支持这类修改,这意味着开发人员只能自行构造正确的编译单元。针对这个问题,快速修正工具的建议是创建方法 m1,这也是在模拟声明完整性。

2.4 技术挑战

自动化解决上述编译错误是具有一定的挑战性:

首先,尽管创建一个合成类并向其中添加方法声明很容易实现,但推断问答帖中多个代码段是否应该合并却很困难。始终将每个代码片段独立成单个编译单元可能会导致编译错误,就如图 2 中的第 22 行所示。而若是总将所有的代码片段集合在一起,则可能会导致 already.defined 错误。

其次,通常情况下很难自动识别问答帖子中的类型引用具体指向哪些库。这是因为大多数代码片段都使用简化名称引用库类。简化名称提供的信息通常不足以让工具链接到正确的完整名称(链接到正确的类)。如何在缺少完整名称的情况下确定正确的外部依赖关系,是问答代码片段合成面临的主要技术挑战。

3 CSNIPPEX 技术概览

CSNIPPEX:问答网站的可编译代码段的自动合成

图 3:CSnippEx 概览

设计和构建 CSnippEx 框架的最终目的是为了能够利用问答代码片段合成可编译的 Java 源代码文件。 图 3 对 CSnippEx 框架的整体技术流程进行了概述。首先,CSnippEx 通过将 2.2 节中介绍的基线合成技术应用于给定的 Q&A 帖子,来获取若干编译组;然后,在给定的时间内,迭代地对编译组进行优化。该迭代过程将持续到编译组可以成功通过编译为止。总地来说,CSnippEx 采用了一种反馈制导的方法:利用 Java 编译器的反馈(编译错误消息),指导代码片段的合成。每一轮迭代,CSnippEx 都会根据前一次迭代返回的编译错误来对当前编译组进行优化,并在优化完成之后尝试编译执行新版本的编译组。如果依然有新的编译错误产生,并且没有达到规定时限的情况下,迭代过程将继续。而这些错误信息将作为反馈材料应用于下一轮迭代。

CSnippEx 包含三个功能组件,他们分别用于处理编译单元推断、依赖关系恢复记忆以及支撑 Eclipse 的快速修复机制。

  • 编译单元推断: 该组件通过合并同一帖子中多个代码片段的方式解决类型声明缺失的问题。编译单元推断组件将自动地把这些代码段划分为多个编译单元,同时根据 Java 编译器的反馈信息决定是否对其中某些代码片段(编译单元)进行合并。
  • 依赖解析: 该组件主要解决的外部依赖缺失的问题。该组件会自动地识别对应的外部库、下载 JAR 归档文件、并最终生成正确的导入声明。该组件利用集群假设和反馈制导思想协作完成技术目标:前者可帮助工具确定最合适的(最优的)导入声明簇;而后者则可通过排除引起编译错误的导入声明来对解决方案进行进一步优化。
  • 代码修正: 该组件主要解决由于错误语法、错误拼写和缺少变量声明导致的编译错误。 该组件主要依据 Eclipse 快速修正工具的给出修复建议,自动执行代码补全、修复编译错误。

请注意,这三个组件的执行顺序非常重要: 在前两个组件工作完成之前,对生成的编译组(单元)应用快速修复只能做到模拟声明完整性,并不能获得真正可用的编译组。总的来说,编译单元推断和依赖关系解析是本研究工作的核心部分,它们将为代码修正组件预备好所需的工作环境。

推荐阅读
使用高级蓝牙 5.2 SoC 构建安全的低功耗 IoT 设备 2020-06-17 18:11
如何使用api 深度学习、RNN 编码器-解码器 2021-01-29 16:13
BUCK降压斩波电路原理、升降压斩波电路公式、升降压斩波电路优缺点 2023-04-24 16:24
FPGA! SignalTap II 软件调试-新建工程图解 2019-11-13 14:59
1117 LDO电路输出电压遇到的问题分析 2020-01-21 09:38