添加编译警告,除了使用上文 开发 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 层不同操作符,计算的嵌套
最后的效果
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
-
然后是各种简单的运算
3.1.2 算 if 嵌套语句复杂度
关注的是,二元操作符节点
遇到了 3 层,就要报错了
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 重复
起因,如图
解决,遇到一个就报错,结束
代码修改
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++ 允许写连续的判断,不带括号
实际上该用例,对于语义分析来说,有一个等价
这样 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
- 填入内容
CC 的路径是
/yourPath/llvm-projectX/build/bin/clang
CXX 的路径是
/yourPath/llvm-projectX/build/bin/clang++
- 设置文件的编译选项
要这个警告
-Wcomplex-condition