Clang ASTMatcher 學習

或許對了發表於2020-11-03

Clang ASTMatcher 學習

前言

在上一章中介紹了一個通過遞迴遍歷整個 AST 樹的方法 RecursiveASTVisitor ,但當我們開始使用它來實現一些功能時發現可能由於程式碼結構比較複雜,想要找到我們需要的目的碼節點需要編寫大量的程式碼來實現。本章介紹一個新的功能 clang ASTMatcher ,它提供了給我們一些語法、介面可以通過類似命令列的方式來實現快速匹配我們需要的節點,並且配合 clang-query 來快速驗證查詢語法的正確性,大大提高效率。

ASTMatcher 介紹

一些基礎的概念,參考了這篇文章,主要的內容如下:

  • ASTMatcher 主要時允許使用者通過 matcher 提供的規則編寫一個程式來匹配 AST 節點並能通過訪問節點的 c++ 介面來獲取該 AST 節點的屬性、源位置等任何資訊,其主要由巨集與模板驅動,用法和函數語言程式設計類似,其可實現簡單精準高效的匹配。

  • 主要的規則

    • Note Matchers:匹配特定型別節點;

      • eg: objcPropertyDecl() :匹配 OC 屬性宣告節點。

    • Narrowing Matchers:匹配具有相應屬性的節點;

      • eg: hasName()、hasAttr():匹配具有指定名稱、attribute 的節點。

    • AST Traversal Matchers:允許在節點之間遞迴匹配;;

      • eg: hasAncestor()、hasDescendant():匹配祖、後代類節點。

一下子一堆的概念,是不是一臉懵逼,沒關係後邊我會提供一個可以執行的例子,看再多概念總沒有實際操作一把來的有意思,但是這些基礎的知識還是要先了解下,我們可以先從官方提供的文件入手,ASTMatcher 官方文件

看不太懂沒有關係,也可以直接來看下這個例子:

recordDecl(hasName("Foo"), isDerivedFrom("Bar"))
1
  • 在這段程式碼中,

    recordDecl

     

    hasName

     

    isDerivedFrom

    ,都屬於 clang matcher 提供給我們的功能函式,具體的功能可以在這個

    規則

    網站中查詢到。

    • 以 recordDecl 為例,搜尋下可以得到下圖的結果,檢視描述可以發現他是用來匹配類、結構體、聯合體宣告的節點。因此 recordDecl() 的含義就是匹配所有類、結構體、聯合體宣告的節點。 在這裡插入圖片描述

    • 但是有可能在一個 AST 樹中有超級多的宣告,因此我們需要縮小查詢範圍,查詢 hasName 可以知道它是用來匹配具有指定名稱的節點。因此 recordDecl(hasName("Foo")) 的含義就是匹配所有類、結構體、聯合體宣告的節點中名稱為 Foo 的節點

    • isDerivedFrom(xxx) 是匹配從 xxx 派生的類,recordDecl(hasName("Foo"), isDerivedFrom("Bar")) 這麼編寫就是進一步縮小定位的方位,匹配所有類、結構體、聯合體宣告的節點中名稱為 Foo 的節點且必須是從 Bar 派生出來的節點

recordDecl(anyOf(hasName("Foo"), isDerivedFrom("Bar")))
1
  • 讓我們修改下演示的程式碼,可以發現 anyOf 這個關鍵字,它表示

     

    hasName("Foo")

     

     

    isDerivedFrom("Bar")

     

    這兩個條件有一個成立即可匹配(類似 |)。

    • 類似的還有 allOf(A, B) 標識 A 與 B 要同時成立才可以(類似 & ),unless(A) 匹配 A 不成立的節點(類似 ! )。

    • anyOf is like “or”, allOf can implement “and”, and unless is like “not”.

使用 clang-query

當我們編寫了匹配器想要測試怎麼辦,難道改一點就要重新編譯連結執行,好在 clang 提供了好用的 clang-query 工具可以方便我們驗證編寫的匹配器語法是否正確。首先準備一些測試程式碼,原始碼見文末

