如何利用 Clang 为 App 提质

2021-01-04  本文已影响0人  forping

本文是<<iOS开发高手课>> 第八篇学习笔记.

当第三方工具无法满足所有的业务技术规范监控。我们通过 Clang 提供的丰富接口功能就可以开发出静态分析工具,进而管控代码质量。

除此之外,基于 Clang 还可以开发出用于代码增量分析、代码可视化、代码质量报告来保障 App 质量的系统平台,比如CodeChecker
当分析问题的人都不在电脑边,无法及时处理问题。这时,我们就需要一款在线网页代码导航工具,比如 Mozilla 开发的 DXR,方便在便携设备上去操作、分析问题.

clang

Clang 的优势:

你可以通过: https://code.woboq.org/llvm/clang/ 查看 Clang 的源码,它不光工程代码量巨大,而且工具也非常多,相互间的关系复杂。但是,好在 Clang 提供了一个易用性很高的黑盒 Driver,用于封装前端命令和工具链的命令,使得其易用性得到了很大的提升。

Clang 做了哪些事?

Clang 提供了什么能力?

LibClang

LibClang 提供了一个稳定的高级 C 接口,Xcode 使用的就是 LibClang。

LibClang 可以访问 Clang 的上层高级抽象的能力,比如获取所有 Token、遍历语法树、代码补全等。

由于 API 很稳定,Clang 版本更新对其影响不大。但是,LibClang 并不能完全访问到 Clang AST 信息。使用 LibClang 可以直接使用它的 C API。官方也提供了 Python binding 脚本供你调用。还有开源的 node-js/ruby binding。还有个第三方开源的 Objective-C 写的ClangKit 库可供使用。

Clang Plugins

Clang Plugins 可以让你在 AST 上做些操作,这些操作能够集成到编译中,成为编译的一部分。

插件是在运行时由编译器加载的动态库,方便集成到构建系统中。使用 Clang Plugins 一般都是希望能够完全控制 Clang AST,同时能够集成在编译流程中,可以影响编译的过程,进行中断或者提示。

LibTooling

LibTooling 是一个 C++ 接口,通过 LibTooling 能够编写独立运行的语法检查和代码重构工具。

LibTooling 的优势如下:

与 Clang Plugins 相比,LibTooling 无法影响编译过程;与 LibClang 相比,LibTooling 的接口没有那么稳定,也无法开箱即用,当 AST 的 API 升级后需要更新接口的调用。

但是,LibTooling 基于能够完全控制 Clang AST 和可独立运行的特点,可以做的事情就非常多了。

在 LibTooling 的基础之上有个开发人员工具合集 Clang tools,Clang tools 作为 Clang 项目的一部分,已经提供了一些工具,主要包括:

如果你打算基于 LibTooling 来开发工具,Clang tools 将会是很好的范例。
官方教程: http://clang.llvm.org/docs/LibASTMatchersTutorial.html

开发clang插件

编译clang

首先下载llvm和clang源码

git clone https://git.llvm.org/git/llvm.git/
cd llvm/tools
git clone https://git.llvm.org/git/clang.git/
ninja编译
brew install cmake
/*
ninja如果安装失败,可以直接从github获取release版放入【/usr/local/bin】中
https://github.com/ninja-build/ninja/releases
*/
brew install ninja

在LLVM源码同级目录下新建一个【llvm_build】目录(最终会在【llvm_build】目录下生成【build.ninja】)

cd llvm_build
// cmake -G Ninja llvm文件夹 -DCMAKE_INSTALL_PREFIX=LLVM的安装路径
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=/Volumes/Study/llvm_all/llvm_release
ninja
ninja install
Xcode编译

在llvm同级目录下新建一个【llvm_xcode】目录

cd llvm_xcode
cmake -G Xcode ../llvm

创建clang插件文件

Clang的源码在llvm/tools/clang/下
主要功能在includelib文件夹.
tools文件夹下是使用Clang的库实现的一些工具。我们的插件也是基于Clang库,所以插件文件也创建在tools下边.

