OC底层原理三十二:LLVM插件(Copy修饰符检测)

OC底层原理 学习大纲

  • 上一节,我们熟悉了LLVM完整流程

本节:

  • 自定义Clang插件是实现Copy属性修饰符检测智能提示
    (最终的使用场景可能并没那么通用,但主要目的是彻底熟悉LLVM
  • 文末会附上最终代码使用方法
  1. 配置LLVM环境
  2. 自定义插件
  • 如果你也跟我一样充满期待。那我们就开始吧。

1. 配置LLVM环境

⚠️ ⚠️ ⚠️ 【注意】

  1. LLVM源码2.29G编译后文件将近30G,请确保电脑硬盘空间足够
  2. 编译时,电脑温度会飙升90多度CPU资源占满,请用空调伺候着,可能黑屏
  3. 编译时间长达1个多小时,请合理安排时间。

如果以上3点,你确定能接受,那我们就开始吧。

1.1 LLVM下载

  • clangclang-tools-extracompiler-rtlibcxxlibcxxabillvm五个库:(我下载的都是11.0.0版本的)
    image.png
  • 解压移除名称中的版本号
image.png
  • 按以下顺序将文件夹移到指定位置
  1. clang-tools-extra移到clang文件夹中的clang/tools文件中
  2. clang文件夹移到llvm/tools
  3. compiler-rtlibcxxlibcxxabi都移到llvm/projects
    image.png
image.png

1.2 安装cmake

  • 查看brew列表,检查是否安装过cmake,如果有,就跳过此步骤
brew list
  • 如果没有,就使用brew安装:
brew install cmake
  • 如果报权限错误,可sudo chown -R `whoami`:admin /usr/local/share放开权限
    image.png

1.3 编译

  • llvm同级目录创建build文件夹,cd到build文件夹,运行cmake命令,将llvm编译成Xcode项目
cd build
cmake -G Xcode ../llvm   
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="Release" ../llvm
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="debug" ../llvm 

注意:

    1. build文件夹是存放cmake生成的Xcode文件的。放哪里都可以。
    1. cmake编译的对象是llvm文件。所以使用cmake -G Xcode ../llvm编译并生成Xcode文件时,请核对llvm文件路径
  • 成功之后,可以看到生成的Xcode文件:

    image.png

  • 打开LLVM.xcodeproj

选择手动创建Schemes

image.png

  • 添加clangclangTooling两个Target,并完成两个target的编译
    (此处可能需要1小时cpu占满,请适当给电脑降温😂)
image.png
  • 编译成功后,我们的准备工作完成了。可以正式开始插件开发

2. 自定义插件

2.1 添加插件

  • 我们在llvm/tools/clang/tools文件夹中,创建HTPlugin文件夹,并新增两个文件:
    image.png
  1. CMakeLists.txt内容:add_llvm_library( HTPlugin MODULE BUILDTREE_ONLY HTPlugin.cpp)
  2. HTPlugin.cpp:空文件
    image.png
  • llvm/tools/clang/tools文件夹的CMakeLists.txt文件尾部,加上add_clang_subdirectory(HTPlugin)
image.png
  • 创建好后,我们回到build文件夹,cmake -G Xcode ../llvm 重新编译生成Xcode文件。
开始编译
编译完成

ps: 我的电脑系统版本是:

image.png

  • 打开build文件夹中新生成的LLVM.xcodeproj:
  • Command + 鼠标左键点击文件夹,折叠所有文件夹。 在Loadable modules中,可以看到我们自己创建的HTPlugin文件夹:
    image.png
  • 搜索选择 HTPlugin
    image.png

2.2 书写插件

2.2.1 体验单文件顶级节点解析

1. 在HTPlugn.cpp文件中加入以下代码

// 1. 声明命名空间,创建插件
namespace HTPlugin {
    
    // 4. 自定义HTConsumer,继承ASTConsumer
    // 进入`ASTConsumer`,查看它的结构,有很多选择
    //    本例重载[HandleTopLevelDecl]顶级节点的解析和[HandleTranslationUnit]文件解析结束回调
    class HTConsumer: public ASTConsumer {
    private:
    public:
        
        // 4.1 顶级节点解析中
        bool HandleTopLevelDecl(DeclGroupRef D) {
            cout<<"解析中..."<<endl;
            return true;
        }
        
        // 4.2 单个文件解析结束
        void HandleTranslationUnit(ASTContext &Ctx) {
            cout<<"文件解析完毕!"<<endl;
        }
        
    };

    // 2. 创建插件(继承PluginASTAction 实现自定义的ASTAction)
    // 【目的】读取AST语法树的所有节点。
    // 【如何重写】 进入`PluginASTAction`,查看它的结构,重载[ParseArgs]和[CreateASTConsumer]
    class HTASTAction: public PluginASTAction {
    public:
        
        // 2.1 解析成功,就返回true。 我们直接写true
        bool ParseArgs(const CompilerInstance &CI,
                       const std::vector<std::string> &arg) {
            return true;
        }
        
        // 2.2 创建一个语法树对象
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                  StringRef InFile){
            // 使用自定义的HTConsumer,继承自ASTConsumer。
//            return unique_ptr<ASTConsumer>(new ASTConsumer());
            return unique_ptr<HTConsumer>(new HTConsumer());
        }
    };

}

