OCLint 实现 Code Review 结合 Shell 脚本

Stella981
• 阅读 661

OCLint 实现 Code Review - 给你的代码提提质量

工程代码质量,一个永恒的话题。好的质量的好处不言而喻,团队成员间除了保持统一的风格和较高的自我约束力之外,还需要一些工具来统计分析代码质量问题。

本文就是针对 OC 项目,提出的一个思路和实践步骤的记录,最后形成了一个可以直接用的脚本。如果觉得文章篇幅过长,则直接可以下载脚本

OCLint is a static code analysis tool for improving quality and reducing defects by inspecting C, C++ and Objective-C code and looking for potential problems ...

从官方的解释来看,它通过检查 C、C++、Objective-C 代码来寻找潜在问题,来提高代码质量并减少缺陷的静态代码分析工具

OCLint 的下载和安装

有3种方式安装,分别为 Homebrew、源代码编译安装、下载安装包安装。 区别:

  • 如果需要自定义 Lint 规则,则需要下载源码编译安装
  • 如果仅仅是使用自带的规则来 Lint,那么以上3种安装方式都可以

1. Homebrew 安装

在安装前,确保安装了 homebrew。步骤简单快捷

brew tap oclint/formulae   
brew install oclint

2. 安装包安装

  • 进入 OCLint 在 Github 中的地址,选择 Release。选择最新版本的安装包(目前最新版本为:oclint-0.13.1-x86_64-darwin-17.4.0.tar.gz)

  • 解压下载文件。将文件存放到一个合适的位置。(比如我选择将这些需要的源代码存放到 Document 目录下)

  • 在终端编辑当前环境的配置文件,我使用的是 zsh,所以编辑 .zshrc 文件。(如果使用系统的终端则编辑 .bash_profile 文件)

    OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release
    export PATH=$OCLint_PATH/bin:$PATH
    
  • 将配置文件 source 一下。

    source .zshrc // 如果你使用系统的终端则执行 soucer .bash_profile
    
  • 验证是否安装成功。在终端输入 oclint --version

3. 源码编译安装

  • homebrew 安装 CMake 和 Ninja 这2个编译工具

    brew install cmake ninja
    
  • 进入 Github 搜索 OCLint,clone 源码

    gc https://github.com/oclint/oclint
    
  • 进入 oclint-scripts 目录,执行 ./make 命令。这一步的时间非常长。会下载 oclint-json-compilation-database、oclint-xcodebuild、llvm 源码以及 clang 源码。并进行相关的编译得到 oclint。且必须使用翻墙环境不然会报 timeout。如果你的电脑支持翻墙环境,但是在终端下不支持翻墙,可以查看我的这篇文章

    ./make
    
  • 编译结束,进入同级 build 文件夹,该文件夹下的内容即为 oclint。可以看到 build/oclint-release。方式2下载的安装包的内容就是该文件夹下的内容。

  • cd 到根目录,编辑环境文件,比如我 zsh 对应的 .zshrc 文件。编辑下面的内容

      OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release
      export PATH=$OCLint_PATH/bin:$PATH
    
  • source 下 .zhsrc 文件

    source .zshrc // source .bash_profile
    
  • 进入 oclint/build/oclint-release 目录执行脚本

    cp ~/Documents/oclint/build/oclint-release/bin/oclint* /usr/local/bin/
    ln -s ~/Documents/oclint/build/oclint-release/lib/oclint /usr/local/lib
    ln -s ~/Documents/oclint/build/oclint-release/lib/clang /usr/local/lib
    

    这里使用 ln -s,把 lib 中的 clang 和 oclint 链接到 /usr/local/bin 目录下。这样做的目的是为了后面如果编写了自己创建的 lint 规则,不必要每次更新自定义的 rule 库,必须手动复制到 /usr/local/bin 目录下。

  • 验证下 OCLint 是否安装成功。输入 oclint --version

    OCLint 实现 Code Review 结合 Shell 脚本

    注意:如果你采用源码编译的时候直接 clone 官方的源码会有问题,编译不过,所以提供了一个可以编译过的版本。分支切换到 llvm-7.0。

4. xcodebuild 的安装

xcode 下载安装好就已经成功安装了

5. xcpretty 的安装

先决条件,你的机器已经安装好了 Ruby gem.

gem install xcpretty

二、 自定义 Rule

