開發(fā)clang plugin過程中,痛點之一是如何在ParseAST階段快速找到自己想處理的AST Notes,以前我們使用RecursiveASTVisitor 去遞歸遍歷、逐層查找,不僅代碼冗余,而且效率低下。直到后來clang提供了ASTMatcher,使我們可以精準高效的匹配到我們想處理的節(jié)點。
一、Visitors vs matchers
先來看兩個例子,對比一下:
// NO.1:找到自己源碼中必然成立 & body為空的IfStmt()節(jié)點
if (11) {
}
if (0.01) {
}
if ('a') {
}
if (!1) {
}
if (~1){
}
對于上面這種語句節(jié)點查找,以往我們會自定義一個繼承于RecursiveASTVisitor,然后在bool CustomASTVisitor::VisitStmt(Stmt *s)里面逐層查找,如下:
bool CustomASTVisitor::VisitStmt(Stmt *s) {
//獲取Stmt note 所屬文件文件名
string filename = context->getSourceManager().getFilename(s->getSourceRange().getBegin()).str();
//判斷是否在自己檢查的文件范圍內
if(filename.find(SourceRootPath)!=string::npos) {
if(isa<IfStmt>(s)){
IfStmt *is = (IfStmt *)s;
DiagnosticsEngine &diagEngine = context->getDiagnostics();
SourceLocation location = is->getIfLoc();
LangOptions LangOpts;
LangOpts.ObjC2 = true;
PrintingPolicy Policy(LangOpts);
Expr *cond = is->getCond();
Stmt * thenST = is->getThen();
string sExpr;
raw_string_ostream rsoExpr(sExpr);
thenST->printPretty(rsoExpr, 0, Policy);
string body = rsoExpr.str();
remove_blank(body);
//檢查Body
if(!body.compare("") || !body.compare("{}")){
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Don't use empty body in If Statement.");
diagEngine.Report(location, diagID);
}
bool isNot = false;
int flag = -1;
//檢查一元表達式
if(isa<UnaryOperator>(cond)){
UnaryOperator *uo = (UnaryOperator *)cond;
if(uo->getOpcode()==UO_LNot || uo->getOpcode()==UO_Not){
isNot = true;
cond = uo->getSubExpr();
}
}
if(isa<IntegerLiteral>(cond)){
IntegerLiteral *il = (IntegerLiteral*)cond;
flag =il->getValue().getBoolValue();
}
else if(isa<CharacterLiteral>(cond)){
CharacterLiteral *cl = (CharacterLiteral*)cond;
flag =cl->getValue();
}
else if(isa<FloatingLiteral>(cond)){
FloatingLiteral *fl = (FloatingLiteral*)cond;
flag =fl->getValue().isNonZero();
}
if(flag != -1){
flag = flag?1:0;
if(isNot)
flag = 1-flag;
if(flag){
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Body will certainly be executed when condition true.");
diagEngine.Report(location, diagID);
}
else{
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Body will never be executed when condition false.");
diagEngine.Report(location, diagID);
}
}
}
}
}
正如你看到,這個示例還只是檢查一個只包含Literal的IfStmt,如果需要檢查一個IfStmt (Expr)的話,代碼嵌套層次會增減更多,而且上面VisitStmt(Stmt *s) ,會遍歷UserSourceFile + IncludeSystemFile里所有的Stmt,對于只想匹配UserSourceFile里特定類型的Stmt的我們來說,大大增加了我們的查找時間。
相反,如果使用ASTMatcher 來匹配的話,你只需在consumer里聲明 MatchFinder finder然后:
//body 為空
finder.addMatcher(ifStmt(isExpansionInMainFile(),hasThen(compoundStmt(statementCountIs(0)))).bind("ifStmt_empty_then_body"), &handlerForMatchResult);
//condition_always_true
finder.addMatcher(ifStmt(isExpansionInMainFile(),hasCondition(integerLiteral(unless(equals(false))))).bind("condition_always_true"), &handlerForMatchResult);
finder.addMatcher(ifStmt(isExpansionInMainFile(),hasCondition(floatLiteral(unless(equals(false))))).bind("condition_always_true"), &handlerForMatchResult);
//condition_always_false
finder.addMatcher(ifStmt(isExpansionInMainFile(),hasCondition(characterLiteral(equals(false)))).bind("condition_always_false"), &handlerForMatchResult);
對于包含一元運算符的:
finder.addMatcher(ifStmt(isExpansionInMainFile(),hasCondition(unaryOperator(hasOperatorName("~"),hasUnaryOperand(integerLiteral(unless(equals(false))))))).bind("condition_always_false"), &handlerForMatchResult);
如上,只針對UserSourceFile,IfStmt,添加特定的Node Matcher,配合Narrowing Matcher、AST Traversal Matcher實現(xiàn)快速準確匹配。
接著你只需在MatchCallBack object的 void CustomHandler::run(const MatchFinder::MatchResult &Result) handle match result:
if (const IfStmt *stmtIf = Result.Nodes.getNodeAs<IfStmt>("ifStmt_empty_then_body")) {
SourceLocation location = stmtIf->getIfLoc();
diagWaringReport(location, "Don't use empty body in IfStmt", NULL);
} else if (const IfStmt *stmtIf = Result.Nodes.getNodeAs<IfStmt>("condition_always_true")) {
SourceLocation location = stmtIf->getIfLoc();
diagWaringReport(location, "Body will certainly be executed when condition true", NULL);
} else if (const IfStmt *stmtIf = Result.Nodes.getNodeAs<IfStmt>("condition_always_false")) {
SourceLocation location = stmtIf->getIfLoc();
diagWaringReport(location, "Body will never be executed when condition false.", NULL);
}
綜上使用ASTMatcher match Notes:簡單、精準、高效.
二、AST Matcher Reference
點擊標題可查看clang 提供的所有不同類型的匹配器及相應說明,主要分下面三類:
- Note Matchers:匹配特定類型節(jié)點,
eg. objcPropertyDecl() :匹配OC屬性聲明節(jié)點 - Narrowing Matchers:匹配具有相應屬性的節(jié)點
eg.hasName()、hasAttr():匹配具有指定名稱、attribute的節(jié)點 - AST Traversal Matchers:允許在節(jié)點之間遞歸匹配
eg.hasAncestor()、hasDescendant():匹配祖、后代類節(jié)點
多數(shù)情況下會在Note Matchers的基礎上,根據(jù)-ast-dump的AST結構,有序交替組合narrowing Matchers、traversal matchers,直接匹配到我們感興趣的節(jié)點。
如:self.myArray = [self modelOfClass:[NSString class]]; 匹配檢查OC指定方法的調用是否正確,以前我們會通過給想匹配的方法綁定自定義的attribute,然后通過匹配BinaryOperator,逐層遍歷到objcMessageExpr,再遍歷Attr *attr : methodDecl->attrs(),查看是否有我們自定義的attr來匹配。
但是現(xiàn)在我們可以直接使用組合的matcher,來直接匹配到調用點,先看下相應AST結構:

