基於LLVM開發Clang插件進行代碼風格檢查

這個不少人都作過,文章也挺多的,我也是參考別人文章的,不過直到真正實現仍是踩了許多坑,因此記錄下來,或許對其餘人有幫助。其實LLVM和Clang我尚未好好研究過,以前大部分都是用Swift開發,代碼風格檢查都是用的Swiftlint,因此此次選擇OC的代碼檢查做爲開始,經過實踐找找感受和興趣,以後再一點一點精進。ios

下載源碼

代開終端c++

sudo mkdir llvm
sudo chown `whoami` llvm
cd llvm
export LLVM_HOME=`pwd`

git clone -b release_60 https://github.com/llvm-mirror/llvm.git llvm  
git clone -b release_60 https://github.com/llvm-mirror/clang.git llvm/tools/clang  
git clone -b release_60 https://github.com/llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra  
git clone -b release_60 https://github.com/llvm-mirror/compiler-rt.git llvm/projects/compiler-rt
複製代碼

下載源碼會比較慢,另外網上有其餘文章,有的文章比較老,版本也比較老,建議用最新的release_60git

安裝cmake

若是沒有安裝cmake須要安裝一下,後面須要用。github

brew update
brew install cmake

複製代碼

開始編寫插件

cd到llvm/llvm/tools/clang/examplesxcode

1.打開這個目錄下的CMakeLists.txt文件,而後添加add_subdirectory(CodeChecker)bash

Screen Shot 2019-05-20 at 10.40.00 PM.png

2.在當前目錄建立新的文件夾CodeChecker,並cd到CodeCheckerapp

mkdir CodeChecker
cd CodeChecker
複製代碼

3.在新建的CodeChecker目錄下建立三個文件ide

touch  CMakeLists.txt
touch  CodeChecker.cpp
touch  CodeChecker.exports
複製代碼

Screen Shot 2019-05-20 at 10.59.22 PM.png

在新建立的CMakeLists.txt中添加ui

if( NOT MSVC ) # MSVC mangles symbols differently
  if( NOT LLVM_REQUIRES_RTTI )
    if( NOT LLVM_REQUIRES_EH )
      set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/CodeChecker.exports)
    endif()
  endif()
endif()

add_llvm_loadable_module(CodeChecker CodeChecker.cpp)

if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
  target_link_libraries(CodeChecker ${cmake_2_8_12_PRIVATE}
    clangAST
    clangBasic
    clangFrontend
    LLVMSupport
    )
endif()
複製代碼

CodeChecker.cpp文件中加入this

#include <iostream>
#include <stdio.h>
#include <string>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <functional>
#include <vector>
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Sema/Sema.h"

using namespace clang;
using namespace std;

namespace
{
    static vector<string> split(const string &s, char delim)
    {
        vector<string> elems;
        stringstream ss;
        ss.str(s);
        string item;
        while (getline(ss, item, delim)) {
            elems.push_back(item);
        }
        return elems;
    }

    class CodeVisitor : public RecursiveASTVisitor<CodeVisitor>
    {
    private:
        CompilerInstance &Instance;
        ASTContext *Context;

    public:

        void setASTContext (ASTContext &context) {
            this -> Context = &context;
        }

    private:

        /** 判斷是否爲用戶源碼 @param decl 聲明 @return true 爲用戶源碼,false 非用戶源碼 */
        bool isUserSourceCode (Decl *decl) {
            string filename = Instance.getSourceManager().getFilename(decl->getSourceRange().getBegin()).str();

            if (filename.empty())
                return false;

            //非XCode中的源碼都認爲是用戶源碼
            if(filename.find("/Applications/Xcode.app/") == 0)
                return false;

            return true;
        }