使用 clang-query 如下:

在這裡插入圖片描述

  • 這個命令的前半段沒有什麼好講解的,目標檔案就是 ./test/test.cxx。

    • 我們需要重點關注下 -p “./build/compile_commands.json”。

      • -p 的解釋如下圖,指定 compile_commands.json 這個檔案的路徑所在。 在這裡插入圖片描述

      • compile_commands.json 檔案包含了編譯過程中的巨集定義、標頭檔案路徑、編譯器等資訊,而 clang 在解析生成 AST 樹時需要預編譯,所以需要這些資訊防止標頭檔案找不到等等問題。compile_commands.json 我們可以通過 cmake 來生成,參考這篇文章在這裡插入圖片描述

      • 我們在之前的文章中一直使用 CmakeTools 工具,它的配置比較簡單如下圖設定即可。 在這裡插入圖片描述

  • 接下來執行 clang-query 命令,可以看到一些原始碼相關的警告後我們進入到 clang-query 的命令列內: 在這裡插入圖片描述

    • 執行 m functionDecl(isExpansionInMainFile()) 執行我們的匹配語句,可以看到它列印出了我們 test.cxx 檔案中所有的函式宣告,與我們預期時相符的。預設列印的簡略資訊,如果想要看詳細的 AST 結構資訊,可以設定 output 方式: 在這裡插入圖片描述

  • 這樣我們就可以不停的嘗試當前的 matcher 語句是否正確。

在程式中使用 AST Mathcer

使用 matcher 我這舉兩個簡單的方法:

  1. 可以將直接將匹配器通過使用 newFrontendActionFactory 實現一個 FrontendAction 並傳遞給 ClangTool 直接去執行,舉例如下:

    int FunctionToAnalyzeCodeTree(int argc, const char** argv)
    {
        auto FuncDeclMatcher =
            functionDecl(isExpansionInMainFile(),
                        anyOf(hasAncestor(cxxRecordDecl().bind("methodclass")), unless(hasAncestor(cxxRecordDecl()))),
                        anyOf(forEachDescendant(callExpr().bind("callExprFunction")),
                            unless(forEachDescendant(callExpr().bind("callExprFunction")))))
                .bind("FunctiondFeclWithCall"); //bind 不瞭解沒有關係 後邊會講到
        CommonOptionsParser OptionsParser(argc, argv, ToolingSampleCategory);
        ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList());
        Func_Call FuncCall;
        MatchFinder Finder;
        Finder.addMatcher(FuncDeclMatcher, &FuncCall);
        return Tool.run(newFrontendActionFactory(&Finder).get());
    }
    123456789101112131415
  2. 我們也可以通過前面文章中使用的方法,自己實現一個 FrontendAction 在 CreateAstConsumer 時構建我們需要的匹配器,舉例如下:

    class MyFrontendAction : public ASTFrontendAction
    {
    public:
        MyFrontendAction() = default;
        void EndSourceFileAction() override
        {
            auto m = getCompilerInstance().getDiagnostics().getNumWarnings();
            spdlog::info("{} Warning\n", m);
        }
        std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance& CI, StringRef file) override
        {
            llvm::errs() << "** Creating AST consumer for: " << file << "\n";
            auto m = CI.getDiagnostics().getNumWarnings();
            spdlog::info("{}", m);
            auto FuncDeclMatcher =
                functionDecl(isExpansionInMainFile(),
                            anyOf(hasAncestor(cxxRecordDecl().bind("methodclass")), unless(hasAncestor(cxxRecordDecl()))),
                            anyOf(forEachDescendant(callExpr().bind("callExprFunction")),
                                unless(forEachDescendant(callExpr().bind("callExprFunction")))))
                    .bind("FunctiondFeclWithCall");
            Finder.addMatcher(FuncDeclMatcher, &FuncCall);
            return Finder.newASTConsumer();
        }
    
    private:
        Func_Call FuncCall;
        MatchFinder Finder;
    };
    
    int FunctionToAnalyzeCodeError(int argc, const char** argv)
    {
        CommonOptionsParser op(argc, argv, ToolingSampleCategory);
        ClangTool Tool(op.getCompilations(), op.getSourcePathList());
    
        // ClangTool::run accepts a FrontendActionFactory, which is then used to
        // create new objects implementing the FrontendAction interface. Here we use
        // the helper newFrontendActionFactory to create a default factory that will
        // return a new MyFrontendAction object every time.
        // To further customize this, we could create our own factory class.
        return Tool.run(newFrontendActionFactory<MyFrontendAction>().get());
    }
    1234567891011121314151617181920212223242526272829303132333435363738394041
  3. 你可能發現了一個特殊的類 Func_Call 這個是我自己定義的,他繼承 MatchFinder::MatchCallback ,當使用 MatchFinder 的 addMatcher 方法中與匹配器一起註冊進去後,每當我們的匹配器匹配到相應的節點就會呼叫 run 方法,我們只需要重寫它的 run 方法實現需要的功能即可。

    class Func_Call : public MatchFinder::MatchCallback
    {
    public:
        void run(const MatchFinder::MatchResult& Result) override
        {
        }
    };
    1234567

