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 這一塊有其餘的所得的話,會再撰文分享~

相關文章
相關標籤/搜索