最近项目正在完善一套代码规范,相比于用管理人的方式去规定必须要怎么做,不如直接用工程化的思想,让编译器帮我们检查,更多关于CleanCode的内容。我们可以从语法分析或静态分析(语义分析)入手,检查代码。对于代码规范,一般是命名、结构、写法上的规定,通过检查语法结构就能搞定,不用结合语境。并且语法分析相比于静态分析,也可以在更早的阶段暴露出问题。

对于静态分析的话,已有OCLint开源方案。对于语法分析的话,通过Clang Plugin就可以做到,Clang Plugin就是一个Clang的编译器前端插件,使用Clang提供的语法分析环境。不得不说,着实佩服滴滴,早在这方面做过探索,相关文章

虽说,代码规范这个东西不可能出现一个万金油开源方案,然后团队直接上手使用,每个团队都要根据自身情况,量身定做团队内的规范。但是,我这里提供一个有高拓展性的代码规范检查基础工具,供任何团队在其上面轻松做定制、拓展。

本篇文章就介绍一下如何做一个Clang Plugin和我提供的面向Objective-C的开源方案Sherlock。对于Swift已有开源方案SwiftLint

Clang Plugin

在制作自己的Clang Plugin或使用Sherlock之前,我们先要找一个样例工程,让Clang Plugin整合到Xcode中,把整个整合过程流畅的跑通。这有命令行和生成Xcode两种方式,原理都一样。如果你想试试命令行的方式,就参考滴滴的那篇文章。我使用的是Xcode的方式,更加便于调试代码。整个插入过程都是参照这篇文章,这篇文章写的时间在2014年,clang当时的版本还是3.3,我写这篇文章的时候已经是2017年,clang的版本为3.9。文章中的一些步骤需要小改一下。

整体的流程是这样的,先将LLVM和Clang源码先clone下来,然后通过cmake工具编译一遍,确保没问题。再将你写的插件,通过cmake工具编译一遍,生成dylib。你写的插件由于需要引用Clang提供的接口,在生成dylib时,需要与Clang和LLVM的一些lib链接,所以要clone下来源码。得到dylib后,在Xcode的Plugin文件夹下,加入HackedClang.xcplugin和HackedBuildSystem.xcspec。最后,在你需要加入Plugin的工程中加入dylib,配置一下就行了。下面直接进入命令行开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cd /放llvm和clang源码的根路径
sudo mkdir llvm
sudo chown `你的用户名` llvm
cd llvm
export LLVM_HOME=`pwd`

git clone -b release_39 https://github.com/llvm-mirror/llvm.git llvm
git clone -b release_39 https://github.com/llvm-mirror/clang.git llvm/tools/clang
git clone -b release_39 https://github.com/llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra
git clone -b release_39 https://github.com/llvm-mirror/compiler-rt.git llvm/projects/compiler-rt

mkdir llvm_build
cd llvm_build
cmake ../llvm -DCMAKE_BUILD_TYPE:STRING=Release
make -j`sysctl -n hw.logicalcpu`

接着下载文章中的样例工程。以下几点要注意:

注意clone的版本,release_39是写这篇文章时的最新版本3.9。

由于LLVM和Clang的版本变动,以前的一些lib不需要再链接,再加上新版本基于c++11,需要对CMakeList做以下修改:

  • 注意替换你设置的根目录,确保CMakeList中的四个include都可以找到。
  • set (LLVM_LIBS)删掉LLVMJIT、LLVMipa
  • set (USER_LIBS)加入z(压缩lib)
  • 加入set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD “c++11”)
  • 加入set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY “libc++”)

然后用以下命令生成Xcode:

1
2
3
4
mkdir build
cd build
cmake -G Xcode ..
open ToyClangPlugin.xcodeproj

然后build,成功后可以在lib/Debug/目录下找到dylib。

然后下载HackXcodeing,并且注意以下两点:

将HackXcodeing中的ExecPath中的根路径更改为你的根路径。

将HackedClang.xcspec文件中的NO = ( “-Wno-receiver-is-weak” ); 更改为NO = ();

HackXcodeing源文件作者署名是Apple,但是网上查不到原出处,Apple官方应该没有摆明支持这个东西,但这和线上App没关系,可以放心用,只不过以后Xcode升级可能会ban掉这种方式。下一步,加入到Xcode的插件文件夹下。

1
2
3
sudo mv HackedClang.xcplugin `xcode-select -print-path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins

sudo mv HackedBuildSystem.xcspec `xcode-select -print-path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications

注意,原文中有一步是关掉language Modules,不要做这一步。

接着,在Compiler for C/C++/Objective-C中选择Clang LLVM Trunk:

xcode_clangplugin_compiler

最后,在OTHER_CFLAGS中加入dylib的路径:

1
2
3
4
5
6
7
8
-Xclang 
-load
-Xclang
/放llvm和clang源码的根路径/llvm/toy_clang_plugin/build/lib/Debug/ToyClangPlugin.dylib
-Xclang
-add-plugin
-Xclang
ToyClangPlugin

xcode_clangplugin_flags

run一下你的项目,就可以看到插件成功插入了。

Sherlock

Yep, This is “the detective”, the one with a magnifying glass.