OClint 提供了 70+ 项的检查规则,你可以直接去使用。但是某些时候你需要制作自己的检测规则,接下来就说说如何自定义 lint 规则。

  1. 进入 ~/Document/oclint 目录,执行下面的脚本

    oclint-scripts/scaffoldRule CustomLintRules -t ASTVisitor
    

    其中,CustomLintRules 就是定义的检查规则的名字, ASTVisitor 就是你继承的 lint 规则

    可以继承的规则有:ASTVisitor、SourceCodeReader、ASTMatcher。

  2. 执行上面的脚本,会生成下面的文件

    • Documents/oclint/oclint-rules/rules/custom/CustomLintRulesRule.cpp
    • Documents/oclint/oclint-rules/test/custom/CustomLintRulesRuleTest.cpp
  3. 要方便的开发自定义的 lint 规则,则需要生成一个 xcodeproj 项目。切换到项目根目录,也就是 Documents/oclint,执行下面的命令

     mkdir Lint-XcodeProject
     cd Lint-XcodeProject
     touch generate-lint-rules.sh
     chmod +x generate-lint-rules.sh
    

    给上面的 generate-lint-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
    
  4. 执行 generate-lint-rules.sh 脚本(./generate-lint-rules.sh)。如果出现下面的 Log 则说明生成 xcodeproj 项目成功

OCLint 实现 Code Review 结合 Shell 脚本 OCLint 实现 Code Review 结合 Shell 脚本

  1. 打开步骤4生成的项目,看到有很多文件夹,代表 oclint 自带的 lint 规则,我们自定义的 lint 规则在最下面。 OCLint 实现 Code Review 结合 Shell 脚本

关于如何自定义 lint 规则的具体还没有深入研究,这里给个例子

点击查看示例代码
#include "oclint/AbstractASTVisitorRule.h"
#include "oclint/RuleSet.h"

using namespace std;
using namespace clang;
using namespace oclint;
#include <iostream>

class MVVMRule : public AbstractASTVisitorRule<MVVMRule>
{
public:
    virtual const string name() const override
    {
        return "Property in 'ViewModel' Class interface should be readonly.";
    }

    virtual int priority() const override
    {
        return 3;
    }

    virtual const string category() const override
    {
        return "mvvm";
    }
    
    virtual unsigned int supportedLanguages() const override
    {
        return LANG_OBJC;
    }

#ifdef DOCGEN
    virtual const std::string since() const override
    {
        return "0.18.10";
    }

    virtual const std::string description() const override
    {
        return "Property in 'ViewModel' Class interface should be readonly.";
    }

    virtual const std::string example() const override
    {
        return R"rst(
.. code-block:: cpp

    @interface FooViewModel : NSObject // This is a "ViewModel" Class.
    
    @property (nonatomic, strong) NSObject *bar; // should be readonly.
    
    @end
        )rst";
    }

    virtual const std::string fileName() const override
    {
        return "MVVMRule.cpp";
    }

#endif

    virtual void setUp() override {}
    virtual void tearDown() override {}

    /* Visit ObjCImplementationDecl */
    bool VisitObjCImplementationDecl(ObjCImplementationDecl *node)
    {
        ObjCInterfaceDecl *interface = node->getClassInterface();
        
        bool isViewModel = interface->getName().endswith("ViewModel");
        if (!isViewModel) {
            return false;
        }
        for (auto property = interface->instprop_begin(),
            propertyEnd = interface->instprop_end(); property != propertyEnd; property++)
        {
            clang::ObjCPropertyDecl *propertyDecl = (clang::ObjCPropertyDecl *)*property;
            if (propertyDecl->getName().startswith("UI")) {
                addViolation(propertyDecl, this);
            }
            auto attrs = propertyDecl->getPropertyAttributes();
            bool isReadwrite = (attrs & ObjCPropertyDecl::PropertyAttributeKind::OBJC_PR_readwrite) > 0;
            if (isReadwrite && isViewModel) {
                addViolation(propertyDecl, this);
            }
        }
        return true;
    }
};

static RuleSet rules(new MVVMRule());
  1. 修改自定义规则后就需要编译。成功后在 Products 目录下会看到对应名称的 CustomLintRulesRule.dylib 文件,就需要复制到 /Documents/oclint/oclint-release/lib/oclint/rules。讲道理,生成新的 lint rule 文件,需要把新的 dylib 文件复制到 /usr/local/lib。因为我们在源代码安装的第4部,设置了 ln -s 链接,所以不需要每次复制到相应文件夹。

但是还是比较麻烦,每次都需要编译新的 lint rule 之后需要将相应的 dylib 文件复制到源代码目录下的 oclint-release/lib/oclint/rules 目录下,本着「可以偷懒绝不动手」的原则,在自定义的 rule 的 target 中,在 Build Phases 选项下 CMake PostBuild Rules 中的脚本下将下面的代码复制进去

