Clang 之旅--实现一个自定义检查规范的 Clang 插件

Clang 之旅系列文章:
Clang 之旅--使用 Xcode 开发 Clang 插件
Clang 之旅--[翻译]添加自定义的 attribute
Clang 之旅--实现一个自定义检查规范的 Clang 插件html


前言

在 Clang 之旅系列文章开篇的时候,我说到过本身接触 Clang 的直接缘由就是想实现一个自定义的检查需求:是否有办法在编译阶段检查某个方法的参数与返回值的类型相同,若是类型不一致的话能抛出编译错误的提示。如今我已经根据本身的需求完成了这个插件,这篇文章会讲解这个插件的实现思路,对应的代码在这里:github.com/VernonVan/S…前端


具化需求

首先我先将需求具化一下,以前说的比较宽泛。git

试想咱们有这么一个函数 modelOfClassgithub

- (__kindof NSObject *)modelOfClass:(Class)modelClass 
{
    if ([modelClass isKindOfClass:[NSString class]]) {
        return [[NSString alloc] init];
    } else if ([modelClass isKindOfClass:[NSArray class]]) {
        return [[NSArray alloc] init];
    }
    return nil;
}
复制代码

modelOfClass 接受一个 Class 类型的参数,而后会根据 Class 对应的类进行不一样的操做,最终返回处理好的 Class 对应类的实例对象。咱们用 __kindof NSObject * 返回值类型来保证返回的必定是 NSObject 或者其子类,能保证的也只有这样而已。可是,存在这样一种错误的调用方式,可是却能经过编译:app

@property (nonatomic, strong) NSString *myString;
@property (nonatomic, strong) NSArray *myArray;

- (void)someMethod
{
    self.myString = [self modelOfClass:[NSString class]];
    self.myArray = [self modelOfClass:[NSString class]];
}
复制代码

能够发现,someMethod 中有两行 modelOfClass 的函数调用。第一行调用是正确的,NSString * 类型的属性 myString 调用时传入的是 [NSString class];第二行调用是错误的,NSArray * 类型的属性 myArray 调用时传入的是 [NSString class]。也就是说,在 Objective-C 语言中,并无一种办法可以检查函数调用时参数类型和返回值类型是彻底一致的。less

这个需求是从我所在公司的项目中抽象简化出来的,你们看不出来这个函数到底是用来干什么的,可能会以为这个需求并不常见,没有什么通用性。可是这篇文章但愿读者看了以后能以小见大,触类旁通,更重要的是学到怎么样使用通用的方式,根据本身的需求实现自定义检查规范的 Clang 插件。模块化


最终效果

咱们来看看最终实现的效果:函数

最终实现了上面所说的类型检查,同时还给出了对应的修改方法(FixIt),点击修改就能改为正确的参数类型🎉🎉🎉 下面就来讲说具体是怎么实现的。ui


抽象语法树(Abstract syntax tree)

抽象语法树,英文简称为 AST,是编译过程当中语法分析阶段的产物,也是咱们做为外部开发者与 Clang 进行交互的最重要的方式。因此咱们最重要的就是学会怎么样阅读、分析语法树。atom

在命令行中输入如下命令,打印 main.m 文件对应的语法树到命令行中:

clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.3.sdk -fmodules -fsyntax-only -Xclang -ast-dump main.m
复制代码

在我写这篇文章时 Xcode 版本是9.3,对应的是 iPhoneSimulator11.3.sdk,你须要进入该目录查看你的 sdk 版本,而后修改 -isysroot 命令后的 sdk 路径


打印出来的语法树以下图:


编译前端 Clang 首先进行词法分析(Lexical Analysis),把源文件的字符流拆分一个一个的 token;而后 token 进入语法分析(Semantic Analysis),将这些 token 组合成语法树。左边的缩进表明了语法树节点的从属关系,语法树上的每个节点的名字都能在 Clang 源码中找到对应的类。