// 3. 注册插件 (参数1:插件名称, 参数2:插件描述)
static FrontendPluginRegistry::Add<HTPlugin::HTASTAction> HT("HTPlugin", "this is HTPlugin");

2. Command+B 编译一次
3. 创建一个测试文件demo.m

image.png

  • demo.m内容:
int sum(int a);
int a = 10;
int b = 20;

int sum(int a) {
    int b = 10;
    return 10 + b;
}

int sum2(int a, int b) {
    int c = 10;
    return a + b + c;
}

4. cdClangDemo文件夹,使用我们的clang编译demo.m文件:

/Users/ht/Desktop/llvm/build/Debug/bin/clang -usysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk/ -Xclang -load -Xclang /Users/ht/Desktop/llvm/build/Debug/lib/HTPlugin.dylib -Xclang -add-plugin -Xclang HTPlugin -c demo.m

格式分析:

  • /Users/ht/Desktop/llvm/build/Debug/bin/clang:
    自己插件编译后clang文件,在build/Debug/bin/文件夹中:
    image.png
  • /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk/
    使用自己本机SDK绝对路径。 (我这是14.2)
    image.png
  • /Users/ht/Desktop/llvm/build/Debug/lib/HTPlugin.dylib:
    自己插件编译后HTPlugin.dylib插件动态库绝对路径,在build/Debug/lib/文件夹中:
    image.png
  • 指定使用HTPlugin 编译demo.m文件
image.png

