1. 程式人生 > >CLANG技術分享系列三:API有效性檢查

CLANG技術分享系列三:API有效性檢查

CLANG技術分享系列三:API有效性檢查

01 NOV 2016 . CATEGORY: TECH COMMENTS 
#CLANG

問題背景

   iOS API(Class/Protocol的Interface,Property,Const,Function)的start/deprecated的判斷依賴兩個方面:
   1.獲得所有的API呼叫,包括OC訊息傳送,C API呼叫,常量使用等;
   2.獲取所有的API支援版本資訊
   將1中的API呼叫,在2中做搜尋比對,並結合APP的deploy target和最新的支援系統,判斷API的有效性,如果API在deploy target之後開始或者在當前系統之前(包括當前系統)
廢棄,則提示使用者。 第二個問題可以參見我的另一篇blog(XCODE8 API文件解析),本文主要介紹問題1.

XCODE8 API文件解析

分析APP原始碼API資料結構準備

1.類介面以及繼承體系(clsInterfHierachy)
此資料結構記錄了所有位於AST(抽象語法樹)上的介面內容,最終的解析結果如下圖所示:

clang-validate-ios-api-clsInterfHierachy

以AppDelegate為例,interfs代表其提供的介面(注:他的property window對應的getter和setter也被認為是interf一部分);isInSrcDir代表此類是否位於使用者目錄(將workspace的根目錄作為引數傳給clang)下,protos代表其conform的協議,superClass代表介面的父類。
這些資訊獲取入口位於VisitDecl(Decl *decl)的過載函式裡,相關的decl有ObjCInterfaceDecl,ObjCCategoryDecl,ObjCPropertyDecl,ObjCMethodDecl.

2.
介面方法呼叫(clsMethod) 此資料結構記錄了所有包含原始碼的OC方法,最終解析結果如下所示:

clang-validate-ios-api-clsMethod

以-[AppDelegate application:didFinishLaunchingWithOptions:]為例,callee代表其呼叫到的介面(此處為可以明確型別的,對於形如id<XXXDelegate>隨後介紹),filename為此方法所在的檔名,range為方法所在的範圍,sourceCode為方法的具體實現原始碼。
這些資訊獲取入口位於VisitDecl(Decl *decl)和VisitStmt(Stmt *stmt)的過載函式裡,相關的decl有ObjCMethodDecl,stmt有ObjCMessageExpr.
此處除過正常的-/+[Cls message]為,還有其他較多的需要考慮的情形,已知且支援的分析包括:
NSObject
協議的performSelector方法簇,[obj performSelector:@selector(XXX)]不僅代表了[obj performSelector:]也代表了[obj XXX].(下同) 手勢/按鈕的事件處理selector addTarget:action:/initWithTarget:action:/addTarget:action:forControlEvents: NSNotificationCener新增通知處理Selector addObserver:selector:name:object: UIBarButtonItem新增事件處理Selector initWithImage:style:target:action:/initWithImage:landscapeImagePhone:style:target:action:/initWithTitle:style:target:action:/initWithBarButtonSystemItem:target:action: Timer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:/timerWithTimeInterval:target:selector:userInfo:repeats:/initWithFireDate:interval:target:selector:userInfo:repeats: NSThread detachNewThreadSelector:toTarget:withObject:/initWithTarget:selector:object: CADisplayLink displayLinkWithTarget:selector: KVO機制 IBAction機制 如基於xib/Storyboard的ViewController中`-(IBAction)onBtnPressed:(id)sender`方法,認為暗含了`+[ViewController的 alloc]`對於`+[ViewController的 onBtnPressed:]`的呼叫關係。 [XXX new] 暗含了+[XXX alloc]和-[XXX init] 3.類Property對應的getter/setter(clsPropertyGS) 此資料結構記錄了所有類的property與getter/setter之間的對應關係,最終結果如下所示:

clang-validate-ios-api-clsPropertyGS

這些資訊獲取入口位於VisitDecl(Decl *decl)的過載函式裡,相關的decl有ObjCPropertyDecl.

4.變數/列舉/變數等(funcEnumVar)
此資料結構記錄了所有使用到的函式,列舉和變數,最終結果如下所示:

clang-validate-ios-api-funcEnumVar

