Clang插件


文档摘要

测试项目的目标是创建一个Clang插件,用于估算类的复杂性。具体来说,如果一个类的方法数量超过某个阈值,那么这个类就被认为是复杂的。我们将利用到目前为止所获得的所有知识来完成这个项目。这包括使用递归访问者和Clang诊断。此外,我们还将为我们的项目创建一个LIT测试。开发插件将需要一个独特的LLVM构建配置,这将是我们首先要做的事情。 插件将作为一个共享对象创建,LLVM安装应该支持动态库(见第1.3.1节,使用CMake配置): cmake -G Ninja -DCMAKEBUILDTYPE=Debug -DCMAKEINSTALLPREFIX=..

测试项目的目标是创建一个Clang插件,用于估算类的复杂性。具体来说,如果一个类的方法数量超过某个阈值,那么这个类就被认为是复杂的。我们将利用到目前为止所获得的所有知识来完成这个项目。这包括使用递归访问者和Clang诊断。此外,我们还将为我们的项目创建一个LIT测试。开发插件将需要一个独特的LLVM构建配置,这将是我们首先要做的事情。

插件将作为一个共享对象创建,LLVM安装应该支持动态库(见第1.3.1节,使用CMake配置):

cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug
-DCMAKE_INSTALL_PREFIX=../install -DLLVM_TARGETS_TO_BUILD="X86"
-DLLVM_ENABLE_PROJECTS="clang" -DLLVM_USE_SPLIT_DWARF=ON
-DBUILD_SHARED_LIBS=ON ../llvm

图4.31:用于Clang插件项目的CMake配置

使用了第1.4节中的构建配置,如图1.12所示。在配置中,为安装工件设置了一个文件夹
../install,将构建目标限制为X86平台,并只启用clang项目。此外,启用了调试符号的大小优化,并使用动态库而不是静态链接。

接下来,需要构建和安装clang:

$`ninja install
\end{shell}

完成了clang的构建和安装,就可以继续编写项目的CMakeLists.txt文件。

\mySubsubsection{4.6.2.}{插件的CMake构建配置}

使用图3.20作为我们的插件构建配置的基础,更改项目名称为classchecker,而ClassComplexityChecker.cpp将作为主要源文件。文件的主要部分如图4.32所示。如图所示,将构建一个动态库(第18-20行),而不是之前的测试项目中的可执行文件。另一个修改是在第12行,设置了一个配置参数来定位LLVM构建文件夹。这个参数是必要的,以便找到LIT可执行文件,正如之前在第4.5.2节,LLVM测试框架中提到的,LIT可执行文件不包括在标准安装过程中。为了支持LIT测试的调用,还需要进行一些修改,但会稍后在图4.44中讨论这些。

\begin{cmake}
message(STATUS "$LLVM_HOME found: $ENV{LLVM_HOME}")
set(LLVM_HOME$ENVLLVM_HOME CACHE PATH "Root of LLVM installation") set(LLVM_LIB ${LLVM_HOME}/lib)
set(LLVM_DIR$LLVM_LIB/cmake/llvm) set(LLVM_BUILD $ENV{LLVM_BUILD} CACHE PATH "Root of LLVM build")
find_package(LLVM REQUIRED CONFIG)
include_directories($LLVM_INCLUDE_DIRS) link_directories(${LLVM_LIBRARY_DIRS})

Add the plugin's shared library target

add_library(classchecker MODULE
ClassChecker.cpp
)
set_target_properties(classchecker PROPERTIES COMPILE_FLAGS "-fno-rtti")
target_link_libraries(classchecker
LLVMSupport
clangAST
clangBasic
clangFrontend
clangTooling
)
\end{cmake}

\begin{center}
图4.32:class complexity插件的CMakeLists.txt文件
\end{center}

完成构建配置后,可以开始编写插件的主要代码。将创建的第一个组件是一个递归访问者类,名为ClassVisitor。

\mySubsubsection{4.6.3.}{递归访问者类}

访问者类位于ClassVisitor.hpp文件中(见图4.33),这是一个递归访问者,处理clang::CXXRecordDecl,这是C++类声明的AST节点。第13-16行计算方法的数量,并在超过阈值时在第19-25行发出诊断信息。

\begin{cpp}
#include "clang/AST/ASTContext.h"
#include "clang/AST/RecursiveASTVisitor.h"

namespace clangbook {
namespace classchecker {
class ClassVisitor : public clang::RecursiveASTVisitor {
public:
explicit ClassVisitor(clang::ASTContext *C, int T)
: Context(C), Threshold(T) {}

bool VisitCXXRecordDecl(clang::CXXRecordDecl *Declaration) {
if (Declaration->isThisDeclarationADefinition()) {
int MethodCount = 0;
for (const auto *M : Declaration->methods()) {
MethodCount++;
}
if (MethodCount > Threshold) {
clang::DiagnosticsEngine &D = Context->getDiagnostics();
unsigned DiagID =
D.getCustomDiagID(clang::DiagnosticsEngine::Warning,
"class %0 is too complex: method count = %1");
clang::DiagnosticBuilder DiagBuilder =
              D.Report(Declaration->getLocation(), DiagID);
DiagBuilder << Declaration->getName() << MethodCount;
}
}
return true;
}

private:
 clang::ASTContext *Context;
int Threshold;
};
} // namespace classchecker
} // namespace clangbook
\end{cpp}

\begin{center}
图4.33:ClassVisitor.hpp的源代码
\end{center}

诊断消息在行20-22中构建,诊断消息接受两个参数:类名和类的函数数量。这些参数在第22行以'%1'和'%2'占位符编码。这些参数的实际值在第25行传递,使用DiagBuild对象构建诊断消息。这个对象是clang::DiagnosticBuilder类的实例,实现了资源获取即初始化(RAII)模式。当对象超出作用域时,会自动调用其析构函数,并在此时发出实际的诊断信息。

\begin{myNotic}{重要提示}
C++中,RAII原则是一种常见的习惯用法,用于通过将资源与对象的生存期联系起来来管理资源生命周期。当对象超出作用域时,其析构函数会自动调用,这为释放对象持有的资源提供了机会。
\end{myNotic}

ClassVisitor是在AST消费者类中创建的,这是我们的下一个主题。

\mySubsubsection{4.6.4.}{插件AST消费者类}

AST消费者类在ClassConsumer.hpp中实现,并在我们的AST访问者测试项目中代表标准AST消费者(参见图3.9)。代码在图4.35中展示。

\begin{cpp}
namespace clangbook {
namespace classchecker {
class ClassConsumer : public clang::ASTConsumer {
public:
explicit ClassConsumer(clang::ASTContext *Context, int Threshold)
: Visitor(Context, Threshold) {}

virtual void HandleTranslationUnit(clang::ASTContext &Context) {
Visitor.TraverseDecl(Context.getTranslationUnitDecl());
}

private:
ClassVisitor Visitor;
};
} // namespace classchecker
} // namespace clangbook
\end{cpp}

\begin{center}
图4.34:ClassConsumer.hpp的源代码
\end{center}

代码在第10行初始化Visitor,并在第13行使用Visitor类遍历声明,从最顶层的声明(翻译单元声明)开始。消费者必须从特殊的AST动作类创建,这是我们接下来要讨论的。

\mySubsubsection{4.6.5.}{插件AST动作类}

插件AST动作的代码如图4.35所示:

\begin{itemize}
\item
第7行:从clang::PluginASTAction派生ClassAction。

\item
第10-13行:实例化ClassConsumer并利用MethodCountThreshold,这是从可选插件参数派生的。

\item
第15-25行:处理插件的可选阈值参数
\end{itemize}

\begin{cpp}
namespace clangbook {
namespace classchecker {
class ClassAction : public clang::PluginASTAction {
protected:
std::unique_ptrclang::ASTConsumer
CreateASTConsumer(clang::CompilerInstance &CI, llvm::StringRef) {
return std::make_unique(&CI.getASTContext(),
MethodCountThreshold);
}

bool ParseArgs(const clang::CompilerInstance &CI,
const std::vectorstd::string &args) {
for (const auto &arg : args) {
if (arg.substr(0, 9) == "threshold") {
auto valueStr = arg.substr(10); // Get the substring after "threshold="
MethodCountThreshold = std::stoi(valueStr);
return true;
}
}
return true;
}
ActionType getActionType() { return AddAfterMainAction; }

private:
int MethodCountThreshold = 5; // default value
};
} // namespace classchecker
} // namespace clangbook
\end{cpp}

\begin{center}
图4.35:ClassAction.hpp的源代码
\end{center}

我们几乎完成了,并准备初始化插件。

\mySubsubsection{4.6.6.}{插件代码}

插件注册在ClassChecker.cpp文件中,如图4.36所示。

\begin{cpp}
#include "clang/Frontend/FrontendPluginRegistry.h"

#include "ClassAction.hpp"

static clang::FrontendPluginRegistry::Addclangbook::classchecker::ClassAction
X("classchecker", "Checks the complexity of C++ classes");
\end{cpp}

\begin{center}
图4.36:ClassChecker.cpp的源代码
\end{center}

大多数初始化都隐藏在辅助类中,只需要将实现传递给lang::FrontendPluginRegistry::Add。

现在准备构建和测试我们的Clang插件。

\mySubsubsection{4.6.7.}{构建和运行插件代码}

需要指定LLVM项目的安装文件夹的路径。其余的步骤是之前使用过的标准步骤,见图3.11:

\begin{shell}
export LLVM_HOME=<...>/llvm-project/install
mkdir build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ..
ninja classchecker
\end{shell}

\begin{center}
图4.37:为Clang插件配置和构建命令
\end{center}

构建工件将位于build文件夹中。然后,可以像以下这样在测试文件上运行插件,其中是要编译的文件路径:

\begin{shell}`$ <...>/llvm-project/install/bin/clang -fsyntax-only
 -fplugin=./build/libclasschecker.so  <filepath>

