C++ 静态分析工具(`Clang-Tidy`, `Cppcheck`)自定义规则编写

哈喽,各位好!

今天咱们来聊点硬核的——C++静态分析工具的自定义规则编写。作为一名“编程专家”(咳咳,各位轻点拍),我将尽量用大家都能听懂的“人话”,带大家一起深入 Clang-TidyCppcheck 的世界,看看如何打造属于自己的代码质量卫士。

静态分析:代码的“X光”

在深入自定义规则之前,咱们先简单回顾一下静态分析的概念。简单来说,静态分析就是在不运行代码的情况下,对代码进行检查。它就像给代码照“X光”,能提前发现潜在的问题,比如:

  • 内存泄漏
  • 空指针解引用
  • 未使用的变量
  • 代码风格不一致
  • 潜在的性能瓶颈
  • 违反编码规范

这些问题如果在运行时才暴露出来,往往会花费大量的时间和精力去调试。而静态分析工具则可以在编码阶段就将它们扼杀在摇篮里。

Clang-Tidy vs. Cppcheck:两位“代码医生”

Clang-TidyCppcheck 是 C++ 领域两款常用的静态分析工具,它们各有特点:

特性 Clang-Tidy Cppcheck
优点 基于 Clang 编译器,理解 C++ 语法语义更透彻;可扩展性强,方便自定义规则;集成度高,与 IDE 配合良好;诊断信息精确。 轻量级,速度快;易于上手;支持多种编码规范;误报率相对较低。
缺点 配置相对复杂;自定义规则编写门槛较高。 对 C++ 新特性的支持可能不够及时;自定义规则编写能力相对较弱。
适用场景 对代码质量要求极高的大型项目;需要深度定制的静态分析需求;希望与 IDE 集成,在编码过程中实时检查。 中小型项目;快速检查代码质量;希望降低误报率。
自定义规则 基于 Clang AST (抽象语法树),需要深入理解 C++ 语法和 Clang API。 基于简单的模式匹配,相对容易上手,但能力有限。
推荐程度 高。适合对代码质量有较高要求的项目和团队。 中。适合快速检查代码质量,或者作为 Clang-Tidy 的补充。

Clang-Tidy 自定义规则:深入 AST 的探险之旅

Clang-Tidy 的强大之处在于其基于 Clang AST 的分析能力。这意味着你可以像操作一颗树一样,遍历 C++ 代码的语法结构,找到你感兴趣的节点,并进行检查。

1. 环境搭建

首先,你需要安装 Clang 和 LLVM 的开发环境。具体步骤因操作系统而异,可以参考官方文档。安装完成后,确保 clang-tidy 命令可以在终端中运行。

2. 创建一个骨架项目

创建一个简单的项目目录,包含以下文件:

  • MyCheck.cpp: 你的自定义检查规则的实现文件。
  • MyCheck.h: 你的自定义检查规则的头文件。
  • CMakeLists.txt: 用于构建项目的 CMake 配置文件。

3. 编写 MyCheck.h

#ifndef MYCHECK_MYCHECK_H
#define MYCHECK_MYCHECK_H

#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/Lex/Lexer.h"
#include "clang/Sema/Sema.h"

namespace clang {
namespace tidy {
namespace mycheck {

class MyCheck : public clang::ast_matchers::MatchFinder::MatchCallback {
public:
  MyCheck(CompilerInstance &CI, StringRef Name) : CI(CI), Name(Name) {}
  virtual void run(const clang::ast_matchers::MatchFinder::MatchResult &Result) override;

private:
  CompilerInstance &CI;
  StringRef Name;
};

class MyCheckAction : public PluginASTAction {
protected:
  std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                   StringRef file) override {
    Finder.addMatcher(
        // 在这里定义你的 AST 匹配器
        clang::ast_matchers::functionDecl().bind("func"),
        &Check);
    return Finder.newASTConsumer();
  }

  bool ParseArgs(const CompilerInvocation &CI,
                 const std::vector<std::string> &Args) override {
    return true;
  }

  PluginASTAction::ActionType getActionType() override {
    return PluginASTAction::ActionType::AddBeforeMainAction;
  }

private:
  clang::ast_matchers::MatchFinder Finder;
  MyCheck Check;
};

} // namespace mycheck
} // namespace tidy
} // namespace clang

