Uber 开源自动删除旧代码工具 Piranha,支持 Java、Swift 和 Objective-C 三种语言

近日,Uber 通过官方博客宣布开源了一款可自动删除旧代码的工具 Piranha,该工具已经支持 Objective-C、Swift 和 Java 三种编程语言。

开源 Piranha:自动删除旧代码

长期以来,Uber 会为不同用户提供不同的功能选择,也就是定制化开发。但是,如果某项功能经过验证后证明并不成功,这些代码放在代码库里就造成了技术负担,可能使应用程序变得臃肿、冗杂。一些陈旧的代码还可能会带来不必要的风险,影响终端用户体验,对工程师而言,这些技术债的消除耗时耗力,还会影响他们开发新功能。

为了使删除旧代码的过程自动化,Uber 开源了一款可以扫描源代码并删除其中过时旧代码的工具 Piranha(食人鱼)。Piranha 可以在 Uber Android 和 iOS 代码库中运行,Uber 已使用它删除了大约 2000 行过时代码。据介绍,该工具目前支持 Objective-C、Swift 和 Java 三种编程语言。

项目地址: https://github.com/uber/piranha

如何定位需要删除的代码?

为了方便定位,Uber 开发的程序中存在各种各样的标志,开发人员在 Uber 的标志管理系统中创建一个条目,并输入属性,例如标志的名称、类型、目标推出百分比、目标平台以及标志可操作的地理位置。此外,该标志是人工引入到源代码中的,所以实验平台上的标志可与移动应用程序之间建立一致关联。随后,此标志就可以用来管理应用程序的行为。

从正在运行的应用程序的角度来看,功能标志是单个键,映射到两个或多个条件,例如开 / 关、颜色值、大小和复制文本。在启动时,应用程序先查询标志管理系统,并为应用的每个标志检索特定处理条件,返回的值决定了应用程序中功能的存在和行为。

当逐步推出单个功能时,我们有一个控制条件(未启用该功能)和一个处理条件(已启用该功能)。我们倾向于首先将处理条件应用于小部分用户,如果推广成功,则逐渐将应用扩展到所有相关用户(例如,每个在特定地理位置上的人)。如果在发布期间出现问题,我们可以停止并收回该功能,尽可能减小对用户的影响。

该系统还可以处理同一功能的各种不同实现,例如尝试在不同的用户组上测试不同的接口(例如 A / B 测试)。

最终,我们希望向全球所有用户推广这些功能。有时,我们希望保留某些功能标记使代码中的功能可以正常使用,一旦应用出现问题,我们可以迅速通过功能标记控制应用状态,这样就不会造成整个应用程序瘫痪。但是,由于大多数功能嵌套在其他功能下,这种预留的切断开关还是无法终止大多数功能标志。

自动删除与过时标志相关的代码

当标志过时,我们需要在功能标志管理系统中将其禁用,并从源代码中删除与该标志有关的所有代码,包括目前无法实现的该功能的替代版本,这样能在很大程度上避免技术负担。

但是,这么简单的清理步骤往往会被很多开发者忽略,从而留下技术负担,这些不必要的代码会在多个维度上影响软件开发。

为了解决技术债务问题,Uber 设计并实现了 Piranha。Piranha 原意为“食人鱼”,这个名字是根据工具本身特征而得来的。Piranha 分析了抽象语法数(AST)的程序以生成适当的重构,并将其打包到 diff 中。将 diff 分配给标志的作者以供进一步检查,作者可以按原版进行实施(提交至 master),或在实施之前执行任何其他重构。我们还围绕 Piranha 构建了工作流程,以使其能以可配置的方式定期删除过时代码。

功能标记示例

让我们来看一个简单的示例,该示例说明了 Uber 源代码中功能标志的基本用法。

最初,我们在 RidesExpName 中的标志列表中定义一个名为 RIDES_NEW_FEATURE 的新标志,并将其注册到标志管理系统中。随后,我们使用功能标志 API isTreated 将标志写入代码,并分别在 if 和 else 分支下提供处理 / 控制行为的实现:

复制代码

publicenumRidesExpNameimplementsExpName{
RIDES_NEW_FEATURE,
…
}
if(experiments.isTreated(RIDES_NEW_FEATURE)) {
//implementationfortreatment (on) behavior
}else{
//implementationforcontrol (off) behavior
}

为了使用各种标志值测试代码,对于每个单元测试,我们可以添加注释以指定功能标志的值。下面,当所选择的标志处于已处理状态时,运行 test_new_feature:

复制代码

@Test
@RidesExpTest(treated=RidesExpName.RIDES_NEW_FEATURE)
public void test_new_feature() {
…
}

当 RIDES_NEW_FEATURE 失效时,所有与之相关的代码都需要从代码库中删除。这包括:

  1. RidesExpName 中的定义;
  2. isTreated API 中的使用;
  3. @RidesExpTest. 注释

此外,else-branch 的内容、目前无法实现的控制行为的实现都必须删除。我们还希望删除与那些已经删除行为相关的所有测试代码。不删除这些代码会逐渐增加源代码的复杂性,使整体系统更难维护。

自动化挑战