图4.38:如何在测试文件上运行Clang插件

如果使用一个定义了具有三个方法的类的测试文件test.cpp(见图4.39),将不会收到任何警告。

class Simple public: void func1() void func2() void func3() ;

图4.39:Clang插件的测试:test.cpp

然而,如果指定一个更小的阈值,将为该文件收到警告:

图4.40:Clang插件在test.cpp上运行

现在是时候为我们的插件创建一个LIT测试了。

从一个项目组织描述开始。我们将采用Clang源代码中常用的模式,并将测试放在test文件夹中。这个文件夹将包含以下文件:

  • lit.site.cfg.py.in :
    这是主要的配置文件,一个CMake配置文件。标记为’@…@’的模式替换为在CMake配置中定义的相应值,这个文件还加载了lit.cfg.py。

  • lit.cfg.py : 这是LIT测试的主要配置文件。

  • simple_test.cpp : 这是我们的LIT测试文件。

基本工作流程如下:CMake将lit.site.cfg.py.in作为模板,并在build/test文件夹中生成相应的lit.site.cfg.py。这个文件然后在IT测试用作执行测试的种子。

有两个LIT测试配置文件。第一个在图4.41中显示。

config.ClassComplexityChecker_obj_root = "@CMAKE_CURRENT_BINARY_DIR@"
config.ClassComplexityChecker_src_root = "@CMAKE_CURRENT_SOURCE_DIR@"
config.ClangBinary = "@LLVM_HOME@/bin/clang" config.FileCheck =
"@FILECHECK_COMMAND@"

lit_config.load_config( config,
os.path.join(config.ClassComplexityChecker_src_root, "test/lit.cfg.py"))

图4.41:lit.site.cfg.py.in文件

这个文件是一个CMake模板,将转换成Python脚本。最关键的部分是第6-7行,其中主要LIT配置加载。来自主源树,并且不会复制到build文件夹。

随后的配置在图4.42中显示。这是一个Python脚本,包含LIT测试的主要配置。

# lit.cfg.py import lit.formats

config.name = ’classchecker’ config.test_format =
lit.formats.ShTest(True) config.suffixes = [’.cpp’]
config.test_source_root = os.path.dirname(__file__)