cd llvm/tools/clang/tools && mkdir fp-plugin

修改/llvm/tools/clang/tools目录下的CMakeLists.txt文件,在末尾增加

add_clang_subdirectory(fp-plugin)

fp-plugin目录下新建一个名为FPPlugin.cpp的文件。

touch FPPlugin.cpp

fp-plugin目录下新建一个名为CMakeLists.txt的文件

touch CMakeLists.txt 

编辑 CMakeLists.txt 文件,

有可能会随着版本的变化导致CMakeLists.txt的内容在编译的时候使用cmake命令会编译不通过。
建议参照LLVM.xcodeproj工程下的Loadable modules里其他插件的CMakeLists.txt内容进行编写。

add_llvm_library(FPPlugin MODULE FPPlugin.cpp PLUGIN_TOOL clang)

if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
  target_link_libraries(FPPlugin PRIVATE
    clangAST
    clangBasic
    clangFrontend
    LLVMSupport
    )
endif()

目录文件创建完成之后,利用cmake重新生成一下Xcode项目。在llvm_xcode目录下执行

cmake -G Xcode ../llvm

插件源代码在 Xcode 项目中的Loadable modules目录下可以找到,这样就可以直接在 Xcode 里编写插件代码。

编写插件

这里编写一个 当使用应该用copy修饰的属性(NSString, NSArray),而没有使用copy修饰的代码检测工具

插件编写即 重载Clang编译过程的函数,实现自定义需求(分析),大多数情况都是对源代码分析。

插件文件(.cpp)结构
结构.jpg

上图是Clang Plugin执行的过程,分别有CompilerInstance,FrontendActionASTConsumer

CompilerInstance:是一个编译器实例,综合了一个 Compiler 需要的 objects,如 Preprocessor,ASTContext(真正保存 AST 内容的类),DiagnosticsEngine,TargetInfo 等。

FrontendAction:是一个基于 Consumer 的抽象语法树(Abstract Syntax Tree/AST)前端 Action 抽象基类,对于 Plugin,我们可以继承至系统专门提供的PluginASTAction来实现我们自定义的 Action,我们重载CreateASTConsumer()函数返回自定义的Consumer,来读取 AST Nodes。

unique_ptr <ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
    return unique_ptr <QTASTConsumer> (new QTASTConsumer);
}

ASTConsumer:是一个读取抽象语法树的抽象基类,我们可以重载下面两个函数:

除了上面提到的这几个类,还有两个比较重要的类,分别是RecursiveASTVisitorMatchFinder

RecursiveASTVisitor:是一个特别有用的类,使用它可以访问任意类型的 AST 节点。

MatchFinder:是一个 AST 节点的查找过滤匹配器,可以使用addMatcher函数去匹配自己关注的 AST 节点。

抽象语法树

语法树AST
当源码为

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>

@protocol TTProtocol <NSObject>

- (void)protocolMethod;

@end

@interface HelloViewController : UIViewController
- (void)hahaha;
- (instancetype)sayHello;
- (void)sayOK:(NSString *)content toSomeOne:(NSString *)name;
+ (void)clmethod;
@end
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import "Hello.h"

@interface HelloViewController () <TTProtocol>

@property (nonatomic, assign) NSInteger index;

@end

@interface HelloViewController (hehehe)

@end

@implementation HelloViewController
- (NSInteger)hahaha {
    NSInteger a = 100;
    a += 1;
    return a;
}
- (instancetype)sayHello {
    NSLog(@"Hi");
    return self;
}

- (void)sayOK:(NSString *)content toSomeOne:(NSString *)name {
    NSLog(@"123123");
}

- (void)protocolMethod {
    NSLog(@"3333");
}

+ (void)clmethod {
    NSLog(@"32233");
}
@end

输入如下命令,将示例代码解析为语法树:

clang -Xclang -ast-dump -fsyntax-only Hello.m

语法树结构为

#import <UIKit/UIKit.h>
        ^~~~~~~~~~~~~~~