Sherlock是一个有高拓展性的代码规范检查基础工具,您的团队可以在Sherlock的基础上,轻松做定制、拓展。Sherlock是一个C++写的Clang Plugin,面向检查Objective-C代码。注意,使用Sherlock前必须要先将Clang Plugin整合到Xcode中。

buildIn base rule

首先看看Sherlock提供的基本规则,首先是给Warning的:

  • interface_illegalcharacter:项目文件命名中有”“。
  • project_prefix:项目中间接或直接继承自UIVIewController的controller命名没有以XX开头。
  • property_atomic:属性声明为atomic。
  • property_unsafe:属性声明为unsafe_unretained。
  • property_copy:NSArray、NSNumber、NSString、block类型属性声明不为copy。
  • weak_protocol:protocol类型的属性声明不为weak。
  • category_methodnaming:给系统类添加的Category方法命名没有以XX开头。
  • initializer_design:类没有用Designated Initializer设计init初始化方法。

然后是直接给Error的:

  • mutable_property_copy:可变数据类型属性声明为copy。(copy会制作一个不可变数据深复制)
  • weak_proxy:调用NSTimer或CADisplayLink方法,并将self作为参数传入。(容易引起循环引用,推荐使用WeakProxy)
  • super_call:调用[super respondsToSelector:]或[super conformsToProtocol:]。(这样调用,等价于检查自己是否实现,没意义)
  • observe_pairing:在一个编译单位中,添加NSNotificationCenter或KVO的观察者和移除观察者的操作数量不配对。(忘记移除观察者)

Configure

使用Sherlock前你需要在SherlockConfigure.hpp文件中修改ProjectAbsoluteRootPath为项目的根路径。然后,可以提供一个脚本的路径给RuleAbsolutePath字段,在脚本中进行Configure。现在支持的Configure有以下几项:

  • disabled_rules:将规则名称以”-“开头,紧跟空格,如- initializer_design,可以关掉相应的规则。
  • force_cast:可以键入Error或Warning,强制所有rule报告错误或是警告。
  • project_prefix:键入比如Sherlock,提供项目的前缀名。
  • blacklist_path:键入比如- Pods,忽略所有项目中Pods目录下的编译单元。
  • blacklist_prefix:键入比如- AF,忽略所有Interface、Extension、implementation、Category以AF作为前缀的代码。

注意,所有以- 开头代表的多个选项必须以另起一行~end结束。样例Configure文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
disabled_rules:
- interface_illegal_character
- project_prefix
- property_atomic
- property_unsafe
- property_copy
- weak_protocol
- category_method_naming
- initializer_design
- mutable_property_copy
- weak_proxy
- super_call
- observe_pairing
~end

force_report: Warning

project_prefix: Sherlock

blacklist_path:
- Pods
- ThirdPart
~end

blacklist_prefix:
- AF
- SD
- FM
- YY
~end

还有一点需注意,如果你想忽略所有Pod下的第三方编译单元,只在blacklist_path中加入- Pods是行不通的。预处理的时候,会将引入编译单元合并,任意编译单元引入第三方,都会导致第三方编译单元被检查。如果想要完全忽略第三方编译单元要在blacklist_prefix加入前缀。

CustomRule

Sherlock提供了良好的拓展性,但是你还是需要一个clang的AST知识才能编写检查规则代码。添加一条新规则有如下几步:

  • 为新的规则取一个名字,与原有规则不能重名,名字将作为规则的识别。然后加入到SherlockConfigure.hpp文件中的buildInBaseRules结构里。
  • 在Sherlock.cpp创建一个新方法编写检查规则代码,在调用该方法的地方调用isBuildInBaseRuleEnable方法查看是否被关闭。

如果有更多的自定义Configure,从SherlockParser.hpp中的tupleRuleMap或setRuleMap就可以找到解析出来的字符串。

编写Clang Plugin的“拦路虎”

当然,在写Sherlock的时候也遇到了“拦路虎”,这里和大家分享一下。

  • Clang Plugin必须用C++写。
  • 必须熟悉Clang开放的API,Clang有文档,但是解释语言比较简短,光看文档得到的信息有限。
  • 无法快速打印出所需的Debug信息。
  • 语法分析能力有限,没有程序上下文的信息,不能跨编译单元共享数据,也就是没有跨编译单元的全局数据。

实际上,前三点都能慢慢克服。C++只要会一些基本语法就能快速上手,并不复杂。光看Clang开发的文档确实不靠谱,更可靠的方法就是用clang -fmodules -fsyntax-only -Xclang -ast-dump命令dump出AST然后观察AST,再去源码看看相应类做了什么,很快也能熟悉Clang开放的API。关于无法快速打印出所需的Debug信息,这里提一个建议就是想办法把Debug信息当做Warning打出来。

对于最后一点,Clang Plugin本来就是在做语法分析阶段,不能提供语义分析的能力。如果有更复杂的需求做不了,请先考虑一下是不是因为想在语法分析阶段做静态分析的工作了。

推荐文章

官方文档 https://clang.llvm.org/docs/RAVFrontendAction.html

AST https://jonasdevlieghere.com/understanding-the-clang-ast/