cp /Users/liubinpeng/Documents/oclint/Lint-XcodeProject/rules.dl/Debug/libCustomLintRulesRule.dylib /Users/liubinpeng/Documents/oclint/build/oclint-release/lib/oclint/rules/libCustomLintRulesRule.dylib
  1. 规则限定的3个类说明:

    RuleBase | |-AbstractASTRuleBase | |_ AbstractASTVisitorRule | |_AbstractASTMatcherRule | |-AbstractSourceCodeReaderRule

  • AbstractSourceCodeReaderRule:eachLine 方法,读取每行的代码,如果想编写的规则是需要针对每行的代码内容,则可以继承自该类
  • AbstractASTVisitorRule:可以访问 AST 上特定类型的所有节点,可以检查特定类型的所有节点是递归实现的。在 apply 方法内可以看到代码实现。开发者只需要重载 bool visit* 方法来访问特定类型的节点。其值表明是否继续递归检查
  • AbstractASTMatcherRule:实现 setUpMatcher 方法,在方法中添加 matcher,当检查发现匹配结果时会调用 callback 方法。然后通过 callback 方法来继续对匹配到的结果进行处理
  1. 知其所以然 oclint 依赖与源代码的语法抽象树(AST)。开源 clang 是 oclint 获的语法抽象树的依赖工具。你如果想对 AST 有个了解,可以查看这个视频

如果想查看某个文件的 AST 结构,你可以进入该文件的命令行,然后执行下面的脚本

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

三、 Homebrew 方式安装的 oclint 如何使用自定义规则

  1. 查看 OCLint 安装路径

    which oclint // 输出:/usr/local/bin/oclint ls -al /usr/local/bin/oclint // 输出:本机安装路径

  2. 把上面生成的新的 lint rule 下的 dylib 文件复制到步骤1得到的额本机安装路径下

四、 使用 oclint

在命令行中使用

  1. 如果项目使用了 Cocopod,则需要指定 -workspace xxx.workspace
  2. 每次编译之前需要 clean

实操:

  • 进入项目

    cd /Workspace/Native/iOS/lianhua
    
  • 查看项目基本信息

    xcodebuild -list
    //输出
    information about project "BridgeLabiPhone":
      Targets:
          BridgeLabiPhone
          lint
    
      Build Configurations:
          Debug
          Release
    
      If no build configuration is specified and -scheme is not passed then "Release" is used.
    
      Schemes:
          BridgeLabiPhone
          lint
    
  • 编译

    xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace  clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json
    

    编译成功后,会在项目的文件夹下出现 compile_commands.json 文件

  • 生成 html 报表

     oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html
    

    看到有报错,但是报错信息太多了,不好定位,利用下面的脚本则可以将报错信息写入 log 文件,方便查看

    oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html 2>&1 | tee 1.log
    

    报错信息是:oclint: error: one compiler command contains multiple jobs: 查找资料,解决方案如下

    • 将 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 设置为 NO

    • 在 podfile 中 target 'xx' do 前面添加下面的脚本

      post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO" end end end

    然后继续尝试编译,发现还是报错,但是报错信息改变了,如下

    OCLint 实现 Code Review 结合 Shell 脚本

    看到报错信息是默认的警告数量超过限制,则 lint 失败。事实上 lint 后可以跟参数,所以我们修改脚本如下

    oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html -rc LONG_LINE=9999 -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999
    

    生成了 lint 的结果,查看 html 文件可以具体定位哪个代码文件,哪一行哪一列有什么问题,方便修改。 OCLint 实现 Code Review 结合 Shell 脚本

  • 如果项目工程太大,整个 lint 会比较耗时,所幸 oclint 支持针对某个代码文件夹进行 lint

    oclint-json-compilation-database -i 需要静态分析的文件夹或文件 -- -report-type html -o oclintReport.html  其他的参数
    
  • 参数说明

    名称

    描述

    默认阈值

    CYCLOMATIC_COMPLEXITY

    方法的循环复杂性(圈负责度)

    10

    LONG_CLASS

    C类或Objective-C接口,类别,协议和实现的行数

    1000

    LONG_LINE

    一行代码的字符数

    100

    LONG_METHOD

    方法或函数的行数

    50

    LONG_VARIABLE_NAME

    变量名称的字符数

    20

    MAXIMUM_IF_LENGTH

    if语句的行数

    15

    MINIMUM_CASES_IN_SWITCH

    switch语句中的case数

    3

    NPATH_COMPLEXITY

    方法的NPath复杂性

    200

    NCSS_METHOD

    一个没有注释的方法语句数

    30

    NESTED_BLOCK_DEPTH

    块或复合语句的深度

    5

    SHORT_VARIABLE_NAME

    变量名称的字符数

    3

    TOO_MANY_FIELDS

    类的字段数

    20

    TOO_MANY_METHODS

    类的方法数

    30

    TOO_MANY_PARAMETERS

    方法的参数数

    10