        /** 檢測類名是否存在小寫開頭 @param decl 類聲明 */
        void checkClassNameForLowercaseName(ObjCInterfaceDecl *decl) {
            StringRef className = decl -> getName();

            //類名稱必須以大寫字母開頭
            char c = className[0];
            if (isLowercase(c))
            {
                //修正提示
                std::string tempName = className;
                tempName[0] = toUppercase(c);
                StringRef replacement(tempName);
                SourceLocation nameStart = decl->getLocation();
                SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
                FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);

                //報告警告
                DiagnosticsEngine &D = Instance.getDiagnostics();
                int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "Class name should not start with lowercase letter");
                SourceLocation location = decl->getLocation();
                D.Report(location, diagID).AddFixItHint(fixItHint);
            }
        }

        /** 檢測類名是否包含下劃線 @param decl 類聲明 */
        void checkClassNameForUnderscoreInName(ObjCInterfaceDecl *decl) {
            StringRef className = decl -> getName();

            //類名不能包含下劃線
            size_t underscorePos = className.find('_');
            if (underscorePos != StringRef::npos)
            {
                //修正提示
                std::string tempName = className;
                std::string::iterator end_pos = std::remove(tempName.begin(), tempName.end(), '_');
                tempName.erase(end_pos, tempName.end());
                StringRef replacement(tempName);
                SourceLocation nameStart = decl->getLocation();
                SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
                FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);

                //報告錯誤
                DiagnosticsEngine &diagEngine = Instance.getDiagnostics();
                unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Error, "Class name with `_` forbidden");
                SourceLocation location = decl->getLocation().getLocWithOffset(underscorePos);
                diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
            }
        }


        /** 檢測方法名是否存在大寫開頭 @param decl 方法聲明 */
        void checkMethodNameForUppercaseName(ObjCMethodDecl *decl) {
            //檢查名稱的每部分,都不容許以大寫字母開頭
            Selector sel = decl -> getSelector();
            int selectorPartCount = decl -> getNumSelectorLocs();
            for (int i = 0; i < selectorPartCount; i++)
            {
                StringRef selName = sel.getNameForSlot(i);
                char c = selName[0];
                if (isUppercase(c))
                {
                    //修正提示
                    std::string tempName = selName;
                    tempName[0] = toLowercase(c);
                    StringRef replacement(tempName);
                    SourceLocation nameStart = decl -> getSelectorLoc(i);
                    SourceLocation nameEnd = nameStart.getLocWithOffset(selName.size() - 1);
                    FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);

                    //報告警告
                    DiagnosticsEngine &D = Instance.getDiagnostics();
                    int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "Selector name should not start with uppercase letter");
                    SourceLocation location = decl->getLocation();
                    D.Report(location, diagID).AddFixItHint(fixItHint);
                }
            }
        }

        /** 檢測方法中定義的參數名稱是否存在大寫開頭 @param decl 方法聲明 */
        void checkMethodParamsNameForUppercaseName(ObjCMethodDecl *decl) {
            for (ObjCMethodDecl::param_iterator it = decl -> param_begin(); it != decl -> param_end(); it++)
            {
                ParmVarDecl *parmVarDecl = *it;
                StringRef name = parmVarDecl -> getName();
                char c = name[0];
                if (isUppercase(c))
                {
                    //修正提示
                    std::string tempName = name;
                    tempName[0] = toLowercase(c);
                    StringRef replacement(tempName);
                    SourceLocation nameStart = parmVarDecl -> getLocation();
                    SourceLocation nameEnd = nameStart.getLocWithOffset(name.size() - 1);
                    FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);

                    //報告警告
                    DiagnosticsEngine &D = Instance.getDiagnostics();
                    int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Selector's param name should not start with uppercase letter");
                    SourceLocation location = decl->getLocation();
                    D.Report(location, diagID).AddFixItHint(fixItHint);
                }
            }
        }

        /** 檢測方法實現是否超過500行代碼 @param decl 方法聲明 */
        void checkMethodBodyForOver500Lines(ObjCMethodDecl *decl) {
            if (decl -> hasBody())
            {
                //存在方法體
                Stmt *methodBody = decl -> getBody();

                string srcCode;
                srcCode.assign(Instance.getSourceManager().getCharacterData(methodBody->getSourceRange().getBegin()),
                               methodBody->getSourceRange().getEnd().getRawEncoding() - methodBody->getSourceRange().getBegin().getRawEncoding() + 1);
                vector<string> lines = split(srcCode, '\n');
                if(lines.size() > 500)
                {
                    DiagnosticsEngine &D = Instance.getDiagnostics();
                    unsigned diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Single method should not have body over 500 lines");
                    D.Report(decl -> getSourceRange().getBegin(), diagID);
                }
            }
        }

        /** 檢測屬性名是否存在大寫開頭 @param decl 屬性聲明 */
        void checkPropertyNameForUppercaseName(ObjCPropertyDecl *decl) {
            bool checkUppercaseNameIndex = 0;

            StringRef name = decl -> getName();

            if (name.find('_') == 0)
            {
                //表示如下劃線開頭
                checkUppercaseNameIndex = 1;
            }

            //名稱必須以小寫字母開頭
            char c = name[checkUppercaseNameIndex];
            if (isUppercase(c))
            {
                //修正提示
                std::string tempName = name;
                tempName[checkUppercaseNameIndex] = toLowercase(c);
                StringRef replacement(tempName);
                SourceLocation nameStart = decl->getLocation();
                SourceLocation nameEnd = nameStart.getLocWithOffset(name.size() - 1);
                FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);

                //報告錯誤
                DiagnosticsEngine &D = Instance.getDiagnostics();
                int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "Property name should not start with uppercase letter");
                SourceLocation location = decl->getLocation();
                D.Report(location, diagID).AddFixItHint(fixItHint);
            }
        }

        /** 檢測屬性名是否包含下劃線 @param decl 屬性聲明 */
        void checkPropertyNameForUnderscoreInName(ObjCPropertyDecl *decl) {
            StringRef name = decl -> getName();

            if (name.size() == 1)
            {
                //不須要檢測
                return;
            }

            //類名不能包含下劃線
            size_t underscorePos = name.find('_', 1);
            if (underscorePos != StringRef::npos)
            {
                //修正提示
                std::string tempName = name;
                std::string::iterator end_pos = std::remove(tempName.begin() + 1, tempName.end(), '_');
                tempName.erase(end_pos, tempName.end());
                StringRef replacement(tempName);
                SourceLocation nameStart = decl->getLocation();
                SourceLocation nameEnd = nameStart.getLocWithOffset(name.size() - 1);
                FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);

                //報告錯誤
                DiagnosticsEngine &diagEngine = Instance.getDiagnostics();
                unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Error, "Property name with `_` forbidden");
                SourceLocation location = decl->getLocation().getLocWithOffset(underscorePos);
                diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
            }
        }


        /** 檢測委託屬性是否有使用weak修飾 @param decl 屬性聲明 */
        void checkDelegatePropertyForUsageWeak (ObjCPropertyDecl *decl) {
            QualType type = decl -> getType();
            StringRef typeStr = type.getAsString();

            //Delegate
            if(typeStr.find("<") != string::npos && typeStr.find(">") != string::npos)
            {
                ObjCPropertyDecl::PropertyAttributeKind attrKind = decl -> getPropertyAttributes();

                string typeSrcCode;
                typeSrcCode.assign(Instance.getSourceManager().getCharacterData(decl -> getSourceRange().getBegin()),
                                   decl -> getSourceRange().getEnd().getRawEncoding() - decl -> getSourceRange().getBegin().getRawEncoding());

                if(!(attrKind & ObjCPropertyDecl::OBJC_PR_weak))
                {
                    DiagnosticsEngine &diagEngine = Instance.getDiagnostics();
                    unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Delegate should be declared as weak.");
                    diagEngine.Report(decl -> getLocation(), diagID);
                }
            }
        }


        /** 檢測常量名稱是否存在小寫開頭 @param decl 常量聲明 */
        void checkConstantNameForLowercaseName (VarDecl *decl) {
            StringRef className = decl -> getName();

            //類名稱必須以大寫字母開頭
            char c = className[0];
            if (isLowercase(c))
            {
                //修正提示
                std::string tempName = className;
                tempName[0] = toUppercase(c);
                StringRef replacement(tempName);
                SourceLocation nameStart = decl->getLocation();
                SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
                FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);

                //報告警告
                DiagnosticsEngine &D = Instance.getDiagnostics();
                int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Constant name should not start with lowercase letter");
                SourceLocation location = decl->getLocation();
                D.Report(location, diagID).AddFixItHint(fixItHint);
            }
        }


        /** 檢測變量名稱是否存在大寫開頭 @param decl 變量聲明 */
        void checkVarNameForUppercaseName (VarDecl *decl) {
            StringRef className = decl -> getName();

            //類名稱必須以大寫字母開頭
            char c = className[0];
            if (isUppercase(c))
            {
                //修正提示
                std::string tempName = className;
                tempName[0] = toLowercase(c);
                StringRef replacement(tempName);
                SourceLocation nameStart = decl->getLocation();
                SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
                FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);

                //報告警告
                DiagnosticsEngine &D = Instance.getDiagnostics();
                int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Variable name should not start with uppercase letter");
                SourceLocation location = decl->getLocation();
                D.Report(location, diagID).AddFixItHint(fixItHint);
            }
        }


        /** 檢測變量名稱 @param decl 變量聲明 */
        void checkVarName(VarDecl *decl) {
            if (decl -> isStaticLocal())
            {
                //靜態變量

                if (decl -> getType().isConstant(*this -> Context))
                {
                    //常量
                    checkConstantNameForLowercaseName(decl);
                }
                else
                {
                    //很是量
                    checkVarNameForUppercaseName(decl);
                }

            }
            else if (decl -> isLocalVarDecl())
            {
                //本地變量
                if (decl -> getType().isConstant(*this -> Context))
                {
                    //常量
                    checkConstantNameForLowercaseName(decl);
                }
                else
                {
                    //很是量
                    checkVarNameForUppercaseName(decl);
                }
            }
            else if (decl -> isFileVarDecl())
            {
                //文件定義變量
                if (decl -> getType().isConstant(*this -> Context))
                {
                    //常量
                    checkConstantNameForLowercaseName(decl);
                }
                else
                {
                    //很是量
                    checkVarNameForUppercaseName(decl);
                }
            }
        }

    public:

        CodeVisitor (CompilerInstance &Instance)
        :Instance(Instance)
        {

        }

        /** 觀察ObjC的類聲明 @param declaration 聲明對象 @return 返回 */
        bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration) {
            if (isUserSourceCode(declaration))
            {
                checkClassNameForLowercaseName(declaration);
                checkClassNameForUnderscoreInName(declaration);
            }

            return true;
        }


        /** 觀察類方法聲明 @param declaration 聲明對象 @return 返回 */
        bool VisitObjCMethodDecl(ObjCMethodDecl *declaration) {
            if (isUserSourceCode(declaration))
            {
                checkMethodNameForUppercaseName(declaration);
                checkMethodParamsNameForUppercaseName(declaration);
                checkMethodBodyForOver500Lines(declaration);
            }

            return true;
        }


        /** 觀察類屬性聲明 @param declaration 聲明對象 @return 返回 */
        bool VisitObjCPropertyDecl(ObjCPropertyDecl *declaration) {
            if (isUserSourceCode(declaration))
            {
                checkPropertyNameForUppercaseName(declaration);
                checkPropertyNameForUnderscoreInName(declaration);
                checkDelegatePropertyForUsageWeak(declaration);
            }

            return true;
        }

        /** 觀察變量聲明 @param declaration 聲明對象 @return 返回 */
        bool VisitVarDecl(VarDecl *declaration) {
            if (isUserSourceCode(declaration))
            {
                checkVarName(declaration);
            }

            return true;
        }

        /** 觀察枚舉常量聲明 @param declaration 聲明對象 @return 返回 */
        // bool VisitEnumConstantDecl (EnumConstantDecl *declaration)
        // {
        // return true;
        // }
    };

    class CodeConsumer : public ASTConsumer
    {
        CompilerInstance &Instance;
        std::set<std::string> ParsedTemplates;
    public:
        CodeConsumer(CompilerInstance &Instance,
                     std::set<std::string> ParsedTemplates)
        : Instance(Instance), ParsedTemplates(ParsedTemplates), visitor(Instance)
        {

        }

        bool HandleTopLevelDecl(DeclGroupRef DG) override {
            return true;
        }

        void HandleTranslationUnit(ASTContext& context) override {
            visitor.setASTContext(context);
            visitor.TraverseDecl(context.getTranslationUnitDecl());
        }

    private:
        CodeVisitor visitor;
    };

    class CodeASTAction : public PluginASTAction
    {
        std::set<std::string> ParsedTemplates;
    protected:
        std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                       llvm::StringRef) override
        {
            return llvm::make_unique<CodeConsumer>(CI, ParsedTemplates);
        }

        bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &args) override {
            // DiagnosticsEngine &D = CI.getDiagnostics();
            // D.Report(D.getCustomDiagID(DiagnosticsEngine::Error,
            // "My plugin Started..."));

            return true;
        }
    };
}