解析匹配節點

  1. 當有節點匹配到後會呼叫 MatchCallback 的 run 方法,它會傳入一個 MatchResult 的引數,我們可以通過引數中包含的資訊來實現一些基礎的功能,本小節就通過一個查詢當前原始檔中所有的函式宣告的例子來講解。

    • 首先分析下匹配器:

      • functionDecl() 查詢所有的函式與方法的宣告節點,我們可以發現它有一個 bind() 方法,作用是可以將所有匹配到的節點繫結到 bind 方法傳入的字串標識上,當我們需要使用時就可以通過這個標識拿到匹配的節點。

      • isExpansionInMainFile() 表示是當前目標檔案中的定義,不會匹配到你 include 的標頭檔案中去。

      • anyOf() 裡可以發現前邊是查詢類中的方法定義,後邊是 unless 又將範圍給取消了,主要是想將查詢到的類方法類名給 bind 起來方便使用,而對於匹配器的匹配範圍沒有什麼影響。

      • 第二個 anyOf() 也是同理,查詢所有函式宣告體中呼叫的函式節點,這麼些貌似有些挫,暫時還沒有找到更好的匹配器寫法,有知道的同學麻煩告知下,謝謝。

      • 其實總的來說這個匹配器就是匹配目標原始檔中所有的函式宣告,包括全域性的函式宣告,以及類方法的宣告。其他的匹配項僅僅是為了將相關聯的節點繫結起來方便使用。

    functionDecl(isExpansionInMainFile(),
                anyOf(hasAncestor(cxxRecordDecl().bind("methodclass")), unless(hasAncestor(cxxRecordDecl()))),
                anyOf(forEachDescendant(callExpr().bind("callExprFunction")),
                    unless(forEachDescendant(callExpr().bind("callExprFunction")))))
        .bind("FunctiondFeclWithCall");
    12345
  2. 知道了匹配器的功能接下來然我們實現一個列印出函式宣告相關資訊的功能:

    class Func_Call : public MatchFinder::MatchCallback
    {
    public:
        void run(const MatchFinder::MatchResult& Result) override
        {
            std::string classname;
            std::string functionname;
            std::string functionparms;
            std::string callexprname;
            std::string callexprparms;
    
            clang::LangOptions LangOpts;
            LangOpts.CPlusPlus = true;
            clang::PrintingPolicy Policy(LangOpts); //指定標誌為c++ 模式,用於從expr 獲取型別字串
            //從 Result 引數中獲得當前的函式宣告節點,這裡就使用到了匹配器 bind 的標識
            if (auto const* functionDecl = Result.Nodes.getNodeAs<FunctionDecl>("FunctiondFeclWithCall"))
            {
                if (!functionDecl->hasBody()) //判斷函式是否有函式體,僅有一個宣告的沒有實現的函式停止解析
                {
                    return;
                }
                //以下就是獲取函式的名稱、引數、返回值相關資訊,介面描述都很清晰,主要注意函式引數獲取方式用到的 QualType 使用方法
                functionname = functionDecl->getNameAsString();
                functionname += " | ";
                functionname += functionDecl->getQualifiedNameAsString();
                functionparms = "Return: ";
                functionparms += functionDecl->getReturnType().getAsString();
                if (functionDecl->getNumParams() > 0)
                {
                    functionparms += " | Param: ";
                    for (unsigned int i = 0; i < functionDecl->getNumParams(); i++)
                    {
                        auto param = functionDecl->getParamDecl(i);
                        functionparms += QualType::getAsString(param->getType().split(), Policy);
                        functionparms += "  ";
                        functionparms += functionDecl->getParamDecl(i)->getNameAsString();
                        functionparms += " | ";
                    }
                }
                else
                {
                    functionparms += " | Param: NULL";
                }
            }
            // 獲取當前方法函式宣告所在的類,如果是一個全域性函式非類方法則這個節點是沒有的
            if (auto const* classdecl = Result.Nodes.getNodeAs<CXXRecordDecl>("methodclass"))
            {
                classname = classdecl->getNameAsString();
            }
            // 獲取函式體中所呼叫的其他函式資訊
            if (auto const* callexprtdec = Result.Nodes.getNodeAs<CallExpr>("callExprFunction"))
            {
                auto func = callexprtdec->getDirectCallee();
                callexprname = func->getNameInfo().getAsString();
                if (!callexprname.empty())
                {
                    callexprname += " | ";
                    callexprname += func->getQualifiedNameAsString();
                    callexprparms = "Return: ";
                    callexprparms += func->getReturnType().getAsString();
                    if (func->getNumParams() > 0)
                    {
                        callexprparms += " | Param: ";
                        for (unsigned int i = 0; i < func->getNumParams(); i++)
                        {
                            auto param = func->getParamDecl(i);
                            callexprparms += QualType::getAsString(param->getType().split(), Policy);
                            callexprparms += "  ";
                            callexprparms += func->getParamDecl(i)->getNameAsString();
                            callexprparms += " | ";
                        }
                    }
                    else
                    {
                        callexprparms += " | Param: NULL";
                    }
                }
            }
            else
            {
                callexprparms = "NULL";
                callexprname = "CALL NULL";
            }
    
            spdlog::info("analysis result classname[{}] function[{} type:{}]  callexpr[{} type:{}]\n", classname.c_str(),
                        functionname.c_str(), functionparms.c_str(), callexprname.c_str(), callexprparms.c_str());
        }
    };
    12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  • 以上,我們就將 clang ASTMathcer 基礎概念、使用方法說完了。