config.substitutions.append((’ config.substitutions.append((’
config.substitutions.append((’

图4.42:lit.cfg.py文件

第4-7行定义了基本配置;例如,第6行决定了哪些文件应该用于测试。测试文件夹中的所有以’.cpp’扩展名的文件都将用作LIT测试。

第9-11行详细说明了LIT测试中将使用的替换。这些包括clang二进制文件的路径(第9行)、包含插件的动态库的路径(第10行)以及FileCheck实用程序的路径(第11行)。

我们只定义了一个基本的LIT测试,simple_test.cpp,如图4.43所示。

// RUN:

class Simple public: void func1() void func2() ;

// CHECK: :[[@LINE+1]]:[0-9]+: warning: class Complex is too
complex: method count = 6 class Complex public: void func1() void
func2() void func3() void func4() void func5() void func6() ;

图 4.43: simple_test.cpp 文件

可以观察到第1行中使用的替换。路径指向clang二进制文件、插件动态库和FileCheck实用程序。在第9行中使用了特殊模式,这些模式工具可以识别。

最后一块拼图是CMake配置。这将设置lit.site.cfg.py.in中所需的变量,以便进行替换,并且定义了一个自定义目标来运行LIT测试。

为了支持LIT测试,CMakeLists.txt文件需要进行一些调整。必要的变化在图4.44中显示。

find_program(LIT_COMMAND llvm-lit PATH ${LLVM_BUILD}/bin) find_program(FILECHECK_COMMAND FileCheck$LLVM_BUILD/bin) if(LIT_COMMAND
AND FILECHECK_COMMAND) message(STATUS
"`LIT_COMMAND found:`LIT_COMMAND") message(STATUS
"`FILECHECK_COMMAND found:`FILECHECK_COMMAND")

# Point to our custom lit.cfg.py set(LIT_CONFIG_FILE
"$`{CMAKE_CURRENT_SOURCE_DIR}/test/lit.cfg.py")

Configure lit.site.cfg.py using current settings

configure_file("$CMAKE_CURRENT_SOURCE_DIR/test/lit.site.cfg.py.in" "${CMAKE_CURRENT_BINARY_DIR}/test/lit.site.cfg.py"
@ONLY)

Add a custom target to run tests with lit

add_custom_target(check-classchecker
COMMAND$LIT_COMMAND -v ${CMAKE_CURRENT_BINARY_DIR}/test
COMMENT "Running lit tests for classchecker clang plugin"
USES_TERMINAL)
else()
message(FATAL_ERROR "It was not possible to find the LIT executables at`$LLVM_BUILD/bin")
endif()

图4.44:在CMakeLists.txt中的LIT测试配置

第31行和第32行,搜索必要的实用程序,llvm-lit和FileCheck。它们依赖于$LLVM_BUILD环境变量,在配置文件(见图4.32的第12行)中也验证了这个变量。第41-43行是生成lit.site.cfg.py的关键步骤,该文件来自提供的模板文件lit.site.cfg.py.in。最后,在第46-49行,建立了一个自定义目标来执行LIT测试。

现在我们准备开始LIT测试。

要启动LIT测试,必须设置一个环境变量,该变量指向构建文件夹,编译项目,然后执行自定义目标check-classchecker。以下是执行这些操作的方法:

xport LLVM_BUILD=<...>/llvm-project/build export
LLVM_HOME=<...>/llvm-project/install rm -rf build; mkdir build; cd
build cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug .. ninja classchecker
ninja check-classchecker

图4.45:Clang插件的配置、构建和检查命令

执行这些命令后,可能会看到以下输出:

... [2/2] Linking CXX shared module libclasschecker.so [0/1] Running
lit tests for classchecker clang plugin – Testing: 1 tests, 1 workers –
PASS: classchecker :: simple_test.cpp (1 of 1)

Testing Time: 0.12s Passed: 1

图4.46:LIT测试执行

我们完成了我们的第一个综合项目,包含了一个实用的Clang插件,可以通过补充插件参数进行定制。此外,还包括相应的测试,可以执行以验证其功能。


发布者: 作者: 转发
评论区 (0)
U