TranslationUnitDecl 0x7fae7b832008 <<invalid sloc>> <invalid sloc> <undeserialized declarations>
|-TypedefDecl 0x7fae7b8328a0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7fae7b8325a0 '__int128'
|-TypedefDecl 0x7fae7b832910 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7fae7b8325c0 'unsigned __int128'
|-TypedefDecl 0x7fae7b8329b0 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *'
| `-PointerType 0x7fae7b832970 'SEL *' imported
|   `-BuiltinType 0x7fae7b832800 'SEL'
|-TypedefDecl 0x7fae7b832a98 <<invalid sloc>> <invalid sloc> implicit id 'id'
| `-ObjCObjectPointerType 0x7fae7b832a40 'id' imported
|   `-ObjCObjectType 0x7fae7b832a10 'id' imported
|-TypedefDecl 0x7fae7b832b78 <<invalid sloc>> <invalid sloc> implicit Class 'Class'
| `-ObjCObjectPointerType 0x7fae7b832b20 'Class' imported
|   `-ObjCObjectType 0x7fae7b832af0 'Class' imported
|-ObjCInterfaceDecl 0x7fae7b832bd0 <<invalid sloc>> <invalid sloc> implicit Protocol
|-TypedefDecl 0x7fae7b832f68 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x7fae7b832d40 'struct __NSConstantString_tag'
|   `-Record 0x7fae7b832ca0 '__NSConstantString_tag'
|-TypedefDecl 0x7fae7b870200 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7fae7b832fc0 'char *' imported
|   `-BuiltinType 0x7fae7b8320a0 'char'
|-TypedefDecl 0x7fae7b870508 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7fae7b8704b0 'struct __va_list_tag [1]' 1
|   `-RecordType 0x7fae7b8702f0 'struct __va_list_tag'
|     `-Record 0x7fae7b870258 '__va_list_tag'
|-ImportDecl 0x7fae7c1025d8 <Hello.m:2:1> col:1 implicit Foundation
|-ImportDecl 0x7fae7c102618 <./Hello.h:2:1> col:1 implicit Foundation
|-ObjCProtocolDecl 0x7fae7bab6df8 <line:4:1, line:8:2> line:4:11 TTProtocol
| |-ObjCProtocol 0x7fae7c102660 'NSObject'
| `-ObjCMethodDecl 0x7fae7bab6f80 <line:6:1, col:23> col:1 - protocolMethod 'void'
|-ObjCInterfaceDecl 0x7fae7bab7108 <line:10:1, line:15:2> line:10:12 HelloViewController
| |-ObjCImplementation 0x7fae7bac0d08 'HelloViewController'
| |-ObjCMethodDecl 0x7fae7bab7238 <line:11:1, col:15> col:1 - hahaha 'void'
| |-ObjCMethodDecl 0x7fae7bab73e8 <line:12:1, col:25> col:1 - sayHello 'instancetype':'id'
| |-ObjCMethodDecl 0x7fae7c11f8f8 <line:13:1, col:61> col:1 - sayOK:toSomeOne: 'void'
| | |-ParmVarDecl 0x7fae7c11f988 <col:16, col:27> col:27 content 'NSString *'
| | `-ParmVarDecl 0x7fae7c11f9f0 <col:46, col:57> col:57 name 'NSString *'
| `-ObjCMethodDecl 0x7fae7c11fb78 <line:14:1, col:17> col:1 + clmethod 'void'
|-ObjCCategoryDecl 0x7fae7c11fd00 <Hello.m:5:1, line:9:2> line:5:12
| |-ObjCInterface 0x7fae7bab7108 'HelloViewController'
| |-ObjCProtocol 0x7fae7bab6df8 'TTProtocol'
| |-ObjCPropertyDecl 0x7fae7c12f308 <line:7:1, col:41> col:41 index 'NSInteger':'long' assign readwrite nonatomic unsafe_unretained
| |-ObjCMethodDecl 0x7fae7c12f388 <col:41> col:41 implicit - index 'NSInteger':'long'
| `-ObjCMethodDecl 0x7fae7c12f4e8 <col:41> col:41 implicit - setIndex: 'void'
|   `-ParmVarDecl 0x7fae7c12f578 <col:41> col:41 index 'NSInteger':'long'
|-ObjCCategoryDecl 0x7fae7bac0c28 <line:11:1, line:13:2> line:11:12 hehehe
| `-ObjCInterface 0x7fae7bab7108 'HelloViewController'
`-ObjCImplementationDecl 0x7fae7bac0d08 <line:15:1, line:38:1> line:15:17 HelloViewController
  |-ObjCInterface 0x7fae7bab7108 'HelloViewController'
  |-ObjCMethodDecl 0x7fae7bac0da0 <line:16:1, line:20:1> line:16:1 - hahaha 'NSInteger':'long'
  | |-ImplicitParamDecl 0x7fae7c135330 <<invalid sloc>> <invalid sloc> implicit self 'HelloViewController *'
  | |-ImplicitParamDecl 0x7fae7c135398 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
  | |-VarDecl 0x7fae7c135e10 <line:17:5, col:19> col:15 used a 'NSInteger':'long' cinit
  | | `-ImplicitCastExpr 0x7fae7c135e98 <col:19> 'NSInteger':'long' <IntegralCast>
  | |   `-IntegerLiteral 0x7fae7c135e78 <col:19> 'int' 100
  | `-CompoundStmt 0x7fae7c135fb0 <line:16:21, line:20:1>
  |   |-DeclStmt 0x7fae7c135eb0 <line:17:5, col:22>
  |   | `-VarDecl 0x7fae7c135e10 <col:5, col:19> col:15 used a 'NSInteger':'long' cinit
  |   |   `-ImplicitCastExpr 0x7fae7c135e98 <col:19> 'NSInteger':'long' <IntegralCast>
  |   |     `-IntegerLiteral 0x7fae7c135e78 <col:19> 'int' 100
  |   |-CompoundAssignOperator 0x7fae7c135f38 <line:18:5, col:10> 'NSInteger':'long' '+=' ComputeLHSTy='long' ComputeResultTy='long'
  |   | |-DeclRefExpr 0x7fae7c135ec8 <col:5> 'NSInteger':'long' lvalue Var 0x7fae7c135e10 'a' 'NSInteger':'long'
  |   | `-ImplicitCastExpr 0x7fae7c135f20 <col:10> 'long' <IntegralCast>
  |   |   `-IntegerLiteral 0x7fae7c135f00 <col:10> 'int' 1
  |   `-ReturnStmt 0x7fae7c135fa0 <line:19:5, col:12>
  |     `-ImplicitCastExpr 0x7fae7c135f88 <col:12> 'NSInteger':'long' <LValueToRValue>
  |       `-DeclRefExpr 0x7fae7c135f68 <col:12> 'NSInteger':'long' lvalue Var 0x7fae7c135e10 'a' 'NSInteger':'long'
  |-ObjCMethodDecl 0x7fae7bac0f28 <line:21:1, line:24:1> line:21:1 - sayHello 'instancetype':'id'
  | |-ImplicitParamDecl 0x7fae7c136008 <<invalid sloc>> <invalid sloc> implicit used self 'HelloViewController *'
  | |-ImplicitParamDecl 0x7fae7c136070 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
  | `-CompoundStmt 0x7fae7c1362a8 <col:26, line:24:1>
  |   |-CallExpr 0x7fae7c1361f0 <line:22:5, col:16> 'void'
  |   | |-ImplicitCastExpr 0x7fae7c1361d8 <col:5> 'void (*)(id, ...)' <FunctionToPointerDecay>
  |   | | `-DeclRefExpr 0x7fae7c1360d8 <col:5> 'void (id, ...)' Function 0x7fae7baba0d8 'NSLog' 'void (id, ...)'
  |   | `-ImplicitCastExpr 0x7fae7c136218 <col:11, col:12> 'id':'id' <BitCast>
  |   |   `-ObjCStringLiteral 0x7fae7c136158 <col:11, col:12> 'NSString *'
  |   |     `-StringLiteral 0x7fae7c136138 <col:12> 'char [3]' lvalue "Hi"
  |   `-ReturnStmt 0x7fae7c136298 <line:23:5, col:12>
  |     `-ImplicitCastExpr 0x7fae7c136280 <col:12> 'instancetype':'id' <BitCast>
  |       `-ImplicitCastExpr 0x7fae7c136268 <col:12> 'HelloViewController *' <LValueToRValue>
  |         `-DeclRefExpr 0x7fae7c136230 <col:12> 'HelloViewController *' lvalue ImplicitParam 0x7fae7c136008 'self' 'HelloViewController *'
  |-ObjCMethodDecl 0x7fae7baba628 <line:26:1, line:28:1> line:26:1 - sayOK:toSomeOne: 'void'
  | |-ImplicitParamDecl 0x7fae7c1362e0 <<invalid sloc>> <invalid sloc> implicit self 'HelloViewController *'
  | |-ImplicitParamDecl 0x7fae7c136348 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
  | |-ParmVarDecl 0x7fae7baba6b8 <col:16, col:27> col:27 content 'NSString *'
  | |-ParmVarDecl 0x7fae7baba720 <col:46, col:57> col:57 name 'NSString *'
  | `-CompoundStmt 0x7fae7c1364a0 <col:62, line:28:1>
  |   `-CallExpr 0x7fae7c136460 <line:27:5, col:20> 'void'
  |     |-ImplicitCastExpr 0x7fae7c136448 <col:5> 'void (*)(id, ...)' <FunctionToPointerDecay>
  |     | `-DeclRefExpr 0x7fae7c1363b0 <col:5> 'void (id, ...)' Function 0x7fae7baba0d8 'NSLog' 'void (id, ...)'
  |     `-ImplicitCastExpr 0x7fae7c136488 <col:11, col:12> 'id':'id' <BitCast>
  |       `-ObjCStringLiteral 0x7fae7c136428 <col:11, col:12> 'NSString *'
  |         `-StringLiteral 0x7fae7c136408 <col:12> 'char [7]' lvalue "123123"
  |-ObjCMethodDecl 0x7fae7baba8a8 <line:30:1, line:32:1> line:30:1 - protocolMethod 'void'
  | |-ImplicitParamDecl 0x7fae7c1364e8 <<invalid sloc>> <invalid sloc> implicit self 'HelloViewController *'
  | |-ImplicitParamDecl 0x7fae7c136550 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
  | `-CompoundStmt 0x7fae7c1366b0 <col:24, line:32:1>
  |   `-CallExpr 0x7fae7c136670 <line:31:5, col:18> 'void'
  |     |-ImplicitCastExpr 0x7fae7c136658 <col:5> 'void (*)(id, ...)' <FunctionToPointerDecay>
  |     | `-DeclRefExpr 0x7fae7c1365b8 <col:5> 'void (id, ...)' Function 0x7fae7baba0d8 'NSLog' 'void (id, ...)'
  |     `-ImplicitCastExpr 0x7fae7c136698 <col:11, col:12> 'id':'id' <BitCast>
  |       `-ObjCStringLiteral 0x7fae7c136638 <col:11, col:12> 'NSString *'
  |         `-StringLiteral 0x7fae7c136618 <col:12> 'char [5]' lvalue "3333"
  |-ObjCMethodDecl 0x7fae7babaa20 <line:34:1, line:36:1> line:34:1 + clmethod 'void'
  | |-ImplicitParamDecl 0x7fae7c1366f8 <<invalid sloc>> <invalid sloc> implicit self 'Class':'Class'
  | |-ImplicitParamDecl 0x7fae7c136760 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
  | `-CompoundStmt 0x7fae7c1368c0 <col:18, line:36:1>
  |   `-CallExpr 0x7fae7c136880 <line:35:5, col:19> 'void'
  |     |-ImplicitCastExpr 0x7fae7c136868 <col:5> 'void (*)(id, ...)' <FunctionToPointerDecay>
  |     | `-DeclRefExpr 0x7fae7c1367c8 <col:5> 'void (id, ...)' Function 0x7fae7baba0d8 'NSLog' 'void (id, ...)'
  |     `-ImplicitCastExpr 0x7fae7c1368a8 <col:11, col:12> 'id':'id' <BitCast>
  |       `-ObjCStringLiteral 0x7fae7c136848 <col:11, col:12> 'NSString *'
  |         `-StringLiteral 0x7fae7c136828 <col:12> 'char [6]' lvalue "32233"
  |-ObjCIvarDecl 0x7fae7c134fa8 <line:7:41> col:41 implicit _index 'NSInteger':'long' synthesize private
  |-ObjCPropertyImplDecl 0x7fae7c135008 <<invalid sloc>, col:41> <invalid sloc> index synthesize
  | |-ObjCProperty 0x7fae7c12f308 'index'
  | `-ObjCIvar 0x7fae7c134fa8 '_index' 'NSInteger':'long'
  |-ObjCMethodDecl 0x7fae7c135138 <col:41> col:41 implicit - index 'NSInteger':'long'
  `-ObjCMethodDecl 0x7fae7c135298 <col:41> col:41 implicit - setIndex: 'void'
    `-ParmVarDecl 0x7fae7c12f578 <col:41> col:41 index 'NSInteger':'long'