其他

獲取錯誤資訊

  1. 想要獲取到原始碼中的錯誤、警告資訊,需要我們實現一個 DiagnosticConsumer 方法,並在 ASTFrontendAction 的 CreateAstConsumer 呼叫時,註冊掉 CompilerInstance 的 Diagnostic 引擎中去作為 client ,這樣當有警告、錯誤發生時就會回撥到我們重寫的 HandleDiagnostic 方法中去,下邊是一個例子。

    • CompilerInstance 作為類似編譯器的一個實體提供各種功能,而 Diagnostic 提供了跟蹤處理原始碼時可能出現的錯誤並進行報告的功能。具體的 CompilerInstance 與 Diagnostic 的概念感覺又是一個坑,現在還沒有梳理想要了解的需要自己查下相關資料。

class BlankDiagConsumer : public clang::DiagnosticConsumer
{
public:
    BlankDiagConsumer() = default;
    ~BlankDiagConsumer() override = default;
    void HandleDiagnostic(DiagnosticsEngine::Level DiagLevel, const Diagnostic& Info) override
    {
        SmallString<100> OutStr;
        Info.FormatDiagnostic(OutStr);

        llvm::raw_svector_ostream DiagMessageStream(OutStr);
        auto aa = FullSourceLoc(Info.getLocation(), Info.getSourceManager()).getFileLoc();
        int Line = aa.getLineNumber();
        spdlog::info("{} DiagLevel = {} Message = {} at Line = {}", __FUNCTION__, DiagLevel,
                     DiagMessageStream.str().str().c_str(), Line);
    }
};