在 Xcode 中使用

  • 在项目的 TARGETS 下面,点击下方的 "+" ,选择 cross-platform 下面的 Aggregate。输入名字,这里命名为 Lint OCLint 实现 Code Review 结合 Shell 脚本

  • 选择对应的 TARGET -> lint。在 Build Phases 下 Run Script 下写下面的脚本代码

    export LC_CTYPE=en_US.UTF-8
    cd ${SRCROOT}
    xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json && oclint-json-compilation-database -e Pods -- -report-type xcode
    
  • 说明,虽然有时候没有编译通过,但是看到如下图的关于代码相关的 warning 则达到目的了。 OCLint 实现 Code Review 结合 Shell 脚本

  • lint 结果如下,根据相应的提示信息对代码进行调整。当然这只是一种参考,不一定要采纳 lint 给的提示。 OCLint 实现 Code Review 结合 Shell 脚本

脚本化

每次都在终端命令行去写 lint 的脚本,效率很低,所以想做成 shell 脚本。需要的同学直接直接拷贝进去,直接在工程的根目录下使用,我这边是一个 Cocopod 工程。拿走拿走别客气

#!/bin/bash

COLOR_ERR="\033[1;31m"    #出错提示
COLOR_SUCC="\033[0;32m"  #成功提示
COLOR_QS="\033[1;37m"  #问题颜色
COLOR_AW="\033[0;37m"  #答案提示
COLOR_END="\033[1;34m"     #颜色结束符

# 寻找项目的 ProjectName
function searchProjectName () {
  # maxdepth 查找文件夹的深度
  find . -maxdepth 1 -name "*.xcodeproj"
}

function oclintForProject () {
    # 预先检测所需的安装包是否存在
    if which xcodebuild 2>/dev/null; then
        echo 'xcodebuild exist'
    else
        echo '🤔️ 连 xcodebuild 都没有安装,玩鸡毛啊? 🤔️'
    fi

    if which oclint 2>/dev/null; then
        echo 'oclint exist'
    else
        echo '😠 完蛋了你,玩 oclint 却不安装吗,你要闹哪样 😠'
        echo '😠 乖乖按照博文:https://github.com/FantasticLBP/knowledge-kit/blob/master/第一部分%20iOS/1.63.md 安装所需环境 😠'
    fi
    if which xcpretty 2>/dev/null; then
        echo 'xcpretty exist'
    else
        gem install xcpretty
    fi


    # 指定编码
    export LANG="zh_CN.UTF-8"
    export LC_COLLATE="zh_CN.UTF-8"
    export LC_CTYPE="zh_CN.UTF-8"
    export LC_MESSAGES="zh_CN.UTF-8"
    export LC_MONETARY="zh_CN.UTF-8"
    export LC_NUMERIC="zh_CN.UTF-8"
    export LC_TIME="zh_CN.UTF-8"
    export xcpretty=/usr/local/bin/xcpretty # xcpretty 的安装位置可以在终端用 which xcpretty找到

    searchFunctionName=`searchProjectName`
    path=${searchFunctionName}
    # 字符串替换函数。//表示全局替换 /表示匹配到的第一个结果替换。 
    path=${path//.\//}  # ./BridgeLabiPhone.xcodeproj -> BridgeLabiPhone.xcodeproj
    path=${path//.xcodeproj/} # BridgeLabiPhone.xcodeproj -> BridgeLabiPhone
    
    myworkspace=$path".xcworkspace" # workspace名字
    myscheme=$path  # scheme名字

    # 清除上次编译数据
    if [ -d ./derivedData ]; then
        echo -e $COLOR_SUCC'-----清除上次编译数据derivedData-----'$COLOR_SUCC
        rm -rf ./derivedData
    fi

    # xcodebuild clean
    xcodebuild -scheme $myscheme -workspace $myworkspace clean


    # # 生成编译数据
    xcodebuild -scheme $myscheme -workspace $myworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json

    if [ -f ./compile_commands.json ]; then
        echo -e $COLOR_SUCC'编译数据生成完毕😄😄😄'$COLOR_SUCC
    else
        echo -e $COLOR_ERR'编译数据生成失败😭😭😭'$COLOR_ERR
        return -1
    fi

    # 生成报表
    oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \
    -rc LONG_LINE=200 \
    -disable-rule ShortVariableName \
    -disable-rule ObjCAssignIvarOutsideAccessors \
    -disable-rule AssignIvarOutsideAccessors \
    -max-priority-1=100000 \
    -max-priority-2=100000 \
    -max-priority-3=100000

    if [ -f ./oclintReport.html ]; then
        rm compile_commands.json
        echo -e $COLOR_SUCC'😄分析完毕😄'$COLOR_SUCC
    else 
        echo -e $COLOR_ERR'😢分析失败😢'$COLOR_ERR
        return -1
    fi
    echo -e $COLOR_AW'将为大爷自动打开 lint 的分析结果'$COLOR_AW
    # 用 safari 浏览器打开 oclint 的结果
    open -a "/Applications/Safari.app" oclintReport.html
}

oclintForProject

同类型的文章:

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这