5. 编译完成后,会生成demo.o文件。是一个object目标文件(不懂目标文件,可回顾上一节

image.png

2.2.2 观察属性修饰符节点名

1. 创建工程

  • ClangDemo文件夹中,创建demo工程:

    image.png

  • ViewController.m文件中加入测试代码

@interface ViewController()

// NSString、NSArray、NSDictionary 应使用Copy修饰
@property (nonatomic, strong) NSString * name;   
@property (nonatomic, strong) NSArray * arrs;
@property (nonatomic, strong) NSDictionary *dicts;

@end

2. 使用系统clang编译ViewController.m文件

  • cdViewController.m的文件夹,使用系统clang编译ViewController.m文件:
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk  -fmodules -fsyntax-only -Xclang -ast-dump  ViewController.m
image.png
  • 可以发现在AST语法树中,ObjCPropertyDecl属性节点
2.2.3 过滤ObjCPropertyDecl节点
  • 修改HTPlugin文件:
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"

using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;

// 1. 声明命名空间,创建插件
namespace HTPlugin {
    
    // 5. 自定义HTMatchCallBack(过滤器回调类) 继承自MatchFinder的MatchCallBack
    class HTMatchCallBack: public MatchFinder::MatchCallback {
    public:
        // 5.2 重写run方法
        void run(const MatchFinder::MatchResult &Result) {
            
            // 通过Result拿到了所有节点, 通过自定义的节点标识,拿到我们标记的所有节点
            const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
            
            // 过滤空情况
            if (propertyDecl) {
                // 拿到类型,转string,打印
                string typeStr = propertyDecl->getType().getAsString();
                cout<<"--- 拿到了: 【"<<typeStr<<"】 ----"<<endl;
            }
            
        }
    };


    // 4. 自定义HTConsumer,继承ASTConsumer
    // 进入`ASTConsumer`,查看它的结构,有很多选择
    //    本例重载[HandleTopLevelDecl]顶级节点的解析和[HandleTranslationUnit]文件解析结束回调
    class HTConsumer: public ASTConsumer {
    private:
        // 4.3 添加属性:AST节点查找过滤器
        MatchFinder matcher;
        // 5.1 添加属性:MatchFinder过滤器的回调函数
        HTMatchCallBack callback;
    public:
        
        // 4.5 声明构造方法(出厂就添加一个MatchFinder过滤器)
        HTConsumer() {
            // 添加MatchFinder (参数1: 过滤的节点, 参数2, 过滤后的回调)
            //    查看MatchFinder中的MatchCallBack的结构,仿写一个回调函数
            // 此处,过滤`objcPropertyDecl`属性节点,手动添加节点标识`objcPropertyDecl`
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
        }
        
        // 4.1 顶级节点解析中
        bool HandleTopLevelDecl(DeclGroupRef D) {
            cout<<"解析中..."<<endl;
            return true;
        }
        
        // 4.2 单个文件解析结束
        void HandleTranslationUnit(ASTContext &Ctx) {
            cout<<"文件解析完毕!"<<endl;
            // 4.4 解析完成后,将语法树给到`matcher`过滤器
            matcher.matchAST(Ctx);
        }
        
    };

    // 2. 创建插件(继承PluginASTAction 实现自定义的ASTAction)
    // 【目的】读取AST语法树的所有节点。
    // 【如何重写】 进入`PluginASTAction`,查看它的结构,重载[ParseArgs]和[CreateASTConsumer]
    class HTASTAction: public PluginASTAction {
    public:
        
        // 2.2 解析成功,就返回true。 我们直接写true
        bool ParseArgs(const CompilerInstance &CI,
                       const std::vector<std::string> &arg) {
            return true;
        }
        
        // 2.3 创建一个语法树对象
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                  StringRef InFile){
            // 使用自定义的HTConsumer,继承自ASTConsumer。
            // return unique_ptr<ASTConsumer>(new ASTConsumer());
            return unique_ptr<HTConsumer>(new HTConsumer());
        }
    };

}

// 3. 注册插件 (参数1:插件名称, 参数2:插件描述)
static FrontendPluginRegistry::Add<HTPlugin::HTASTAction> HT("HTPlugin", "this is HTPlugin");
  • Command+B编译
  • cdllvm/ClangDemo/Demo/Demo文件夹。 使用我们自己的clangHTPlugin插件,编译ViewController.m文件:
/Users/ht/Desktop/llvm/build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk -Xclang -load -Xclang /Users/ht/Desktop/llvm/build/Debug/lib/HTPlugin.dylib  -Xclang -add-plugin -Xclang HTPlugin -c ViewController.m

分享我掉的一个坑:

  • 这是我自己,因为我之前之前写了MacOS项目,所以创建Demo时,默认创建了MacOS工程,然后我使用了iPhoneSimulator14.2.sdkiOS SDK来编译。一直报'Cocoa/Cocoa.h' file not found
    image.png

出现Cocoa/Cocoa.h的问题,都是MacOS的问题,iOS使用Fundation库。macOS使用Cocoa库。


  • 可以看到打印了非常多的属性节点信息:

    image.png

  • 其中大部分都是系统文件属性,我们需要排除这些,定位自己代码属性