从上述语法树解析结果来看,- (void)hahaha函数在头文件中的定义:

| |-ObjCMethodDecl 0x7fae7bab7238 <line:11:1, col:15> col:1 - hahaha 'void'

和源文件中的实现:

  |-ObjCMethodDecl 0x7fae7bac0da0 <line:16:1, line:20:1> line:16:1 - hahaha 'NSInteger':'long'

属性index 相关的ast

| |-ObjCPropertyDecl 0x7fae7c12f308 <line:7:1, col:41> col:41 index 'NSInteger':'long' assign readwrite nonatomic unsafe_unretained
| |-ObjCMethodDecl 0x7fae7c12f388 <col:41> col:41 implicit - index 'NSInteger':'long'
| `-ObjCMethodDecl 0x7fae7c12f4e8 <col:41> col:41 implicit - setIndex: 'void'
|   `-ParmVarDecl 0x7fae7c12f578 <col:41> col:41 index 'NSInteger':'long'

|-ObjCIvarDecl 0x7fae7c134fa8 <line:7:41> col:41 implicit _index 'NSInteger':'long' synthesize private
  |-ObjCPropertyImplDecl 0x7fae7c135008 <<invalid sloc>, col:41> <invalid sloc> index synthesize
  | |-ObjCProperty 0x7fae7c12f308 'index'
  | `-ObjCIvar 0x7fae7c134fa8 '_index' 'NSInteger':'long'
  |-ObjCMethodDecl 0x7fae7c135138 <col:41> col:41 implicit - index 'NSInteger':'long'
  `-ObjCMethodDecl 0x7fae7c135298 <col:41> col:41 implicit - setIndex: 'void'
    `-ParmVarDecl 0x7fae7c12f578 <col:41> col:41 index 'NSInteger':'long'

会发现ObjCPropertyDecl,表示的是一个属性声明。其中包含了类名、变量名以及修饰关键字。 我们可以使用MatchFinder匹配ObjCPropertyDecl节点。

完整代码如下

#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;

namespace FPPlugin {
    // FPMatchHandler继承自`MatchFinder::MatchCallback`,我们可以在`run()`函数里面去判断应该使用`copy`关键字修饰,而没有使用 copy 修饰的 property。
    class FPMatchHandler: public MatchFinder::MatchCallback {
    private:
        CompilerInstance &CI;
        
        bool isUserSourceCode(const string filename) {
            if (filename.empty()) return false;
            
            // 非Xcode中的源码都认为是用户源码
            if (filename.find("/Applications/Xcode.app/") == 0) return false;
            
            return true;
        }
       // 是否需要使用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:
        FPMatchHandler(CompilerInstance &CI) :CI(CI) {}
        
        void run(const MatchFinder::MatchResult &Result) {
        // 获得结点
            const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
        //  如果结点存在并且是用户的代码
            if (propertyDecl && isUserSourceCode(CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str()) ) {
            // 获得结点的属性
                ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
             // 获得结点的类型
                string typeStr = propertyDecl->getType().getAsString();
            // 如果需要使用copy修饰
                if (propertyDecl->getTypeSourceInfo() && isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
                // 输出
                    cout<<"--------- "<<typeStr<<": 不是使用的 copy 修饰--------"<<endl;
                // 获得报错
                    DiagnosticsEngine &diag = CI.getDiagnostics();
                 // 报错
                    diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "--------- %0 不是使用的 copy 修饰--------")) << typeStr;
                }
            }
        }
    };
    
    class FPASTConsumer: public ASTConsumer {
    private:
        MatchFinder matcher;
        FPMatchHandler handler;
    public:
        FPASTConsumer(CompilerInstance &CI) :handler(CI) {
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);
        }
        
        void HandleTranslationUnit(ASTContext &context) {
            matcher.matchAST(context);
        }
    };

    class FPASTAction: public PluginASTAction {
    public:
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef iFile) {
            return unique_ptr<FPASTConsumer> (new FPASTConsumer(CI));
        }
        
        bool ParseArgs(const CompilerInstance &ci, const std::vector<std::string> &args) {
            return true;
        }
    };
}
// 注册插件
static FrontendPluginRegistry::Add<FPPlugin::FPASTAction> X("FPPlugin", "The FPPlugin desc");

最后CMD+B编译生成.dylib文件,找到插件对应的.dylib,右键show in finder

插件的使用

验证

我们可以在终端中使用命令的方式进行验证

//自己编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk/ -Xclang -load -Xclang 插件(.dylib)路径 -Xclang -add-plugin -Xclang 插件名 -c 资源文件(.h或者.m)
/Volumes/Study/llvm_all/llvm_release/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk/ -Xclang -load -Xclang /Volumes/Study/llvm_all/llvm_release/lib/FPPlugin.dylib  -Xclang -add-plugin -Xclang FPPlugin -c ./Hello.m

效果如下

--------- NSString *: 不是使用的 copy 修饰--------
In file included from ./Hello.m:1:
./Hello.h:12:1: warning: --------- NSString * 不是使用的 copy 修饰--------
@property (nonatomic , strong) NSString *qqq;
^
1 warning generated.
Xcode 集成插件
// -Xclang -load -Xclang (.dylib)动态库路径 -Xclang -add-plugin -Xclang 插件名字(namespace 的名字,名字不对则无法使用插件)
-Xclang -load -Xclang /Volumes/Study/llvm_all/llvm_release/lib/FPPlugin.dylib  -Xclang -add-plugin -Xclang FPPlugin

error: unable to load plugin '/Volumes/Study/llvm_all/llvm_release/lib/FPPlugin.dylib': 'dlopen(/Volumes/Study/llvm_all/llvm_release/lib/FPPlugin.dylib, 9): Symbol not found: __ZN4llvm23EnableABIBreakingChecksE
  Referenced from: /Volumes/Study/llvm_all/llvm_release/lib/FPPlugin.dylib
  Expected in: flat namespace
 in /Volumes/Study/llvm_all/llvm_release/lib/FPPlugin.dylib'
warning: Could not read serialized diagnostics file: error("Failed to open diagnostics file") (in target 'HelloWordApp' from project 'HelloWordApp')
Command CompileC failed with a nonzero exit code

Build Settings栏目中Add User-Defined Setting,分别是CCCXX

CC对应的是自己编译的clang的绝对路径,CXX对应的是自己编译的clang++的绝对路径。

如果遇到了Unknow argument -index-store-path Cannot specify -o when generating multiple output files错误

clang-10: error: unknown argument: '-index-store-path'
clang-10: error: cannot specify -o when generating multiple output files

则可以在Build Settings栏目中搜索index,将Enable Index-Wihle-Building FunctionalityDefault改为NO

修改为 LibTooling

在编写代码过程中,必不可少的一步就是Debug,没有任何程序是一蹴而就的.

在使用Plugin的模式下我们是不能打断点进行 Debug 的,但是我们可以在代码中加日志,然后在终端中执行命令看日志进行 Debug。这种方式的效率又太低

我们只需要把.dylib动态库变成可执行文件就能打断点 debug。LibTooling是一个不错的选择。使用 LibTooling 的话,我们只需要改动很少部分的代码就可以。

创建 LibTooling 项目及代码调整

我们创建一个名为FPPluginTooling的项目。
创建过程跟 创建插件步骤差不多只需要把FPPlugin替换为FPPluginTooling就可以。但FPPluginTooling目录下的CMakeLists.txt`的文件内容为