class MyFrontendAction : public ASTFrontendAction
{
public:
    MyFrontendAction() = default;
    void EndSourceFileAction() override
    {
        auto& DE = getCompilerInstance().getDiagnostics();
        spdlog::info("{} Warning\n", DE.getNumWarnings());
    }
    std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance& CI, StringRef file) override
    {
        llvm::errs() << "** Creating AST consumer for: " << file << "\n";
        auto FuncDeclMatcher =
            functionDecl(isExpansionInMainFile(),
                         anyOf(hasAncestor(cxxRecordDecl().bind("methodclass")), unless(hasAncestor(cxxRecordDecl()))),
                         anyOf(forEachDescendant(callExpr().bind("callExprFunction")),
                               unless(forEachDescendant(callExpr().bind("callExprFunction")))))
                .bind("FunctiondFeclWithCall");
        Finder.addMatcher(FuncDeclMatcher, &FuncCall);
        CI.getDiagnostics().setClient(&ctr);
        return Finder.newASTConsumer();
    }

private:
    Func_Call FuncCall;
    MatchFinder Finder;
    BlankDiagConsumer ctr;
};
12345678910111213141516171819202122232425262728293031323334353637383940414243444546

測試程式碼 test.cxx

//
//test.h
//
#ifndef __TEST__H__
#define __TEST__H__
#include <string>
class TestClass2
{
public:
    TestClass2();
    ~TestClass2();

    std::string TestFunction(int test);
};

class TestClass3
{
public:
    TestClass3();
    ~TestClass3();

    std::string TestFunction3(int test);
};
#endif
//
//test.cxx
//
#include "spdlog/spdlog.h"
#include "test.h"
#include <iostream>

extern int testfunction(int a);
enum class Cpp11Enum
{
    RED = 10,
    BLUE = 20
};

struct Wowza
{
    virtual ~Wowza() = default;
    virtual void foo(int i = 0) = 0;
};

struct Badabang : Wowza
{
    void foo(int) override;

    bool operator==(const Badabang& o) const;
};

void testif_else(int ww)
{
    int h = 1;
    if (int b = 1)
    {
        int a = 10;
    }
    else if (h == 1)
    {
        int b = 20;
    }
    else
    {
        int c = 20;
    }

    for (int i = 0; int b = 2 + i < 10; i++)
    {
        h++;
    }
}

std::string notcallall(TestClass3 b)
{
    TestClass3 tmp = b;
    for (int i = 0; int b = 2 + i < 10; i++)
    {
        int h = 0;
        h++;
    }
}

class testclassparent
{
public:
    testclassparent()
    {
        spdlog::info("{} call\n", __FUNCTION__);
        parentFunction(1, 'a');
    }

    ~testclassparent()
    {
        int adsds = 0;
        int adsdsd = 0;
    }

    void parentFunction(int test1, char test2) { spdlog::info("{} call {} -- {}\n", __FUNCTION__, test1, test2); }
};
class testclass : public testclassparent
{
    testclass()
    {
        spdlog::info("{} call\n", __FUNCTION__);
        testfunction(3);
        parentFunction(3, 'c');
        TestClass2 a;
        a.TestFunction(4);
    }
};


TestClass3::TestClass3()
{
    int adsds = 0;
    spdlog::info("{} {}call\n", __FUNCTION__, adsds);
}
TestClass3::~TestClass3()
{
    int adsds = 0;
    spdlog::info("{} {}call\n", __FUNCTION__, adsds);
}
std::string TestClass3::TestFunction3(int test)
{
    int sss;
    spdlog::info("{} {}{}call\n", __FUNCTION__, test, sss);
    return "a";
}
template <typename T> void bar(T&& t);

相關文章