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

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

前言

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

具化需求

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

试想我们有这么一个函数 modelOfClass

- (__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 或者其子类,能保证的也只有这样而已。但是,存在这样一种错误的调用方式,但是却能通过编译:

@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 语言中,并没有一种办法能够检查函数调用时参数类型和返回值类型是完全一致的。

这个需求是从我所在公司的项目中抽象简化出来的,大家看不出来这个函数究竟是用来干什么的,可能会觉得这个需求并不常见,没有什么通用性。但是这篇文章希望读者看了之后能以小见大,举一反三,更重要的是学到怎么样使用通用的方式,根据自己的需求实现自定义检查规范的 Clang 插件。

最终效果

我们来看看最终实现的效果:

演示效果

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

抽象语法树(Abstract syntax tree)

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

在命令行中输入以下命令,打印 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 路径

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

AST

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

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

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

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

    ObjCMethodDecl

    Clang 的文档注释可以说相当齐全了,ObjCMethodDecl 代表了一个类方法或者实例方法。所有的 public: 域中的方法都是我们可以用的,比如说 Selector getSelector() 可以获取该方法的 SelectorArrayRef<ParmVarDecl*> parameters() 可以获取获取该方法的参数列表等等。

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

详细的 AST 树的分析可以查看官方的教程:http://clang.llvm.org/docs/IntroductionToTheClangAST.html

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

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

ASTMatcher

比如可以用 objcPropertyDecl 来匹配到 Objective-C 的类属性,ASTMatcher 可以用一种类似链式语法的方式将一系列的 Matcher 串起来,比如可以用 cxxRecordDecl(unless(hasName("X"))) 来匹配到满足类名不为 X 的所有 C++ 类。

具体的 ASTMatcher 的使用方法可以查看这篇教程:https://eli.thegreenplace.net/2014/07/29/ast-matchers-and-clang-refactoring-tools

实现思路

基础知识铺垫完了,现在我们来拆解一下我们的需求。首先我们需要有一种方式标记需要进行这种检查的函数,总不至于所有函数调用我们都去检查一遍吧😹 这时候就可以想到可以通过 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:https://github.com/VernonVan/SameTypeClangPlugin

结语

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342