2.2.4 过滤系统文件节点

可以通过编译器实例(文件)CompilerInstance,读取每个实例路径。代码如下:(新增6)

// 1. 声明命名空间,创建插件
namespace HTPlugin {
    
    // 5. 自定义HTMatchCallBack(过滤器回调类) 继承自MatchFinder的MatchCallBack
    class HTMatchCallBack: public MatchFinder::MatchCallback {
    private:
        // 6.2 添加属性:编译器实例
        CompilerInstance &compilerInstance;
    public:
        
        // 6.3 声明构造方法,入参新增CompilerInstance编译器实例,并赋值给compilerInstance
        HTMatchCallBack(CompilerInstance &CI):compilerInstance(CI) { }
        
        // 5.2 重写run方法 (回调的执行函数)
        void run(const MatchFinder::MatchResult &Result) {
            
            // 通过Result拿到了所有节点, 通过自定义的节点标识,拿到我们标记的所有节点
            const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
            
            // 过滤空情况
            if (propertyDecl) {
                
                // 6.4 打印文件名称
                string fileName = compilerInstance.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
                
                // 拿到类型,转string,打印
                string typeStr = propertyDecl->getType().getAsString();
                
                cout<<"--- 拿到了: 【"<<typeStr<<"】 ----文件路径:"<<fileName<<endl;
            }
            
        }
    };

    // 4. 自定义HTConsumer,继承ASTConsumer
    // 进入`ASTConsumer`,查看它的结构,有很多选择
    //    本例重载[HandleTopLevelDecl]顶级节点的解析和[HandleTranslationUnit]文件解析结束回调
    class HTConsumer: public ASTConsumer {
    private:
        // 4.3 添加属性:AST节点查找过滤器
        MatchFinder matcher;
        // 5.1 添加属性:MatchFinder过滤器的回调函数
        HTMatchCallBack callback;
    public:
        // 4.5 声明构造方法(出厂就添加一个MatchFinder过滤器)
        //  6.4 入参新增:编译器实例CI,并赋值给callback
        HTConsumer(CompilerInstance &CI):callback(CI) {
            // 添加MatchFinder (参数1: 过滤的节点, 参数2, 过滤后的回调)
            //    查看MatchFinder中的MatchCallBack的结构,仿写一个回调函数
            // 此处,过滤`objcPropertyDecl`属性节点,手动添加节点标识`objcPropertyDecl`
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
        }
        
        // 4.1 顶级节点解析中
        bool HandleTopLevelDecl(DeclGroupRef D) {
            cout<<"解析中..."<<endl;
            return true;
        }
        
        // 4.2 单个文件解析结束
        void HandleTranslationUnit(ASTContext &Ctx) {
            cout<<"文件解析完毕!"<<endl;
            // 4.4 解析完成后,将语法树给到`matcher`过滤器
            matcher.matchAST(Ctx);
        }
        
    };

    // 2. 创建插件(继承PluginASTAction 实现自定义的ASTAction)
    // 【目的】读取AST语法树的所有节点。
    // 【如何重写】 进入`PluginASTAction`,查看它的结构,重载[ParseArgs]和[CreateASTConsumer]
    class HTASTAction: public PluginASTAction {
    public:
        
        // 2.2 解析成功,就返回true。 我们直接写true
        bool ParseArgs(const CompilerInstance &CI,
                       const std::vector<std::string> &arg) {
            return true;
        }
        
        // 2.3 创建一个语法树对象
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                  StringRef InFile){
            // 使用自定义的HTConsumer,继承自ASTConsumer。
            // return unique_ptr<ASTConsumer>(new ASTConsumer());
            // 6.1 存储CompilerInstance实例(它是每个编译文件,可以通过它读取编译文件的路径,剔除系统文件的干扰)
            return unique_ptr<HTConsumer>(new HTConsumer(CI));
        }
    };

}