static clang::FrontendPluginRegistry::Add<CodeASTAction>
X("CodeChecker", "Code Checker");

複製代碼

使用cmake編譯源代碼

1.cd到llvm,注意不是llvm/llvm,執行

mkdir llvm_build && cd llvm_build
cmake -G Xcode ../llvm -DCMAKE_BUILD_TYPE:STRING=MinSizeRel

複製代碼

2.而後會在llvm_build文件目錄中看到LLVM.xcodeproj,用xcode打開,選擇Automatically Create Schemes

Screen Shot 2019-05-20 at 11.01.35 PM.png

3.編譯 clang,CodeChecker,libclang

Screen Shot 2019-05-20 at 11.07.04 PM.png
Screen Shot 2019-05-20 at 11.04.24 PM.png
Screen Shot 2019-05-20 at 11.04.49 PM.png

4.在llvm_build/Debug/lib目錄下能夠找到咱們的插件,以下圖:

Screen Shot 2019-05-20 at 11.09.41 PM.png

Xcode集成Plugin

建立須要加載插件的項目,在Build Settings欄目中的OTHER_CFLAGS添加上以下內容:

-Xclang -load -Xclang (.dylib)動態庫路徑 -Xclang -add-plugin -Xclang 插件名字

複製代碼

我把插件拷貝的桌面了,因此個人是:

-Xclang -load -Xclang /Users/roy.cao/Desktop/CodeChecker.dylib -Xclang -add-plugin -Xclang CodeChecker

複製代碼

Screen Shot 2019-05-20 at 11.45.39 PM.png

而後你build項目,可能有unable to load plugin '/Users/roy.cao/Desktop/CodeChecker.dylib'的error,這是因爲Clang插件須要對應的Clang版原本加載,若是版本不一致會致使編譯錯誤。

Screen Shot 2019-05-20 at 11.28.02 PM.png

在Build Settings欄目中新增兩項用戶定義的設置,分別爲CC和CXX CC對應的是本身編譯的clang的絕對路徑,CXX對應的是本身編譯的clang++的絕對路徑

Screen Shot 2019-05-20 at 11.31.47 PM.png

/Users/roy.cao/llvm/llvm_build/Debug/bin/clang
/Users/roy.cao/llvm/llvm_build/Debug/bin/clang++

複製代碼

再build,會有如下錯誤:

Screen Shot 2019-05-20 at 11.37.39 PM.png

在Build Settings欄目中搜索index,將Enable Index-Wihle-Building Functionality的Default改成NO.

Screen Shot 2019-05-20 at 11.39.40 PM.png

再build可能會出現一大堆系統庫的 symbol not found 錯誤

Screen Shot 2019-05-20 at 11.41.49 PM.png

這個時候須要在剛剛OTHER_CFLAGS的

-Xclang -load -Xclang /Users/roy.cao/Desktop/CodeChecker.dylib -Xclang -add-plugin -Xclang CodeChecker

複製代碼

後面再加上-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.0.sdk

注意iPhoneSimulator12.0.sdk,每一個人的可能不一樣,須要到目錄下看看本身的版本。 那麼完整的就是下圖這樣:

Screen Shot 2019-05-20 at 11.54.47 PM.png

再build就能作代碼風格檢查啦,慶祝一下吧~~~~

Screen Shot 2019-05-20 at 11.52.35 PM.png

參考文章:
LLVM & Clang 入門
使用Xcode開發iOS語法檢查的Clang插件
Clang 之旅--使用 Xcode 開發 Clang 插件

相關文章
相關標籤/搜索