所以對應的組合matcher為:
matcher.addMatcher(binaryOperator(hasDescendant(opaqueValueExpr(hasSourceExpression(objcMessageExpr(hasSelector("modelOfClass:"))))),
isExpansionInMainFile()).bind("binaryOperator_modelOfClass"), &handlerForMatchResult);
開發(fā)過程中,寫完一個matcher如何檢驗是否合理正確有效?如果每次都是寫完之后去工程跑plugin校驗,那是相當煩的。有什么快捷的辦法嗎?clang-query了解一下?
三、clang-query
-
作用:
- test matchers :交互式檢驗
- explore AST:探索AST 結構&關系
-
使用:
- 首先通過make clang-query 編譯獲取tool(詳細步驟看上篇)
- 然后準備好你的Test project & 一份導出的
compile_commands.json - 然后執(zhí)行下面命令
clang-query -p /path/to/compile_commands.json_dir \ -extra-arg-before "-isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk" \ ./*.m --注:
-p:compile_commands.json所在目錄路徑,
-extra-arg-before:編譯指令前拼接的擴展參數(shù)
./*.m 編譯當前目錄下所有.m文件
-編譯附加選項,添加的話不會再從database中加載,-目前沒任何選擇
執(zhí)行之后進入clang-query 命令行界面:
//let 給匹配表達式設置個別名
clang-query> let main isExpansionInMainFile()
clang-query> match ifStmt(main,hasCondition(unaryOperator(hasOperatorName("~"),hasUnaryOperand(integerLiteral(unless(equals(false))))))).bind("false")
Match #1:
/Users/yaso/Desktop/YJ/T/Testclang/Testclang/ViewController.m:39:3: note: "false" binds here
if (~1) {
^~~~~~~~~
/Users/yaso/Desktop/YJ/T/Testclang/Testclang/ViewController.m:39:3: note: "root" binds here
if (~1) {
^~~~~~~~~
1 match.
clang-query>
如上,直接match matcher,然后執(zhí)行正確匹配到相應的結果,并且高亮提示我們bind 的字符串,下面的root 是系統(tǒng)自帶,可以set bind-root false關閉。且寫matcher過程中,支持tab提示補全很方便。

四、結語
所以多熟悉熟悉AST Matcher Reference 里提供的matchers,配合clang-query 快速檢驗正確性,將使我們PaserAST的效率將會成倍提升。