set(LLVM_LINK_COMPONENTS
    Support
)

add_clang_executable(FPPluginTooling
    FPPluginTooling.cpp
)

target_link_libraries(FPPluginTooling
    PRIVATE
    clangAST
    clangBasic
    clangDriver
    clangFormat
    clangLex
    clangParse
    clangSema
    clangFrontend
    clangTooling
    clangToolingCore
    clangRewrite
    clangRewriteFrontend
)

if (UNIX)
    set(CLANGXX__LING_OR_COPY create_symlink)
else()
    set(CLANGXX_LINK_OR_COPY copy)
endif()

llvm_xcode目录下执行$ cmake -G Xcode ../llvm,重新生成一下Xcode项目。Tooling项目在 Xcode 的Clang executables目录下可以找到。
将之前 FPPlugin 的代码复制过来,新增三个头文件

#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Tooling/Tooling.h"

新增一个命名空间

using namespace clang::tooling;

将FPASTAction的继承改为继承至ASTFrontendAction。

将FrontendPluginRegistry注册插件的方式注释。更改为main()函数方式

static llvm::cl::OptionCategory OptsCategory("QTPlugin");
int main(int argc, const char **argv) {
    CommonOptionsParser op(argc, argv, OptsCategory);
    ClangTool Tool(op.getCompilations(), op.getSourcePathList());
    return Tool.run(newFrontendActionFactory<FPPlugin::FPASTAction>().get());
}
输入源
/Users/geneqiao/Desktop/HelloWordApp/HelloWordApp/Hello.m
--
-isysroot
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
-isystem
-I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/10.0.0/include
-I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1
-I/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include
-F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks

上面在 -- 后面的参数,是传递给CI的Compilation DataBase的,而不是这个命令行工具本身的。比如我们的Hello.m,因为有#import <UIKit/UIKit.h>这么一条语句,以及继承了UIViewController,那么语法分析器(Sema)读到这里的时候就需要知道UIViewController的定义是从哪里来的,换句话说就是它需要找到定义UIViewController的地方。怎么找呢?通过指定的-I、-F这些参数指定的目录来寻找。--后面的参数,可以理解为如果你要编译Hello.m需要什么参数,那么这个后面就要传递什么参数给我们的 QTPlugin,否则就会看到Console里打出找不到xxx定义或者xxx.h文件的错误。当然因为一般的编译指令,会有-c参数指定源文件,但是--后面并不需要,因为我们在--前面就指定了。

-- 这种传参的方式还有另外一种方法,使用-extra-arg="xxxx"的方式指定编译参数,这样就不需要--了。

-extra-arg="-Ixxxxxx"
-extra-arg="-Fxxxxxx"
-extra-arg="-isysroot xxxxxx"
xxxxxx表示的路径

总结

Clang 提供的能力都是基于 Clang AST 接口的。这个接口的功能非常强大,除了能够获取符号在源码中的位置,还可以获取方法的调用关系,类型定义和源码里的所有内容。

以这个接口为基础,再利用 LibClang、 Clang Plugin 和 LibTooling 这些封装好的工具,就足够我们去开发出满足静态代码分析需求的工具了。

参考链接:
https://github.com/CYBoys/Blogs/blob/master/LLVM_Clang/LLVM%20%26%20Clang%20%E5%85%A5%E9%97%A8.md
https://time.geekbang.org/column/article/87844

上一篇下一篇

猜你喜欢

热点阅读