// 3. 注册插件 (参数1:插件名称, 参数2:插件描述)
static FrontendPluginRegistry::Add<HTPlugin::HTASTAction> HT("HTPlugin", "this is HTPlugin");

  • Command+B 编译后,使用自己的clangHTPlugin编译ViewController.m文件:
/Users/ht/Desktop/llvm/build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk -Xclang -load -Xclang /Users/ht/Desktop/llvm/build/Debug/lib/HTPlugin.dylib  -Xclang -add-plugin -Xclang HTPlugin -c ViewController.m
  • 可以发现,所有系统文件,地址都是以/Applications/Xcode.app/开头。

    image.png

  • 通过区分文件地址,可以剔除所有系统文件:(新增7)

#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"

using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;

// 1. 声明命名空间,创建插件
namespace HTPlugin {
    
    // 5. 自定义HTMatchCallBack(过滤器回调类) 继承自MatchFinder的MatchCallBack
    class HTMatchCallBack: public MatchFinder::MatchCallback {
    private:
        // 6.2 添加属性:编译器实例
        CompilerInstance &compilerInstance;
        
        // 7. 判断是否是用户代码的函数(通过文件地址判断)
        bool isUserSourceCode(const string fileName) {
            
            // 空,不是用户的
            if (fileName.empty()) return false;
            
            // Xcode中的源码,都是系统的
            if (fileName.find("/Applications/Xcode.app/") == 0) return false;
            
            // 其他情况,都是用户的
            return true;
            
        }
    public:
        
        // 6.3 声明构造方法,入参新增CompilerInstance编译器实例,并赋值给compilerInstance
        HTMatchCallBack(CompilerInstance &CI):compilerInstance(CI) { }
        
        // 5.2 重写run方法 (回调的执行函数)
        void run(const MatchFinder::MatchResult &Result) {
            
            // 通过Result拿到了所有节点, 通过自定义的节点标识,拿到我们标记的所有节点
            const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
            
            // 过滤空情况
            if (propertyDecl) {
                
                // 6.4 打印文件名称
                string fileName = compilerInstance.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
                
                // 7.1 不是用户的文件,就不往下打印
                if (!isUserSourceCode(fileName)) return;
                
                // 拿到类型,转string,打印
                string typeStr = propertyDecl->getType().getAsString();
                
                cout<<"--- 拿到了: 【"<<typeStr<<"】 ----文件路径:"<<fileName<<endl;
            }
            
        }
    };

    // 4. 自定义HTConsumer,继承ASTConsumer
    // 进入`ASTConsumer`,查看它的结构,有很多选择
    //    本例重载[HandleTopLevelDecl]顶级节点的解析和[HandleTranslationUnit]文件解析结束回调
    class HTConsumer: public ASTConsumer {
    private:
        // 4.3 添加属性:AST节点查找过滤器
        MatchFinder matcher;
        // 5.1 添加属性:MatchFinder过滤器的回调函数
        HTMatchCallBack callback;
    public:
        // 4.5 声明构造方法(出厂就添加一个MatchFinder过滤器)
        //  6.4 入参新增:编译器实例CI,并赋值给callback
        HTConsumer(CompilerInstance &CI):callback(CI) {
            // 添加MatchFinder (参数1: 过滤的节点, 参数2, 过滤后的回调)
            //    查看MatchFinder中的MatchCallBack的结构,仿写一个回调函数
            // 此处,过滤`objcPropertyDecl`属性节点,手动添加节点标识`objcPropertyDecl`
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
        }
        
        // 4.1 顶级节点解析中
        bool HandleTopLevelDecl(DeclGroupRef D) {
            cout<<"解析中..."<<endl;
            return true;
        }
        
        // 4.2 单个文件解析结束
        void HandleTranslationUnit(ASTContext &Ctx) {
            cout<<"文件解析完毕!"<<endl;
            // 4.4 解析完成后,将语法树给到`matcher`过滤器
            matcher.matchAST(Ctx);
        }
        
    };

