Hades Logo
只有通过别人的眼睛,才能真正地了解自己 ——《云图》
背景
作为全球最大的互联网 + 生活服务平台,美团点评近年来在业务上取得了飞速的发展。为支持业务的快速发展,移动研发团队规模也逐渐从零星的小作坊式运营,演变为千人级研发军团协同作战。
在公司蓬勃发展的大背景下,移动项目架构也有了全新的演进方向:需要支持高效的集成策略,支持研发流程自动化等等,最终提升研发效能,加速产品迭代和交付能力。
虽然高效的研发交付体系帮助 App 项目缩短了迭代周期,但井喷式的模块发版和频繁的项目集成,使得纯人工的项目维护和质量保证变得“独木难支”。
静态分析需求
上图漫画中,列举了大型项目在持续优化和维护过程中较为常见的几类需求。这些需求主要包括以下几个方面:
在 CI 流程中加入静态准入检查,避免繁琐的人工 Review 以及减少人工 Review 可能带来的失误。
为了推进项目的优化过程,需要方法数监控、宏定义分析等代码分析报表和监控。
零 PV 报表、依赖分析和头文件引用规范、无用代码分析等项目优化方案。
不难发现,这些需求的本质是:借助代码静态分析能力,提升项目可持续发展所需要的自动化水平。针对 C/Objective-C 主流的静态分析开源项目包括:Static Analyzer、Infer、OCLint 等。但是,这些分析工具对我们而言存在一些问题:
开发成本高,收益有限,研发参与积极性不够。
针对局部代码分析,跨编译单元以及全局性分析较难。
增量分析困难,CI 静态检查效率低下。
工具性较强,大部分只作代码规范检查,应用范畴局限。
接入和维护成本高,难以平台化。
针对以上背景和现有方案的不足,我们决定自研基于语义的静态分析框架。
Hades 项目简介
大众点评静态分析框架 Hades,取名源于古希腊神话中的冥王
。冥王 Hades 公正无私,能够审视灵魂的是非善恶。
Hades 框架支持语义分析能力,我们希望这种能力不仅仅能够去实现一个传统的 Lint 工具,而且能成为创造更多能力的基础,可以帮助我们更轻松地审视代码,理解把控大型项目。
Hades 方案选型
文本处理方式
首先,最简单的静态分析是字符匹配和文本处理。这种方式虽然实现简单,但是存在能力上限,也不可能在语义理解上有足够的把控力。另外,以正则匹配为核心建立的工具栈难以得到持续优化。为了分析项目的依赖关系,我们需要判断代码中的符号含义以及符号间关系(如包含哪些类,类中有哪些方法等),分析过程的正则表达式如下图所示。
正则匹配模式
由此可见,繁琐的文本匹配不仅可读性差,也存在容易分析出错的问题。
基于编译器的静态分析方案
我们需求的本质是对代码进行分析,而在源代码编译过程中,语法分析器会创建出抽象语法树(Abstract Syntax Tree 缩写为 AST)。AST 是源代码的抽象语法结构的树状表现形式,树上的每个节点都表示源码的一种结构。
AST 描述
以上图为例,代码块区域是用 Objective-C 和 TypeScript 编写的一个简单条件语句源码,下面是其对应的抽象语法结构表达。这种树状的结构表达,省略了一些细节(比如:没有生成括号节点),从图中的这种映射关系中我们也可以发现:
源码的语法结构是可以通过明确的数据结构表示的。
大多数编程语言都可以用相似的 AST 表达的。
对于 C/Objective-C 而言,主流编译器是 Clang/LLVM(Low Level Virtual Machine)的,它是一个开源的编译器架构,并被成功应用到多个应用领域。Clang(发音为/klæŋ/,不是C浪)是 LLVM的一个编译器前端,它目前支持 C, C++, Objective-C 等编程语言。Clang 会对源程序进行词法分析和语义分析,将分析结果转换为 AST。现有方案中不少 Lint 工具便是基于 Clang 的,Clang 包含了以下特点:
编译速度快:Clang 的编译速度远快于 GCC。
占用内存小:Clang 生成的 AST 所占用的内存是 GCC 的五分之一左右。
模块化设计:Clang 采用基于库的模块化设计,易于 IDE 集成及其他用途的重用。
因此,借助 Clang 的模块化设计和高效编译等诸多优点,Hades 也将更容易开发和升级维护。Clang 对源码强有力的分析能力也是主流静态分析工具的不二之选。
Clang AST 初识
Clang 项目非常庞大。仅仅是 Clang AST 相关代码就超过 10W+ 行代码。如何利用 Clang 实现 AST 分析工作,这里可以参考官网提供的文档 Choosing the Right Interface for Your Application ,以下是三种方式:
LibClang
提供 C 语言的稳定接口,支持Python Binding。AST 并不完整,不能完全掌控 Clang AST。
Clang Plugins
提供 C++ 接口,更新快,不能保留上下文信息。插件的存在形式是一个动态链接库,不能在构建环境外独立存在。
LibTooling
提供 C++ 接口,更新快,可以通过标准的 main() 函数作为入口,可独立运行,能够完全掌控 AST,相比 Plugin 更容易设置。
这里我们选择可独立运行并且能完全掌控 AST 的 LibTooling 作为 Hades 的基础。
在使用 Clang 的学习过程中,基本的概念便是表示 AST 的节点类型,这里重要的几点是:
ASTContext。
ASTContext 是编译实例用来保存 AST 相关信息的一种结构,也包含了编译期间的符号表。我们可以通过
TranslationUnitDecl * getTranslationUnitDecl():
方法得到整个翻译单元的 AST 的入口节点。节点类型。
AST 通过三组核心类构建:Decl (declarations)、Stmt (statements)、Type (types)。其它节点类型并不会从公共基类继承,因此,没有用于访问树中所有节点的通用接口。
遍历方式。
为了分析 AST,我们需要遍历语法树。Clang 提供了两种方式:RecursiveASTVisitor 和 ASTMatcher。RecursiveASTVisitor 能够让我们以深度优先的方式遍历 Clang AST 节点。我们可以通过扩展类并实现所需的 VisitXXX 方法来访问特定节点。
ASTMatcher API 提供了一种域特定语言(DSL)来构建基于 Clang AST 的谓词,它能高效地匹配到我们感兴趣的节点。
除了这两种方式外,LibClang 也提供了 Cursors 来遍历 AST。更多细节内容可以前往 :clang.llvm.org 。
常用开源工具的不足
通过上一章节的介绍,我们大致了解了 Clang 的基本特点。 但是在实践开发过程中发现:通过 Clang API 去遍历和分析 AST 的源码树形结构较为复杂。现有静态分析方案(如:OCLint),大多是直接给出封装好的 Lint 工具,扩展方面也是提供脚手架生成 Rule 文件,然后在 Rule 中编写访问特定 AST 节点的方法(例如:VisitObjCMethodDecl 方法用来访问 Objective-C 的方法定义)。
因此,现有方案大多数只提供了直接访问 AST 的方式,而且这种方式较为“局部”。每实现一个实际需求需要耗费大量精力去理解如何从 AST 分析映射到源码的语义逻辑。
但是,Code Review 时我们并不会将目标代码转换为 AST 然后再去分析代码的语义如何,更多的是直接理解代码的具体逻辑和调用关系。AST 树状结构分析的复杂性容易带来理解上的差异鸿沟。因此,这也不利于调动业务研发团队的积极性,很多基于源码分析工作也难以落地。
Hades 核心实现
为了让分析过程更清晰,我们需要在 AST 的基础之上再进行一次抽象。本章节主要内容包含:Hades 的整体架构、为什么要定义语义模型、定义什么样的语义模型、如何输出语义模型以及模型的序列化和持久化。
Hades 总体架构
按照 Hades 的架构目标进行基础方案选型以后,我们来看下 Hades 的整体技术框架,可以用下图所示的四层架构表示:
作者:美团技术团队
链接:https://www.jianshu.com/p/ba0e22d15554
共同学习,写下你的评论
评论加载中...
作者其他优质文章