自动检测过时标志并删除关联代码,这项技术目前还存在一定困难,这一过程中需要确定这些标志是否被人使用了以及哪些人拥有标志,再到其代码编写的细节都是很复杂的问题。因此,克服这些挑战是 Piranha 发展的关键。

使用静态分析构建 Piranha

考虑到 Piranha 的应用背景,我们设想可以通过应用静态分析来删除因过时标志遗留下来的废旧代码。

我们确定了清理的三个关键维度:

  • 删除紧邻功能标志 API 的代码。
  • 删除由于执行上一步而无法访问的代码,我们将此称为深度清洁。
  • 删除与功能标志有关的测试代码。

我们根据在代码库中观察到的编码模式,选择了一种迭代设计技术的实用方法。

我们观察到了三种标志 API:

  • 返回布尔值的 布尔型 API ,用于确定执行所采用的控制路径。
  • 更新 API ,用于更新正在运行的系统中的功能标志值。
  • 返回非布尔值原始值(整数、双精度等)的 参数 API ,该值与从后端控制的实验值相对应。

重构技术解析输入源代码的 AST,以检测使用功能标志 API 的存在。对于布尔 API,我们简化布尔表达式。如果结果值是布尔常量,我们将适当地重构代码。例如,如果布尔 API 作为 if 语句的一部分出现并返回为 true,我们将通过删除整个 if 语句,然后将其替换为 then 语句来重构代码。

如果更新 API,我们只需删除相应的语句。我们不处理参数 API,因为解决它们所需的工程工作量很大,而它们在代码库中出现的频率却低得多。

由于我们观察到布尔 API 不一定要在条件语句中使用,因此我们为重构设计了第二条路径。我们确定右侧是布尔型 API(Piranha 已将其简化为常量)的分配,并跟踪被分配值的变量。同样,我们跟踪返回一个布尔 API 的 wrapper 方法,该 API 简化为常量。随后,我们确定使用被分配值变量或条件语句中的 wrapper 方法,以执行重构。

最后,如果标记注释与输入处理行为匹配,我们只需删除测试的注释,如果不匹配,则要丢弃整个测试来处理标记注释测试。

Piranha 在 Uber 的应用实践

我们实现了 Piranha 在 Objective-C、Swift 和 Java 程序中的重构。PiranhaJava 能重构 Java 应用程序中与过时的功能标志相关的代码,尤其是针对 Android 平台的代码。它在 Java 的 Error Error Prone 上作为 Error Error Prone 插件实现。PiranhaSwift 使用 SwiftSyntax 在 Swift 中实现,用于重构 Swift 代码。PiranhaObjC 用于清理 Objective-C 程序中的代码,并在 C ++ 中作为 Clang 插件实现,内部使用 AST 匹配器和重写器来解析和重写 AST。

尽管 Piranha 作为独立工具执行代码重构任务,但是开发人员总是想不起及时清理代码,因此它的使用频率并不算高。正如 Piranha 自动化标记清除一样,我们需要一个系统来自动启动这些清除。

在 Uber,我们建立了工作流 pipeline,该 pipeline 定期生成差异和任务以清除陈旧的功能标志。Piranha pipeline 在标志管理系统中查询陈旧标志列表,并且对于这些标志中的每个标志,分别启用 Piranha,输入陈旧标志的名称、其所有人以及预期的输出行为(处理或控制)。

图注:在我们的 Piranha 工作流中,标志 pipeline 系统定期将可能过时的标志列表发送给 Piranha,Piranha 会生成一个差异并将其发送给原始标志作者。然后作者可以确定是否要放置差异。

上图展示了 Piranha 工作流 pipeline 架构图。Piranha 生成一个 diff (即拉取请求),并将其放入代码审阅系统中,该标志的原始作者为默认审阅者。作者可以接受 diff ,或者根据需要对其进行修改,也可以拒绝修改并将该标志标记为不过期。pipeline 还在任务管理系统中生成了一个清理任务,以跟踪每个生成 diff 的状态。由于开发人员可能无法及时发现问题,因此我们还引入了一个名为 PiranhaTidy 的提醒机器人,定期添加打开 Piranha 相关任务的提醒。我们观察到,目前使用 Piranha 自动生成 diff 的时间不超过 3 分钟。

使用 Piranha 删除代码

我们很高兴地宣布,Piranha 支持三种语言,包括 Java、Swift 和 Objective-C。如想要使用 Piranha,代码需满足以下条件:

  • 广泛使用功能标志
  • 具有特定的 API 以控制功能标志的行为
  • 用 Java、Swift 或 Objective-C 实现

作者介绍

Murali Krishna Ramanathan

Murali Krishna Ramanathan 是 Uber 的编程系统研究科学家。他目前致力于构建程序分析工具,以提高开发人员的生产率。

Lazaro Clapp

Lazaro Clapp 是 Uber 编程系统团队的高级工程师,他主要在为 Java 应用程序开发静态分析工具。

Rajkishore Barik

Rajkishore Barik 是 Uber 编程系统团队的编程系统研究科学家和技术经理。他目前致力于构建工具来理解数据中心的性能异常,包括为 Swift 和 Go 开发静态分析和转换工具。

Manu Sridharan

Manu Sridharan 曾是 Uber 的一名工程师,现在是加利福尼亚大学河滨分校计算机科学与工程副教授。