如何利用 Clang 为 App 提质

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

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

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

clang

Clang 的优势:

  • 第一,对于使用者来说,Clang 编译的速度非常快,对内存的使用率非常低,并且兼容 GCC。

  • 第二,对于代码诊断来说, Clang 也非常强大,Xcode 也是用的 Clang。使用 Clang 编译前端,可以精确地显示出问题所在的行和具体位置,并且可以确切地说明出现这个问题的原因,并指出错误的类型是什么,使得我们可以快速掌握问题的细节。这样的话,我们不用看源码,仅通过 Clang 突出标注的问题范围也能够了解到问题的情况。

  • 第三,Clang 对 typedef 的保留和展开也处理得非常好。typedef 可以缩写很长的类型,保留 typedef 对于粗粒度诊断分析很有帮助。但有时候,我们还需要了解细节,对 typedef 进行展开即可。

  • 第四,Fix-it 提示也是 Clang 提供的一种快捷修复源码问题的方式。在宏的处理上,很多宏都是深度嵌套的, Clang 会自动打印实例化信息和嵌套范围信息来帮助你进行宏的诊断和分析。

  • 第五,Clang 的架构是模块化的。除了代码静态分析外,利用其输出的接口还可以开发用于代码转义、代码生成、代码重构的工具,方便与 IDE 进行集成。

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

Clang 做了哪些事?

  • 首先,Clang 会对代码进行词法分析,将代码切分成 Token。我们可以把这些 Token 类型,分为下面这 4 类。
    关键字:语法中的关键字,比如 if、else、while、for 等;
    标识符:变量名;
    字面量:值、数字、字符串;
    特殊符号:加减乘除等符号。

  • 接下来,词法分析完后就会进行语法分析,将输出的 Token 先按照语法组合成语义,生成类似 VarDecl 这样的节点,然后将这些节点按照层级关系构成抽象语法树(AST)。
    其中 TranslationUnitDecl 是根节点,表示一个编译单元;Decl 表示一个声明;Expr 表示的是表达式;Literal 表示字面量,是一个特殊的 Expr;Stmt 表示陈述。
    除此之外,Clang 还有众多种类的节点类型。Clang 里,节点主要分成 Type 类型、Decl 声明、Stmt 陈述这三种,其他的都是这三种的派生。通过扩展这三类节点,就能够将无限的代码形态用有限的形式来表现出来了。

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-check、clang-fixit、clang-format;
  • 可以完全控制 Clang AST;
  • 能够和 Clang Plugins 共用一份代码。

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

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

  • 改变代码:可以改变 Clang 生成代码的方式。基于现有代码可以做出大量的修改。还可以进行语言的转换,比如把 OC 语言转成 JavaScript 或者 Swift。
  • 做检查:检查命名规范,增加更强的类型检查,还可以按照自己的定义进行代码的检查分析。
  • 做分析:对源码做任意类型分析,甚至重写程序。给 Clang 添加一些自定义的分析,创建自己的重构器,还可以基于工程生成相关图形或文档进行分析。

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

  • 语法检查工具 clang-check;
  • 自动修复编译错误工具 clang-fixit;
  • 自动代码格式工具 clang-format;
  • 新语言和新功能的迁移工具;
  • 重构工具。

如果你打算基于 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:是一个读取抽象语法树的抽象基类,我们可以重载下面两个函数:

  • HandleTopLevelDecl():解析顶级的声明(像全局变量,函数定义等)的时候被调用。
  • HandleTranslationUnit():在整个文件都解析完后会被调用。

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

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

  • VisitStmt():分析表达式。
  • VisitDecl():分析所有声明。

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 集成插件
  • 加载插件:
    打开需要加载插件的Xcode项目,在Build Settings栏目中的OTHER_CFLAGS添加上如下内容:
// -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

  • 设置编译器:
    由于Clang插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误
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 这些封装好的工具,就足够我们去开发出满足静态代码分析需求的工具了。

  • 比如,我们可以使用 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

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

推荐阅读更多精彩内容