    // 2. 创建插件(继承PluginASTAction 实现自定义的ASTAction)
    // 【目的】读取AST语法树的所有节点。
    // 【如何重写】 进入`PluginASTAction`,查看它的结构,重载[ParseArgs]和[CreateASTConsumer]
    class HTASTAction: public PluginASTAction {
    public:
        
        // 2.2 解析成功,就返回true。 我们直接写true
        bool ParseArgs(const CompilerInstance &CI,
                       const std::vector<std::string> &arg) {
            return true;
        }
        
        // 2.3 创建一个语法树对象
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                  StringRef InFile){
            // 使用自定义的HTConsumer,继承自ASTConsumer。
            // return unique_ptr<ASTConsumer>(new ASTConsumer());
            // 6.1 存储CompilerInstance实例(它是每个编译文件,可以通过它读取编译文件的路径,剔除系统文件的干扰)
            return unique_ptr<HTConsumer>(new HTConsumer(CI));
        }
    };

}

// 3. 注册插件 (参数1:插件名称, 参数2:插件描述)
static FrontendPluginRegistry::Add<HTPlugin::HTASTAction> HT("HTPlugin", "this is HTPlugin");
  • Command+B 编译后,使用自己的clangHTPlugin编译ViewController.m文件:
image.png
  • 可以发现,我们已成功过滤系统消息,只留下我们自己的代码信息。
2.2.5 定位代码错误,添加错误提示
  • 添加错误修饰符定位提示的代码:(新增8)
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"

using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;

// 1. 声明命名空间,创建插件
namespace HTPlugin {
    
    // 5. 自定义HTMatchCallBack(过滤器回调类) 继承自MatchFinder的MatchCallBack
    class HTMatchCallBack: public MatchFinder::MatchCallback {
    private:
        // 6.2 添加属性:编译器实例
        CompilerInstance &compilerInstance;
        
        // 7. 判断是否是用户代码的函数(通过文件地址判断)
        bool isUserSourceCode(const string fileName) {
            
            // 空,不是用户的
            if (fileName.empty()) return false;
            
            // Xcode中的源码,都是系统的
            if (fileName.find("/Applications/Xcode.app/") == 0) return false;
            
            // 其他情况,都是用户的
            return true;
            
        }
        
        // 8. 判断是否应该copy修饰
        bool isShouldUseCopy(const string typeStr) {
            
            if (typeStr.find("NSString") != string::npos ||
                typeStr.find("NSArray") != string::npos ||
                typeStr.find("NSDictionary") != string::npos) {
                
                return true;
                
            }
            
            return false;
            
        }
        
    public:
        
        // 6.3 声明构造方法,入参新增CompilerInstance编译器实例,并赋值给compilerInstance
        HTMatchCallBack(CompilerInstance &CI):compilerInstance(CI) { }
        
        // 5.2 重写run方法 (回调的执行函数)
        void run(const MatchFinder::MatchResult &Result) {
            
            // 通过Result拿到了所有节点, 通过自定义的节点标识,拿到我们标记的所有节点
            const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
            
            // 过滤空情况
            if (propertyDecl) {
                
                // 6.4 打印文件名称
                string fileName = compilerInstance.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
                
                // 7.1 不是用户的文件,就不往下打印
                if (!isUserSourceCode(fileName)) return;
                
                // 拿到类型,转string,打印
                string typeStr = propertyDecl->getType().getAsString();
                
                // 8.1 找出应该使用copy的属性
                if (isShouldUseCopy(typeStr)) {
                    
                    // 拿到节点描述 (ObjCPropertyAttribute::Kind 是枚举,其中kind_copy = 0x20)
                    ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
                    
                    // 当前修饰符不为copy,提示
                    if (!(attrKind & ObjCPropertyAttribute::kind_copy)) {
                        
                        // 诊断引擎
                        DiagnosticsEngine &diag = compilerInstance.getDiagnostics();
                        
                        // Report 报告
                        // 参数1: 定位位置
                        // 参数2: 设置等级和提示文案
                        diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0这个地方推荐使用copy!!!"))<<typeStr;
                    }
                    
                }
                
            }
            
        }
    };