从图中挑几个点来解释一下(对应图中的红色标注):

  1. ObjCImplementationDecl 节点表明了 Objective-C 类中的 @implementation 部分的内容

  2. ObjCMethodDecl 节点表明了 Objective-C 中的函数定义,咱们在 Clang 源码中查看一下对应类的定义

    Clang 的文档注释能够说至关齐全了,ObjCMethodDecl 表明了一个类方法或者实例方法。全部的 public: 域中的方法都是咱们能够用的,好比说 Selector getSelector()能够获取该方法的 SelectorArrayRef<ParmVarDecl*> parameters() 能够获取获取该方法的参数列表等等。

  3. 框中的语法块表明了源文件中 self.myString = [self modelOfClass:[NSString class]]; 语句,BinaryOperator 表明了二元操做符(包括赋值的“=”),能够经过 BinaryOperator 类的 Expr *getLHS()Expr *getRHS() 分别取得“=”左右两边的语句。

详细的 AST 树的分析能够查看官方的教程:clang.llvm.org/docs/Introd…


那么多种的 AST 节点中应该怎么只获取本身感兴趣的节点呢?

Clang 提供了 ASTMatcher 类供咱们进行 AST 节点的查找过滤,有一篇专门解释罗列各类各样的 ASTMatcher官方文档能够查看。

好比能够用 objcPropertyDecl 来匹配到 Objective-C 的类属性,ASTMatcher 能够用一种相似链式语法的方式将一系列的 Matcher 串起来,好比能够用 cxxRecordDecl(unless(hasName("X"))) 来匹配到知足类名不为 X 的全部 C++ 类。

具体的 ASTMatcher 的使用方法能够查看这篇教程:eli.thegreenplace.net/2014/07/29/…


实现思路

基础知识铺垫完了,如今咱们来拆解一下咱们的需求。首先咱们须要有一种方式标记须要进行这种检查的函数,总不至于全部函数调用咱们都去检查一遍吧😹 这时候就能够想到能够经过 attribute 的方式标记函数!

关于 attribute 的知识,能够查看孙源大神的这篇文章:Clang Attributes 黑魔法小记,讲解了多种常见不常见的 attribute 的使用场景

另一篇就是官方关于如何在 Clang 中添加自定义的 attribute 的文档:How to add an attribute,我本身也翻译了这篇文档,请戳中文版

这里不讲解怎么添加自定义的 attribute,比较简单,就是按最简单的模板添加的。添加完了以后,得在 modelOfClass 后面加上一句 __attribute__((objc_same_type)),表明 modelOfClass 在每次被调用时都会进行自定义的检查,这样才能出现上面演示效果图中的检查结果(objc_same_type 就是我所添加的 attribute 的名字)。

- (__kindof NSObject *)modelOfClass:(Class)modelClass __attribute__((objc_same_type))复制代码


具体该怎么检查呢?分红如下几个步骤:

  1. 首先判断语法树上的节点是不是赋值语句(Clang 中用 BinaryOperator 表征赋值语句)。若是是,进入第 2 步
  2. BinaryOperatorgetLHS()getRHS() 函数分别得到左右的表达式
  3. 若是左边表达式是 Objective-C 类的属性的话,获取该属性对应的类型 A。进入第 4 步
  4. 若是右边表达式是 Objective-C 的函数调用,且被调用的函数是有咱们上面所定义 attribute((objc_same_type)) 的话(能够经过 ObjCMethodDeclattrs() 方法得到 Objective-C 函数的全部的 attribute),获取该函数的参数对应的类型 B
  5. 对比 A 和 B 的类型是否一致,若是不一致,则弹出类型不一致的编译警告,并提出恰当的修改方法(如效果演示图所示)

具体的实现代码和使用方法查看 Github:github.com/VernonVan/S…


结语

最终花了不到 200 行代码就完成了这个小小的功能,可是却花了我将近一个月的业余时间,中间也作了不少无用功,在错误的道路上走了一段时间才发现本身作的彻底是错的,幸亏最后仍是成功找到了正确的方法。不过,本身也收获了不少的技能点,好比说阅读源码的能力,得益于 LLVM 良好的代码设计和模块化,让我一个门外汉也能比较快速的从庞大的代码中找到本身想要的部分;好比说 CMake 构建工程的知识、C++ 语言以及查找阅读英文文档的能力。收获仍是比较多的🍹🍹🍹

接下来若是在 LLVM && Clang 这一块有其余的所得的话,会再撰文分享~

相关文章
相关标签/搜索