CLANG技術分享系列二:程式碼風格檢查(A CLANG PLUGIN APPROACH)
阿新 • • 發佈:2018-11-22
轉:http://kangwang1988.github.io/tech/2016/10/31/check-code-style-using-clang-plugin.html
背景
一致的程式碼風格檢查已經是老生常談了,統一規範的程式碼風格不僅可提高程式碼的可讀性,可維護性,減少問題的發生,閱讀體驗也是蠻不錯的。 在系列一介紹了Clang外掛的原理後,本文將介紹如何用Clang外掛完成程式碼風格的檢查及修改提示。
良好的程式碼風格長什麼樣?
網上已經有很多總結,比如類字首,駝峰命名,函式體不過長,語義清晰,良好的格式和縮排,init/dealloc不傳送訊息,NSString使用copy ,delegate若引用等等,這裡不再贅述,本文主要參考Objective-C-Coding-Guidelines-In-Chinese一文。
Objective-C-Coding-Guidelines-In-Chinese
使用CLANG檢查程式碼風格
Clang適合於解決給定格式的情形,對於清晰的命名等語義相關,語法糖(dot-syntax在Clang輸入前已被修改),未知型別(如面對一個String並不知道是不是用於通知的名稱)並不適用。
下面就介紹若干常見的可用於Clang分析的Case及處理方式:
1.類命名:(假設需要字首為XX,不能包含_)
@interface user_Model : NSObject;
@end
所有的宣告分析需要過載
bool RecursiveASTVisitor::VisitDecl(Decl *decl);
表示式分析則需要過載
bool RecursiveASTVisitor::VisitStmt(Stmt *stmt);
其中當decl:ObjCInterfaceDecl代表類宣告,decl:ObjCImplDecl代表類定義,參考此兩個類的方法即可完成名稱的判斷
2.ivar以下劃線開頭
@interface user_Model : NSObject{
NSString *ivar2;
}
當VisitDecl (Decl *decl)中的decl為ObjCIvarDecl代表例項變數宣告,參考其方法即可分析變數名是否以下劃線開頭
3.NSString使用copy屬性
@interface user_Model : NSObject
@property (nonatomic,strong) NSString *name;
@end
當VisitDecl(Decl *decl);中的decl為ObjCPropertyDecl時代表屬性,其提供了查詢Attribute的方法,返回的ObjCPropertyDecl::PropertyAttributeKind可用於檢查是否是copy。
4.使用內建的資料型別NSInteger而不是int
@interface user_Model : NSObject
@property (assign,nonatomic) int age;
@end
當VisitDecl(Decl *decl);中的decl為ObjCPropertyDecl時代表屬性,其提供了查詢type的方法,即此例中的int,可用於提醒使用者使用NSInteger而不是int.
5.函式宣告多個引數時,分行展示且冒號對齊
@interface user_Model : NSObject
- (instancetype)initWithMAC:(NSString *)mac AzIp:(NSString *)az_ip AzDns:(NSString *)az_dns Token:(NSString *)token Email:(NSString *)email;
當VisitDecl(Decl *decl);中的decl為ObjCMethodDecl時代表方法,當其為宣告param_size介面返回的引數數目大於引數限制時,獲取其名稱起始位置中間的原始碼,對其進行format(分行顯示且冒號對齊),判斷如果format之後的型別和先前不一致,提示使用者修改。
6.函式變數名長度不得超過指定字元數
@interface user_Model : NSObject
- (void)varNameTest:(NSInteger)numberOfPeopleOnTheUsOlympicTeam;
@end
同5,當中的decl為ObjCMethodDecl時,遍歷其params,每一個將是一個ParmVarDecl型別代表引數的變數,其getNameAsString介面可用於判斷變數名長度並當超過限制時提示使用者修改。
7.方法程式碼行數不超過限制
@implementation user_Model
- (void)methodLenTest:(long long)nSize{
........}
@end
同5,當中的decl為ObjCMethodDecl且hasBody時,getBody()獲取其函式體Stmt,再利用SourceManager獲取其body的原始碼(string型別),這樣就可以判斷程式碼行數是不是超過限制了。
8.函式引數使用內建資料型別
@interface user_Model
- (void)writeVideoFrameWithData:(NSData *)frameData timeStamp:(int)timeStamp;
@end
同6,當中的decl為ObjCMethodDecl時,遍歷其params,每一個將是一個ParmVarDecl型別代表引數的變數,其getType()介面獲取到的型別轉成string即可判斷引數型別是否為int,如果是提醒使用者。
9.列舉使用NS_ENUM
typedef enum {
CStyleEnumTypeDefault,
}CStyleEnumType;
當VisitDecl(Decl *decl);中的decl為EnumDecl時代表列舉定義,通過void Decl::print(raw_ostream &Out, unsigned Indentation = 0,bool PrintInstantiation = false) const;此方法可以獲取到原始程式碼,通過判斷是否包含以typedef enum/enum開頭判斷是否是enum,如果是提醒使用者使用NS_ENUM/NS_OPTIONS。
10.delegate為weak
@interface XXViewController : UIViewController
@property(nonatomic,strong) id<XXViewControllerDelegate> delegate;
@end
同3,當VisitDecl(Decl *decl);中的decl為ObjCPropertyDecl時代表屬性,其getType()轉為String獲取到的型別裡如果包含'<'和'>'則認為是delegate,同3也可判斷出是否是weak。
11.delegate書寫格式形如id<XXXDelegate> delegate
@interface XXViewController : UIViewController
@property(nonatomic,weak) id< UITextFieldDelegate> tfDelegate;
@end
參考10處理delegate,並結合5對於ObjCMethodDecl名稱的處理可以判斷是否需要提示使用者修改格式。
12.屬性使用nonatomic
@interface XXViewController : UIViewController
@property (copy) NSString *hint;
@end
參考3可以判斷屬性是否為atomic,如果是提醒使用者使用nonatomic。
13.訊息傳送多引數時,分行展示且冒號對齊
[model initWithMAC:nil AzIp:nil AzDns:nil Token:nil Email:nil];
當VisitStmt(Stmt *stmt)中的stmt為ObjCMessageExpr時,代表OC訊息傳送,其中model為receiver,initWithMac:AZip:AzDns:Token:Email:為sel.參考5,可以獲取訊息傳送的原始碼並format作比較,如果不一樣則提示使用者修改。
14.不在init/alloc中向物件傳送訊息
@implementation XXViewController
- (instancetype)init{
if(self = [super init]){
[self func1];
}
return self;
}
- (void)dealloc{
[self func2];
}
此處略複雜。因為Clang的AST分析是DPS的,這就使得當一個訊息被髮送時,其一般處於一個ObjCImplDecl/ObjCCategoryDecl(此處用objcClsImpl記錄類名)->ObjCMethodDecl(此處用objcSelector記錄當前Sel)裡面.
因此當VisitStmt(Stmt *stmt)發現stmt是ObjCMessageExpr時,且stmt通過print得到的字串去掉空格之後以[self開頭,即當前在給self發訊息。加上檢視當前的objcClsImpl/objcSelector即可知道這種方法呼叫關係。當發現objcSelector為init開頭或者dealloc時,且對self發訊息時提醒使用者。
請注意這裡的方法呼叫關係,請注意這裡的方法呼叫關係,請注意這裡的方法呼叫關係,此係列還將有一期關於程式碼量分析與優化的分享,這種呼叫關係即是基礎之一。
15.If表示式判斷條件必然成立/不成立分析
if(!10)
NSLog(@"1");
if(10)
NSLog(@"2");
if(true)
NSLog(@"4");
if(false)
NSLog(@"5");
當VisitStmt(Stmt *stmt)中的stmt為IfStmt時,其getCond()介面返回用於判斷的條件cond。針對一元表示式,cond為UnaryOperator,需要使用getSubExpr()獲取新的cond並脫去一元運算子,再去分析後面的內容。對於常量條件請參考IntegerLiteral,CharacterLiteral,FloatingLiteral。結合一元表示式型別和常亮的bool內容,即可判斷出If的body一定會走到或走不到並提醒使用者。
程式碼FIXITHINT功能
clang本身提供診斷的功能,可使用
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Class name should start with prefix XX");
diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
以在指定位置location,報告錯誤/警告和原因,並新增替換的Hint.
寫在後面
如上所述,Clang外掛用於風格檢查,可以發現和修改的更多是一種格式上的約定和某些明顯的不容許或無效邏輯,雖可解決不少問題,但是也有其侷限性。實際工作中,一方面可限制使用某些固定的風格,更重要的是保持團隊風格的統一和規範,提高其可讀性。