    // 4. 自定义HTConsumer,继承ASTConsumer
    // 进入`ASTConsumer`,查看它的结构,有很多选择
    //    本例重载[HandleTopLevelDecl]顶级节点的解析和[HandleTranslationUnit]文件解析结束回调
    class HTConsumer: public ASTConsumer {
    private:
        // 4.3 添加属性:AST节点查找过滤器
        MatchFinder matcher;
        // 5.1 添加属性:MatchFinder过滤器的回调函数
        HTMatchCallBack callback;
    public:
        // 4.5 声明构造方法(出厂就添加一个MatchFinder过滤器)
        //  6.4 入参新增:编译器实例CI,并赋值给callback
        HTConsumer(CompilerInstance &CI):callback(CI) {
            // 添加MatchFinder (参数1: 过滤的节点, 参数2, 过滤后的回调)
            //    查看MatchFinder中的MatchCallBack的结构,仿写一个回调函数
            // 此处,过滤`objcPropertyDecl`属性节点,手动添加节点标识`objcPropertyDecl`
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
        }
        
        // 4.1 顶级节点解析中
        bool HandleTopLevelDecl(DeclGroupRef D) {
//            cout<<"解析中..."<<endl;
            return true;
        }
        
        // 4.2 单个文件解析结束
        void HandleTranslationUnit(ASTContext &Ctx) {
//            cout<<"文件解析完毕!"<<endl;
            // 4.4 解析完成后,将语法树给到`matcher`过滤器
            matcher.matchAST(Ctx);
        }
        
    };

    // 2. 创建插件(继承PluginASTAction 实现自定义的ASTAction)
    // 【目的】读取AST语法树的所有节点。
    // 【如何重写】 进入`PluginASTAction`,查看它的结构,重载[ParseArgs]和[CreateASTConsumer]
    class HTASTAction: public PluginASTAction {
    public:
        
        // 2.2 解析成功,就返回true。 我们直接写true
        bool ParseArgs(const CompilerInstance &CI,
                       const std::vector<std::string> &arg) {
            return true;
        }
        
        // 2.3 创建一个语法树对象
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                  StringRef InFile){
            // 使用自定义的HTConsumer,继承自ASTConsumer。
            // return unique_ptr<ASTConsumer>(new ASTConsumer());
            // 6.1 存储CompilerInstance实例(它是每个编译文件,可以通过它读取编译文件的路径,剔除系统文件的干扰)
            return unique_ptr<HTConsumer>(new HTConsumer(CI));
        }
    };

}

// 3. 注册插件 (参数1:插件名称, 参数2:插件描述)
static FrontendPluginRegistry::Add<HTPlugin::HTASTAction> HT("HTPlugin", "this is HTPlugin");
  • Command+B 编译后,使用自己的clangHTPlugin编译ViewController.m文件:
    image.png
2.2.6 Xcode集成自定义插件

1. Demo工程添加自定义插件 Build Settings ->Other C Flags 添加:

-Xclang -load -Xclang /Users/ht/Desktop/llvm/build/Debug/lib/HTPlugin.dylib -Xclang -add-plugin -Xclang HTPlugin

/Users/ht/Desktop/llvm/build/Debug/lib/HTPlugin.dylib是自己的HTPlugin.dylib绝对路径

image.png

2. Command + B编译,报错

  • Clang插件需要使用对应的版本加载版本不一致导致的编译错误

    image.png

  • Build Settings栏目新增两项用户定义的设置:

    image.png

  • 添加CCCXX两个设置:

CC : 填写clang绝对路径
CXX:填写clang++绝对路径

image.png

image.png

  • Build Settings搜索index:
    image.png
2.2.7 编译成功

Command+B编译,编译成功,查看ViewController.m文件:

image.png
  • 修改name的修饰符为copyCommand+B编译后看,name已经不报错了。

    image.png

  • 恭喜你。 成功了!

    image.png

通过这个小插件,应该对语法树编译流程,有了更深刻认识。😃

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容