5.協議的介面與繼承體系(protoInterfHierachy)
此資料結構記錄了所有位於AST(抽象語法樹)上的協議內容,最終的解析結果如下圖所示:

clang-validate-ios-api-protoInterfHierachy​ 其中各欄位定義同clsInterfHierachy. 這些資訊獲取入口位於VisitDecl(Decl *decl)的過載函式裡,相關的decl有ObjCProtocolDecl,ObjCPropertyDecl,ObjCMethodDecl.

6.協議Property對應的getter/setter(protoPropertyGS)
此資料結構記錄了所有協議的property與getter/setter之間的對應關係,最終結果如下所示:

clang-validate-ios-api-protoPropertyGS

這些資訊獲取入口位於VisitDecl(Decl *decl)的過載函式裡,相關的declObjCPropertyDecl.

7.協議方法的呼叫(protoInterfCall)
此資料結構記錄了所有如:-[SiriUIViewController viewDidLoad]呼叫了-[id<INIntentHandlerProviding> handlerForIntent:]的形式,最終結果如下所示:

clang-validate-ios-api-protoInterfCall

這些資訊獲取入口位於VisitStmt(Stmt *stmt)的過載函式裡,相關的stmt是ObjCMessageExpr.

處理過的蘋果官方文件

此處過程可參見上文提到的我的另一篇部落格XCODE8 API文件解析,此處只截圖說明下處理過的各種API對應版本支援資訊的資料結構。

clang-validate-ios-api-vercls

clang-validate-ios-api-verClsInterfs

clang-validate-ios-api-verClsProperty

clang-validate-ios-api-verConst

clang-validate-ios-api-verFunc

clang-validate-ios-api-verProto

clang-validate-ios-api-verProtoInterfs

clang-validate-ios-api-verProtoProperty

最終分析

對於變數,列舉,函式等,只需要拿到名稱之後到對應的verXXX.json去比對,如果發現了匹配,提取其對應的支援版本資訊,並與傳入clang的deployTarget和當前最新支援的系統作比較,根據需要確定是否輸出警告。
對於類,需要在其繼承層次上去檢視每一個基類的支援版本資訊,做判斷,直到到達NSOBject或者已經有警告為止;
對於-/+[Cls Sel]這種,首先需要從clsMethod.json中提取所有的-/+[Cls Sel]呼叫(包括呼叫者和被呼叫者),然後針對每一個-/+[Cls Sel],沿著兩個維度去搜索。一個是類繼承體系(一直到已經存在的匹配方法/追溯到到NSObject類/遇到警告),一個是Protocol引用體系(一直到已經存在的匹配方法/追溯到到NSObject協議/遇到警告)。這兩個維度裡,判斷的時候針對-/+[Cls Sel]既要考慮Sel可能是一個Sel,也可能是一個Property,嘗試Property時需把Sel隱射到property上(使用clsPropertyGS),再同VerClsProperty比較方可。
對於-/+[Protocol Sel]這種,首先從protoInterfCall.json提取所有的-/+[Protocol Sel](只有被呼叫,呼叫者已在-/+[Cls Sel]有處理)。然後沿著Protocol引用體系去處理,處理時同-/+[Cls Sel],需要考慮Sel和Property兩個可能。
一個簡單的分析結果如下圖:

clang-validate-ios-api-warnCls

clang-validate-ios-api-warnClsSel

clang-validate-ios-api-warnProtoSel

檢出示例工程

侷限

說到底這種靜態的分析適合的時比較容易判斷出訊息接收者型別的例子,面對靜態分析的型別和實際不一致,或者靜態分析不出來型別的時候,是無能為力的。
此外,這種分析,對於程式碼的書寫規範有要求。例如一個Class實現了某個Protocol,一定要在聲明裡說明,或者Property中delegate是id<XXXDelegate>的時候也要註明。當然,這種規範的工作本來就是應該的。

如何與XCODE整合

現有的機制是
1.書寫ClangPlugin
2.書寫分析可執行檔案
3.使用使用者編譯的Clang載入ClangPlugin去編譯並生成各種中間檔案。
4.Build結束的時候,使用Xcode提供的post_build_action_shell機制呼叫分析工具,生成最終結果。

如果需要與Xcode整合,那麼利用4之後生成的結果檔案,結合 Clang技術分享系列二:程式碼風格檢查提到的FixItHint功能,在Xcode中提示使用者。