#endif // MYCHECK_MYCHECK_H

4. 编写 MyCheck.cpp

#include "MyCheck.h"
#include "clang/AST/ASTContext.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Lex/Lexer.h"

using namespace clang;
using namespace clang::ast_matchers;
using namespace clang::tidy;
using namespace clang::tidy::mycheck;

void MyCheck::run(const MatchFinder::MatchResult &Result) {
  if (const FunctionDecl *func = Result.Nodes.getNodeAs<FunctionDecl>("func")) {
    // 在这里编写你的检查逻辑
    DiagnosticsEngine &DE = Result.Context->getDiagnostics();
    SourceLocation loc = func->getLocation();

    // 举例:检查函数名是否以 "my" 开头
    if (!func->getNameInfo().getName().getAsString().startswith("my")) {
      DE.Report(loc, DE.getCustomDiagID(DiagnosticIDs::Warning, "Function name should start with 'my'"))
          << func;
    }
  }
}

static FrontendPluginRegistry::Add<MyCheckAction>
X("my-check", "Checks function names.");

5. 编写 CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(MyCheck)

find_package(Clang REQUIRED)

include_directories(${CLANG_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})

add_library(MyCheck MODULE MyCheck.cpp)

target_link_libraries(MyCheck clangFrontend clangAST clangBasic clangLex clangSema)

set_target_properties(MyCheck PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    PREFIX ""
    MODULE_EXTENSION "so"
)

6. 构建项目

mkdir build
cd build
cmake ..
make

构建成功后,你会在 build 目录下找到一个名为 MyCheck.so 的动态链接库。

7. 使用自定义规则

创建一个简单的 C++ 文件 test.cpp

int add(int a, int b) {
  return a + b;
}

int myAdd(int a, int b) {
  return a + b;
}

然后,运行 clang-tidy

clang-tidy -load ./build/MyCheck.so -checks='-*,my-check' test.cpp -- -std=c++17

你应该会看到类似以下的输出:

test.cpp:1:1: warning: Function name should start with 'my' [my-check]
int add(int a, int b) {
^~~

恭喜你!你已经成功编写并运行了你的第一个 Clang-Tidy 自定义规则!

深入理解 Clang-Tidy 的核心概念

  • AST (抽象语法树): C++ 代码的语法结构的树形表示。Clang-Tidy 的核心就是对 AST 的分析。
  • Matcher (匹配器): 用于在 AST 中查找特定节点的工具。例如,functionDecl() 可以匹配函数声明节点。
  • Callback (回调函数): 当 Matcher 找到匹配的节点时,会调用 Callback 函数。你可以在 Callback 函数中编写你的检查逻辑。
  • Diagnostic (诊断信息): 当发现问题时,Clang-Tidy 会生成 Diagnostic 信息,用于向用户报告问题。

编写更复杂的规则:一个例子

假设我们要编写一个规则,检查函数参数是否使用了 const 修饰符。

1. 修改 MyCheck.h

#ifndef MYCHECK_MYCHECK_H
#define MYCHECK_MYCHECK_H

#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/Lex/Lexer.h"
#include "clang/Sema/Sema.h"

namespace clang {
namespace tidy {
namespace mycheck {

class ConstParameterCheck : public clang::ast_matchers::MatchFinder::MatchCallback {
public:
  ConstParameterCheck(CompilerInstance &CI, StringRef Name) : CI(CI), Name(Name) {}
  virtual void run(const clang::ast_matchers::MatchFinder::MatchResult &Result) override;

private:
  CompilerInstance &CI;
  StringRef Name;
};

class ConstParameterCheckAction : public PluginASTAction {
protected:
  std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
                                                   StringRef file) override {
    Finder.addMatcher(
        // 匹配函数声明,并且参数没有 const 修饰
        functionDecl(hasParameter(parmVarDecl(unless(hasType(qualType(isConstQualified())))))).bind("func"),
        &Check);
    return Finder.newASTConsumer();
  }

  bool ParseArgs(const CompilerInvocation &CI,
                 const std::vector<std::string> &Args) override {
    return true;
  }

  PluginASTAction::ActionType getActionType() override {
    return PluginASTAction::ActionType::AddBeforeMainAction;
  }

private:
  clang::ast_matchers::MatchFinder Finder;
  ConstParameterCheck Check;
};

} // namespace mycheck
} // namespace tidy
} // namespace clang

