添加编译警告的另一种方式:clang 开发入门

2,606 阅读9分钟

添加编译警告,除了使用上文 开发 clang 插件:0 基础感受底层组 提到的 clang 插件 ,

也可以直接开发 clang

1, 开发 clang,使用 ninja ,才能保证正常的开发速度

使用 Xcode 编译 clang, 就慢了

可以使用 ninja + Xcode 配合开发

使用 Xcode 的代码自动补全、代码提示、编译检查、函数跳转,方便

1.1 安装 ninja

采用 ninja 构建
  • 检查安装了没有

brew list | grep ^ninja

  • 去安装

brew install ninja

1.2 下载工程

下载 llvm-project

git clone https://github.com/llvm/llvm-project

1.3 代码生成与编译

这一步,会在后面反复使用,简称为 job_O
  • 使用 Ninja 生成 llvm 项目

进入

cd /Users/jzd/Movies/A_B/llvm-project

等价于

cd /yourPath/llvm-project

代码生成

cmake -S llvm -B build -G Ninja -DLLVM_ENABLE_PROJECTS="clang;libcxx;libcxxabi"

  • 项目编译

cd /yourPath/llvm-project/build

ninja clang

效果:

➜ build git:(main) ninja clang

[3345/3345] Creating executable symlink bin/clang

补充: Xcode 相关,见上文

1.4 本文的例子是,检查 if 语句的过度嵌套

看下效果,简单的 if 判断,不报错

复杂的,才报错

要解决的问题

怎样算复杂? 至少 3 层不同操作符,计算的嵌套

截屏2021-09-01 下午7.33.53.png

最后的效果

截屏2021-09-01 下午11.52.57.png

2. clang 开发,阶段一,识别 if 语句的 AST, 和简单的 warning 处理

添加 if 语句过度嵌套的编译警告
来一小段,编译原理

编译啊,预处理,语法分析,词法分析,语义分析,拿到 AST

拿到完整的抽象语法树,分析 if 的节点,是不是过于复杂

  • 先对代码进行解析,parse

  • 再语义分析,semantic analysis

2.1 定位到 clang 源代码,对 AST 中的 if 节点,写日志

定位到语义分析文件

/yourPath/llvm-project/clang/lib/Sema/SemaStmt.cpp

里面的这个方法,IF statement

添加两行日志代码,代码生成与编译,就是 job_O

补充: 怎么定位到的,可以看我在 CSDN 的笔记 clang 学习辅助

StmtResult Sema::ActOnIfStmt(SourceLocation IfLoc, bool IsConstexpr,
                             SourceLocation LParenLoc, Stmt *InitStmt,
                             ConditionResult Cond, SourceLocation RParenLoc,
                             Stmt *thenStmt, SourceLocation ElseLoc,
                             Stmt *elseStmt) {

//...

// 添加下面两句
llvm:: dbgs() << "处于 ActOnIfStmt, 发现了 if 条件判断 \n";
    CondExpr->dump();

  return BuildIfStmt(IfLoc, IsConstexpr, LParenLoc, InitStmt, Cond, RParenLoc,
                     thenStmt, ElseLoc, elseStmt);
}
2.1.1, 看简单效果
  • 上例子

➜ build git:(main) ✗ cat /xxx/test.cpp

代码很简单

void test(int a, int b){
    if (a > 0 && b > 0){
        
    }
}
  • 命令, ( 这一步,调试频繁,下文简称为 job_debug )

➜ build git:(main) ✗ /yourPath/llvm-project/build/bin/clang -c /xxx/test.cpp

dump 到的 AST

处于 ActOnIfStmt, 发现了 if 条件判断 
BinaryOperator 0x7fe8e9075ea0 '_Bool' '&&'
|-BinaryOperator 0x7fe8e9075e08 '_Bool' '>'
| |-ImplicitCastExpr 0x7fe8e9075df0 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7fe8e9075db0 'int' lvalue ParmVar 0x7fe8e9075b68 'a' 'int'
| `-IntegerLiteral 0x7fe8e9075dd0 'int' 0
`-BinaryOperator 0x7fe8e9075e80 '_Bool' '>'
  |-ImplicitCastExpr 0x7fe8e9075e68 'int' <LValueToRValue>
  | `-DeclRefExpr 0x7fe8e9075e28 'int' lvalue ParmVar 0x7fe8e9075be8 'b' 'int'
  `-IntegerLiteral 0x7fe8e9075e48 'int' 0

