从Oclint开始接触Clang编译

4,321 阅读7分钟

##前置工作

  • Oclint(静态代码分析工具)
  • Xcpretty(格式化输出工具)
    • 安装: sudo gem install -n /usr/local/bin xcpretty
  • Cmake(编译工具,这里用来构建LLVM和Xcode工程)
    • 来源:cmake.org/download/
    • 安装:运行cmake图形界面程序,在左上角的选项栏中选择Tools,点击How to install for Command Line Use,官方给出了三种安装cmake command line tool的方法,即终端能够识别cmake命令的方法。我选择了官方给出的第二种方法,即复制sudo"/Applications/CMake.app/Contents/bin/cmake-gui" --install 命令到终端,然后运行
  • Ninja(比cmake更小的编译工具,这里用来构建LLVM)
    • 来源:github.com/ninja-build…
    • 安装:将下好的Ninja解压, 拷贝到一个系统目录中 /usr/bin 来完成安装(此处有坑,该文件夹权限获取:重启Mac,按住command+R,进入recovery模式。选择打开Utilities下的终端,输入:csrutil disable并回车,然后正常重启Mac即可).

##前言 Clang是llvm的编译器前端,非常适合进行源码分析.目前开源的oclint就是基于clang进行的代码静态检查.工作中遇到了一些问题需要进行代码分析,所以学习了一下相关知识.我们日常用的clang有这两种: 1.Xcode内部自带的Clang Static Analyzer(简称CSA) 2.OCLint 这两者都是基于Clang的前端编译,CSA由于被内置,所以使用起来比较方便,但是可用的检查规则比较少,只有16条,大部分是核心向的功能例如空指针检测,类型转换检测,空判断检测,内存泄漏检测,无法检测代码风格,可扩展性比较差。 OCLint可用的检查规则有70+条,比OSA支持的规则多很多,还支持自定义规则,所以选择了OCLint。

##准备开发环境

  • 安装Oclint,得到的目录结构如下:
.
├── README.md
├── oclint-core
├── oclint-driver
├── oclint-metrics
├── oclint-reporters
├── oclint-rules
└── oclint-scripts

  • cd进入oclint-scripts文件加,执行./make。大约30分钟后编译完成,大概过程是下载LLVM、clang的源代码,编译LLVM、clang与OCLint的默认规则。

  • 编译成功后就可以写规则并且编译执行了。为了方便,OCLint提供了一个叫scaffoldRule的脚本程序,它在oclint-rules目录下。我们通过他传入要生成的规则名,级别,类型,脚本就会在目录oclint-rules/rules/custom/自动帮我们生成一个模板代码,并且加入编译路径中。举个例子:

# 生成一个名为BGTestRule类型是ASTVisitor的规则模板
python scaffoldRule BGTestRule -t ASTVisitor

生成两个文件:

# CMakeLists.txt 是对规则BGTestRule的编译描述,由make程序在编译时使用。

├── custom
│   ├── CMakeLists.txt
│   └── BGTestRule.cpp

  • 接着就可以对新添加的内容进行编译了,不过相比于重新执行./make来说有一个更加优雅的办法,就是将规则相关的内容整合成一个Xcode工程,并且我们的每个规则都是一个scheme,编译时可以只选择编译那个选择的规则生成对应的dylib。很简单,OCLint工程使用CMakeLists的方式维护各个文件的依赖关系,我们可以使用CMake自带的功能将这些CMakeLists生成一个xcodeproj工程文件。
# 在OCLint源码目录下建立一个文件夹,我这里命名为oclint-xcoderules

mkdir oclint-xcoderules
cd oclint-xcoderules

# 创建一个脚本(代码如下段),并执行它(我写的方便修改参数,其实里面就一句命令,直接执行也行)(PS:刚创建的文件是没有执行权限的,不要忘了chmod)

./create-xcode-rules.sh

脚本内容:

#! /bin/sh -e

cmake -G Xcode -D CMAKE_CXX_COMPILER=../build/llvm-install/bin/clang++  -D CMAKE_C_COMPILER=../build/llvm-install/bin/clang -D OCLINT_BUILD_DIR=../build/oclint-core -D OCLINT_SOURCE_DIR=../oclint-core -D OCLINT_METRICS_SOURCE_DIR=../oclint-metrics -D OCLINT_METRICS_BUILD_DIR=../build/oclint-metrics -D LLVM_ROOT=../build/llvm-install/ ../oclint-rules

执行

sh ./create-xcode-rules.sh

于是我们就得到了Xcode工程:

├── CMakeCache.txt
├── CMakeFiles
├── CMakeScripts
├── OCLINT_RULES.build
├── OCLINT_RULES.xcodeproj
├── cmake_install.cmake
├── create-xcode-rules.sh
├── lib
├── rules
└── rules.dl