#endif // MYCHECK_MYCHECK_H

2. 修改 MyCheck.cpp

#include "MyCheck.h"
#include "clang/AST/ASTContext.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Lex/Lexer.h"

using namespace clang;
using namespace clang::ast_matchers;
using namespace clang::tidy;
using namespace clang::tidy::mycheck;

void ConstParameterCheck::run(const MatchFinder::MatchResult &Result) {
  if (const FunctionDecl *func = Result.Nodes.getNodeAs<FunctionDecl>("func")) {
    DiagnosticsEngine &DE = Result.Context->getDiagnostics();
    SourceLocation loc = func->getLocation();

    // 报告错误
    DE.Report(loc, DE.getCustomDiagID(DiagnosticIDs::Warning, "Function parameter should be const"))
        << func;
  }
}

static FrontendPluginRegistry::Add<ConstParameterCheckAction>
X("const-parameter-check", "Checks if function parameters are const.");

3. 修改 CMakeLists.txt

add_library 的第一个参数改为 ConstParameterCheck,并将 FrontendPluginRegistry::Add 中的第一个参数改为 "const-parameter-check"

4. 重新构建项目

make

5. 使用自定义规则

修改 test.cpp

int add(int a, int b) {
  return a + b;
}

int myAdd(const int a, int b) {
  return a + b;
}

然后,运行 clang-tidy

clang-tidy -load ./build/MyCheck.so -checks='-*,const-parameter-check' test.cpp -- -std=c++17

你应该会看到类似以下的输出:

test.cpp:1:1: warning: Function parameter should be const [const-parameter-check]
int add(int a, int b) {
^~~

这个例子展示了如何使用 hasParameterunless 组合更复杂的 Matcher,来实现更精确的检查。

Cppcheck 自定义规则:简单而实用

相比 Clang-TidyCppcheck 的自定义规则编写要简单得多。它主要基于简单的模式匹配,你可以定义一些规则,当代码中出现符合这些模式的代码时,Cppcheck 就会发出警告。

1. 编写规则文件

创建一个 XML 文件,例如 myrules.xml,用于定义你的规则。

<?xml version="1.0"?>
<rules version="2">
  <rule id="missingReturn" severity="warning" scope="function">
    <pattern>
      <statement>return;</statement>
    </pattern>
    <message>Function {function} has a missing return statement.</message>
  </rule>
</rules>

这个规则很简单,它会检查函数中是否缺少 return 语句。

2. 使用自定义规则

cppcheck --rule-file=myrules.xml test.cpp

Cppcheck 自定义规则的局限性

  • 基于模式匹配: 只能基于简单的文本模式进行匹配,无法理解 C++ 的语法语义。
  • 表达能力有限: 无法表达复杂的逻辑关系。
  • 容易产生误报: 模式匹配可能会匹配到一些不应该匹配的代码。

总结:选择适合你的工具

  • 如果你需要深度定制的静态分析,并且对 C++ 语法和 Clang API 有深入的了解,那么 Clang-Tidy 是你的最佳选择。
  • 如果你需要快速检查代码质量,或者你的项目比较小,那么 Cppcheck 可能更适合你。
  • 你也可以将两者结合使用,Cppcheck 用于快速检查,Clang-Tidy 用于深度分析。

最后,一些建议

  • 从小处着手: 从简单的规则开始,逐步增加复杂度。
  • 多看文档: Clang 和 LLVM 的文档非常详细,是学习 Clang-Tidy 的重要资源。
  • 参考现有规则: Clang-Tidy 已经有很多内置的规则,可以作为学习的例子。
  • 不要害怕调试: 自定义规则的编写过程中难免会遇到问题,不要害怕调试,多尝试,多思考。
  • 编写单元测试: 为你的自定义规则编写单元测试,确保其正确性。

希望今天的分享对大家有所帮助! 静态分析是一个非常广阔的领域,还有很多东西值得我们去探索。 让我们一起努力,写出更高质量的 C++ 代码!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注