2.2 从 dump 日志,到报错 warning

2.2.1 warning 源文件修改

进入到语义分析的警告表格

/yourPath/llvm-project/clang/include/clang/Basic/DiagnosticSemaKinds.td

添加一行警告,于文件结尾



// 添加这一行
def warn_if_condition_too_complex: Warning<"if 语句,太过复杂,修下吧 ...">;


} // end of sema component.
2.2.2 继续修改语义分析文件

/yourPath/llvm-project/clang/lib/Sema/SemaStmt.cpp

辅助命令

open /yourPath/llvm-project/clang/lib/Sema/SemaStmt.cpp

  • 添加方法
using namespace clang;
using namespace sema;

// 这里添加


// 需要两个参数,
// if 条件的 AST
// 和 semantic self, 用 Sema 来报错

void DiagnoseIf(const Expr * If, Sema &S){
    // Diag
    // 第一个参数,编译警告的位置
    // 第二个参数,哪一种编译警告
    
    // << If->getSourceRange();
    // 添加源代码范围,产生高亮效果,
    S.Diag(If->getExprLoc(), diag:: warn_if_condition_too_complex) << If->getSourceRange();
    
    
}


  • 调用方法

还是上面提到的,语义分析方法

StmtResult Sema::ActOnIfStmt(SourceLocation IfLoc, bool IsConstexpr,
                             SourceLocation LParenLoc, Stmt *InitStmt,
                             ConditionResult Cond, SourceLocation RParenLoc,
                             Stmt *thenStmt, SourceLocation ElseLoc,
                             Stmt *elseStmt) {

//...

// 添加调用
   DiagnoseIf(Cond.Condition.get(), *this);

  return BuildIfStmt(IfLoc, IsConstexpr, LParenLoc, InitStmt, Cond, RParenLoc,
                     thenStmt, ElseLoc, elseStmt);
}
2.2.3 看效果

还是上面的例子, 简单的 cpp 代码

生成代码,再编译, job_O 一下

使用编译出的 clang 调试, job_debug

建好的 warning,投入使用