打开OCLINT_RULES.xcodeproj:

  • 选择自己的规则,编译,成功后就可以在Products下看到生成的dylib了.

  • Jenkins/Xcode 中写入oclint的命令:

myworkspace=XXXXXX.xcworkspace # 替换workspace的名字
myscheme=XXXXX # 替换scheme的名字
xcodebuild -workspace $myworkspace -scheme $myscheme clean&&
xcodebuild -workspace $myworkspace -scheme $myscheme \
-configuration Debug \
COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database -o compile_commands.json&&
oclint-json-compilation-database -e Pods -- \
-report-type pmd -o pmd.xml  \
-R 这里是你编译生成的dylib目录 (ps :/oclint/oclint-xcoderules/rules.dl/Debug) \
-max-priority-1=9999 \
-max-priority-2=9999 \
-max-priority-3=9999; \
rm compile_commands.json;
if [ -f ./pmd.xml ]; then echo '-----分析完毕-----'
else echo "-----分析失败-----"; fi

##如何自定义规则 通过查看官方文档,我们发现OCLint的原理是调用clang的API把一个个源文件生成一个一个AST语法树,然后遍历树中的每个节点传入各个规则的整个过程,在Xcode8之前,Xcode各种代码分析插件也是这样的原理操作的,详情看参考文档4,5。

###分析默认规则 下面的例子是默认规则中较为简单的一个:判断行长度是否超过了限制长度

class LongLineRule : public AbstractSourceCodeReaderRule
{
public:
	//重载方法  输出规则名称
    virtual const string name() const override
    {
        return "long line";
    }
	//重载方法  输出规则优先级
    virtual int priority() const override
    {
        return 3;
    }
	//重载方法  输出规则分类
    virtual const string category() const override
    {
        return "size";
    }

	#ifdef DOCGEN
	//重载方法  输出规则使用版本
    virtual const std::string since() const override
    {
        return "0.6";
    }
	//重载方法  输出规则描述
    virtual const std::string description() const override
    {
        return "当某行代码长度过长, "
            "它很大程度上损害了可读性。建议将长行代码分割成多行.";
    }
	//重载方法  输出规则示例
    virtual const std::string example() const override
    {
        return R"rst(
	.. code-block:: cpp

    void example()
    {
        int a012345678901234567890123456789...1234567890123456789012345678901234567890123456789;
    }
        )rst";
    }
	//重载方法  输出规则可配置参数
    virtual const std::map<std::string, std::string> thresholds() const override
    {
        std::map<std::string, std::string> thresholdMapping;
        thresholdMapping["LONG_LINE"] = "每行代码长度限制, 默认长度 200 .";
        return thresholdMapping;
    }
	#endif
	//核心回调 这个回调返回源文件中的每一行 规则方法就写这里
    virtual void eachLine(int lineNumber, string line) override
    {
        int threshold = RuleConfiguration::intForKey("LONG_LINE", 200);
        int currentLineSize = line.size();
        if (currentLineSize > threshold)
        {
            string description = "该行长度: " + toString<int>(currentLineSize) +
            " 字符,默认规则限制: " + toString<int>(threshold);
            //返回闭包 此时产生一个警告
            addViolation(lineNumber, 1, lineNumber, currentLineSize, this, description);
        }
    }
};

###简单的自定义规则: 到这里我们看新创建的规则模板就会发现,模板中这些以Visit开头的百十个函数都是OCLint提供给开发者的回调函数.只要在这写回调方法中写规则的核心代码就OK啦. 示例:

	//重载方法 源文件中所有的变量都会到这里 传入的参数是AST树节点的类型
 	bool VisitVarDecl(VarDecl *declaration)
    {
        checkVarName(declaration);
        return true;
    }
    //检测变量是否合法  
    void checkVarName(VarDecl *decl)
    {
        StringRef className = decl -> getName();
        
        //必须以小写字母开头
        char c = className[0];
        if (isUppercase(c))
        {
            //修正提示
            std::string tempName = className;
            tempName[0] = toUppercase(c);
            StringRef replacement(tempName);
            
            string description = tempName + "首字母应该为小写";
            //抛出警告
            addViolation(decl, this, description);
        }
    }

分析AST语法树

如无必要,勿增实体, 分析AST的文章好多,详情看参考文档2,6.

###难点 规则代码需用C++来写,复杂规则需要查文档->dump看AST语法树->生成dylib->复制dylib到OCLint规则目录下->执行检测->查看报告->发现问题->修改代码重新生成dylib,暂时还未找到解决方法.

##总结 如上所述,基于Clang的Oclint用于风格检查,可以发现和修改的更多是一种格式上的约定和某些明显的不容许或无效逻辑,虽可解决不少问题,但是也有其局限性。实际工作中,一方面可限制使用某些固定的风格,更重要的是保持团队风格的统一和规范,提高其可读性。

##参考文档