➜  build git:(main) ✗ /Users/jzd/Movies/A_B/llvm-projectX/build/bin/clang -c /xxx/test.cpp 
/xxx/test.cpp:13:15: warning: if 语句,太过复杂,修下吧 ...
    if (a > 0 && b > 0){
        ~~~~~~^~~~~~~~
1 warning generated.

警告的控制,通过诊断组 diagnostic group

体现在,通过命令行的 flag , 激活该编译警告,或使该编译警告失效

这次换了一个文件路径

/yourPath/llvm-project/clang/include/clang/Basic/DiagnosticGroups.td

在文尾添加

def ComplexIf: DiagGroup<"complex-condition">;

接着改,上面的语义分析的警告表格

/yourPath/llvm-project/clang/include/clang/Basic/DiagnosticSemaKinds.td

修改警告的定义

DefaultIgnore , 这个的意思是,让该警告默认失效

def warn_if_condition_too_complex: Warning<"if 语句,太过复杂,修下吧 ...">, InGroup<ComplexIf>, DefaultIgnore;

修改调用处的代码,代码位置见上面

// 精确使用, 编译警告

if(!Diags.isIgnored(diag:: warn_if_condition_too_complex, Cond.Condition.get()->getExprLoc())){
     // 这个 if 判断,算性能优化

        DiagnoseIf(Cond.Condition.get(), *this);
    }
效果看下

job_O 下后,

job_debug 等价于

➜ build git:(main) ✗ /yourPath/llvm-project/build/bin/clang -Wno-complex-condition -c /xxx/test.cpp

默认的选项是,使失效,

因为 CPU 计算编译警告,需要时间

-Wno-complex-condition, 默认不需要 warning no , 这个编译组 complex-condition

激活 if 检查的编译警告,使用选项 -Wcomplex-condition

( 这一步,新的调试,下文简称为 job_debug1 )

/yourPath/llvm-project/build/bin/clang -Wcomplex-condition -c /xxx/test.cpp

3. clang 开发,阶段 2,复杂 if 语句的 warning 处理

3.1 ,计算出 if 嵌套语句的复杂度

回到了我们的 DSA

3.1.1 感性认识,简单代码语句的 AST 展开
  • 函数的声明 f1 ,对应声明表达式,declaration reference expresssion

  • 对函数 f1,做了一次隐式转换,implicit cast expression

拿到了 f1 的函数指针

  • 拿到函数指针调用的结果,f1(),call expression

  • 然后是各种简单的运算

截屏2021-09-01 下午7.38.34.png

3.1.2 算 if 嵌套语句复杂度

关注的是,二元操作符节点

遇到了 3 层,就要报错了

截屏2021-09-01 下午7.53.09.png

3.1.3 修改代码
  • 计算层级,对树深度优先遍历
void DiagnoseIf(const Expr * IfRoot, Sema &S, const Expr * CurrentExpr, int CurrentNestingLevel){
        
    // 忽略函数的 Imp 指针转化
    CurrentExpr = CurrentExpr->IgnoreParenImpCasts();
    // dyn_cast 动态转化下,看是不是二元操作符
    if (const auto * BinaryOp = dyn_cast<BinaryOperator>(CurrentExpr)){
        // 看这个二元操作符,是不是 && 或 ||
        if (BinaryOp->getOpcode() == BO_LAnd || BinaryOp->getOpcode() == BO_Or){
            if (CurrentNestingLevel >= 2){
                S.Diag(IfRoot->getExprLoc(), diag:: warn_if_condition_too_complex) << IfRoot->getSourceRange();
            }
            else{
                // 对树,深度优先遍历,有一个简单的递归
                
                DiagnoseIf(IfRoot, S, BinaryOp->getLHS(), CurrentNestingLevel + 1);
                DiagnoseIf(IfRoot, S, BinaryOp->getRHS(), CurrentNestingLevel + 1);
            }
        }
    }
    
}
  • 调用部分
DiagnoseIf(Cond.Condition.get(), *this, Cond.Condition.get(), 0);
3.1.5 验证下

编译 job_O + 调试 job_debug1

  • 新的用例
void test(int a, int b, int c, int d, int e){
    if (a > 0){ } // 0

    if (a > 0 || b > 0){ } // 1

    if ((a > 0 || b > 0) && c > 0){ } // 2

    if (((a > 0 || b > 0) && c > 0) || d > 0){ }  // 3

    if ((((a > 0 || b > 0) && c > 0) || d > 0) && e > 0){ }   // 4

    if (((a > 0 || b > 0) && (c > 0 || d > 0)) || e > 0){ }   // 5

}

  • 结果

例子 0~4 , 正常

例子 5,warning 出现重复,

重复的 warning,算噪音 noisy

简单的算法错误

把对应的树,画一下,就明白了

➜  build git:(main) ✗ /yourPath/llvm-project/build/bin/clang -Wcomplex-condition -c /xxx/test.cpp
/xxx/test.cpp:18:37: warning: if 语句,太过复杂,修下吧 ... [-Wcomplex-condition]
    if (((a > 0 || b > 0) && c > 0) || d > 0){ }
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
/xxx/test.cpp:20:48: warning: if 语句,太过复杂,修下吧 ... [-Wcomplex-condition]
    if ((((a > 0 || b > 0) && c > 0) || d > 0) && e > 0){ }
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
/xxx/test.cpp:22:48: warning: if 语句,太过复杂,修下吧 ... [-Wcomplex-condition]
    if (((a > 0 || b > 0) && (c > 0 || d > 0)) || e > 0){ }
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
/xxx/test.cpp:22:48: warning: if 语句,太过复杂,修下吧 ... [-Wcomplex-condition]
    if (((a > 0 || b > 0) && (c > 0 || d > 0)) || e > 0){ }
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
4 warnings generated.

3.2, 解决 bug 1, warning 重复

起因,如图

截屏2021-09-01 下午10.06.01.png

解决,遇到一个就报错,结束

代码修改

bool DiagnoseIf(const Expr * IfRoot, Sema &S, const Expr * CurrentExpr, int CurrentNestingLevel){
        
    // 忽略函数的 Imp 指针转化
    CurrentExpr = CurrentExpr->IgnoreParenImpCasts();
    // dyn_cast 动态转化下,看是不是二元操作符
    if (const auto * BinaryOp = dyn_cast<BinaryOperator>(CurrentExpr)){
        // 看这个二元操作符,是不是 && 或 ||
        if (BinaryOp->getOpcode() == BO_LAnd || BinaryOp->getOpcode() == BO_LOr){
            if (CurrentNestingLevel >= 2){
                S.Diag(IfRoot->getExprLoc(), diag:: warn_if_condition_too_complex) << IfRoot->getSourceRange();
                return false;
            }
            else{
                // 对树,深度优先遍历,有一个简单的递归
                if (DiagnoseIf(IfRoot, S, BinaryOp->getLHS(), CurrentNestingLevel + 1)){
                    if (DiagnoseIf(IfRoot, S, BinaryOp->getRHS(), CurrentNestingLevel + 1)){
                        return true;
                    }
                    else{
                        return false;
                    }
                }
                else{
                    return false;
                }
            }
        }
    }
    return true;
}
改完,看效果

流程走一走,

两个 job 走一遍

正常 ( 还是刚才的用例 )

➜  build git:(main) ✗ /yourPath/llvm-project/build/bin/clang -Wcomplex-condition -c /xxx/test
/xxx/test:18:37: warning: if 语句,太过复杂,修下吧 ... [-Wcomplex-condition]
    if (((a > 0 || b > 0) && c > 0) || d > 0){ }
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
/xxx/test.cpp:20:48: warning: if 语句,太过复杂,修下吧 ... [-Wcomplex-condition]
    if ((((a > 0 || b > 0) && c > 0) || d > 0) && e > 0){ }
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
/xxx/test.cpp:22:48: warning: if 语句,太过复杂,修下吧 ... [-Wcomplex-condition]
    if (((a > 0 || b > 0) && (c > 0 || d > 0)) || e > 0){ }
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
3 warnings generated.

4. clang 开发,阶段 3,不断 debug, 遇到新的情况

4.1 , 要考虑 AST 的构造方式

4.1.1 新的用例

简单的重复,看起来,没有嵌套

if (a > 0 || b > 0 || c > 0 || d > 0 || e > 0){ }
4.1.2 跑一下 ( 流程同上 )
/xxx/test.cpp:24:42: warning: if 语句,太过复杂,修下吧 ... [-Wcomplex-condition]
    if (a > 0 || b > 0 || c > 0 || d > 0 || e > 0){ }
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
4.1.3 分析问题

C++ 允许写连续的判断,不带括号

实际上该用例,对于语义分析来说,有一个等价

截屏2021-09-01 下午10.25.19.png

这样 AST 的二元操作符判断,就嵌套了三层

需要回避这种情况

4.1.4 解决

辅助方法

// 约定
// 啥也不是, 0
// && , 1
// || , 2

int valOfExpresion(const Expr * BOp){
    BOp = BOp->IgnoreParenImpCasts();
    if (const auto * BinaryOp = dyn_cast<BinaryOperator>(BOp)){
        if (BinaryOp->getOpcode() == BO_LAnd){
            return 1;
        }
        else if (BinaryOp->getOpcode() == BO_LOr){
            return 2;
        }
    }
    return 0;
}

修改方法,

增加判断,如果父二元操作符节点和子二元操作符节点,相等

本层计数,忽略


bool DiagnoseIf(const Expr * IfRoot, Sema &S, const Expr * CurrentExpr, int CurrentNestingLevel){
    
    // 忽略函数的 Imp 指针转化
    CurrentExpr = CurrentExpr->IgnoreParenImpCasts();
    // dyn_cast 动态转化下,看是不是二元操作符
    if (const auto * BinaryOp = dyn_cast<BinaryOperator>(CurrentExpr)){
        // 看这个二元操作符,是不是 && 或 ||
        if (BinaryOp->getOpcode() == BO_LAnd || BinaryOp->getOpcode() == BO_LOr){
            
                // 对树,深度优先遍历,有一个简单的递归
                int val = valOfExpresion(CurrentExpr);
                int valLhs = valOfExpresion(BinaryOp->getLHS());
                // levelLhs , 考虑的是,本层的访问,是要纳入计算,还是要忽略
                int levelLhs = 1;
                
                if (val == valLhs){
                    levelLhs = 0;
                }
                int valRhs = valOfExpresion(BinaryOp->getRHS());
                int levelRhs = 1;
                if (val == valRhs){
                    levelRhs = 0;
                }
                if (CurrentNestingLevel >= 2){
                      S.Diag(IfRoot->getExprLoc(), diag:: warn_if_condition_too_complex) << IfRoot->getSourceRange();
                      return false;
                }
                else if (DiagnoseIf(IfRoot, S, BinaryOp->getLHS(), CurrentNestingLevel + levelLhs)){
                    if (DiagnoseIf(IfRoot, S, BinaryOp->getRHS(), CurrentNestingLevel + levelRhs)){
                        return true;
                    }
                    else{
                        return false;
                    }
                }
                else{
                    return false;
                }
            
        }
    }
    return true;
}

4.1.5 错误示范

把返回前置

这样嵌套 2 层,这边就报错了

判断为 ||&&, 再决定返回,

考虑了本层,+ 1 层,3 层嵌套,才报错

levelLhs 的值, 考虑的是,本层的访问,是要纳入计算,还是要忽略

levelRhs 的值, 也一样

bool DiagnoseIf(const Expr * IfRoot, Sema &S, const Expr * CurrentExpr, int CurrentNestingLevel){
    if (CurrentNestingLevel >= 2){
        S.Diag(IfRoot->getExprLoc(), diag:: warn_if_condition_too_complex) << IfRoot->getSourceRange();
        return false;
    }
    // 忽略函数的 Imp 指针转化
    CurrentExpr = CurrentExpr->IgnoreParenImpCasts();
    // dyn_cast 动态转化下,看是不是二元操作符
    if (const auto * BinaryOp = dyn_cast<BinaryOperator>(CurrentExpr)){
        // 看这个二元操作符,是不是 && 或 ||
        if (BinaryOp->getOpcode() == BO_LAnd || BinaryOp->getOpcode() == BO_LOr){
            
                // 对树,深度优先遍历,有一个简单的递归
                int val = valOfExpresion(CurrentExpr);
                int valLhs = valOfExpresion(BinaryOp->getLHS());
                // levelLhs , 考虑的是,本层的访问,是要纳入计算,还是要忽略
                int levelLhs = 1;
                
                if (val == valLhs){
                    levelLhs = 0;
                }
                int valRhs = valOfExpresion(BinaryOp->getRHS());
                int levelRhs = 1;
                if (val == valRhs){
                    levelRhs = 0;
                }
                if (DiagnoseIf(IfRoot, S, BinaryOp->getLHS(), CurrentNestingLevel + levelLhs)){
                    if (DiagnoseIf(IfRoot, S, BinaryOp->getRHS(), CurrentNestingLevel + levelRhs)){
                        return true;
                    }
                    else{
                        return false;
                    }
                }
                else{
                    return false;
                }
            
        }
    }
    return true;
}

错误示范效果

/xxx/test.cpp:16:26: warning: if 语句,太过复杂,修下吧 ... [-Wcomplex-condition]
    if ((a > 0 || b > 0) && c > 0){ }
        ~~~~~~~~~~~~~~~~~^~~~~~~~

4.2 , 其他情况

4.2.1, if 语句里面,存在宏的展开

宏的展开,是在预处理的时候,

我们处理 AST , 是在语义分析阶段,需要规避

解决,略

4.2.2, C++ 的模版函数,重复报错

解决,略

5. 将 clang 添加 Xcode

操作比较简单

  • 自定义两个用户设置

CC 和 CXX

截屏2021-09-01 下午11.54.21.png

  • 填入内容

CC 的路径是

/yourPath/llvm-projectX/build/bin/clang

CXX 的路径是

/yourPath/llvm-projectX/build/bin/clang++

截屏2021-09-01 下午11.55.25.png

  • 设置文件的编译选项

要这个警告

-Wcomplex-condition

截屏2021-09-01 下午11.57.34.png

github repo